游戏背景介绍
贪吃蛇游戏是一款经典的小游戏,它的玩法很简单,就是控制蛇吃食物,每吃一个食物蛇的长度就会加一,直到蛇撞到墙壁或者撞到自己时游戏结束,最终的得分是蛇的长度减一。
JavaFX
用Java开发桌面端首选就是JavaFX,它的推出用来取代Swing(一个古老的Java桌面端框架)。
虽然都说Java开发桌面端性能不行,但是我们的Java开发工具IntelliJ IDEA的界面是由JavaFX构建的。最开始的我的世界(Minecraft)这款游戏是Java开发的,虽然没有使用Java标准GUI库(它自己的游戏引擎和自定义的用户界面),但也足以证明Java的魅力。
游戏规则
- 初始时,蛇的长度为一,位于游戏界面的中心位置。
- 每次随机生成一块食物,食物不能出现在蛇的身体上。
- 蛇可以通过四个方向键上下左右移动,不能撞到墙壁或自己的身体。
- 每吃一块食物,蛇的长度加一。
- 穿过左边的墙壁,出现在右边;穿过上边的墙壁,出现在下面;反之亦然。
- 游戏结束时,弹出得分对话框,点击重新开始新游戏。
代码结构
本教程主要涉及的代码文件是SnakeGame.java
,整个代码文件的框架如下:
import java.util.ArrayDeque;
import java.util.Deque;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class SnakeGame extends Application {
// 游戏界面的宽度
private static final int WIDTH = 20;
// 游戏界面的高度
private static final int HEIGHT = 20;
// 每个格子的大小
private static final int SIZE = 20;
// 蛇的速度
private static final int SPEED = 5;
// 蛇的身体
private Deque<Point> snake = new ArrayDeque<>();
// 蛇的初始方向
private Direction direction = Direction.RIGHT;
// 食物的位置
private Point food;
// 游戏是否结束
private boolean gameOver = false;
// 游戏是否暂停
private boolean gamePaused = false;
@Override
public void start(Stage primaryStage) throws Exception {
// 界面初始化
// ...
// 初始化游戏
// ...
// 动画循环
// ...
}
// 界面初始化方法
private void initGUI() {
// ...
}
// 初始化游戏方法
private void initGame() {
// ...
}
// 蛇的移动方法
private void move() {
// ...
}
// 检测碰撞方法
private void checkCollision() {
// ...
}
// 生成食物方法
private void generateFood() {
// ...
}
// 绘制游戏画面方法
private void paint(GraphicsContext gc) {
// ...
}
// 显示游戏结束对话框方法
private void showGameOverDialog() {
// ...
}
// 方向枚举类
private enum Direction {
UP, DOWN, LEFT, RIGHT
}
// 坐标点类
private static class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
// ...
}
@Override
public int hashCode() {
// ...
}
}
public static void main(String[] args) {
launch(args);
}
}
逻辑分析
在实现贪吃蛇游戏之前,我们需要先了解一下游戏的逻辑。
- 在游戏界面内,不断地移动蛇的位置。
- 蛇的移动方向可以通过键盘上的上下左右四个方向键来控制。
- 当蛇头碰到边界或碰到自己的身体时,游戏结束。
- 当蛇头碰到食物时,就会吃掉食物,长度加1,随后继续向前移动。
- 吃掉食物后,会重新生成一个新的食物,判断新食物的位置是否和已有的蛇的位置冲突。
实现步骤
下面分步骤进行实现,每一个步骤都结合代码,逻辑清晰。
步骤1:界面初始化
在start
方法中进行界面的初始化,包括创建Canvas
、GraphicsContext
等,并将Canvas
添加到StackPane
作为根节点,最后显示舞台。代码如下:
@Override
public void start(Stage primaryStage) throws Exception {
// 创建Canvas
Canvas canvas = new Canvas(WIDTH * SIZE, HEIGHT * SIZE);
GraphicsContext gc = canvas.getGraphicsContext2D();
// 创建根节点
StackPane root = new StackPane(canvas);
root.setAlignment(Pos.CENTER);
// 创建场景
Scene scene = new Scene(root);
scene.setOnKeyPressed(event -> {
KeyCode keyCode = event.getCode();
switch (keyCode) {
// ...
}
});
// 显示舞台
primaryStage.setScene(scene);
primaryStage.setTitle("贪吃蛇游戏");
primaryStage.setResizable(false);
primaryStage.show();
}
步骤2:初始化游戏
在游戏开始前,需要初始化一些参数,包括蛇的位置、食物位置、游戏状态等。具体实现代码如下:
// 初始化游戏方法
private void initGame() {
// 清空蛇的身体
snake.clear();
// 在游戏界面的中心生成蛇头
int x = WIDTH / 2;
int y = HEIGHT / 2;
snake.add(new Point(x, y));
// 生成食物
generateFood();
// 初始化游戏状态
gameOver = false;
gamePaused = false;
}
步骤3:蛇的移动
在游戏中,蛇可以通过键盘上的上下左右四个方向键来控制移动方向。我们可以在Scene
的按键监听事件中实现,根据按下的方向键修改蛇的移动方向。具体代码实现如下:
// Scene的按键监听事件
scene.setOnKeyPressed(event -> {
KeyCode keyCode = event.getCode();
switch (keyCode) {
case UP:
if (direction != Direction.DOWN) {
direction = Direction.UP;
}
break;
case DOWN:
if (direction != Direction.UP) {
direction = Direction.DOWN;
}
break;
case LEFT:
if (direction != Direction.RIGHT) {
direction = Direction.LEFT;
}
break;
case RIGHT:
if (direction != Direction.LEFT) {
direction = Direction.RIGHT;
}
break;
case P:
gamePaused = !gamePaused;
break;
case R:
initGame();
break;
default:
break;
}
});
在每次动画循环中,根据蛇的移动方向来计算移动后的新位置。如果新位置在蛇的身体上或者超出了边界,就说明游戏结束了。判断蛇是否吃到了食物,如果吃到了就让蛇的身体变长,并在新位置生成一个新的食物。
// 蛇的移动方法
private void move() {
Point head = snake.getFirst();
Point newHead = null;
switch (direction) {
case UP:
newHead = new Point(head.getX(), head.getY() - 1);
break;
case DOWN:
newHead = new Point(head.getX(), head.getY() + 1);
break;
case LEFT:
newHead = new Point(head.getX() - 1, head.getY());
break;
case RIGHT:
newHead = new Point(head.getX() + 1, head.getY());
break;
default:
break;
}
// 判断是否撞到自己的身体
if (snake.contains(newHead)) {
gameOver = true;
showGameOverDialog();
return;
}
// 判断是否撞到墙壁
if (newHead.getX() < 0 || newHead.getX() >= WIDTH ||
newHead.getY() < 0 || newHead.getY() >= HEIGHT) {
gameOver = true;
showGameOverDialog();
return;
}
// 更新蛇的位置
snake.addFirst(newHead);
// 判断是否吃到了食物
if (newHead.equals(food)) {
// 如果吃到了食物,就让蛇的身体变长
generateFood();
} else {
// 如果没有吃到食物,就让蛇的尾巴消失
snake.removeLast();
}
}
步骤4:检测碰撞
在每次蛇的移动后,需要检测蛇是否撞到了自己的身体。如果撞到了,说明游戏结束了。具体代码实现如下:
// 检测碰撞方法
private void checkCollision() {
Point head = snake.getFirst();
for (Point point : snake) {
if (point != head && point.equals(head)) {
gameOver = true;
showGameOverDialog();
break;
}
}
}
步骤5:生成食物
每个食物都是在游戏界面上随机出现的,食物不能出现在蛇的身体上。生成食物时,可以使用do-while
循环来判断是否有重合的情况。具体代码实现如下:
// 生成食物方法
private void generateFood() {
boolean validPosition;
int x, y;
do {
validPosition = true;
x = (int) (Math.random() * WIDTH);
y = (int) (Math.random() * HEIGHT);
for (Point point : snake) {
if (point.getX() == x && point.getY() == y) {
validPosition = false;
break;
}
}
} while (!validPosition);
food = new Point(x, y);
}
步骤6:绘制游戏画面
在Canvas
上通过GraphicsContext
绘制蛇、食物等游戏元素,实现游戏的画面。具体代码实现如下:
// 绘制游戏画面方法
private void paint(GraphicsContext gc) {
// 清空画布
gc.clearRect(0, 0, WIDTH * SIZE, HEIGHT * SIZE);
// 绘制蛇身
gc.setFill(javafx.scene.paint.Color.GREEN);
for (Point point : snake) {
gc.fillRect(point.getX() * SIZE, point.getY() * SIZE, SIZE, SIZE);
}
// 绘制头部
gc.setFill(javafx.scene.paint.Color.DARKGREEN);
Point head = snake.getFirst();
gc.fillRect(head.getX() * SIZE, head.getY() * SIZE, SIZE, SIZE);
// 绘制食物
gc.setFill(javafx.scene.paint.Color.RED);
gc.fillRect(food.getX() * SIZE, food.getY() * SIZE, SIZE, SIZE);
}
步骤7:显示游戏结束对话框
当游戏结束时,弹出得分对话框,点击重新开始新游戏。具体代码实现如下:
// 显示游戏结束对话框方法
private void showGameOverDialog() {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("游戏结束");
alert.setHeaderText(null);
alert.setContentText("游戏结束,您的得分是:" + (snake.size() - 1));
alert.show();
alert.setOnHidden(event -> {
initGame();
});
}
至此,贪吃蛇游戏的实现已经完成了。
完整代码如下:
package org.example;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.util.ArrayDeque;
import java.util.Deque;
public class SnakeGame extends Application {
private static final int WIDTH = 20; // 游戏界面的宽度
private static final int HEIGHT = 20; // 游戏界面的高度
private static final int SIZE = 20; // 每个格子的大小
private static final int SPEED = 5; // 蛇的速度
private Deque<Point> snake = new ArrayDeque<>(); // 蛇的身体
private Direction direction = Direction.RIGHT; // 蛇的初始方向
private Point food; // 食物的位置
private boolean gameOver = false; // 游戏是否结束
private boolean gamePaused = false; // 游戏是否暂停
@Override
public void start(Stage primaryStage) throws Exception {
Canvas canvas = new Canvas(WIDTH * SIZE, HEIGHT * SIZE);
GraphicsContext gc = canvas.getGraphicsContext2D();
StackPane root = new StackPane(canvas);
root.setAlignment(Pos.CENTER);
Scene scene = new Scene(root);
scene.setOnKeyPressed(event -> {
KeyCode keyCode = event.getCode();
switch (keyCode) {
case UP:
if (direction != Direction.DOWN) {
direction = Direction.UP;
}
break;
case DOWN:
if (direction != Direction.UP) {
direction = Direction.DOWN;
}
break;
case LEFT:
if (direction != Direction.RIGHT) {
direction = Direction.LEFT;
}
break;
case RIGHT:
if (direction != Direction.LEFT) {
direction = Direction.RIGHT;
}
break;
case P:
gamePaused = !gamePaused;
break;
case R:
initGame();
break;
default:
break;
}
});
primaryStage.setScene(scene);
primaryStage.setTitle("贪吃蛇游戏");
primaryStage.setResizable(false);
primaryStage.show();
initGame();
new AnimationTimer() {
private long lastUpdateTime;
@Override
public void handle(long now) {
if (now - lastUpdateTime >= 1_000_000_000 / SPEED) { // 调整蛇的速度
lastUpdateTime = now;
if (!gameOver && !gamePaused) {
move();
checkCollision();
paint(gc);
}
}
}
}.start();
}
// 初始化游戏
private void initGame() {
snake.clear();
snake.add(new Point(WIDTH / 2, HEIGHT / 2));
generateFood();
gameOver = false;
gamePaused = false;
}
// 蛇的移动
private void move() {
Point head = snake.getFirst();
Point newHead = null;
switch (direction) {
case UP:
newHead = new Point(head.getX(), head.getY() - 1);
break;
case DOWN:
newHead = new Point(head.getX(), head.getY() + 1);
break;
case LEFT:
newHead = new Point(head.getX() - 1, head.getY());
break;
case RIGHT:
newHead = new Point(head.getX() + 1, head.getY());
break;
default:
break;
}
// 判断是否撞到自己的身体
if (snake.contains(newHead)) {
gameOver = true;
showGameOverDialog();
return;
}
// 判断是否撞到墙壁
if (newHead.getX() < 0 || newHead.getX() >= WIDTH ||
newHead.getY() < 0 || newHead.getY() >= HEIGHT) {
gameOver = true;
showGameOverDialog();
return;
}
snake.addFirst(newHead);
if (newHead.equals(food)) {
generateFood();
} else {
snake.removeLast();
}
}
// 检测碰撞
private void checkCollision() {
Point head = snake.getFirst();
for (Point point : snake) {
if (point != head && point.equals(head)) {
gameOver = true;
showGameOverDialog();
break;
}
}
}
// 生成食物
private void generateFood() {
boolean validPosition;
int x, y;
do {
validPosition = true;
x = (int) (Math.random() * WIDTH);
y = (int) (Math.random() * HEIGHT);
for (Point point : snake) {
if (point.getX() == x && point.getY() == y) {
validPosition = false;
break;
}
}
} while (!validPosition);
food = new Point(x, y);
}
// 绘制游戏画面
private void paint(GraphicsContext gc) {
// 清空画布
gc.clearRect(0, 0, WIDTH * SIZE, HEIGHT * SIZE);
// 绘制蛇身
gc.setFill(javafx.scene.paint.Color.GREEN);
for (Point point : snake) {
gc.fillRect(point.getX() * SIZE, point.getY() * SIZE, SIZE, SIZE);
}
// 绘制头部
gc.setFill(javafx.scene.paint.Color.DARKGREEN);
Point head = snake.getFirst();
gc.fillRect(head.getX() * SIZE, head.getY() * SIZE, SIZE, SIZE);
// 绘制食物
gc.setFill(javafx.scene.paint.Color.RED);
gc.fillRect(food.getX() * SIZE, food.getY() * SIZE, SIZE, SIZE);
}
// 显示游戏结束对话框
private void showGameOverDialog() {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("游戏结束");
alert.setHeaderText(null);
alert.setContentText("游戏结束,您的得分是:" + (snake.size() - 1));
alert.show();
alert.setOnHidden(event -> {
initGame(); // 游戏结束后重新开始游戏
});
}
// 方向枚举类
private enum Direction {
UP, DOWN, LEFT, RIGHT
}
// 坐标点类
private static class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return x * 31 + y;
}
}
public static void main(String[] args) {
launch(args);
}
}
关注微信公众号:“小虎哥的技术博客”。我们会定期发布关于Java技术的详尽文章,让您能够深入了解该领域的各种技巧和方法,让我们一起成为更优秀的程序员👩💻👨💻!
相关文章源码放在:gitee仓库、github仓库上。