Java小游戏——贪吃蛇

Java小游戏之贪吃蛇

系统目标

贪吃蛇是一个益智类游戏,通过本游戏的设计和实现,可以提升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等

运行效果展示

在这里插入图片描述

发布了19 篇原创文章 · 获赞 8 · 访问量 8950

猜你喜欢

转载自blog.csdn.net/qq_44832215/article/details/104091637