原文: https://zetcode.com/tutorials/javagamestutorial/snake/

在 Java 2D 游戏教程的这一部分中,我们创建一个 Java 贪食蛇游戏克隆。 源代码和图像可以在作者的 Github Java-Snake-Game 存储库中找到。

贪食蛇

贪食蛇是较旧的经典视频游戏。 它最初是在 70 年代后期创建的。 后来它被带到 PC 上。 在这个游戏中,玩家控制蛇。 目的是尽可能多地吃苹果。 蛇每吃一个苹果,它的身体就会长大。 蛇必须避开墙壁和自己的身体。 该游戏有时称为 Nibbles 。

Java 贪食蛇游戏的开发

蛇的每个关节的大小为 10 像素。 蛇由光标键控制。 最初,蛇具有三个关节。 如果游戏结束,则在面板中间显示"Game Over"消息。

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Dimension;
  4. import java.awt.Font;
  5. import java.awt.FontMetrics;
  6. import java.awt.Graphics;
  7. import java.awt.Image;
  8. import java.awt.Toolkit;
  9. import java.awt.event.ActionEvent;
  10. import java.awt.event.ActionListener;
  11. import java.awt.event.KeyAdapter;
  12. import java.awt.event.KeyEvent;
  13. import javax.swing.ImageIcon;
  14. import javax.swing.JPanel;
  15. import javax.swing.Timer;
  16. public class Board extends JPanel implements ActionListener {
  17. private final int B_WIDTH = 300;
  18. private final int B_HEIGHT = 300;
  19. private final int DOT_SIZE = 10;
  20. private final int ALL_DOTS = 900;
  21. private final int RAND_POS = 29;
  22. private final int DELAY = 140;
  23. private final int x[] = new int[ALL_DOTS];
  24. private final int y[] = new int[ALL_DOTS];
  25. private int dots;
  26. private int apple_x;
  27. private int apple_y;
  28. private boolean leftDirection = false;
  29. private boolean rightDirection = true;
  30. private boolean upDirection = false;
  31. private boolean downDirection = false;
  32. private boolean inGame = true;
  33. private Timer timer;
  34. private Image ball;
  35. private Image apple;
  36. private Image head;
  37. public Board() {
  38. initBoard();
  39. }
  40. private void initBoard() {
  41. addKeyListener(new TAdapter());
  42. setBackground(Color.black);
  43. setFocusable(true);
  44. setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
  45. loadImages();
  46. initGame();
  47. }
  48. private void loadImages() {
  49. ImageIcon iid = new ImageIcon("src/resources/dot.png");
  50. ball = iid.getImage();
  51. ImageIcon iia = new ImageIcon("src/resources/apple.png");
  52. apple = iia.getImage();
  53. ImageIcon iih = new ImageIcon("src/resources/head.png");
  54. head = iih.getImage();
  55. }
  56. private void initGame() {
  57. dots = 3;
  58. for (int z = 0; z < dots; z++) {
  59. x[z] = 50 - z * 10;
  60. y[z] = 50;
  61. }
  62. locateApple();
  63. timer = new Timer(DELAY, this);
  64. timer.start();
  65. }
  66. @Override
  67. public void paintComponent(Graphics g) {
  68. super.paintComponent(g);
  69. doDrawing(g);
  70. }
  71. private void doDrawing(Graphics g) {
  72. if (inGame) {
  73. g.drawImage(apple, apple_x, apple_y, this);
  74. for (int z = 0; z < dots; z++) {
  75. if (z == 0) {
  76. g.drawImage(head, x[z], y[z], this);
  77. } else {
  78. g.drawImage(ball, x[z], y[z], this);
  79. }
  80. }
  81. Toolkit.getDefaultToolkit().sync();
  82. } else {
  83. gameOver(g);
  84. }
  85. }
  86. private void gameOver(Graphics g) {
  87. String msg = "Game Over";
  88. Font small = new Font("Helvetica", Font.BOLD, 14);
  89. FontMetrics metr = getFontMetrics(small);
  90. g.setColor(Color.white);
  91. g.setFont(small);
  92. g.drawString(msg, (B_WIDTH - metr.stringWidth(msg)) / 2, B_HEIGHT / 2);
  93. }
  94. private void checkApple() {
  95. if ((x[0] == apple_x) && (y[0] == apple_y)) {
  96. dots++;
  97. locateApple();
  98. }
  99. }
  100. private void move() {
  101. for (int z = dots; z > 0; z--) {
  102. x[z] = x[(z - 1)];
  103. y[z] = y[(z - 1)];
  104. }
  105. if (leftDirection) {
  106. x[0] -= DOT_SIZE;
  107. }
  108. if (rightDirection) {
  109. x[0] += DOT_SIZE;
  110. }
  111. if (upDirection) {
  112. y[0] -= DOT_SIZE;
  113. }
  114. if (downDirection) {
  115. y[0] += DOT_SIZE;
  116. }
  117. }
  118. private void checkCollision() {
  119. for (int z = dots; z > 0; z--) {
  120. if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
  121. inGame = false;
  122. }
  123. }
  124. if (y[0] >= B_HEIGHT) {
  125. inGame = false;
  126. }
  127. if (y[0] < 0) {
  128. inGame = false;
  129. }
  130. if (x[0] >= B_WIDTH) {
  131. inGame = false;
  132. }
  133. if (x[0] < 0) {
  134. inGame = false;
  135. }
  136. if (!inGame) {
  137. timer.stop();
  138. }
  139. }
  140. private void locateApple() {
  141. int r = (int) (Math.random() * RAND_POS);
  142. apple_x = ((r * DOT_SIZE));
  143. r = (int) (Math.random() * RAND_POS);
  144. apple_y = ((r * DOT_SIZE));
  145. }
  146. @Override
  147. public void actionPerformed(ActionEvent e) {
  148. if (inGame) {
  149. checkApple();
  150. checkCollision();
  151. move();
  152. }
  153. repaint();
  154. }
  155. private class TAdapter extends KeyAdapter {
  156. @Override
  157. public void keyPressed(KeyEvent e) {
  158. int key = e.getKeyCode();
  159. if ((key == KeyEvent.VK_LEFT) && (!rightDirection)) {
  160. leftDirection = true;
  161. upDirection = false;
  162. downDirection = false;
  163. }
  164. if ((key == KeyEvent.VK_RIGHT) && (!leftDirection)) {
  165. rightDirection = true;
  166. upDirection = false;
  167. downDirection = false;
  168. }
  169. if ((key == KeyEvent.VK_UP) && (!downDirection)) {
  170. upDirection = true;
  171. rightDirection = false;
  172. leftDirection = false;
  173. }
  174. if ((key == KeyEvent.VK_DOWN) && (!upDirection)) {
  175. downDirection = true;
  176. rightDirection = false;
  177. leftDirection = false;
  178. }
  179. }
  180. }
  181. }

首先,我们将定义游戏中使用的常量。

  1. private final int B_WIDTH = 300;
  2. private final int B_HEIGHT = 300;
  3. private final int DOT_SIZE = 10;
  4. private final int ALL_DOTS = 900;
  5. private final int RAND_POS = 29;
  6. private final int DELAY = 140;

B_WIDTHB_HEIGHT常数确定电路板的大小。 DOT_SIZE是苹果的大小和蛇的点。 ALL_DOTS常数定义了板上可能的最大点数(900 = (300 * 300) / (10 * 10))。 RAND_POS常数用于计算苹果的随机位置。 DELAY常数确定游戏的速度。

  1. private final int x[] = new int[ALL_DOTS];
  2. private final int y[] = new int[ALL_DOTS];

这两个数组存储蛇的所有关节的 x 和 y 坐标。

  1. private void loadImages() {
  2. ImageIcon iid = new ImageIcon("src/resources/dot.png");
  3. ball = iid.getImage();
  4. ImageIcon iia = new ImageIcon("src/resources/apple.png");
  5. apple = iia.getImage();
  6. ImageIcon iih = new ImageIcon("src/resources/head.png");
  7. head = iih.getImage();
  8. }

loadImages()方法中,我们获得了游戏的图像。 ImageIcon类用于显示 PNG 图像。

  1. private void initGame() {
  2. dots = 3;
  3. for (int z = 0; z < dots; z++) {
  4. x[z] = 50 - z * 10;
  5. y[z] = 50;
  6. }
  7. locateApple();
  8. timer = new Timer(DELAY, this);
  9. timer.start();
  10. }

initGame()方法中,我们创建蛇,在板上随机放置一个苹果,然后启动计时器。

  1. private void checkApple() {
  2. if ((x[0] == apple_x) && (y[0] == apple_y)) {
  3. dots++;
  4. locateApple();
  5. }
  6. }

如果苹果与头部碰撞,我们会增加蛇的关节数。 我们称locateApple()方法为随机放置一个新的Apple对象。

move()方法中,我们有游戏的关键算法。 要了解它,请看一下蛇是如何运动的。 我们控制蛇的头。 我们可以使用光标键更改其方向。 其余关节在链上向上移动一个位置。 第二关节移动到第一个关节的位置,第三关节移动到第二个关节的位置,依此类推。

  1. for (int z = dots; z > 0; z--) {
  2. x[z] = x[(z - 1)];
  3. y[z] = y[(z - 1)];
  4. }

该代码将关节向上移动。

  1. if (leftDirection) {
  2. x[0] -= DOT_SIZE;
  3. }

这条线将头向左移动。

checkCollision()方法中,我们确定蛇是否击中了自己或撞墙之一。

  1. for (int z = dots; z > 0; z--) {
  2. if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
  3. inGame = false;
  4. }
  5. }

如果蛇用头撞到其关节之一,则游戏结束。

  1. if (y[0] >= B_HEIGHT) {
  2. inGame = false;
  3. }

如果蛇击中了棋盘的底部,则游戏结束。

Snake.java

  1. package com.zetcode;
  2. import java.awt.EventQueue;
  3. import javax.swing.JFrame;
  4. public class Snake extends JFrame {
  5. public Snake() {
  6. initUI();
  7. }
  8. private void initUI() {
  9. add(new Board());
  10. setResizable(false);
  11. pack();
  12. setTitle("Snake");
  13. setLocationRelativeTo(null);
  14. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  15. }
  16. public static void main(String[] args) {
  17. EventQueue.invokeLater(() -> {
  18. JFrame ex = new Snake();
  19. ex.setVisible(true);
  20. });
  21. }
  22. }

这是主要的类。

  1. setResizable(false);
  2. pack();

setResizable()方法会影响某些平台上JFrame容器的插入。 因此,在pack()方法之前调用它很重要。 否则,蛇的头部与右边界和底边界的碰撞可能无法正常进行。

Java 贪食蛇 - 图1

图:贪食蛇

这是 Java 中的贪食蛇游戏。