贪吃蛇是一个益智类游戏,通过本游戏的设计和实现,可以提升Java技术能力,提升自己独立开发的能力及掌握项目的开发流程。
系统环境:Windows
开发工具:Eclipse Mars、JDK1.8
操作流程需求:
在一定范围内,生成一条蛇和随机生成一个食物,当蛇吃到食物之后,蛇身变长,通过键盘的方向键可以控制蛇的运行方向,当蛇头碰到障碍物或者碰到蛇身的时候游戏结束。并且还可以控制游戏的暂停、继续、重新开始以及结束游戏。
绘制图形需求:
绘制一个简洁清晰的小蛇和食物。其中小蛇包括:蛇头、蛇身、蛇尾
小蛇:绘制成圆形的图形,其中蛇头填充为黄色,蛇身和蛇尾填充为蓝色
食物:绘制成方形的图形,食物要求填充为红色
显示需求:
当游戏开始后,小蛇是一直移动的状态,当小蛇吃掉食物以后,蛇身变长同时食物消失,食物重新随机产生,蛇身随着蛇的移动是不断刷新。
按键控制需求:
通过键盘的方向键(WASD或者上下左右)进行蛇的移动,当蛇头碰到边界或者蛇身,游戏结束。
通过按钮实现游戏的暂停、继续、重新开始、结束游戏。
面向对象技术
GUI编程技术
事件处理机制
多线程技术
GUI编程:
Java提供了三个包实现GUI编程
Java.awt提供了绘制图形、填充颜色和字体样式
Javax.swing提供了各种组件(窗体、面板、按钮、文本框等等)
Java.awt.event提供了事件处理机制
多线程技术:
贪吃蛇的移动是持续性的,只要贪吃蛇没有死亡就可以继续移动,这里需要使用多线程技术
主线程
键盘监听线程
状态监听线程
绘制动画线程
事件处理机制:
事件驱动模型三大要素:
事件(Event Object):用户交互行为产生的一种效果(鼠标事件,键盘事件,窗口事件)
事件源(Event Source):触发事件的源头,不同的事件源会触发不同的事件类型
事件监听器(Event listener):负责监听事件源发出的各种事件
事件编程的步骤:
1) 编写一个事件处理类(事件监听者)
2) 事件处理类要实现监听接口 KeyListener
3) 重写事件处理方法
4) 指定事件监听器,注册监听(事件的响应者)
Config类
此类中定义了容器的大小ROWS(行)=22,COLS(列)=35,SPAN(每个矩形的像素)=20
定义了上下左右,定义了判断贪吃蛇存活的标志:isLive,定义了判断游戏暂停和继续的标志:isPause
//常量配置
public class Config {
public static final int ROWS = 22;//行
public static final int COLS = 35;//列
public static final int SPAN = 20;// 每个矩形的像素
public static final String U = "u";// 方向-上
public static final String D = "d";// 方向-下
public static final String L = "l";// 方向-左
public static final String R = "r";// 方向-右
public static boolean isLive = true;// 贪吃蛇是否存活的标识
public static boolean isPause = true;// 继续或者暂停游戏
}
Button类
增加了pause(暂停游戏)、continu(继续游戏)、restart(重新游戏)、exit(退出游戏)按钮,并注册按钮监听,定义了事件响应处理方法
//按钮
public class Button extends JPanel implements ActionListener{
MyPanel myPanel;
JButton pause;// 暂停游戏
JButton continu;// 继续游戏
JButton restart;// 重新游戏
JButton exit;// 退出游戏
public Button(MyPanel myPanel) {
this.myPanel = myPanel;
this.setBounds(0, 440, 706, 60);
pause = new JButton("暂停游戏");
continu = new JButton("继续游戏");
restart = new JButton("重新开始");
exit = new JButton("退出游戏");
this.add(pause);
this.add(continu);
this.add(restart);
this.add(exit);
// 注册按钮监听
pause.addActionListener(this);
continu.addActionListener(this);
restart.addActionListener(this);
exit.addActionListener(this);
}
// ActionEvent:获取事件作用的对象
@Override
public void actionPerformed(ActionEvent e) {
// 监听对象是暂停游戏
if (e.getSource() == pause) {
Config.isPause = false;
}
// 监听对象是继续游戏
if (e.getSource() == continu) {
Config.isPause = true;
// 设置键盘监听焦点
myPanel.setFocusable(true);
myPanel.requestFocus();
}
// 监听对象是重新开始游戏
if (e.getSource() == restart) {
// 1.停掉当前线程
myPanel.snakeThread.stopThread();
// 2.重新生成蛇和食物
Food food = new Food();
myPanel.food = food;
myPanel.snake = new Snake(food);
// 将控制条件还原到初始状态
Config.isPause = true;
Config.isLive = true;
// 3.创建新的线程对象(内部类对象)
SnakeThread snakeThread = myPanel.new SnakeThread();
// 4.启动线程
snakeThread.start();
myPanel.snakeThread = snakeThread;
// 获取键盘焦点
myPanel.setFocusable(true);
myPanel.requestFocus();
}
// 监听对象是退出游戏
if (e.getSource() == exit) {
System.exit(0);
}
}
}
Food类
此类中定义了绘制食物、随机生成食物位置、获取食物坐标的方法
// 食物
public class Food {
// 所在行
private int row;
// 所在列
private int col;
// 构造器
public Food() {
repair();
}
// 绘制食物
public void draw(Graphics g) {
// 设置画笔颜色
g.setColor(Color.RED);
// 填充矩形(x,y,width,height)
g.fillRect(col*Config.SPAN, row*Config.SPAN, Config.SPAN, Config.SPAN);
}
// 随机生成食物的位置
public void repair() {
// 取值范围 0-Config.ROWS
row = new Random().nextInt(Config.ROWS);
col = new Random().nextInt(Config.COLS);
}
// 获取食物坐标
public Rectangle getFoodRec() {
return new Rectangle(col*Config.SPAN, row*Config.SPAN, Config.SPAN, Config.SPAN);
}
}
Snake类
此类中定义了绘制蛇、吃食物、控制蛇的移动等方法
其中链接蛇采用了双向链表的方法
// 贪吃蛇
public class Snake {
Node head;// 蛇头
Node body;// 蛇身
Node tail;// 蛇尾
Food food;// 食物
// 初始化贪吃蛇起始位置以及贪吃蛇的前进方向
public Snake(Food food) {
// 创建蛇头、蛇身、蛇尾节点
head = new Node(7, 13, Config.R);
body = new Node(7, 12, Config.R);
tail = new Node(7, 11, Config.R);
// 绑定蛇头、蛇身、蛇尾之间的关系
head.next = body;
body.pre = head;
body.next = tail;
tail.pre = body;
// 初始化食物对象
this.food = food;
}
// 绘制蛇
public void draw(Graphics g) {
// 蛇有多个节点,需要取出每个节点,然后把每个节点绘制出来
for (Node n = head;n!=null;n = n.next) {
// 调用节点画图的方法
n.draw(g);
}
}
// 贪吃蛇移动
public void move() {
// 1.添加蛇头 2.去除蛇尾 3.吃食物 4.死亡检测
addHead();// 添加蛇头
removeTail();// 去除蛇尾
deadCheck();// 死亡检测
}
// 添加蛇头
public void addHead() {
// 根据蛇头的方向判断
Node node = null;
switch (head.dir) {
case Config.R:
node = new Node(head.row,head.col+1, head.dir);
break;
case Config.L:
node = new Node(head.row, head.col-1, head.dir);
break;
case Config.U:
node = new Node(head.row-1, head.col, head.dir);
break;
case Config.D:
node = new Node(head.row+1, head.col, head.dir);
break;
default:
break;
}
// 绑定节点和蛇头的关系
node.next = head;
head.pre = node;
head = node;// 将新的蛇头节点赋值给原来的蛇头
}
// 去除蛇尾
public void removeTail() {
// 1.把蛇尾设为null,蛇尾的上一个节点的下一个指针为null
tail.pre.next = null;
// 2.把蛇尾的上一个节点赋值给蛇尾
tail = tail.pre;
}
// 控制贪吃蛇移动的方向
public void keyControl(KeyEvent e) {
// 上下左右 通过对键盘的判断来修改蛇头的移动方向,从而控制贪吃蛇的移动方向
switch (e.getKeyCode()) {
case KeyEvent.VK_UP:
if (head.dir.equals(Config.D)) {
break;
}
head.dir = Config.U;
break;
case KeyEvent.VK_DOWN:
if (head.dir.equals(Config.U)) {
break;
}
head.dir = Config.D;
break;
case KeyEvent.VK_LEFT:
if (head.dir.equals(Config.R)) {
break;
}
head.dir = Config.L;
break;
case KeyEvent.VK_RIGHT:
if (head.dir.equals(Config.L)) {
break;
}
head.dir = Config.R;
break;
}
}
/*
* 吃食物
* 1.判断贪吃蛇蛇头坐标和食物坐标是否重合
* 2.重新生成一条新的贪吃蛇(添头不去尾)
* 3.重新随机生成食物
*/
public void eat() {
// 判断两个矩形是否相交(蛇头是否碰到食物)
Rectangle a = getHeadRec();
Rectangle b = food.getFoodRec();
if (a.intersects(b)) {
addHead();// 添加蛇头
food.repair();// 随机生成食物
}
}
//获取蛇头坐标
public Rectangle getHeadRec() {
// 获取蛇头矩形坐标
return new Rectangle(head.col*Config.SPAN, head.row*Config.SPAN, Config.SPAN, Config.SPAN);
}
// 检测贪吃蛇是否死亡
public void deadCheck() {
// 1.蛇头碰到边界
// 行的范围:0-Config.ROWS-1
// 列的范围:0-Config.COLS-1
if (head.row<0||head.col<0||head.row>Config.ROWS-1||head.col>Config.COLS-1) {
// 将贪吃蛇的状态改成死亡
Config.isLive = false;
}
// 2.蛇头不能碰到蛇身
// 遍历蛇身,判断蛇身每一个节点是否和蛇头重合
for (Node n = head.next; n!=null; n = n.next) {
// 判断蛇头的位置和当前蛇身节点的位置是否相同
if (head.row == n.row && head.col == n.col) {
Config.isLive = false;
break;
}
}
}
}
Node类
此类绘制了贪吃蛇的分割节点,判断蛇头并将蛇头绘制成黄色,蛇身绘制成蓝色
// 贪吃蛇分割的节点
public class Node {
int row;// 行
int col;// 列
Node next;// 下一个节点指针
Node pre;// 上一个节点指针
String dir;// 蛇前进的方向
// 构造器:初始化贪吃蛇的位置信息及制定贪吃蛇的前进方向
public Node(int row,int col,String dir) {
this.row = row;
this.col = col;
this.dir = dir;
}
// 绘制节点
public void draw(Graphics g) {
// 如果当前节点的上一个节点为null,则当前节点就是蛇头
if (this.pre == null) {
// 绘制蛇头的颜色为黄色
g.setColor(Color.YELLOW);
}else {
g.setColor(Color.BLUE);
}
g.fillOval(col*Config.SPAN, row*Config.SPAN, Config.SPAN, Config.SPAN);
}
}
Mypanel类
在Mypanel类中定义容器大小背景并启动多线程,在每次绘制容器中调用snake.move、food.draw、snake.draw、snake.eat方法,在贪吃蛇线程中判断蛇的存亡,判断游戏继续或者暂停,实例化KeyListener中的方法
public class MyPanel extends JPanel implements KeyListener{
// 创建食物对象
Food food = new Food();
// 创建一个贪吃蛇对象
Snake snake = new Snake(food);
// 创建线程对象
SnakeThread snakeThread = new SnakeThread();
public MyPanel() {
// 设置容器坐标及大小
this.setBounds(0, 0, 700, 440);
// 设置容器背景色
this.setBackground(Color.PINK);
// 启动线程
snakeThread.start();
// 注册键盘监听器
this.addKeyListener(this);
}
// 绘制容器
@Override
public void paint(Graphics g) {
super.paint(g);
// 设置绘制的颜色
g.setColor(Color.GRAY);
// 绘制横线
for (int i = 0; i < Config.ROWS; i++) {
// 使用当前颜色在点 (x1, y1) 和 (x2, y2) 之间画一条线
g.drawLine(0, Config.SPAN * i, Config.COLS * Config.SPAN, Config.SPAN * i);
}
// 绘制竖线
for (int i = 0; i < Config.COLS; i++) {
g.drawLine(Config.SPAN * i, 0 , Config.SPAN * i , Config.ROWS * Config.SPAN);
}
// 贪吃蛇移动
snake.move();
// 画食物
food.draw(g);
// 画蛇
snake.draw(g);
// 吃食物
snake.eat();
}
// 贪吃蛇的线程
class SnakeThread extends Thread{
boolean flag = true;// 重新开始
@Override
public void run() {
// Config.isLive:判断贪吃蛇是否存活
while (Config.isLive && flag) {
try {
// 当贪吃蛇没有死亡的时候,则继续移动
Thread.sleep(300);// 当前线程休眠0.3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// Config.isPause== true:代表继续游戏
// Config.isPause== false:代表暂停游戏
if (Config.isLive && Config.isPause) {
// 重新绘制图形,具有页面刷新的效果
// 重绘的执行流程 repaint()-->调用awt线程-->update()方法-->paint()
repaint();
}
if (!Config.isLive) {
// 弹出一个结束游戏的对话框
JOptionPane.showMessageDialog(MyPanel.this, "游戏结束");
}
}
}
// 停止线程的方法
public void stopThread() {
flag = false;
}
}
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyPressed(KeyEvent e) {
// 调用贪吃蛇的控制方向方法
snake.keyControl(e);
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
}
MyFrame类
Myframe类中定义窗体信息,主函数调用整个方法
// JFrame图形化界面设计——容器
public class MyFrame extends JFrame {
MyPanel myPanel = new MyPanel();
Button button = new Button(myPanel);
public MyFrame() {
// 设置窗体标题
this.setTitle("贪吃蛇v1.0");
// 设置窗体初始位置及大小
this.setBounds(300, 50, 706, 500);
// 设置当关闭窗口的时候,保证JVM退出
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置布局管理器为null 清空布局
this.setLayout(null);
// 设置此窗体是否可由用户调整大小
this.setResizable(false);
// 添加控件
this.add(myPanel);
// 设置键盘监听焦点
// 设置是否允许获取焦点
myPanel.setFocusable(true);
// 获取焦点
myPanel.requestFocus();
// 添加按钮
this.add(button);
// 显示
this.setVisible(true);
}
public static void main(String[] args) {
new MyFrame();
}
}
整个程序中需要导入的包有swing、awt等