Java实现贪吃蛇游戏
相信很多人大学时候都学过Java这门语言吧,这门课一般最后有一个大作业,就是用Java写一个小软件,比方说简单的聊天软件、贪吃蛇、计算器等等。这个游戏就是我自己刚学习Java的时候写的,当然刚开始都是边百度边写,很多功能当时就是只考虑了如何实现,而没有考虑这样实现合不合理。所以一开始的代码是写的非常烂的,主要是当时自己也才开始学习Java不久,所以对Java的很多语法和特性并不熟悉。后来自己闲得无聊的时候又对程序更改过很多次了,其中有过对程序结构的调整,接口的重新设计,以及很多不合理代码的改写。所以放在这里,也是给那些或许需要学习Java的同学一点参考吧。
作为以前诺基亚手机上的经典游戏,贪吃蛇和俄罗斯方块一样,都曾经在我们的童年给我们带来了很多乐趣。世间万物斗转星移,诺基亚曾经作为手机业的龙头老大,现如今也一步步走向衰落,被收购,最后都退出手机业务了,而贪吃蛇这款游戏也基本上没人玩了,甚至在新一代人的印象中都已毫无记忆。但是,作为Java初学者,这款游戏其实还是非常适合去自己实现一下的。毕竟贪吃蛇游戏规则非常简单,游戏界面也非常简单,你不需要去花费时间去设计游戏的玩法和游戏UI如何设计,也不需要去为这个游戏需要很多素材。在一定程度上也能锻炼初学者的编程能力,熟悉Java的语法。
初版(2017年7月15号)游戏界面如下:
初版代码了大约是700行。事实上初版的界面和代码我后来都做了一定的修改,因为刚开始的代码实在太烂了,我都不好意思发出来误导新人。
第一版整体界面很简单,因为当时就是想先快速实现出来看看效果,能玩就行。这一版本的蛇没有用图片,而是直接用Java自带的函数画出来的。所以游戏启动非常快,不需要加载图片资源。
设置里能把周围彩色的边框去掉,加上去只是让玩家知道边界在哪。同时网格线也能去掉。这一版玩家只有碰到周围的墙才会死,碰到自己是不会死的。游戏界面按空格键可以暂停和开始,按ESC键可以重新开始。
这一版游戏界面的宽度(横向的格子数)和高度(纵向的格子数)分别通过Scene类里面的 width 和 height 的值来指定,我现在界面上是设置成了width= 20,height=20。
这里需要提醒大家一点的是:二维数组中的一个坐标(x, y)和 Java界面上显示的坐标(y, x)刚好是相反的,所以在产生坐标的时候不要搞反了。
最新版本(2020年12月15号)游戏界面如下:
这一版本是第6版,代码量大约是2000行。
可以看到,第6版游戏界面丰富了许多,具体包括:
1. 游戏界面是背景图片,而不是之前的纯白色,并且能在设置菜单里面进行更换。
2. 贪吃蛇也不是方块了,头部和身体结点都是图片,并且都可以在设置菜单进行更换。
3. 界面上有障碍物(墙)了。玩家碰到障碍物是会死的。这一版本玩家碰到自己也会死的。
4. 界面上食物有好几种类别,不同类别食物对应的分数不一样,出现概率也不一样,每次界面上的食物被吃光时会产生新的食物。
5. 玩家蛇可以发射子弹来击毁前面的障碍物,通过吃到特定的食物来获取子弹。
6. 玩家蛇的速度是可以在设置里面设置的,默认是“行走”速度,和AI蛇一样。
7. 加入了AI蛇,AI蛇每次都是找到距离自己最近的食物,再去吃掉它,图中的黑色箭头是它找到的到距离自己最近的食物的路径。
8. 由于AI蛇用的是静态寻路算法,就是它找到自己的目标后是不会察觉周围路况变化的,为了防止玩家蛇刻意挡在AI蛇前面造成AI蛇死亡,目前的规则是AI蛇可以穿过玩家蛇,但是玩家蛇的头不能碰到AI蛇。
9. 玩家蛇在吃掉了AI蛇的目标食物后,AI蛇会自动寻找下一个距离自己最近的食物去吃掉。
10. AI蛇有可能走进死胡同里或者自己把自己缠死,这样的话游戏也会自动结束,其他情况下AI蛇一般不会死。
11. 玩家蛇的速度默认和AI蛇保持一致(行走),玩家蛇若和AI蛇保持相同的速度,相同时间内,长度可能没有AI蛇长,如果玩家蛇速度比AI快,并且每次都去抢AI的食物,AI就比不过玩家。
12. 游戏地图是可以通过map文件夹下的txt文件进行设置的,详细设置规则可以参考GitHub。
第5版中存在的一些功能在第6版中被我去掉了,包括以下部分:
1. 长按方向键可以加速
2. 障碍物每隔一定时间会自动移动和变化形状
3. 食物每隔一段时间会自动刷新
由于代码比较多,为了方便阅读,游戏各个版本的代码和相关文件都放到了我的 👉 Github 👈 上了,有需要的同学可以去看一下😉
下面我简单介绍一下游戏的主要实现思路(以第一版为例):
首先我们必须清楚的一点是:Java是纯面向对象编程(OOP)的一门语言,所以程序中所有的功能都必须封装到某个类中去。
那么我们就需要去思考我们这个游戏需要用到哪些类。事实上,这个过程并不简单,因为我们要去思考游戏中哪些实体会对应一个类,而这个类又需要封装好哪些功能,不同的类之间有哪些通信需求(数据交换),我们怎么让它们能够简单地访问到自己所需要的数据。这些都是值得自己去思考的。
事实上,这个问题我一开始(2017年7月份)思考的也不是很清楚,有些压根就没考虑到,所以随着自己的代码越来越多,各种不合理的问题就会逐渐暴露出来,比如:抽象层次不合理,接口设计不合理。
大家肯定能想到的是,我们的贪吃蛇肯定要是一个单独的类。它内部封装好了一些属于它的数据和方法。同时,它也需要暴露一些接口给外界调用。
例如:贪吃蛇需要记录它当前的方向,长度,各个身体结点坐标等等。
于是我们首先需要去写一个snake类。我第一版中snake类内容如下:
1 package xjx; 2 3 import java.util.LinkedList; 4 import java.util.Deque; 5 import java.util.Random; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.ScheduledExecutorService; 8 import java.util.concurrent.TimeUnit; 9 10 public class Snake { 11 private Scene GameUI;//母窗体,即游戏主界面 12 public Direction direction = Direction.RIGHT;//蛇当前前进的方向,初始化默认向右移动 13 private Deque<Coordinate> body = new LinkedList<>();//用于描述蛇身体节点的数组,保存蛇身体各个节点的坐标 14 private ScheduledExecutorService executor;//刷新线程 15 private Coordinate food;//食物坐标 16 17 public Snake(Scene GameUI){ 18 this.GameUI = GameUI; 19 Coordinate head = new Coordinate(0, 0);//初始化头部在(0,0)位置 20 body.addFirst(head); 21 produceFood(); 22 run(); 23 } 24 25 public Coordinate randomCoor(){ 26 int rows = GameUI.height, cols = GameUI.width; 27 Random rand = new Random(); 28 Coordinate res; 29 int x = rand.nextInt(cols-1); 30 int y = rand.nextInt(rows-1); 31 32 while(true) { 33 boolean tag = false; 34 for(Coordinate coor : body){ 35 if(x == coor.y && y == coor.x){ 36 x = rand.nextInt(cols-1); 37 y = rand.nextInt(rows-1); 38 tag = true; 39 break; 40 } 41 } 42 43 if(!tag){ 44 break; 45 } 46 } 47 res = new Coordinate(x, y); 48 return res; 49 } 50 51 //蛇身体移动 52 public void move(){ 53 Coordinate head, next_coor = new Coordinate(0,0); 54 if(direction == Direction.UP){ 55 head = body.getFirst();//获取头部 56 next_coor = new Coordinate(head.x,head.y - 1);//头部向上移动一个单位后的坐标 57 } else if(direction == Direction.DOWN){ 58 head = body.getFirst();//获取头部 59 next_coor = new Coordinate(head.x,head.y + 1);//头部向下移动一个单位后的坐标 60 } else if(direction == Direction.LEFT){ 61 head = body.getFirst();//获取头部 62 next_coor = new Coordinate(head.x - 1,head.y);//头部向左移动一个单位后的坐标 63 } else if(direction == Direction.RIGHT){ 64 head = body.getFirst();//获取头部 65 next_coor = new Coordinate(head.x + 1,head.y);//头部向右移动一个单位后的坐标 66 } 67 68 if(checkDeath(next_coor)) {//判断下一步是否死亡 69 new Music("music//over.wav").start(); 70 GameUI.quit = true; 71 GameUI.pause = true; 72 GameUI.die = true; 73 GameUI.repaint(); 74 } else { 75 Coordinate next_node = new Coordinate(next_coor); 76 body.addFirst(next_node);//添头 77 if(!checkEat(next_coor)) {//没吃到食物就去尾,否则不用去掉,因为添加的头刚好是吃到一个食物后增长的一节 78 body.pollLast();//去尾 79 }else{ 80 new Music("music//eat.wav").start(); 81 GameUI.updateLength(body.size()); 82 produceFood(); 83 } 84 } 85 } 86 87 //判断一个坐标位置是否是蛇死亡的位置 88 public boolean checkDeath(Coordinate coor){ 89 int rows = GameUI.height, cols = GameUI.width; 90 return coor.x < 0 || coor.x >= cols || coor.y < 0 || coor.y >= rows; 91 } 92 93 public boolean checkEat(Coordinate coor){ 94 return food.x == coor.x && food.y == coor.y; 95 } 96 97 public Deque<Coordinate> getBodyCoors(){ 98 return body; 99 } 100 101 public Coordinate getFoodCoor(){ 102 return food; 103 } 104 105 public void produceFood(){ 106 food = randomCoor(); 107 } 108 109 public void show(){ 110 GameUI.repaint(); 111 } 112 113 public void quit(){ 114 executor.shutdownNow();//退出线程 115 } 116 117 public void run(){ 118 executor = Executors.newSingleThreadScheduledExecutor(); 119 int speed = 500;//用于描述蛇移动速度的变量,其实是作为蛇刷新线程的时间用的 120 executor.scheduleAtFixedRate(() -> { 121 if (!GameUI.pause && !GameUI.quit) { 122 move(); 123 show(); 124 } 125 }, 0, speed, TimeUnit.MILLISECONDS); 126 } 127 }
这里面有几个重要的部分:
private Deque<Coordinate> body = new LinkedList<>();//用于描述蛇身体节点的数组,保存蛇身体各个节点的坐标
body这个队列保存了我们这条蛇每一个结点的坐标,这个 Coordinate 类比较简单,就是封装了x坐标和y坐标的值,因为无论是贪吃蛇,还是食物,或者是障碍物,其实都只是一个坐标,这个和它长什么样子,如何显示是没关系的,所以我们需要准备好这个后面会经常用到的数据结构。
1 //坐标数据结构,用于表示蛇身体,食物,障碍物的坐标 2 //这里的坐标用方块的序号表示,实际显示时再换成屏幕真实坐标(即像素点) 3 public class Coordinate { 4 public int x,y; 5 6 public Coordinate(int x0,int y0){ 7 x = x0; 8 y = y0; 9 } 10 11 public Coordinate(Coordinate temp){ 12 x = temp.x; 13 y = temp.y; 14 } 15 }
1 public void run(){ 2 executor = Executors.newSingleThreadScheduledExecutor(); 3 int speed = 500;//用于描述蛇移动速度的变量,其实是作为蛇刷新线程的时间用的 4 executor.scheduleAtFixedRate(() -> { 5 if (!GameUI.pause && !GameUI.quit) { 6 move(); 7 show(); 8 } 9 }, 0, speed, TimeUnit.MILLISECONDS); 10 }
然后就是这个run方法,这个run方法是干嘛的呢?
我们知道,贪吃蛇在我们不对它进行控制的时候它自己也是会朝着之前的方向移动的,那么自己进行移动如何实现呢?
没错,就是通过一个线程进行实现,简单来说,就是每隔500毫秒执行一次内部的move()和show()。
这个线程我之前是通过下面这种方式实现的
1 public void run(){ 2 while(!quit){ 3 try { 4 Thread.sleep(500);//每隔500毫秒刷新一次 5 } catch (InterruptedException e) { 6 e.printStackTrace(); 7 } 8 9 move(); 10 show(); 11 } 12 }
后来我去网上搜了一下,是说不建议在循环里面去使用Thread.sleep(500),具体原因大家可以去网上搜一下。
然后这个move函数里面的内容就是每次刷新的时候贪吃蛇往前走一步,如果吃到了食物,身体就增加一节,然后还要判断一下是不是撞墙死了。
show函数自然是刷新游戏界面了,这个就跟你是如何显示贪吃蛇有关了,如果你是用绘图的方式显示贪吃蛇,那么你就直接调用主界面的repaint函数就行了,如果是贴图的方式,那么你可能要在界面上增加一张图片或者删除一张图片。
贪吃蛇这个类是不是很简单,其实主要就是通过一个线程每隔一段时间来刷新一下蛇的坐标,然后再加一些判断,再重新显示一下就行了。
然后再讲一下游戏中另外一个重要的类,Scene
用过unity开发游戏的人都知道,unity中有一个叫做场景(Scene)的概念,一个游戏场景可以简单认为是游戏中各个游戏物体(unity里面称之为gameObject)(例如这里的贪吃蛇、食物和墙)进行交互的场所。
于是我们这里自然也需要建立一个场景类,事实上,我最开始没有建这个类的,最开始就建立了一个snake类,后来随着游戏内容增多,发现很多并不属于snake的内容也放倒了snake这个类当中了,例如界面显示、各种UI相关的数据,显然是不应该放到snake类当中的,所以我们需要额外新建一个Scene类,它的内容如下:
1 package xjx; 2 3 import java.awt.*; 4 import java.awt.event.*; 5 import java.util.Deque; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.ScheduledExecutorService; 8 import java.util.concurrent.TimeUnit; 9 import javax.swing.*; 10 11 public class Scene extends JFrame{ 12 private final Font f = new Font("微软雅黑",Font.PLAIN,15); 13 private final Font f2 = new Font("微软雅黑",Font.PLAIN,12); 14 private JPanel paintPanel;//画板,画线条用的 15 private final JLabel label = new JLabel("当前长度:"); 16 private final JLabel label2 = new JLabel("所花时间:"); 17 private final JLabel Length = new JLabel("1"); 18 private final JLabel Time = new JLabel(""); 19 private Timer timer; 20 public boolean pause = false; 21 public boolean quit = false; 22 public boolean die = false; 23 private boolean show_padding = true; 24 private boolean show_grid = true; 25 public final int pixel_per_unit = 22; //每个网格的像素数目 26 public final int pixel_rightBar = 110; //右边信息栏的宽度(像素) 27 public final int padding = 5; //内边框宽度 28 public final int width = 20; 29 public final int height = 20; 30 private Snake snake; 31 32 public void restart(){//重新开始游戏 33 quit = true; 34 Length.setText("1"); 35 Time.setText(""); 36 37 snake.quit(); 38 snake = null; 39 snake = new Snake(this); 40 41 timer.reset(); 42 43 die = false; 44 quit = false; 45 pause = false; 46 47 System.out.println("\nGame restart..."); 48 } 49 50 public void updateLength(int length){ 51 Length.setText(""+length); 52 } 53 54 //通过方格序号返回其横坐标 55 public int getPixel(int i, int padding, int pixels_per_unit) { 56 return 1+padding+i*pixels_per_unit; 57 } 58 59 public void initUI(){ 60 String lookAndFeel = UIManager.getSystemLookAndFeelClassName(); 61 try { 62 UIManager.setLookAndFeel(lookAndFeel); 63 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e1) { 64 e1.printStackTrace(); 65 } 66 67 Image img = Toolkit.getDefaultToolkit().getImage("image//title.png");//窗口图标 68 setIconImage(img); 69 setTitle("Snake"); 70 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 71 setSize(width*pixel_per_unit+pixel_rightBar, height * pixel_per_unit + 75); 72 setResizable(false); 73 setLayout(null); 74 setLocationRelativeTo(null); 75 76 paintPanel = new JPanel(){ 77 //绘制界面的函数 78 public void paint(Graphics g1){ 79 super.paint(g1); 80 Graphics2D g = (Graphics2D) g1; 81 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); 82 g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,RenderingHints.VALUE_STROKE_NORMALIZE); 83 84 //边框线 85 if(show_padding){ 86 g.setPaint(new GradientPaint(115,135,Color.CYAN,230,135,Color.MAGENTA,true)); 87 g.setStroke( new BasicStroke(4,BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL)); 88 g.drawRect(2, 2, padding*2-4+width*pixel_per_unit, height*pixel_per_unit+6); 89 } 90 91 //网格线 92 if(show_grid) { 93 for(int i = 0; i <= width; i++) { 94 g.setStroke( new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 95 3.5f, new float[] { 15, 10, }, 0f));//虚线 96 g.setColor(Color.black); 97 g.drawLine(padding+i*pixel_per_unit, padding, 98 padding+i*pixel_per_unit,padding+height*pixel_per_unit);//画竖线 99 } 100 101 for(int i = 0;i <= height; i++){ 102 g.drawLine(padding,padding+i*pixel_per_unit, 103 padding+width*pixel_per_unit,padding+i*22);//画横线 104 } 105 } 106 107 //食物 108 Coordinate food = snake.getFoodCoor(); 109 g.setColor(Color.green); 110 g.fillOval(getPixel(food.x, padding, pixel_per_unit), 111 getPixel(food.y, padding, pixel_per_unit), 20, 20); 112 113 //头部 114 Deque<Coordinate> body = snake.getBodyCoors(); 115 Coordinate head = body.getFirst(); 116 g.setColor(Color.red); 117 g.fillRoundRect(getPixel(head.x, padding, pixel_per_unit), 118 getPixel(head.y, padding, pixel_per_unit), 20, 20, 10, 10); 119 120 //身体 121 g.setPaint(new GradientPaint(115,135,Color.CYAN,230,135,Color.MAGENTA,true)); 122 for (Coordinate coor : body){ 123 if(head.x == coor.x && head.y == coor.y) continue; 124 g.fillRoundRect(getPixel(coor.x, padding, pixel_per_unit), 125 getPixel(coor.y, padding, pixel_per_unit), 20, 20, 10, 10); 126 } 127 128 //显示死亡信息 129 if(die) { 130 g.setFont(new Font("微软雅黑",Font.BOLD | Font.ITALIC,30)); 131 g.setColor(Color.BLACK); 132 g.setStroke( new BasicStroke(10,BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL)); 133 134 int x = this.getWidth()/2, y = this.getHeight()/2; 135 g.drawString("Sorry, you die", x-350, y-50); 136 g.drawString("Press ESC to restart", x-350, y+50); 137 } 138 } 139 }; 140 paintPanel.setOpaque(false); 141 paintPanel.setBounds(0, 0, 900, 480); 142 add(paintPanel); 143 144 int info_x = padding*3 + width*pixel_per_unit; 145 add(label);label.setBounds(info_x, 10, 80, 20);label.setFont(f);label.setForeground(Color.black); 146 add(Length);Length.setBounds(info_x, 35, 80, 20);Length.setFont(f);Length.setForeground(Color.black); 147 add(label2);label2.setBounds(info_x, 70, 80, 20);label2.setFont(f);label2.setForeground(Color.black); 148 add(Time);Time.setBounds(info_x, 95, 80, 20);Time.setFont(f);Time.setForeground(Color.black); 149 150 //菜单栏 151 JMenuBar bar = new JMenuBar();bar.setBackground(Color.white);setJMenuBar(bar); 152 JMenu Settings = new JMenu("设置");Settings.setFont(f);bar.add(Settings); 153 JMenu Help = new JMenu("帮助");Help.setFont(f);bar.add(Help); 154 JMenu About = new JMenu("关于");About.setFont(f);bar.add(About); 155 JMenuItem remove_net= new JMenuItem("移除网格");remove_net.setFont(f2);Settings.add(remove_net); 156 JMenuItem remove_padding= new JMenuItem("移除边框");remove_padding.setFont(f2);Settings.add(remove_padding); 157 JMenuItem help = new JMenuItem("Guide...");help.setFont(f2);Help.add(help); 158 JMenuItem about = new JMenuItem("About...");about.setFont(f2);About.add(about); 159 160 //监听器 161 this.addKeyListener(new MyKeyListener()); 162 remove_net.addActionListener(e -> { 163 if(!show_grid) { 164 show_grid = true; 165 remove_net.setText("移除网格"); 166 } else { 167 show_grid = false; 168 remove_net.setText("显示网格"); 169 } 170 paintPanel.repaint(); 171 }); 172 remove_padding.addActionListener(e -> { 173 if(!show_padding) { 174 show_padding = true; 175 remove_padding.setText("移除边框"); 176 } else { 177 show_padding = false; 178 remove_padding.setText("显示边框"); 179 } 180 paintPanel.repaint(); 181 }); 182 about.addActionListener(e -> new About()); 183 help.addActionListener(e -> new Help()); 184 } 185 186 public void run(){ 187 snake = new Snake(this); 188 setFocusable(true); 189 setVisible(true); 190 timer = new Timer(); 191 } 192 193 public static void main(String[] args) { 194 System.out.println("Application starting..."); 195 Scene game = new Scene(); //初始化游戏场景 196 game.initUI(); //初始化游戏界面 197 game.run(); //开始游戏 198 System.out.println("Game start..."); 199 } 200 201 private class Timer{ 202 //计时器类,负责计时,调用方法,new Timer(); 然后主界面就开始显示计时 203 private int hour = 0; 204 private int min = 0; 205 private int sec = 0; 206 207 public Timer(){ 208 this.run(); 209 } 210 211 public void run() { 212 ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 213 executor.scheduleAtFixedRate(() -> { 214 if (!quit && !pause) { 215 sec +=1 ; 216 if(sec >= 60){ 217 sec = 0; 218 min +=1 ; 219 } 220 if(min>=60){ 221 min=0; 222 hour+=1; 223 } 224 showTime(); 225 } 226 }, 0, 1000, TimeUnit.MILLISECONDS); 227 } 228 229 public void reset() { 230 hour = 0; 231 min = 0; 232 sec = 0; 233 } 234 235 private void showTime(){ 236 String strTime; 237 if(hour < 10) strTime = "0"+hour+":"; 238 else strTime = ""+hour+":"; 239 240 if(min < 10) strTime = strTime+"0"+min+":"; 241 else strTime =strTime+ ""+min+":"; 242 243 if(sec < 10) strTime = strTime+"0"+sec; 244 else strTime = strTime+""+sec; 245 246 //在窗体上设置显示时间 247 Time.setText(strTime); 248 } 249 } 250 251 private class MyKeyListener implements KeyListener{ 252 @Override 253 public void keyPressed(KeyEvent e) { 254 int key = e.getKeyCode(); 255 Direction direction = snake.direction; 256 257 if(key == KeyEvent.VK_RIGHT || key == KeyEvent.VK_D) { //向右 258 if(!quit && direction != Direction.LEFT) { 259 snake.direction = Direction.RIGHT; 260 } 261 } else if(key == KeyEvent.VK_LEFT || key == KeyEvent.VK_A) { //向左 262 if(!quit && direction != Direction.RIGHT) { 263 snake.direction = Direction.LEFT; 264 } 265 } else if(key == KeyEvent.VK_UP || key == KeyEvent.VK_W) { //向上 266 if(!quit && direction != Direction.DOWN) { 267 snake.direction = Direction.UP; 268 } 269 } else if(key == KeyEvent.VK_DOWN || key == KeyEvent.VK_S) { //向下 270 if(!quit && direction != Direction.UP) { 271 snake.direction = Direction.DOWN; 272 } 273 } else if(key == KeyEvent.VK_ESCAPE) { //重新开始 274 restart(); 275 } else if(key == KeyEvent.VK_SPACE) { 276 if(!pause) {//暂停 277 pause = true; 278 System.out.println("暂停..."); 279 } else {//开始 280 pause = false; 281 System.out.println("开始..."); 282 } 283 } 284 } 285 286 @Override 287 public void keyReleased(KeyEvent e) { 288 // TODO Auto-generated method stub 289 } 290 291 @Override 292 public void keyTyped(KeyEvent e) { 293 // TODO Auto-generated method stub 294 } 295 } 296 }
其主要功能就是为了游戏UI绘制和各种信息显示以及监听键盘事件。
注意,这个Scene类是所有游戏对象交互的场所,于是游戏中所有的游戏对象在初始化的时候都需要传入这个信息,而且最好设计为只需要传入这个场景类一个参数即可,所以现在再回去看我们snake类的构造函数
1 public Snake(Scene GameUI){ 2 this.GameUI = GameUI; 3 Coordinate head = new Coordinate(0, 0);//初始化头部在(0,0)位置 4 body.addFirst(head); 5 produceFood(); 6 run(); 7 }
现在清楚了这个 Scene GameUI 参数是干嘛用的了吧。
当然,游戏中还有其他很多细节之处需要我们去思考和注意。当时讲到这里游戏的总体思路已经很清晰了。
后面第6版中我新加了一个AI蛇,所以只需要当成是在游戏场景中新加了一个游戏物体,这样的话就可以不更改之前的程序。
1 food = new Foodset(this); 2 if(gameMode == 0) snake = new PlayerSnake(this); 3 else if(gameMode == 1) ai = new AISnake(this); 4 else if(gameMode == 2) { 5 snake = new PlayerSnake(this); 6 ai = new AISnake(this); 7 }
这里的this指的都是当前游戏场景。
还要注意的是Scene是所有游戏对象进行交互的场所,或者说是它们通信的媒介,比方说snake要获取ai的信息是要通过Scene,而不是直接能获取到的,因为snake类和ai类都只能访问到scene中的数据。