贪吃蛇之于程序员就像巴大蝴之于小智
对的,出了新手村就不用了的那种。
里面三个文件,一个是纯打包的jar,一个是能能运行的jar,一个是能运行的exe,我测试没问题,运行不了请检查环境变量和人品。
不过按照习惯,最后还是会把所有代码直接发出来的。
第一次用了下枚举类,还是挺简单的,百度随便搜两篇博客就懂了,用起来也挺智能,最重要的是用1234来表示方向太low了。
像这样:
public enum Orientation {UP, DOWN, LEFT, RIGHT}
之后用起来直接当常量引用就行了(比如Orientation.UP),不过数据类型是Orientation要注意。
我之前一直不敢写这玩意是因为我不知道怎么用格子来表示坐标,直到今天才知道我想多了,格子的左上角就是坐标,剩下的是格子边长的事。
我觉得自己的代码已经挺好看的了,反正比网上某些一个java文件搞定一切的要好一点。
还有一个bug,之前也看到别人有。
描述一下的话就是我模拟贪吃蛇移动的延迟使用的线程睡眠方法,像这样:
Thread.sleep(100)
这带来了一个什么问题呢,就是,虽然我在程序里做了判断,不允许玩家往后退,但是如果你手够快,能在线程睡着的这段时间里连续改两次方向,从结果来说就是往后退了。
举个例子:
蛇正在往右走,你按了方向左,这没有任何作用,因为我加了判断,阻止你这么做。
但是你在100毫秒内先按上,方向就变成了上,但是因为线程还在睡,所以蛇的位置没有更新,这时你又按了左,这样是允许的,因为你是从上变成左,并不是掉头。
结果就是蛇开始往左跑,如果蛇身只有两节,那就是自由掉头,如果大于二,蛇会咬到自己的第三节身体导致游戏失败。
……我也不知道该怎么解决这个bug……以后再说……
就敲着这段字的时间里我发现了一个问题,我是直接实现KeyListener接口的,这个是我抄人家的我错了。
我的做法是做成内部类然后继承KeyAdapter的,直接用接口会多出来两个用不着的方法,太难看了。
但是懒得重新打包,也不想更新版本号了,一会下边发的代码会是新版本的代码。
一个下午加上一个晚上搞定一个贪吃蛇,我还是有点进步的。
1 package snake; 2 3 /** 4 * 枚举类。 5 * 定义了四个方向。 6 * 7 * @author mlxy 8 * @version 1.0 9 */ 10 public enum Orientation { 11 UP, DOWN, LEFT, RIGHT 12 }
1 package snake; 2 3 /** 4 * 运行类。 5 * 内容很简单,初始化框架。 6 * 7 * @author mlxy 8 * @version 1.0 9 */ 10 public class Run { 11 public static void main(String[] args) { 12 new MainFrame(); 13 } 14 }
1 package snake; 2 3 import javax.swing.JFrame; 4 5 /** 6 * 主框架类。 7 * 初始化框架内容和相关属性。 8 * 9 * @author mlxy 10 * @version 1.0 11 */ 12 public class MainFrame extends JFrame { 13 private static final long serialVersionUID = 3693516591938222507L; 14 GamePad gamePad; 15 16 MainFrame() { 17 this.gamePad = new GamePad(); 18 this.add(gamePad); 19 this.setResizable(false); 20 this.pack(); 21 22 this.setTitle("Snake"); 23 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 24 this.setLocationRelativeTo(null); 25 this.setVisible(true); 26 } 27 }
1 package snake; 2 3 import java.awt.Color; 4 import java.awt.Dimension; 5 import java.awt.Graphics; 6 import java.awt.event.KeyAdapter; 7 import java.awt.event.KeyEvent; 8 9 import javax.swing.JPanel; 10 11 /** 12 * 游戏面板类。 13 * 用变量存储了游戏分数,蛇的引用,食物的引用。 14 * 还包括了游戏进行中所需要的所有方法。 15 * 实现了Runnable接口,用以将自身作为一个线程启动。 16 * 包含了一个内部类作为监听器,用以监听玩家的键盘输入。 17 * 18 * @author mlxy 19 * @version 1.0 20 */ 21 public class GamePad extends JPanel implements Runnable { 22 private static final long serialVersionUID = -6102074989461150615L; 23 int point; 24 Snake snake; 25 Food food; 26 27 /** 常量,游戏面板的边长。*/ 28 static int SIDELEN_OF_PAD = 500; 29 /** 常量,一个格子的边长。*/ 30 static int SIDELEN_OF_GRID = 10; 31 32 GamePad() { 33 // 设置偏好大小,+1是为了能看到边界线,因为边界线宽1像素。 34 this.setPreferredSize(new Dimension(SIDELEN_OF_PAD + 1, SIDELEN_OF_PAD + 1)); 35 36 this.setBackground(Color.WHITE); 37 this.addKeyListener(new PlayerInputListener()); 38 39 // 取得焦点,否则无法读取键盘输入。 40 this.setFocusable(true); 41 this.requestFocus(); 42 43 // 初始化三个变量。 44 point = 0; 45 snake = new Snake(); 46 food = new Food(snake); 47 48 // 启动线程。 49 new Thread(this).start(); 50 } 51 52 // 绘制面板内容。 53 @Override 54 public void paintComponent(Graphics g) { 55 super.paintComponent(g); 56 drawBorder(g); 57 drawPoints(g); 58 drawFood(g); 59 drawSnake(g); 60 } 61 62 /** 绘制边界线。*/ 63 void drawBorder(Graphics g) { 64 g.setColor(Color.GREEN); 65 g.drawRect(0, 0, SIDELEN_OF_PAD, SIDELEN_OF_PAD); 66 g.setColor(Color.BLACK); 67 } 68 69 /** 绘制分数字符。*/ 70 void drawPoints(Graphics g) { 71 g.setColor(Color.RED); 72 g.drawString("Points: " + point, 10, 20); 73 g.setColor(Color.BLACK); 74 } 75 76 /** 绘制蛇身。*/ 77 void drawSnake(Graphics g) { 78 g.setColor(Color.BLUE); 79 80 // 以蛇身各节所在格子数乘以每格边长求得坐标并依次绘制。 81 for (int i = 0; i < snake.getLength(); i++) { 82 g.fillRect(snake.getX(i) * SIDELEN_OF_GRID, snake.getY(i) * SIDELEN_OF_GRID, 83 SIDELEN_OF_GRID, SIDELEN_OF_GRID); 84 } 85 g.setColor(Color.BLACK); 86 } 87 88 /** 绘制食物。*/ 89 void drawFood(Graphics g) { 90 g.setColor(Color.MAGENTA); 91 92 // 以食物所在格子数乘以每格边长求得坐标并绘制。 93 g.fillOval(food.getX() * SIDELEN_OF_GRID, food.getY() * SIDELEN_OF_GRID, 94 SIDELEN_OF_GRID, SIDELEN_OF_GRID); 95 } 96 97 /** 向前方移动蛇身。*/ 98 void moveSnake() { 99 //先根据蛇身长度将蛇身顺次移一格。 100 for (int i = snake.getLength() - 1; i > 0; i--) { 101 snake.setX(i, snake.getX(i-1)); 102 snake.setY(i, snake.getY(i-1)); 103 } 104 105 // 根据蛇头朝向决定蛇头出现在哪个方向的格子里。 106 switch (snake.getOrientation()) { 107 case UP: 108 snake.setY(0, snake.getY(0) - 1); 109 break; 110 case DOWN: 111 snake.setY(0, snake.getY(0) + 1); 112 break; 113 case LEFT: 114 snake.setX(0, snake.getX(0) - 1); 115 break; 116 case RIGHT: 117 snake.setX(0, snake.getX(0) + 1); 118 break; 119 default: 120 break; 121 } 122 } 123 124 /** 接收一个方向参数,改变方向。*/ 125 void turn(Orientation orientation) { 126 // 如果输入的方向是后方,则直接跳出方法,蛇不应该后退。 127 switch (orientation) { 128 case UP: 129 if (snake.getOrientation() == Orientation.DOWN) {return;} 130 break; 131 case DOWN: 132 if (snake.getOrientation() == Orientation.UP) {return;} 133 break; 134 case LEFT: 135 if (snake.getOrientation() == Orientation.RIGHT) {return;} 136 break; 137 case RIGHT: 138 if (snake.getOrientation() == Orientation.LEFT) {return;} 139 break; 140 } 141 142 snake.setOrientation(orientation); 143 } 144 145 146 147 /** 进食并改变蛇身长度,之后重新建一个食物。*/ 148 void eat() { 149 snake.setLength(snake.getLength() + 1); 150 point += 100; 151 152 food = new Food(snake); 153 } 154 155 /** 运行函数。*/ 156 public void run() { 157 while (true) { 158 // 进行各项判断之后移动。 159 if (isOutOfBound()) {System.exit(0);} 160 if (isSelfBiting()) {System.exit(0);} 161 if (isOnFood()) {eat();} 162 moveSnake(); 163 164 // 重绘面板并睡眠,睡眠时长随分数增加而缩短,最小20毫秒。 165 repaint(); 166 int sleepTime = 200 - point / 10; 167 if (sleepTime <= 20) {sleepTime = 20;} 168 try { 169 Thread.sleep(sleepTime); 170 } catch (InterruptedException e) { 171 e.printStackTrace(); 172 } 173 } 174 } 175 176 /** 出边界检查。*/ 177 boolean isOutOfBound() { 178 int gridsOneSide = GamePad.SIDELEN_OF_PAD / GamePad.SIDELEN_OF_GRID; 179 if (snake.getX(0) < 0 || snake.getX(0) >= gridsOneSide) { 180 return true; 181 } 182 if (snake.getY(0) < 0 || snake.getY(0) >= gridsOneSide) { 183 return true; 184 } 185 return false; 186 } 187 188 /** 自噬检查。*/ 189 boolean isSelfBiting() { 190 for (int i = 1; i < snake.getLength(); i++) { 191 if (snake.getX(i) == snake.getX(0) && snake.getY(i) == snake.getY(0)) { 192 return true; 193 } 194 } 195 return false; 196 } 197 198 /** 进食检查。*/ 199 boolean isOnFood() { 200 if (snake.getX(0) == food.getX() && snake.getY(0) == food.getY()) { 201 return true; 202 } 203 return false; 204 } 205 206 /** 207 * 按键监听器。 208 * 处理玩家的键盘输入。 209 * 210 * @author mlxy 211 * @version 1.0 212 */ 213 class PlayerInputListener extends KeyAdapter { 214 @Override 215 public void keyPressed(KeyEvent e) { 216 switch (e.getKeyCode()) { 217 case KeyEvent.VK_UP: 218 turn(Orientation.UP); 219 break; 220 case KeyEvent.VK_DOWN: 221 turn(Orientation.DOWN); 222 break; 223 case KeyEvent.VK_LEFT: 224 turn(Orientation.LEFT); 225 break; 226 case KeyEvent.VK_RIGHT: 227 turn(Orientation.RIGHT); 228 break; 229 case KeyEvent.VK_ESCAPE: 230 System.exit(0); 231 break; 232 default: 233 break; 234 } 235 } 236 } 237 }
1 package snake; 2 3 /** 4 * 食物类。 5 * 定义了食物的坐标,内部存储了对蛇的引用以判断生成位置是否在蛇身上。 6 * 7 * @author mlxy 8 * @version 1.0 9 */ 10 public class Food { 11 Snake snake; 12 13 // 食物所在格子数,并非坐标。 14 private int x; 15 private int y; 16 17 Food(Snake snake) { 18 this.snake = snake; 19 20 // 随机初始化食物坐标。 21 int gridsOneSide = GamePad.SIDELEN_OF_PAD / GamePad.SIDELEN_OF_GRID; 22 23 outerWhile: while (true) { 24 x = 1 + (int) (Math.random() * (gridsOneSide - 2)); 25 y = 1 + (int) (Math.random() * (gridsOneSide - 2)); 26 27 // 如果和蛇身重合就重新生成。 28 for (int i = 0; i < snake.getLength(); i++) { 29 if (snake.getX(i) == x && snake.getY(i) == y) { 30 continue outerWhile; 31 } 32 } 33 34 // 生成完成,跳出。 35 break; 36 } 37 } 38 39 public int getX() {return x;} 40 public int getY() {return y;} 41 }
1 package snake; 2 3 /** 4 * 蛇类。 5 * 定义了蛇头的朝向,蛇的长度,以及蛇身各节所在的坐标。 6 * 7 * @author mlxy 8 * @version 1.0 9 */ 10 public class Snake { 11 private Orientation orientation; 12 13 private int length; 14 15 // 蛇身各节所在的格子数,并非坐标。 16 private int[] x; 17 private int[] y; 18 19 Snake() { 20 // 初始化朝向和长度。 21 orientation = Orientation.RIGHT; 22 length = 2; 23 24 // 用游戏面板总格子数初始化数组大小。 25 int numberOfGrid = (int) Math.pow(GamePad.SIDELEN_OF_PAD / GamePad.SIDELEN_OF_GRID, 2); 26 x = new int[numberOfGrid]; 27 y = new int[numberOfGrid]; 28 29 // 初始化最初的蛇身位置。 30 int gridsOneSide = GamePad.SIDELEN_OF_PAD / GamePad.SIDELEN_OF_GRID; 31 x[1] = gridsOneSide / 2 + 1; 32 y[1] = gridsOneSide / 2; 33 x[0] = gridsOneSide / 2; 34 y[0] = gridsOneSide / 2; 35 } 36 37 int getLength() {return length;} 38 void setLength(int newLength) {this.length = newLength;} 39 40 int getX(int index) {return x[index];} 41 int getY(int index) {return y[index];} 42 43 public void setX(int index, int x) {this.x[index] = x;} 44 public void setY(int index, int y) {this.y[index] = y;} 45 46 public Orientation getOrientation() {return orientation;} 47 public void setOrientation(Orientation orientation) {this.orientation = orientation;} 48 }