完整作品见链接:JAVA贪吃蛇大作战大作业_Java贪吃蛇如何实现向左不能向右资源-CSDN文库
猫猫大作战技术报告
本项目的编译环境为jdk-19,用到的编译器为Eclipse,采用图形化编程,基于JAVA的Swing库进行界面的绘制,基于Timer类进行动态任务执行。
本项目包括两个页面:游戏规则介绍页面和游戏界面。游戏开始时,首先展示游戏规则介绍页面,玩家阅读规则后按下Enter键即可开始游戏。游戏开始后,跳转到游戏界面,玩家通过按下空格键正式开始游戏。在游戏中,玩家将遇到小鱼干、炸弹和电脑NPC。玩家需要通过吃掉电脑NPC的身体,使其长度减少到小于3,以取得胜利。同时,玩家还可以通过吃小鱼干来增加自己的长度和速度,增强胜利的能力。每局游戏结束后,会进行计分和最高纪录的统计。玩家可以按下空格键重新开始无限次对决,也可以按下Esc键结束整个游戏,并记录最高得分。
其中游戏界面的初始界面(按下空格键可暂停或开始),运行效果如下:
其中,玩家初始状态方向向右,停留在左上角;电脑NPC初始状态方向向左,停留在右下角;界面上的“Press the space bar to start”提示按下空格键开始;玩家活动界面上还有5个小鱼干和1个炸弹;最右侧一栏是当前的状态栏,包含玩家及电脑蛇长度及速度、比分、最高分的相关信息。
本游戏采取玩家与电脑对决的形式进行,采用得分制,可无限循环畅玩。简易规则界面(同时也是本游戏的游戏规则介绍页面,按下Enter键开始),具体展示如下:
本游戏通过上下左右的按键来控制玩家黑色猫猫的移动方向,电脑蛇是随机移动的,猫猫可穿墙移动。
单局游戏结束的判断条件:若其中一方蛇的长度小于3,则判定游戏结束,本游戏结束共3个状态,若是玩家(黑蛇)长度小于3,则判定玩家失败,电脑得分加一;若是电脑(黄蛇)长度小于3,则判定为玩家胜利,玩家得分加一;若玩家与电脑头相撞,则判定为平局,双方都不加得分。示意图如下:
玩家与电脑可以通过互相吃对方的身体使得对方的长度小于3而将对方杀死,其中,关于玩家吃电脑身体的细节图像化展示如下(电脑吃玩家也同理):
玩家或者电脑在吃到小鱼干时候,长度会加一,同时速度和分数也会对应增加,一个小鱼干增加速度1,分数10。具体示意图如下:
玩家或者电脑在吃到炸弹时候,长度会减一,同时速度和分数也会对应减少,一个小鱼干减少速度1,分数10。同时,被炸到后的蛇会陷入短暂晕眩状态,不能动弹,也增加了被其他蛇吃掉的危险性,具体示意图如下:
同时,本游戏可通过按空格键实现一局游戏的开始与暂停,按下Esc键即可实现游戏的结束。
本游戏共设计了三个类,包括Snake(游戏启动类)、 RuleForm(游戏规则介绍界面类)、Snake_Panel(游戏运行类)。
1、 Snake(游戏启动类)
该类通过Java的Swing库中的Jframe类,实现对窗口的创建与设计,并规定窗口的大小(1115*780),同时通过调用Snake_Panel类实现游戏的开始。
2、 RuleForm(游戏规则介绍界面类)
该类通过加载实现设计好的关于游戏规则的界面的图片并添加到页面,实现游戏规则的显示。
3、 Snake_Panel(游戏启动类)
该类是本项目的核心部分,实现了界面调用及各个功能的实现,还包括背景音乐及音效的设置,具体实现展现在后文。
本文将从以下界面跳转、界面暂停、蛇的移动、蛇的变速、蛇的对抗、蛇的捕食、蛇的晕眩、蛇游戏背景音乐及、数据保存九个方面对该项目进行一个展示。具体流程图绘制如下:
本文就Snake_Panel类的代码实现进行一个详细讲解,具体如下:
1、我们定义一个结构体Coordinate来存储每一个对象(包含猫头、猫身、小鱼干、炸弹)的坐标信息。对应代码如下:
public class Coordinate { public int x, y; } |
2、我们基于结构体Coordinate定义玩家蛇和电脑蛇各个部位的坐标列表及食物的坐标列表,还有炸弹的坐标。对应变量声明如下:
ArrayList ArrayList ArrayList |
3、我们定义ImageIcon类型变量用于存储即将需要用到的图片。对应变量声明格式为“ImageIcon+Name”,具体变量名对应如下:
变量名 |
解释 |
变量名 |
解释 |
Rule |
游戏规则介绍界面 |
down |
玩家蛇方向向下的蛇头 |
title |
游戏标题 |
left |
玩家蛇方向向左的蛇头 |
state |
状态栏标题 |
right |
玩家蛇方向向右的蛇头 |
fish |
食物 |
enemy_up |
电脑蛇方向向上的蛇头 |
bomb |
炸弹 |
enemy_down |
电脑蛇方向向下的蛇头 |
bo |
玩家蛇的身体 |
enemy_left |
电脑蛇方向向左的蛇头 |
up |
玩家蛇方向向上的蛇头 |
enemy_right |
电脑蛇方向向右的蛇头 |
4、我们定义boolean类型变量用于表示当前的状态,对应变量声明格式为“boolean+Name”,具体变量名对应如下:
变量名 |
解释 |
isRuleEnd |
游戏规则是否阅读完毕 |
isStart |
游戏是否开始 |
isFailed |
本局游戏是否失败 |
isWined |
本局游戏是否胜利 |
isEqual |
本局游戏是否平局 |
isBomb |
玩家蛇是否被眩晕 |
enemy_isBomb |
电脑蛇是否被眩晕 |
5、我们定义Clip类型变量用于表示背景音乐及音效,对应变量声明格式为“Clip+Name”,具体变量名对应如下:
变量名 |
解释 |
bgm |
背景音乐 |
eatbgm |
吃到食物的音效 |
bombbgm |
吃到炸弹的音效 |
6、我们定义Random类型变量rand用于生成随机数。
7、我们定义int类型的变量表示各个参数,具体变量名对应如下:
变量名 |
解释 |
变量名 |
解释 |
BlackValue |
玩家比分 |
score |
玩家得分 |
YellowValue |
电脑比分 |
speedValue |
玩家速度 |
maxValue |
玩家最高得分 |
enemy_speedValue |
电脑速度 |
initSpeed |
玩家速度上限 |
enemy_snakeLen |
电脑长度 |
enemy_initSpeed |
电脑速度上限 |
bBomb |
玩家晕眩计时 |
vd |
用于随机变换方向 |
enemy_bBomb |
电脑晕眩计时 |
foodNum |
食物数量 |
bombTime |
晕眩时间 |
snakeLen |
蛇的长度 |
Snake_Panel的构造函数
public Snake_Panel() { loadImage();//加载图片的封装函数 initSnake();//初始化玩家蛇 enemy_initSnake();//初始化电脑蛇 this.setFocusable(true);//可获取键盘事件 this.addKeyListener(this);//添加键盘监听器 timer.start();//开始游戏运行 loadbgm();//加载音乐及音效 enemyTimer.start();//开始敌人蛇的运行 } |
本文通过paintComponent实现界面的绘制,同时其对应的repaint()函数可实现对界面的刷新。由于代码量过大,本文仅对核心部分进行介绍。
1、设置背景颜色
this.setBackground(new Color(100,100,100)); |
2、设置颜色
g.setColor(new Color(224,224,224)); |
3、设置添加的位置(x:25,y:125)及宽高(width:850,Height:600)
g.fillRect(25,125,850,600); |
4、设置字体(arial)及大小(30)
g.setFont(new Font("arial",Font.BOLD,30)); |
5、添加文字,并设置其添加的坐标
g.drawString("BlackCat",900,145); |
6、蛇的绘制,分成两部分:蛇头的绘制、蛇身的绘制。由于玩家和敌人蛇的绘制方式一样,对于敌人蛇的绘制讲解省略。
蛇头的绘制(下标为1),其中,U、D、L、R分别对应上、下、左、右的蛇头方向,通过switch-case对代码进行简化。
switch(direction) { case "U":up.paintIcon(this, g, coordinate.x, coordinate.y);break; case "D":down.paintIcon(this, g, coordinate.x, coordinate.y);break; case "L":left.paintIcon(this, g, coordinate.x, coordinate.y);break; case "R":right.paintIcon(this, g, coordinate.x, coordinate.y);break; } |
蛇身的绘制(从1开始),通过循环遍历蛇身体的每一个坐标进行绘制
for(int i = 1;i < snakeLen; i++) { coordinate = snakeBody.get(i); bo.paintIcon(this, g, coordinate.x, coordinate.y); } |
7、食物的绘制(本游戏设置默认食物的数量为5)
for (int i = 0; i < fishList.size();i++) { coordinate = fishList.get(i);//获取食物的坐标 fish.paintIcon(this, g, coordinate.x, coordinate.y);//根据食物的坐标绘制食物 } |
8、绘制炸弹
bomb.paintIcon(this, g, newBomb.x, newBomb.y); |
9、空白键提示文字绘制
if(!isStart)//isStart控制游戏的开始状态 { //提示信息 g.setColor(Color.WHITE);//设置颜色 g.setFont(new Font("arial",Font.BOLD,40));//设置字体,字体大小40 g.drawString("Press the space bar to start",180,300); } |
10、游戏胜负及平局的判断,由于代码相似,这里仅展示游戏失败的代码,每次游戏结束都会更新一次胜负比分及玩家最高得分。
if(isFailed) { YellowValue++;//敌人胜利,得分加一 g.setColor(Color.RED);//设置字体颜色为红色 g.setFont(new Font("arial",Font.BOLD,50));//设置字体及字体大小 g.drawString("Game Over",300,300);//设置显示的文字及位置 if(maxValue < score) maxValue = score;//更新玩家最高得分 } … |
11、跳转界面,其中isRuleEnd表示玩家看规则是否看完的状态,若玩家看完了,其值为1,否则,其值为0。Rule即笔者绘制的游戏规则介绍界面
if(!isRuleEnd) { Rule.paintIcon(this, g, -45, 0); } |
由于食物与炸弹的位置更新同理,本文仅对食物的位置更新进行详细介绍,其中,游戏界面的长为850,宽为600,其二维直角坐标系的绘制如下:
本游戏界面中的每个小方块的长宽均为25,故实际的大小为(33*19),每个小方块左上角的位置表示该小方块的坐标,为保证生成的坐标信息不与小方块的摆放错位,即保证生成随机数可以在小方块的坐标集合中,我们通过方块映射的方式进行实现,具体示意图如下:
接着将被吃掉的食物的坐标更新为生成的新坐标,并移除原先的旧坐标,具体代码如下:
Coordinate newFish = new Coordinate(); newFish.x = 25 + 25 * rand.nextInt(33);//生成随机坐标 newFish.y = 125 + 25 * rand.nextInt(19); //生成随机坐标 fishList.add(newFish);//将生成的新食物加入当前食物列表 if(i!=-1) fishList.remove(i); //将吃掉的食物移除 |
为实现游戏可以无限次重新开局,我们需要设置一个初始化的函数,对游戏参数初始化为开局的状态(玩家蛇头初始方向设置为右,位于左上角,电脑蛇头初始方向为左,位于右下角,并随机生成若干个食物及一个炸弹)。这里仅展示蛇各个部位身体坐标初始化:
for(int i = 0; i < snakeLen; i++) { Coordinate coordinate = new Coordinate(); coordinate.y = 125; coordinate.x = 100 - i * 25; snakeBody.add(coordinate); } |
通过键盘监听器,我们对各个按键对应的事件进行设置,为提高代码的美观度及清晰度,我们同样采用switch-case的方式列出各个按键对应的功能:
首先我们通过如下代码获取用户按下的按键信息:
int KeyCode = e.getKeyCode(); |
Enter键:对应游戏的开局,将游戏规则阅读状态更新为1,即阅读完毕,并刷新画面进入游戏运行界面,同时读取之前保存的数据,显示历史最高分:
case KeyEvent.VK_ENTER:isRuleEnd = true;repaint();readData();break; |
空格键:根据状态控制游戏的开始与暂停,同时伴随着音乐的开始与暂停。
case KeyEvent.VK_SPACE: if(isFailed || isWined || isEqual) { isFailed = false;isWined = false; isEqual = false;initSnake();enemy_initSnake(); } else isStart = !isStart;repaint(); if(isStart) playbgm(); else stopbgm(); break; |
Esc键:保存数据,显示最高得分,并退出游戏。
case KeyEvent.VK_ESCAPE: frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 200);//设置提示框大小 int option = JOptionPane.showOptionDialog(frame, "是否退出且保存数据?", "确认退出",JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESS AGE, null,new String[]{"取消", "确定"}, "取消"); if (option == JOptionPane.YES_OPTION) { return; } else if (option == JOptionPane.NO_OPTION) { saveData(); System.exit(0); } frame.setVisible(true); break; |
上下左右的按键控制方向
case KeyEvent.VK_UP:if(direction != "D") direction = "U";break; case KeyEvent.VK_DOWN:if(direction != "U") direction = "D";break; case KeyEvent.VK_LEFT:if(direction != "R") direction = "L";break; case KeyEvent.VK_RIGHT:if(direction != "L") direction = "R";break; |
这是本项目最为核心的代码,我们将分别从游戏状态的控制,蛇头吃到食物、炸弹及另一方蛇身对应变化的实现,音乐的加载及播放停止,数据的保存及读取等操作的代码,具体如下:
首先需对当前的游戏状态进行判断,若玩家规则阅读完毕(isRuleEnd),且是游戏开局状态(isStart),同时不是游戏的胜负状态(isEqual),则执行事件
if(isRuleEnd && isStart && !isFailed && !isWined && !isEqual) {…} |
蛇头吃到食物,若蛇头与食物的坐标重合,则播放吃到食物的音效,同时增加蛇的长度和速度,并同时更新吃掉的食物的位置。
for (int i = 0; i < fishList.size();i++) { fish_coordinate = fishList.get(i); if(Math.abs(snakeHead.x - fish_coordinate.x) < 25 && Math.abs(snakeHead.y - fish_coordinate.y) < 25) { //播放吃到食物的音效 eatbgm.stop(); eatbgm.setFramePosition(0); eatbgm.start(); //随机生成食物新坐标 Coordinate coordinate = new Coordinate(); coordinate.y = snakeBody.get(snakeLen - 1).y; coordinate.x = snakeBody.get(snakeLen - 1).x; snakeBody.add(coordinate); snakeLen ++; //蛇长度加1 updateFish(i);// 更新吃掉的食物的位置 updateSpeed();//更新蛇的速度 } } |
蛇头吃到食物,若蛇头与炸弹的坐标重合,则播放碰到炸弹的音效,同时减少蛇的长度和速度,并更新吃掉的炸弹的位置,同时更新玩家蛇的状态为被轰炸,即晕眩状态,当bBomb等待到设定的晕眩时间时,才可以继续移动,由于碰到与碰到食物的代码重复,这里仅展示移动身体的判断:
if(!isBomb || bBomb == bombTime) |
蛇的移动,与蛇绘制同理,分成两部分,分别为蛇身动及蛇头动,蛇身就是跟随着前一个身体节点移动:
for(int i = snakeLen - 1; i > 0; i--) { snakeBody.get(i).x = snakeBody.get(i-1).x; snakeBody.get(i).y = snakeBody.get(i-1).y; } |
蛇头的移动,根据不同的方向控制移动,同时,若蛇头碰到一方边界,则将其更新为该边界对应的另一方边界,实现蛇的“穿越”:
switch(direction) { case "U":snakeHead.y -=25;if(snakeHead.y < 125) snakeHead.y = 700;break; case "D":snakeHead.y +=25;if(snakeHead.y > 700) snakeHead.y = 125;break; case "L":snakeHead.x -=25;if(snakeHead.x < 25) snakeHead.x = 850;break; case "R":snakeHead.x +=25;if(snakeHead.x > 850) snakeHead.x = 25;break; } |
蛇的截断,当一方蛇头碰到另一方的身体时,另一方的身体则进行截断,己方身体则增长对应长度。实现方式即遍历电脑蛇的每一个节点,若发现有节点与玩家头重合,则进行截断操作,并对应更新蛇的长度、速度及分数。
for(int i=1; i < enemy_snakeLen; i++) { if(Math.abs(snakeHead.x - enemy_snakeBody.get(i).x) < 25 && Math.abs(snakeHead.y - enemy_snakeBody.get(i).y) < 25) { for(int j = 1;j < enemy_snakeLen - i + 1;j++) { Coordinate coordinate = new Coordinate(); // 创建Coordinate对象 coordinate.y = snakeBody.get(snakeLen - 1).y; coordinate.x = snakeBody.get(snakeLen - 1).x; snakeBody.add(coordinate); snakeLen ++; score += 10;//一个长度对应10分 } for(int j = enemy_snakeLen - 1;j > i - 1;j--) enemy_snakeBody.remove(j); enemy_snakeLen = i - 1; eatbgm.start(); updateSpeed(); } } |
胜负判断,若是蛇头与蛇头碰撞,则判定为平局,若玩家蛇长度小于3,则判定为失败,若电脑蛇长度小于3,则判定为胜利,同时,每次判定都会对比分进行统计。
for(int i=1; i < enemy_snakeLen; i++) if(Math.abs(snakeHead.x - enemy_snakeBody.get(0).x) < 25 && Math.abs(snakeHead.y - enemy_snakeBody.get(0).y) < 25) { isEqual = true; } else if(snakeLen < 3) isFailed = true; else if(enemy_snakeLen < 3) isWined = true; |
速度更新,注意timer.setDelay里的参数是间隔,越长速度越慢。
speedValue = 2*(snakeLen - 2); timer.setDelay(initSpeed - speedValue); |
加载音乐,并打开音乐,为后续音乐的播放做准备:
bgm = AudioSystem.getClip(); InputStream is = this.getClass().getClassLoader().getResourceAsStream("sounds/b gm.wav"); //转音频(注意转jar时,利用BufferedInputStream先生成一个缓存区) AudioInputStream ais = AudioSystem.getAudioInputStream(new BufferedInputStream(is)); bgm.open(ais);//打开 |
播放音乐
bgm.loop(Clip.LOOP_CONTINUOUSLY); |
停止音乐
bgm.stop(); |
保存数据,对话框提示
try (BufferedWriter writer = new BufferedWriter(new FileWriter("data.txt"))) { writer.write("BlackCat\n"+"SnakeLen: " + snakeLen); … JOptionPane.showMessageDialog(null, "数据已保存到文件", "提示", JOptionPane.INFORMATION_MESSAGE); } catch (IOException e) { JOptionPane.showMessageDialog(null, "保存数据时出现错误"+ e.getMessage(), "提示", JOptionPane.INFORMATION_MESSAGE); } |
读取数据,更新历史最高分
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { String data; while ((data = br.readLine()) != null) { if (data.startsWith("maxValue")) { String[] value = data.split(": "); if (value.length == 2) { maxValue = Integer.parseInt(value[1]); } } } } catch (IOException e) { e.printStackTrace(); } |
加载图片
is = getClass().getClassLoader().getResourceAsStream("images/Rule.png"); Rule = new ImageIcon(ImageIO.read(is)); |
通过系统的学习本学期的JAVA课程,我运用所学,历时一周,成功的完成了本次的大作业。在项目编写的开始,我就想好要与别的同学做的不同,别人都是直接贪吃蛇大作战,都是蛇的移动,千篇一律,故我在页面设计时就动了心思,自己设计界面和“蛇”的样式,在用户观感上先胜一筹。
在大作业完成的过程中,也遇到过许多困难,比如如何实现两条蛇不同速度的运动,如何仅让一方蛇晕眩停留,如何实现界面的跳转等问题,最终查阅相关资料及转换设计思路,成功实现了我想表达的功能。
由于刚开始我仅设置了一个时间Timer来控制全局的运行,所以很难实现两条蛇的不同速度的运动,它们总是会保持相同的速度,同时,这也对我晕眩状态的实现起了阻碍。当时查阅资料也无所收获,网上对于不同蛇速度的实现代码存在欠缺,大部分贪吃蛇的代码都很简单,故我决定自己想想办法,然后决定尝试再顶一个Timer类型的变量来控制电脑蛇,还真就被我实现了。
不过晕眩还是存在问题,于是我换了一种思路,不再从停止当前游戏运行的状态思考,而是从控制蛇不能移动的方向思考,故我设置了一个晕眩时间及用于对晕眩时间进行计数的变量,一旦蛇进入晕眩状态,就停止移动,然后开始计数,计数达到晕眩时间之后蛇才可以继续移动。这便成功实现了我想要的功能。
最后就是界面跳转的部分,我发现直接从Snake游戏启动类对页面进行更换之后,游戏就动不了了,所以我又定义了一个状态变量,用于表示用户是否读完游戏规则,若读完,按下Enter键再实现跳转,要注意页面的刷新。
通过本次项目的编写,我对于JAVA的各种细节有了更深的理解,也因为自行独立完成了该项目获得了满满的成就感。我发现写代码真的会上瘾,在完成一个功能后总会又有新的想法,然后就会不断完善,不过这也需要编写代码时要给之后的改善留一个改善空间,如尽量用变量代替常量参数。
本项目可拓展功能还有很多,比如现在我的食物数量设置的是5,那么我们能不能设置多种模式,比如简单版,将其设置成10,困难版将其设置成3,这种;现在我就设置了两种食物,碰到炸弹晕眩,碰到食物加长加速,那么能不能再增加一些新的类型,比如吃到后会减速,或者蛇的长度成双倍,这些都是简单且可以实现的功能。当然,我现在只设置了一个电脑NPC,再未来可以多设置几条蛇,设置还可以设置多个玩家,玩家的装扮也可以变……
当然,更高级一点的话,我现在对电脑NPC设置的随机移动,可不可以加一个算法,算出哪种路径更容易将玩家杀死,或者哪种路径更容易吃到食物……