Java 贪吃蛇
最后修改于 2023 年 1 月 10 日
在本部分 Java 2D 游戏教程中,我们将创建一个 Java 贪吃蛇游戏克隆。源代码和图片可以在作者的 Github Java-Snake-Game 仓库中找到。
贪吃蛇
贪吃蛇 是一款经典的早期视频游戏。它最早出现在 70 年代末。后来被移植到 PC 上。在这款游戏中,玩家控制一条蛇。目标是尽可能多地吃苹果。每次蛇吃到苹果,它的身体就会变长。蛇必须避开墙壁和自己的身体。这款游戏有时也被称为 Nibbles。
Java 贪吃蛇游戏开发
蛇的每个关节的大小是 10 像素。蛇用光标键控制。最初,蛇有三个关节。如果游戏结束,"Game Over" 消息将显示在游戏板的中间。
package com.zetcode; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.Timer; public class Board extends JPanel implements ActionListener { private final int B_WIDTH = 300; private final int B_HEIGHT = 300; private final int DOT_SIZE = 10; private final int ALL_DOTS = 900; private final int RAND_POS = 29; private final int DELAY = 140; private final int x[] = new int[ALL_DOTS]; private final int y[] = new int[ALL_DOTS]; private int dots; private int apple_x; private int apple_y; private boolean leftDirection = false; private boolean rightDirection = true; private boolean upDirection = false; private boolean downDirection = false; private boolean inGame = true; private Timer timer; private Image ball; private Image apple; private Image head; public Board() { initBoard(); } private void initBoard() { addKeyListener(new TAdapter()); setBackground(Color.black); setFocusable(true); setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT)); loadImages(); initGame(); } private void loadImages() { ImageIcon iid = new ImageIcon("src/resources/dot.png"); ball = iid.getImage(); ImageIcon iia = new ImageIcon("src/resources/apple.png"); apple = iia.getImage(); ImageIcon iih = new ImageIcon("src/resources/head.png"); head = iih.getImage(); } private void initGame() { dots = 3; for (int z = 0; z < dots; z++) { x[z] = 50 - z * 10; y[z] = 50; } locateApple(); timer = new Timer(DELAY, this); timer.start(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } private void doDrawing(Graphics g) { if (inGame) { g.drawImage(apple, apple_x, apple_y, this); for (int z = 0; z < dots; z++) { if (z == 0) { g.drawImage(head, x[z], y[z], this); } else { g.drawImage(ball, x[z], y[z], this); } } Toolkit.getDefaultToolkit().sync(); } else { gameOver(g); } } private void gameOver(Graphics g) { String msg = "Game Over"; Font small = new Font("Helvetica", Font.BOLD, 14); FontMetrics metr = getFontMetrics(small); g.setColor(Color.white); g.setFont(small); g.drawString(msg, (B_WIDTH - metr.stringWidth(msg)) / 2, B_HEIGHT / 2); } private void checkApple() { if ((x[0] == apple_x) && (y[0] == apple_y)) { dots++; locateApple(); } } private void move() { for (int z = dots; z > 0; z--) { x[z] = x[(z - 1)]; y[z] = y[(z - 1)]; } if (leftDirection) { x[0] -= DOT_SIZE; } if (rightDirection) { x[0] += DOT_SIZE; } if (upDirection) { y[0] -= DOT_SIZE; } if (downDirection) { y[0] += DOT_SIZE; } } private void checkCollision() { for (int z = dots; z > 0; z--) { if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) { inGame = false; } } if (y[0] >= B_HEIGHT) { inGame = false; } if (y[0] < 0) { inGame = false; } if (x[0] >= B_WIDTH) { inGame = false; } if (x[0] < 0) { inGame = false; } if (!inGame) { timer.stop(); } } private void locateApple() { int r = (int) (Math.random() * RAND_POS); apple_x = ((r * DOT_SIZE)); r = (int) (Math.random() * RAND_POS); apple_y = ((r * DOT_SIZE)); } @Override public void actionPerformed(ActionEvent e) { if (inGame) { checkApple(); checkCollision(); move(); } repaint(); } private class TAdapter extends KeyAdapter { @Override public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if ((key == KeyEvent.VK_LEFT) && (!rightDirection)) { leftDirection = true; upDirection = false; downDirection = false; } if ((key == KeyEvent.VK_RIGHT) && (!leftDirection)) { rightDirection = true; upDirection = false; downDirection = false; } if ((key == KeyEvent.VK_UP) && (!downDirection)) { upDirection = true; rightDirection = false; leftDirection = false; } if ((key == KeyEvent.VK_DOWN) && (!upDirection)) { downDirection = true; rightDirection = false; leftDirection = false; } } } }
首先,我们将定义游戏中使用的常量。
private final int B_WIDTH = 300; private final int B_HEIGHT = 300; private final int DOT_SIZE = 10; private final int ALL_DOTS = 900; private final int RAND_POS = 29; private final int DELAY = 140;
B_WIDTH
和 B_HEIGHT
常量决定了游戏板的大小。DOT_SIZE
是苹果和蛇身节点的大小。ALL_DOTS
常量定义了游戏板上可能的节点总数(900 = (300*300)/(10*10))。RAND_POS
常量用于计算苹果的随机位置。DELAY
常量决定了游戏的运行速度。
private final int x[] = new int[ALL_DOTS]; private final int y[] = new int[ALL_DOTS];
这两个数组存储了蛇的所有关节的 x 和 y 坐标。
private void loadImages() { ImageIcon iid = new ImageIcon("src/resources/dot.png"); ball = iid.getImage(); ImageIcon iia = new ImageIcon("src/resources/apple.png"); apple = iia.getImage(); ImageIcon iih = new ImageIcon("src/resources/head.png"); head = iih.getImage(); }
在 loadImages()
方法中,我们获取游戏的图片。ImageIcon
类用于显示 PNG 图片。
private void initGame() { dots = 3; for (int z = 0; z < dots; z++) { x[z] = 50 - z * 10; y[z] = 50; } locateApple(); timer = new Timer(DELAY, this); timer.start(); }
在 initGame()
方法中,我们创建蛇,在游戏板上随机定位一个苹果,并启动计时器。
private void checkApple() { if ((x[0] == apple_x) && (y[0] == apple_y)) { dots++; locateApple(); } }
如果苹果与蛇头碰撞,我们将增加蛇的节点数量。我们调用 locateApple()
方法,该方法会随机放置一个新的苹果对象。
在 move()
方法中,我们包含了游戏的核心算法。要理解它,可以看看蛇是如何移动的。我们控制蛇的头部。我们可以用光标键改变它的方向。其余的节点沿着链条向上移动一个位置。第二个节点移动到第一个节点的位置,第三个节点移动到第二个节点的位置,依此类推。
for (int z = dots; z > 0; z--) { x[z] = x[(z - 1)]; y[z] = y[(z - 1)]; }
此代码沿链移动关节。
if (leftDirection) { x[0] -= DOT_SIZE; }
此行将头部向左移动。
在 checkCollision()
方法中,我们确定蛇是否撞到了自己或墙壁。
for (int z = dots; z > 0; z--) { if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) { inGame = false; } }
如果蛇用它的头撞到了它的一个关节,游戏就结束了。
if (y[0] >= B_HEIGHT) { inGame = false; }
如果蛇撞到了游戏板的底部,游戏就结束了。
package com.zetcode; import java.awt.EventQueue; import javax.swing.JFrame; public class Snake extends JFrame { public Snake() { initUI(); } private void initUI() { add(new Board()); setResizable(false); pack(); setTitle("Snake"); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public static void main(String[] args) { EventQueue.invokeLater(() -> { JFrame ex = new Snake(); ex.setVisible(true); }); } }
这是主类。
setResizable(false); pack();
setResizable()
方法会影响某些平台上 JFrame
容器的边框。因此,在调用 pack()
方法之前调用它很重要。否则,蛇头与右边框和下边框的碰撞可能无法正确工作。

这就是 Java 的贪吃蛇游戏。