JavaSE 实战:贪吃蛇游戏
前言
学习 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