游戲背景介紹
貪吃蛇游戲是一款經(jīng)典的小游戲,它的玩法很簡單,就是控制蛇吃食物,每吃一個食物蛇的長度就會加一,直到蛇撞到墻壁或者撞到自己時游戲結(jié)束,最終的得分是蛇的長度減一。

JavaFX
用Java開發(fā)桌面端首選就是JavaFX,它的推出用來取代Swing(一個古老的Java桌面端框架)。
雖然都說Java開發(fā)桌面端性能不行,但是我們的Java開發(fā)工具IntelliJ IDEA的界面是由JavaFX構(gòu)建的。最開始的我的世界(Minecraft)這款游戲是Java開發(fā)的,雖然沒有使用Java標準GUI庫(它自己的游戲引擎和自定義的用戶界面),但也足以證明Java的魅力。
游戲規(guī)則
- 初始時,蛇的長度為一,位于游戲界面的中心位置。
- 每次隨機生成一塊食物,食物不能出現(xiàn)在蛇的身體上。
- 蛇可以通過四個方向鍵上下左右移動,不能撞到墻壁或自己的身體。
- 每吃一塊食物,蛇的長度加一。
- 穿過左邊的墻壁,出現(xiàn)在右邊;穿過上邊的墻壁,出現(xiàn)在下面;反之亦然。
- 游戲結(jié)束時,彈出得分對話框,點擊重新開始新游戲。
代碼結(jié)構(gòu)
本教程主要涉及的代碼文件是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;
// 游戲是否結(jié)束
private boolean gameOver = false;
// 游戲是否暫停
private boolean gamePaused = false;
@Override
public void start(Stage primaryStage) throws Exception {
// 界面初始化
// ...
// 初始化游戲
// ...
// 動畫循環(huán)
// ...
}
// 界面初始化方法
private void initGUI() {
// ...
}
// 初始化游戲方法
private void initGame() {
// ...
}
// 蛇的移動方法
private void move() {
// ...
}
// 檢測碰撞方法
private void checkCollision() {
// ...
}
// 生成食物方法
private void generateFood() {
// ...
}
// 繪制游戲畫面方法
private void paint(GraphicsContext gc) {
// ...
}
// 顯示游戲結(jié)束對話框方法
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);
}
}
邏輯分析
在實現(xiàn)貪吃蛇游戲之前,我們需要先了解一下游戲的邏輯。
- 在游戲界面內(nèi),不斷地移動蛇的位置。
- 蛇的移動方向可以通過鍵盤上的上下左右四個方向鍵來控制。
- 當蛇頭碰到邊界或碰到自己的身體時,游戲結(jié)束。
- 當蛇頭碰到食物時,就會吃掉食物,長度加1,隨后繼續(xù)向前移動。
- 吃掉食物后,會重新生成一個新的食物,判斷新食物的位置是否和已有的蛇的位置沖突。
實現(xiàn)步驟
下面分步驟進行實現(xiàn),每一個步驟都結(jié)合代碼,邏輯清晰。
步驟1:界面初始化
在start方法中進行界面的初始化,包括創(chuàng)建Canvas、GraphicsContext等,并將Canvas添加到StackPane作為根節(jié)點,最后顯示舞臺。代碼如下:
@Override
public void start(Stage primaryStage) throws Exception {
// 創(chuàng)建Canvas
Canvas canvas = new Canvas(WIDTH * SIZE, HEIGHT * SIZE);
GraphicsContext gc = canvas.getGraphicsContext2D();
// 創(chuàng)建根節(jié)點
StackPane root = new StackPane(canvas);
root.setAlignment(Pos.CENTER);
// 創(chuàng)建場景
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:初始化游戲
在游戲開始前,需要初始化一些參數(shù),包括蛇的位置、食物位置、游戲狀態(tài)等。具體實現(xiàn)代碼如下:
// 初始化游戲方法
private void initGame() {
// 清空蛇的身體
snake.clear();
// 在游戲界面的中心生成蛇頭
int x = WIDTH / 2;
int y = HEIGHT / 2;
snake.add(new Point(x, y));
// 生成食物
generateFood();
// 初始化游戲狀態(tài)
gameOver = false;
gamePaused = false;
}
步驟3:蛇的移動
在游戲中,蛇可以通過鍵盤上的上下左右四個方向鍵來控制移動方向。我們可以在Scene的按鍵監(jiān)聽事件中實現(xiàn),根據(jù)按下的方向鍵修改蛇的移動方向。具體代碼實現(xiàn)如下:
// Scene的按鍵監(jiān)聽事件
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;
}
});
在每次動畫循環(huán)中,根據(jù)蛇的移動方向來計算移動后的新位置。如果新位置在蛇的身體上或者超出了邊界,就說明游戲結(jié)束了。判斷蛇是否吃到了食物,如果吃到了就讓蛇的身體變長,并在新位置生成一個新的食物。
// 蛇的移動方法
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:檢測碰撞
在每次蛇的移動后,需要檢測蛇是否撞到了自己的身體。如果撞到了,說明游戲結(jié)束了。具體代碼實現(xiàn)如下:
// 檢測碰撞方法
private void checkCollision() {
Point head = snake.getFirst();
for (Point point : snake) {
if (point != head && point.equals(head)) {
gameOver = true;
showGameOverDialog();
break;
}
}
}
步驟5:生成食物
每個食物都是在游戲界面上隨機出現(xiàn)的,食物不能出現(xiàn)在蛇的身體上。生成食物時,可以使用do-while循環(huán)來判斷是否有重合的情況。具體代碼實現(xiàn)如下:
// 生成食物方法
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繪制蛇、食物等游戲元素,實現(xiàn)游戲的畫面。具體代碼實現(xiàn)如下:
// 繪制游戲畫面方法
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:顯示游戲結(jié)束對話框
當游戲結(jié)束時,彈出得分對話框,點擊重新開始新游戲。具體代碼實現(xiàn)如下:
// 顯示游戲結(jié)束對話框方法
private void showGameOverDialog() {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("游戲結(jié)束");
alert.setHeaderText(null);
alert.setContentText("游戲結(jié)束,您的得分是:" + (snake.size() - 1));
alert.show();
alert.setOnHidden(event -> {
initGame();
});
}
至此,貪吃蛇游戲的實現(xiàn)已經(jīng)完成了。
完整代碼如下:
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; // 游戲是否結(jié)束
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) { // 調(diào)整蛇的速度
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);
}
// 顯示游戲結(jié)束對話框
private void showGameOverDialog() {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("游戲結(jié)束");
alert.setHeaderText(null);
alert.setContentText("游戲結(jié)束,您的得分是:" + (snake.size() - 1));
alert.show();
alert.setOnHidden(event -> {
initGame(); // 游戲結(jié)束后重新開始游戲
});
}
// 方向枚舉類
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);
}
}