基于JavaFX的貪吃蛇小游戲

游戲背景介紹

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

988c51b1-2820-4126-9a7c-09849f10edeb.gif

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);
    }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容