java贪吃蛇小游戏(二)

接着上文粘贴代码,接下来是一个最核心和复杂的类BackgroundPanel

package view;

/*
 *  自定义重绘图片,重写JPanel面板的paint(Graphics g)类
 */
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Label;
import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.LinkedList;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import pojo.Direction;
import util.GetImage;
import util.LocationSize;

public class BackgroundPanel extends JPanel {
    // JPanel实现了序列化接口,给出序列化值,便于程序的修改
	private static final long serialVersionUID = 1L;
	private static int score = 0;// 得分
	public static long millis = 300;// 刷新一次时间
    // 运动速度1-9,速度是利用刷新时间自己定义的公式换算的
	private static int speed = 11 - (int) millis / 50;
    
    //new两个标签,用于显示得分和速度
	private static Label scoreLabel = new Label(Integer.toString(score));
	private static Label speedLabel = new Label(Integer.toString(speed));

	// new四个按钮,开始,设置,退出和帮助按钮
	private JButton startJButton = new JButton(new ImageIcon(GetImage.START));
	private JButton exitJButton = new JButton(new ImageIcon(GetImage.EXIT));
	private JButton setJButton = new JButton(new ImageIcon(GetImage.SETTING));
	private JButton helpJButton = new JButton(new ImageIcon(GetImage.HELP));
    //定义一个变量标识开始按钮是否点击,开始按钮会开启游戏线程,所以只能有效点击一次
    //未点击开始按钮则值为0,点击开始按钮则值为1
	protected int startNumber = 0;

    //定义isRun变量,为了标识程序游戏是否正在进行
	private static boolean isRun = false;
    //贪吃蛇的实时运动方向,用于重绘
	private static Direction currentDirection;
    //记忆贪吃蛇变向后的移动方向,赋值给currentDirection,改变方向
	private static Direction direction;

    // 存储贪吃蛇各个节点的坐标,泛型采用java提供的Point类
	private static LinkedList<Point> snake = new LinkedList<Point>();
    //定义食物变量
	private static Point foodPoint;

	public BackgroundPanel() {//无参构造,用于数据初始化
		initComponents();
		init();
	}

	private static void init() {//游戏数据初始化
		currentDirection = Direction.RIGHT;//开始贪吃蛇向右运动
		direction = Direction.RIGHT;
        // 清空集合,用于重新开始游戏,初始化时snake为空,该语句不起作用,当贪吃蛇撞到墙或者自己的身体时,需要重新开始游戏时,需要先清空snake,在进行位置和长度的初始化
		snake.removeAll(snake);
		// 创建贪吃蛇初始化坐标,初始长度为5(加上头部)
		for (int x = 0; x < 5; x++) {
			Point p = new Point(100 - x * 12, 200);
			snake.add(p);
		}
        //食物位置初始化
		while (true) {// 产生的食物不能位于贪吃蛇的身体上
			boolean flag = true;
			int x = (int) (Math.random() * 558) + 25;
			int y = (int) (Math.random() * 448) + 75;
			for (int i = 0; i < 5; i++) {
				if (x == snake.get(i).x && y == snake.get(i).y) {
					flag = false; // 和贪吃蛇身体坐标相同则标记flag=false不对foodpoint赋值,再次产生x,y坐标
				}
			}
			if (flag) {// 没有相同的坐标,则赋值
				foodPoint = new Point(x, y);
				break;
			}
		}
	}

	private void initComponents() {//组件及其属性的初始化
		// 添加计分标签,位置和大小同前,单独由一个类进行赋值并提供了字段值
		scoreLabel.setBounds(LocationSize.SCORELX, LocationSize.SCORELY,
				LocationSize.SCORELW, LocationSize.SCORELH);
		// 添加速度标签
		speedLabel.setBounds(LocationSize.SPEEDLX, LocationSize.SPEEDLY,
				LocationSize.SPEEDLW, LocationSize.SPEEDLH);
		// 设置标签字体
		scoreLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
		speedLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
		// 设置标签文本居中
		scoreLabel.setAlignment(Label.CENTER);
		speedLabel.setAlignment(Label.CENTER);
        //将标签添加到面板中
		this.add(scoreLabel);
		this.add(speedLabel);
		// 给按钮设置绝对位置和大小
		startJButton.setBounds(LocationSize.STARTBX, LocationSize.STARTBY,
				LocationSize.STARTBW, LocationSize.STARTBH);
		exitJButton.setBounds(LocationSize.EXITBX, LocationSize.EXITBY,
				LocationSize.EXITBW, LocationSize.EXITBH);
		setJButton.setBounds(LocationSize.SETBX, LocationSize.SETBY,
				LocationSize.SETBW, LocationSize.SETBH);
		helpJButton.setBounds(LocationSize.HELPBX, LocationSize.HELPBY,
				LocationSize.HELPBW, LocationSize.HELPBH);
		// 把面板的布局设置为null,便于绝对定位控件
		this.setLayout(null);
		// 将按钮添加到面板中
		this.add(startJButton);
		this.add(exitJButton);
		this.add(setJButton);
		this.add(helpJButton);
		this.validate();//类似刷新的功能,使组件显示
        创建监听器对象实现键盘监听
		KeyAdapter ka = new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				super.keyPressed(e);
				if (startNumber == 1) {// 点击开始按钮之前,键盘监听不生效
					if (e.getKeyCode() == KeyEvent.VK_DOWN) {
						if (isRun && (currentDirection != Direction.UP)) {
							direction = Direction.DOWN;
						}
					}
					if (e.getKeyCode() == KeyEvent.VK_UP) {
						if (isRun && (currentDirection != Direction.DOWN)) {
							direction = Direction.UP;
						}
					}
					if (e.getKeyCode() == KeyEvent.VK_LEFT) {
						if (isRun && (currentDirection != Direction.RIGHT)) {
							direction = Direction.LEFT;
						}
					}
					if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
						if (isRun && (currentDirection != Direction.LEFT)) {
							direction = Direction.RIGHT;
						}
					}
					// 空格键,暂停或开始游戏
					if (e.getKeyCode() == KeyEvent.VK_SPACE) {
						isRun = !isRun;
					}
					// esc键退出游戏
					if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
						System.exit(0);
					}
					// w键加速,注意需要调整到英文输入法按w键,不然会被输入法软件响应
					if (e.getKeyCode() == KeyEvent.VK_W) {
						if (millis > 100) {
							millis -= 50;
							speed = 11 - (int) millis / 50;
							speedLabel.setText(Integer.toString(speed));
						} else {
							isRun = !isRun;
							Object[] options = { "OK" };//速度有最大值,调整到最大值出现弹窗提醒,出现弹窗的时候暂停游戏
							int result = JOptionPane.showOptionDialog(null,
									"速度已经调为最大", "警告",
									JOptionPane.DEFAULT_OPTION,
									JOptionPane.WARNING_MESSAGE, null, options,
									options[0]);
							if (result == 0) {
								isRun = !isRun;
							}
						}
					}
					// s键减速,其他同W键
					if (e.getKeyCode() == KeyEvent.VK_S) {
						if (millis < 500) {
							millis += 50;
							speed = 11 - (int) millis / 50;
							speedLabel.setText(Integer.toString(speed));
						} else {
							isRun = !isRun;
							Object[] options = { "OK" };
							int result = JOptionPane.showOptionDialog(null,
									"速度已经调为最小", "警告",
									JOptionPane.DEFAULT_OPTION,
									JOptionPane.WARNING_MESSAGE, null, options,
									options[0]);
							if (result == 0) {
								isRun = !isRun;
							}
						}

					}

				}
			}
		};
		// 给面板组件和所有的按钮组件都注册侦听器,这样不管谁得到焦点都可以对按键进行侦听
		this.addKeyListener(ka);
		startJButton.addKeyListener(ka);
		exitJButton.addKeyListener(ka);
		helpJButton.addKeyListener(ka);
		setJButton.addKeyListener(ka);
		// 为“开始游戏”按钮增加鼠标点击的监听
		startJButton.addMouseListener(new MyEventListener() {
			// 初始化食物位置
			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(e);
				if (startNumber == 0) {// 开始按钮只能点击一次,再次点击没有效果
					startNumber = 1;
					isRun = true;
					SnakeRun.start();//开启贪吃蛇游戏线程
				}
			}
		});
		// 为“退出游戏”按钮增加鼠标点击的监听
		exitJButton.addMouseListener(new MyEventListener() {
			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(e);
				System.exit(0);
			}
		});
		// 为“游戏设置”按钮增加鼠标点击的监听
		setJButton.addMouseListener(new MyEventListener() {
			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(e);
				// 该部分功能尚未设置
			}
		});
		// 为“帮助”按钮增加鼠标点击的监听
		helpJButton.addMouseListener(new MyEventListener() {
			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(e);
				isRun = !isRun;//显示帮助信息是暂停游戏
				Object[] options = { "返回游戏" };
				int result = JOptionPane
						.showOptionDialog(//弹出对话框,显示帮助信息
								null,
								"点击“开始游戏”按钮可以开始游戏,游戏使用键盘数字键区域的上下左右键操作贪吃蛇转向,"
										+ "\r\n"
										+ "吃掉图中随机产生的金蛋可以得分,点击“退出游戏”按钮可以退出游戏,“游戏设置”按钮的功能没"
										+ "\r\n"
										+ "有实现,游戏期间按“空格键”可以实现游戏的暂停和重新开始,按“ESC键”可以退出游戏,头部碰到"
										+ "\r\n"
										+ "图中的木框墙和自己的身体游戏结束,游戏期间保证在英文输入法状态下按“W键”可以实现贪吃蛇的加速,"
										+ "\r\n" + "按“S键”可以实现贪吃蛇的减速,速度有一定的范围。",
								"帮助信息", JOptionPane.DEFAULT_OPTION,
								JOptionPane.INFORMATION_MESSAGE, null, options,
								options[0]);
				if (result == 0) {
					isRun = !isRun;
				}
			}
		});

	}

	Thread SnakeRun = new Thread() {//创建贪吃蛇移动线程
		public void run() {
			while (true) {
				try {
					sleep(millis);//每次移动前线程休眠一定时间,由此控制移动速度
				} catch (InterruptedException ie) {
					ie.printStackTrace();
				}
				if (isRun) {//将线程休眠放到外面,只有这样,我们才能通过控制isRun来实现游戏的暂停和开始二不用考虑该线程处于什么状态。这样设计,暂停时,线程处于休眠状态
					isRun = BackgroundPanel.run(snake, foodPoint,
							currentDirection, isRun);
					currentDirection = direction;
					repaint();
				}
			}
		}
	};

	/*
	 * 重写paintComponent(Graphics g)方法,更新Panel上面的内容,Graphics为类似于画笔的类
     * 专门用于图形绘制,该方法不能直接调用,我们可以通过调用repaint()方法来调用该方法实现画面的刷新
	 */

	@Override
	protected void paintComponent(Graphics g) { // 重写绘制组件外观
		super.paintComponent(g);//这句不能少,这是repaint()方法来调用该方法的基础

		g.drawImage(GetImage.BACKGROUND, LocationSize.BACKPANELX,
				LocationSize.BACKPANELY, LocationSize.BACKPANELW,
				LocationSize.BACKPANELH, null);// 绘制背景图片与组件大小相同
        //绘制贪吃蛇的身体,因为头部带眼睛和身体不同,分开画
		for (int i = 1; i < snake.size(); i++) {
			g.setColor(Color.green);//设置当前画笔颜色
			g.fillOval(snake.get(i).x, snake.get(i).y, 12, 12);//绘制填充圆
			g.setColor(Color.white);
			g.drawOval(snake.get(i).x + 2, snake.get(i).y + 2, 8, 8);

		}
		g.setColor(new Color(255, 215, 0));
		g.fillOval(foodPoint.x, foodPoint.y, 12, 12);

        //因为头部带眼睛不是中心对称的,所以不同的移动方向绘制的坐标不同,需要根据下次刷新的移动方向绘制,可自己设计颜色和坐标,形状(方形,圆形和扇形)
		if (currentDirection == Direction.RIGHT) {
			g.setColor(Color.green);
			g.fillOval(snake.getFirst().x, snake.getFirst().y, 12, 12);
			g.setColor(Color.black);
			g.fillOval(snake.getFirst().x + 6, snake.getFirst().y + 2, 4, 4);
			g.fillOval(snake.getFirst().x + 6, snake.getFirst().y + 6, 4, 4);
		}
		if (currentDirection == Direction.LEFT) {
			g.setColor(Color.green);
			g.fillOval(snake.getFirst().x, snake.getFirst().y, 12, 12);
			g.setColor(Color.black);
			g.fillOval(snake.getFirst().x + 2, snake.getFirst().y + 2, 4, 4);
			g.fillOval(snake.getFirst().x + 2, snake.getFirst().y + 6, 4, 4);
		}
		if (currentDirection == Direction.UP) {
			g.setColor(Color.green);
			g.fillOval(snake.getFirst().x, snake.getFirst().y, 12, 12);
			g.setColor(Color.black);
			g.fillOval(snake.getFirst().x + 2, snake.getFirst().y + 2, 4, 4);
			g.fillOval(snake.getFirst().x + 6, snake.getFirst().y + 2, 4, 4);
		}
		if (currentDirection == Direction.DOWN) {
			g.setColor(Color.green);
			g.fillOval(snake.getFirst().x, snake.getFirst().y, 12, 12);
			g.setColor(Color.black);
			g.fillOval(snake.getFirst().x + 2, snake.getFirst().y + 6, 4, 4);
			g.fillOval(snake.getFirst().x + 6, snake.getFirst().y + 6, 4, 4);
		}

	}
    //该方法是贪吃蛇移动的核心方法
	public static boolean run(LinkedList<Point> snake, Point food,
			Direction currentDirection, boolean isRun) {
		Point head = snake.getFirst();//获取贪吃蛇的头部
		if (!(hitWall(head) || hitBody(snake))) {// 没有撞到墙和自己的身体
			eg: if (eatFood(head, food)) {//吃到食物和没吃到食物移动方式不同
				while (true) {// 吃到食物需要在图上重新产生食物坐标,产生的食物不能位于贪吃蛇的身体上,同食物的位置初始化代码
					boolean flag = true;
					int x = (int) (Math.random() * 558) + 25;
					int y = (int) (Math.random() * 448) + 75;
					for (int i = 0; i < 5; i++) {
						if (x == snake.get(i).x && y == snake.get(i).y) {
							flag = false; 
						}
					}
					if (flag) {
						foodPoint = new Point(x, y);
						break;
					}
				}
                //贪吃蛇的方向有限且为常量使用了枚举
				switch (currentDirection) {// 吃到食物时,只需要改变贪吃蛇头部的坐标,在添加一个索引为1的节点
				case RIGHT:
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.x += 12;// 头部右移一格
					break eg;
				case LEFT:
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.x -= 12;// 头部左移一格
					break eg;
				case UP:
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.y -= 12;// 头部上移一格
					break eg;
				case DOWN:
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.y += 12;// 头部下移一格
					break eg;
				}
			} else {
				switch (currentDirection) {// 没有吃到食物时,只需要改变贪吃蛇头部和尾部的坐标即可
				case RIGHT:
					snake.removeLast();
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.x += 12;// 头部右移一格
					break eg;
				case LEFT:
					snake.removeLast();
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.x -= 12;// 头部左移一格
					break eg;
				case UP:
					snake.removeLast();
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.y -= 12;// 头部上移一格
					break eg;
				case DOWN:
					snake.removeLast();
					snake.add(1, new Point(head.x, head.y));// 新添加元素占据头部位置
					head.y += 12;// 头部下移一格
					break eg;
				}
			}
			return isRun = true;
		} else {// 撞到墙或者自己的身体
			return isRun = false;//停止游戏
		}
	}

	public static boolean hitWall(Point head) {// 判断是否撞到墙,撞到返回ture
		int x = head.x;// 头部x坐标
		int y = head.y;// 头部y坐标
		if (x < 25 || x > 583 || y < 75 || y > 523) {//这些数字是墙的边界横纵坐标的范围
			Object[] options = { "OK", "CANCEL" };//撞到墙之后出现对话框,重新开始游戏或退出游戏
			int result = JOptionPane.showOptionDialog(null, "撞到墙了,是否重新开始游戏",
					"游戏结束", JOptionPane.DEFAULT_OPTION,
					JOptionPane.WARNING_MESSAGE, null, options, options[0]);
			if (result == 0) {
				init();// 初始化游戏
				return false;
			} else {
				System.exit(0);// 退出游戏
			}
		}
		return false;
	}

	public static boolean hitBody(LinkedList<Point> snake) {// 判断是否撞自己的身体,撞到返回ture
		// 遍历贪吃蛇身体的节点,如果有一个节点的坐标和头部的坐标值x和y差值同时小于12(此处节点的边长为12像素)即满足撞到身体的条件
		for (int i = 1; i < snake.size(); i++) {
			int difx = Math.abs(snake.get(i).x - snake.getFirst().x);
			int dify = Math.abs(snake.get(i).y - snake.getFirst().y);
			if (difx < 12 && dify < 12) {// x和y差值同时小于12即满足撞到身体的条件
				Object[] options = { "OK", "CANCEL" };
				int result = JOptionPane.showOptionDialog(null,
						"撞到自己了,是否重新开始游戏", "游戏结束", JOptionPane.DEFAULT_OPTION,
						JOptionPane.WARNING_MESSAGE, null, options, options[0]);
				if (result == 0) {
					init();// 初始化游戏
					return false;
				} else {
					System.exit(0);// 退出游戏
				}
			}
		}
		return false;
	}

	public static boolean eatFood(Point head, Point food) {// 判断是否吃到食物,吃到返回ture
		int difx = Math.abs(head.x - food.x);
		int dify = Math.abs(head.y - food.y);
		if (difx < 12 && dify < 12) {// x和y差值同时小于12即满足吃到食物的条件
			score++;// 计算得分
			scoreLabel.setText(Integer.toString(score));//将得分实时显示到界面中
			return true;
		}
		return false;
	}
}

猜你喜欢

转载自blog.csdn.net/Will_Zhan/article/details/81486838