三月份的时候用Java写了一个贪吃蛇的小游戏,写完的时候颇有成就感,现在在这里做一下总结。
先把需要用到的图片资源放在这里,分别为上、下、左、右方向的蛇头,蛇身,食物,标题,可以通过右击另存为的方式下载:
我们想要得到的游戏效果如下:
刚进入游戏的初始界面为图1,按空格开始游戏,通过上、下、左、右键改变蛇的移动方向,蛇可以穿过边界从另一端出来,吃到食物后多一个蛇身并且食物刷新位置,按空格键会暂停游戏,界面如图2,再按空格重新开始游戏,如果蛇头碰到自己的身子,那么游戏结束,按p键重新开始游戏,界面如图3,右上角的积分系统可有可无,这里出于篇幅考虑,省去积分系统:
个人认为,要想用Java实现这个游戏,首先需要具备一些最基础的GUI的知识,比如JFrame、JPanel的使用,这里就不再赘述;除此之外,还需要实现画图、键盘监听、计时器的功能,这里通过几个小demo展示一下这几个功能是怎么实现的。
· 画图的实现:主要用到JPanel类的paint(Graphics g)方法和ImageIcon类的paintIcon(Component c, Graphics g, int x, int y),代码如下:
1 import javax.swing.*; 2 import java.awt.*; 3 class PaintDemo extends JPanel{ 4 //得到需要画的图片的资源 5 ImageIcon body=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\body.png"); 6 //构造方法 7 public PaintDemo(){ 8 this.setSize(200,200); 9 this.setBackground(Color.white); 10 } 11 //在new PaintDemo()时会自动调用一次paint方法 12 public void paint(Graphics g){ 13 //ImageIcon类的一个方法,以此就能把对应的图片画在指定的位置上 14 body.paintIcon(this,g,100,100); 15 } 16 } 17 public class SimpleTest { 18 public static void main(String[] args) throws Exception { 19 JFrame f=new JFrame("Demo"); 20 //前两个参数决定了弹出框的位置,后两个参数决定了框的宽高 21 f.setBounds(100,150,200,200); 22 //关闭窗口 23 f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 24 //只有把JPanel放在JFrame里才能看得到JPanel 25 f.add(new PaintDemo()); 26 f.setVisible(true); 27 } 28 }
实现效果如图:
这样一来,我们就可以通过画图,一步步得到游戏的界面了。
· 键盘监听的实现:游戏里我们是要通过键盘监听来实现暂停、蛇的移动和重新开始的功能。在这个demo里,我们实现的是,键盘监听一个JPanel,当我们按下↓键时,控制台输出“你往下了”,按下↑键时,控制台输出“你往上了”,代码如下:
1 import javax.swing.*; 2 import java.awt.*; 3 import java.awt.event.*; 4 class KeyDemo extends JPanel implements KeyListener { 5 public KeyDemo(){ 6 this.setSize(200,200); 7 this.setBackground(Color.white); 8 //让键盘事件监听这个JPanel 9 this.addKeyListener(this); 10 //使焦点保持在这个JPanel上,如果焦点不在这上面,键盘事件就不起作用了。 11 this.setFocusable(true); 12 } 13 public void keyPressed(KeyEvent e) { 14 //得到我们按下的键的对应的数字常量 15 int keyCode=e.getKeyCode(); 16 //如果是↑键 17 if (keyCode==KeyEvent.VK_UP){ 18 System.out.println("你往上了"); 19 }else if (keyCode==KeyEvent.VK_DOWN){ 20 System.out.println("你往下了"); 21 } 22 } 23 public void keyTyped(KeyEvent e) {} 24 public void keyReleased(KeyEvent e) {} 25 } 26 public class SimpleTest { 27 public static void main(String[] args) throws Exception { 28 JFrame f=new JFrame("Demo"); 29 f.setBounds(100,150,200,200); 30 f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 31 f.add(new KeyDemo()); 32 f.setVisible(true); 33 } 34 }
实现效果很简单,就是在控制台有对应的输出。
这样一来,我们就可以通过键盘来控制游戏的暂停、重新开始以及蛇的移动了。
· 计时器的实现:单说计时器可能让人很迷糊,计时器跟贪吃蛇有什么关系,难道这个游戏还有时间限制吗?此计时器非彼计时器,这里的计时器实现了一个重要的功能,就是让我们的蛇“动”了起来。其实我们平时看的喜羊羊等动画,它们本身就是一张张的图片,我们之所以觉得他们是“动”的,是因为这些图片切换的实在太快了,我们的肉眼根本无法察觉到喜羊羊的一个动作其实就是几十张图片的快速切换而已。这也引入了一个叫做“帧”的概念,一帧可以理解为一个画面。如果大家有玩过王者荣耀,应该会注意到游戏里有一个叫做“帧率”的东西,帧率就是每秒播放的帧数,当帧率很低,比如一秒只放两张图片时,就会出现大家口中的“卡成PPT”。而我们的贪吃蛇要想动起来,本质也就是通过高频率地刷新画面,产生“动”的感觉。
在这个demo里,我们实现的是在界面上不停的在随机位置画图,主要用的是Timer类和ActionListener接口,具体代码如下:
1 import javax.swing.*; 2 import java.awt.*; 3 import java.awt.event.*; 4 import java.util.Random; 5 class TimerDemo extends JPanel implements ActionListener { 6 ImageIcon food=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\food.png"); 7 Random r=new Random(); 8 //初始位置 9 int x=r.nextInt(500); 10 int y=r.nextInt(500); 11 //创建一个Timer计时器,在调用timer.start()方法后,每100毫秒就会调用一次对应的actionPerformed()方法 12 Timer timer=new Timer(100,this); 13 public TimerDemo(){ 14 this.setSize(500,500); 15 //在new这个JPanel的时候直接启动计时器 16 timer.start(); 17 } 18 public void paint(Graphics g){ 19 food.paintIcon(this,g,x,y); 20 } 21 public void actionPerformed(ActionEvent e) { 22 x=r.nextInt(500); 23 y=r.nextInt(500); 24 //重新调用一次paint()方法 25 repaint(); 26 } 27 } 28 public class SimpleTest { 29 public static void main(String[] args) throws Exception { 30 JFrame f=new JFrame("Demo"); 31 f.setBounds(100,150,500,500); 32 f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 33 f.add(new TimerDemo()); 34 f.setVisible(true); 35 } 36 }
实现效果如图:
这样一来,我们就可以想办法让蛇动起来了。至于蛇为什么吃到食物可以多一节身体,又为什么穿过边界可以从另一端出来,就需要好好思考了。
有了以上的预备知识,我们就可以着手写贪吃蛇的代码了!
大致可以分为以下几个步骤:
1.设计游戏图纸
2.创建窗口JFrame
3.在窗口上添加画布JPanel
4.在画布上添加标题
5.在画布上添加黑色游戏区
6.在游戏区放置静态的蛇
7.加上开始提示
8.让蛇动起来
9.实现游戏暂停
10.实现转向功能
11.添加食物
12.吃掉食物
13.添加死亡条件
14.实现“重新开始”功能
一些要点
①我们需要把贪吃蛇移动的区域想成一个网格,蛇头其实就是在一个个格子之间移动。
②蛇的方向可以通过设置一个direction变量来控制。
③蛇的部位的坐标可以用两个数组来表示,X数组储存部位的横坐标,Y数组储存部位的纵坐标,于是X[0]、Y[0]为蛇头坐标,X[1]、Y[1]为第一节身体的坐标,以此类推。
④蛇的移动其实就是头部不断移动,然后每节身体取代前一节部位,所以具体实现时只需要考虑头的移动,后面身体的移动就是取代前一部位的位置而已。
⑤蛇吃到食物就是蛇头的坐标与食物的坐标重合,然后蛇的长度加一。
⑥蛇死亡就是蛇头与任意一节身体的坐标重合。
⑦通过标志位来判断游戏的各种状态。
⑧蛇移动的速度是由计时器的第一个参数决定的。如果参数为1000,那么是每秒调用一次,蛇移动的很慢;如果参数为10,那么是每10毫秒调用一次,即每秒调用100次,这样一来蛇移动的速度将非常快。
我的设计图纸:
我的代码实现,具体信息在注释中:
1 package 贪吃蛇; 2 import java.awt.*; 3 import java.awt.event.*; 4 import java.util.Random; 5 import javax.swing.*; 6 class GamePanel extends JPanel implements KeyListener,ActionListener{ 7 //需要用到的图片 8 ImageIcon title=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\title.jpg"); 9 ImageIcon right=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\right.png"); 10 ImageIcon left=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\left.png"); 11 ImageIcon up=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\up.png"); 12 ImageIcon down=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\down.png"); 13 ImageIcon body=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\body.png"); 14 ImageIcon food=new ImageIcon("C:\\Users\\apple\\Desktop\\pic\\food.png"); 15 16 // 计时器,调用start()后,每100ms调用一次actionPerformed()方法,以此达到蛇移动的效果 17 Timer timer = new Timer(100,this); 18 19 //蛇的一些初始信息 20 //count用来让paint()判断是否要在初始位置画蛇 21 int count; 22 //蛇的长度 23 int length; 24 //蛇的部位的坐标数组,数组的长度是蛇的最大长度,不能太小也不用太大,随意就可以了。 25 int[] snakeX=new int[100]; 26 int[] snakeY=new int[100]; 27 //移动的方向 28 String direction; 29 //用方法来初始化蛇的信息,当我们第一次进入游戏或者游戏失败选择重新开始时会调用这个方法 30 public void snakeSetup() { 31 count=1; 32 //初始长度为3 33 length=3; 34 //蛇头初始坐标 35 snakeX[0]=75; 36 snakeY[0]=75; 37 //第一节身体的初始坐标 38 snakeX[1]=50; 39 snakeY[1]=75; 40 //第二节身体的初始坐标 41 snakeX[2]=25; 42 snakeY[2]=75; 43 //蛇头默认的方向 44 direction="right"; 45 } 46 47 //游戏状态 48 boolean isStart=false; 49 boolean isFail=false; 50 51 //食物状态,刚进游戏的时候是没有食物的,就相当于被吃掉了的状态 52 boolean isEaten=true; 53 //食物坐标 54 int foodX; 55 int foodY; 56 Random random=new Random(); 57 //每次paint都会调用一次这个方法,如果食物是被吃掉的状态,则给出新的食物的坐标 58 public void foodPoint() { 59 if(isEaten) { 60 isEaten=!(isEaten); 61 foodX=25+25*random.nextInt(34); 62 foodY=75+25*random.nextInt(24); 63 } 64 } 65 66 //画布 67 public GamePanel() { 68 this.setLayout(null); 69 this.setBackground(Color.WHITE); 70 //初始化蛇的信息 71 this.snakeSetup(); 72 //键盘监听 73 this.addKeyListener(this); 74 //设置焦点 75 this.setFocusable(true); 76 //直接启动timer,蛇之所以不直接移动是因为actionPerformed()中有标志位控制是否做事情。 77 timer.start(); 78 } 79 80 //把需要的组件画上去 81 public void paint(Graphics g) { 82 //每次调用paint使count+1,因为游戏初始自动会调用一次,所以游戏初始的时候到这里count就是2了,以此来判断是否蛇是否是初始化的。 83 count++; 84 //画标题 85 title.paintIcon(this, g, 25, 11); 86 //画游戏面板,一个实心长方形 87 g.fillRect(25, 75, 850, 600); 88 //如果没有食物,给出新的食物的坐标,有的话还是先前的坐标 89 this.foodPoint(); 90 //把食物画上去 91 food.paintIcon(this, g, foodX, foodY); 92 //判断游戏状态 93 //如果是失败状态,显示“失败” 94 if(isFail) { 95 g.setColor(Color.RED); 96 g.setFont(new Font("微软雅黑",Font.BOLD,40)); 97 g.drawString("失败!按p键重新开始!", 300, 200); 98 //如果不是失败也不在游戏中,就是刚进游戏or暂停状态,显示提示信息 99 }else if(!(isStart)) { 100 g.setColor(Color.WHITE); 101 g.setFont(new Font("微软雅黑",Font.BOLD,40)); 102 g.drawString("按空格开始游戏", 300, 200); 103 } 104 //蛇头的位置和方向,direction是根据键盘的输入改变的,up就画向上的蛇头,down就画向下的蛇头,至于移动的方向在actionPerformed()中决定 105 if(direction.equals("right")) { 106 right.paintIcon(this, g, snakeX[0], snakeY[0]); 107 }else if(direction.equals("left")) { 108 left.paintIcon(this, g, snakeX[0], snakeY[0]); 109 }else if(direction.equals("up")) { 110 up.paintIcon(this, g, snakeX[0], snakeY[0]); 111 }else if(direction.equals("down")) { 112 down.paintIcon(this, g, snakeX[0], snakeY[0]); 113 } 114 //蛇身的位置 115 //如果是初始游戏,即count是2,画在初始位置 116 if(count==2) { 117 body.paintIcon(this, g, snakeX[1], snakeY[1]); 118 body.paintIcon(this, g, snakeX[2], snakeY[2]); 119 //否则,根据实时的坐标画,坐标在actionPerformed()中不断地改变 120 }else { 121 for(int i=1;i<this.length;i++) { 122 body.paintIcon(this, g, snakeX[i], snakeY[i]); 123 } 124 } 125 //吃到食物,改变食物的状态,并且长度加一 126 if(snakeX[0]==foodX && snakeY[0]==foodY) { 127 isEaten=true; 128 this.length++; 129 } 130 //失败判定,改变游戏状态为失败,并提示失败信息 131 for(int i=1;i<this.length;i++) { 132 if(snakeX[0]==snakeX[i] && snakeY[0]==snakeY[i]) { 133 isFail=true; 134 g.setColor(Color.RED); 135 g.setFont(new Font("微软雅黑",Font.BOLD,40)); 136 g.drawString("失败!按p键重新开始!", 300, 200); 137 } 138 } 139 } 140 @Override 141 public void keyPressed(KeyEvent e) { 142 //得到你按的是什么键 143 int keyCode=e.getKeyCode(); 144 //如果是空格并且游戏不是失败状态,说明是要开始or暂停游戏 145 if(keyCode==KeyEvent.VK_SPACE && !(isFail)) { 146 isStart=!(isStart); 147 repaint(); 148 //如果是方向键,改变direction信息 149 }else if(keyCode==KeyEvent.VK_UP) { 150 direction="up"; 151 }else if(keyCode==KeyEvent.VK_DOWN) { 152 direction="down"; 153 }else if(keyCode==KeyEvent.VK_LEFT) { 154 direction="left"; 155 }else if(keyCode==KeyEvent.VK_RIGHT) { 156 direction="right"; 157 //如果是p并且游戏是失败的,改变游戏状态,初始化蛇的信息 158 }else if(keyCode==KeyEvent.VK_P && isFail) { 159 isFail=false; 160 this.snakeSetup(); 161 repaint(); 162 } 163 } 164 @Override 165 public void actionPerformed(ActionEvent e) { 166 //如果是在游戏中,会不断地改变坐标,让后一节身体取代前一节部位的位置。 167 //如果不在游戏中或者失败了,这就是个空方法,蛇的坐标不改变,蛇也就不动了。 168 if(isStart && !(isFail)){ 169 for(int i=this.length-1;i>0;i--) { 170 snakeX[i]=snakeX[i-1]; 171 snakeY[i]=snakeY[i-1]; 172 } 173 //如果向右,那么头每次向右移动一格,纵坐标不变,一旦越界,从另一头出来。 174 if(direction.equals("right")) { 175 snakeX[0] +=25; 176 if(snakeX[0]>850) { 177 snakeX[0]=75; 178 } 179 }else if(direction.equals("left")) { 180 snakeX[0] -=25; 181 if(snakeX[0]<25) { 182 snakeX[0]=850; 183 } 184 }else if(direction.equals("up")) { 185 snakeY[0] -=25; 186 if(snakeY[0]<75) { 187 snakeY[0]=650; 188 } 189 }else if(direction.equals("down")) { 190 snakeY[0] +=25; 191 if(snakeY[0]>650) { 192 snakeY[0]=75; 193 } 194 } 195 repaint(); 196 } 197 } 198 @Override 199 public void keyTyped(KeyEvent e) {} 200 @Override 201 public void keyReleased(KeyEvent e) {} 202 } 203 public class 贪吃蛇 { 204 public static void main(String[] args) { 205 JFrame f=new JFrame("贪吃蛇"); 206 f.setBounds(100,150,900,720); 207 f.setResizable(false); 208 f.add(new GamePanel()); 209 f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 210 f.setVisible(true); 211 } 212 }
个人认为这个游戏的核心在于要通过标志位来控制游戏的几种状态,同时还要通过这几个标志位来决定这个蛇是否移动;除此之外,蛇的移动一定得注意到其实只需要控制头的位置,后面的身体只要不断的取代前一节部位的位置就可以了。
当然这只是贪吃蛇的最基本实现,其实还可以加进去很多功能,除了我这里省略了的积分系统,还可以引入等级系统,lv1的时候蛇移动的速度慢,lv5的时候移动快,这通过改变timer的第一个参数的大小就能实现了。更高级的话,甚至可以联系到数据库,搞一个存档功能,联系到网络编程,搞一个联机功能……如果有朝一日我能深谙各种高端的操作了,或许我会回到这最初的起点,写一个超豪华版的贪吃蛇。