首页 > 技术文章 > JavaSE 实战:贪吃蛇游戏

Evan-Gao 2018-09-26 21:06 原文

前言

学习 Java 已经一个月了,作为一个 GameBoy ,梦想之一就是能做出来自己的游戏,于是决定尝试编写贪吃蛇来作为阶段性总结。

经过一天的奋(zhua)战(kuang),终于实现了基本的功能,晚餐愉快地给自己加了鸡腿~

不过万里长城不是一天筑成的,自己的水平还非常有限,想做出合格的游戏还十分困难。这次把代码放出来其实还是蛮羞涩的,毕竟有各种问题,欢迎各路大神批评指正!不过,希望和我一样新学编程的同学们能够找到编程的乐趣从而坚持下去,程序猿永不言败!

总体思路

贪吃蛇作为一个经典游戏,玩法想必是为大家所熟知的。我个人曾经接触过N多个版本,其中最惊艳的是之前老爸(借的)诺基亚手机上的八方向3D科技风贪吃蛇,那时我还在上小学,智能机还没有出世,这个游戏各种华丽的特效让我叹为观止,玩法也独树一帜,既有传统因素,又有探险、收集、竞速等元素在里面。现在应该很难找到了,也不记得具体叫什么名字。。

不过,我还是正视现实老老实实做一个基础简陋款吧。。

游戏规则:

  • 蛇不停移动,过程中可以变换方向,但不得直接变换到相反方向
  • 场上有食物,蛇碰触(吃)到食物就会生长一节
  • 蛇在运动过程中若碰触到边界、障碍或是自己的身体便会死亡,游戏结束
  • 不同的关卡可以设置不同的障碍、不同的移动速度、不同的食物数量,也可以用时间来控制难度

技术选型:

  • 编程语言:JavaSE
  • IDE:eclipse
  • 设计模式:呃开玩笑的并没有设计模式
  • 框架:没用框架纯手打

结构设计:

├──Snake
     ├──src
          ├──com.gx
		  │   ├──GameUtil.java          #游戏工具类
		  │   ├──GameObject.java        #游戏物体父类
		  │   ├──SnakeNode.java         #蛇结点(单元)类
		  │   ├──Food.java              #食物类
		  │   ├──SnakeGameFrame.java    #游戏主窗体类
		  └──images        

P.S. 上面的结构是套用网上的打飞机(真的是打飞机!!!)游戏教程,有很多小游戏也是这种结构。虽然在本项目中感觉有些冗余的部分,但还是值得借鉴的。

主要代码:

1.GameUtil

/**
 * 游戏工具类
 * @author Gao
 * 实现游戏背景和物体的图片资源加载
 */

public class GameUtil {

	private GameUtil() {
		
	}
	
	public static Image getImage(String path) {
		 BufferedImage bi = null;
		 try {
			URL u = GameUtil.class.getClassLoader().getResource(path);
			bi = ImageIO.read(u);
		} catch (IOException e) {
			e.printStackTrace();
		}
		 return bi;
	}
}

2.GameObject

其实这个类我没怎么用上,虽然食物和蛇强行继承了一波,但又重新建了私有变量。。昨天没有意识到,只顾闷着头写了,这里就不改啦,小伙伴们可以把这块优化一下~

/**
 * 游戏物体类
 * @author Gao
 * 构建、绘制游戏物体、碰撞检测
 */
 
public class GameObject {
	
	int width;
	int height;
	int x;
	int y;
	private Image img;
	
	public GameObject(Image img, int x, int y, int width, int height) {
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
		this.img = img;
	}
	
	public GameObject(int x, int y, int width, int height) {
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
	}
	
	public GameObject() {

	}
	
	public void drawSelf(Graphics g) {
		g.drawImage(img, x, y, width, height, null);
	}
	
	public Rectangle getRectangle() {
		return new Rectangle(x, y, width, height);
	}
	
}

3.SnakeNode

继承自GameObject,但其实没用好。

这也是最重要的一个类了。原本我的思路是根据蛇的长度画矩形,但发现转弯的情况十分复杂,于是把蛇分割成一个一个结点(单元),转弯时,每个单元依次获得前面单元的坐标(也是借鉴网上的做法),这样只要控制蛇的头部就可以啦~

/**
 * 蛇结点类
 * @author Gao
 * 蛇的结点(单元)生成以及移动状态判断
 */
 
public class SnakeNode extends GameObject {
	int x;
	int y;
	private int width;
	private int height;
	boolean left, up, right , down, live = true;
	int speed = 20;
	
	public SnakeNode(int x, int y, int width, int height) {
		this.x = x;
		this.y = y;
		this.width = width;
		this.height = height;
	}
	
	public SnakeNode() {
		
	}
	
	public void setLocation(int x, int y) {
		this.x = x;
		this.y = y;
	}

	//在按下一个方向键时,要先把其它方向上的速度清除,不然就叠加啦。而根据规则,如果原来是相反的方向,是不能被清除的
	public void minusDirection(KeyEvent e) {
		switch(e.getKeyCode()) {
		case 37:
			up = false;
			//right = false;
			down = false;
			break;
		case 38:
			left = false;
			right = false;
			//down = false;
			break;
		case 39:
			//left = false;
			up = false;
			down = false;
			break;
		case 40:
			left = false;
			//up = false;
			right = false;
			break;
		}
	}
	
	//根据按下的键给蛇前进方向
	public void addDir(KeyEvent e) {
		switch(e.getKeyCode()) {
		case 37:
			if(!right)
			left = true;
			break;
		case 38:
			if(!down)
			up = true;
			break;
		case 39:
			if(!left)
			right = true;
			break;
		case 40:
			if(!up)
			down = true;
			break;
		}
	}
	
	public void drawBody(Graphics g) {
		g.fillRect(x, y, width, height);
	}
	
	//头部的移动在每次重绘时进行
	public void drawHead(Graphics g) {
		if (left) {
			x -= speed; 
		}
		if (right) {
			x += speed;
		}
		if (up) {
			y -= speed;
		}
		if (down) {
			y += speed;
		}
		g.fillRect(x, y, width, height);
	}
	
	//碰撞检测
	public Rectangle getRectangle() {
		return new Rectangle(x, y, 20, 20);
	}

4.Food

食物类,这个好说。不过依旧没有发挥继承的作用。

/**
 * 食物类
 * @author Gao
 * 在随机位置生成食物
 */
 
public class Food extends GameObject{
	public boolean isEaten = false;
	
	//控制生成的食物不超过屏幕范围,并且能够和蛇相遇(蛇由20*20的矩形组成)
	public Food() {
		x = (int)(Math.random() * 50)*20;
		y = (int)(Math.random() * 30)*20;
	}

	public void draw(Graphics g) {
		Color c = g.getColor();
		g.setColor(Color.GREEN);
		g.fillRect(x, y, 20, 20);
		g.setColor(c);
	}
	
	public Rectangle getRectangle() {
		return new Rectangle(x, y, 20, 20);
	}

	
}

5.SnakeGameFrame

游戏的主窗体类,利用了awt、swing、多线程、事件监听等技术,也是游戏的主体算法部分。

/**
 * 贪吃蛇主窗体
 * @author Gao
 *
 */

public class SnakeGameFrame extends JFrame{
	Image bg = GameUtil.getImage("images/bg.jpg");
	SnakeNode head = null;
	SnakeNode[] body = null;
	Food[] foods = new Food[6];
	private int snakeLength;
	PaintThread paintThread = null;
	
	public SnakeGameFrame() {
		this.setTitle("贪吃蛇 by Gao");
		this.setBounds(300, 300, 1024, 683);
		this.setVisible(true);
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);
	}
	//初始化
	public void initialSnake() {
		int x1 = 180, y1 = 200;
		head = new SnakeNode(200, 200, 20, 20);
		body = new SnakeNode[50];
		snakeLength = 4;
		for(int i = 0;i < snakeLength;i++) {
			body[i] = new SnakeNode(x1, y1, 20, 20);
			x1-=20;
		}
		foods[1] = new Food();
		for(int i=0;i<6;i++) {
			foods[i] = new Food();
		}
		addKeyListener(new KeyMonitor());
	}
	
	public void paint(Graphics g) {
		g.drawImage(bg, 0, 0, 1024, 683, null);
		Color c = g.getColor();
		g.setColor(Color.RED);
		head.drawHead(g);
		g.setColor(Color.CYAN);
		
		//绘制蛇神时判断蛇身是否与蛇头碰撞
		for(int i = 0;i< snakeLength;i++) {
			body[i].drawBody(g);
			if(body[i].getRectangle().intersects(head.getRectangle())) {
				head.live = false;
			}
		}
		
		g.setColor(c);
		
		//判断蛇是否吃到食物
		for(int i=0;i<6;i++) {
			if(foods[i].getRectangle().intersects(head.getRectangle())) {
				snakeLength ++;
				body[snakeLength - 1] = new SnakeNode(body[snakeLength - 2].x, body[snakeLength - 2].y, 20, 20);
				System.out.println("wow");
				foods[i].isEaten = true;
			}
			if(foods[i].isEaten == false)foods[i].draw(g);
		}
	}

	//在线程中控制游戏状态
	class PaintThread extends Thread{
		public void run() {
			while(true) {		
				if(head.live == false) {
					JOptionPane.showMessageDialog(null, "游戏结束,这都能死,下次小心点哦");
					break;
				}
				
				try {	
					for(int i = snakeLength - 1;i > 0;i--) {
						body[i].setLocation(body[i-1].x, body[i-1].y);
					}
					body[0].setLocation(head.x, head.y);
					if(head.x < 0 || head.x > 1004 || head.y < 0 || head.y > 663) {
						head.live = false;
					}
					repaint();
					Thread.sleep(200);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}		
			}
		}
	}
	
	//监听键盘事件,当第一次按键时开始游戏
	class KeyMonitor extends KeyAdapter {
		public void keyPressed(KeyEvent e) {
			head.minusDirection(e);
			head.addDir(e);
			if(paintThread == null) {
				paintThread = new PaintThread();
				paintThread.start();
			}
		}
	}
	
	public static void main(String[] args) {
		SnakeGameFrame snakeGameFrame = new SnakeGameFrame();
		snakeGameFrame.initialSnake();
	}
}

总结

因为只给自己安排了一天时间,所以关卡的设置和计分计时没有完成,对面向对象的思想理解还不够深入,继承等技术使用不熟练,代码也有许多需要完善的地方,做出来的效果也有些丑,不过在过程中还是通过努力解决了一些问题的!下周开始打算学习 JavaEE 的内容了,希望能够顺利!

这次实战的收获如下:

  • 巩固了JavaSE的基本语法
  • 稍微理解了一点程序的结构,写代码也感觉更顺手了
  • 找到了编程的乐趣,感受到了解决问题的喜悦,满足了自我证明的欲望

另外,昨天我把项目上传到了 gayhub github上,感兴趣的小伙伴可以下载下来跑一下(如果不怕麻烦的话文章的代码也可以直接复制),网址如下:

https://github.com/Antabot/Snake-v1.0-

最后,还是欢迎各路大神批评指正!欢迎关注我的博客 Evan-Gao

推荐阅读