接着上文粘贴代码,接下来是一个最核心和复杂的类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;
}
}