掘金 后端 ( ) • 2024-06-26 16:37

概述:

使用Java语言和Swing库来开发一个贪吃蛇游戏。 创建一个完整的贪吃蛇游戏涉及到图形用户界面(GUI)设计、游戏逻辑、事件处理等多个方面。以下是一个使用Java Swing库实现的基本贪吃蛇游戏的示例代码。这个示例提供了游戏的基本框架,包括蛇的移动、食物的生成和蛇的增长。

源码:

直接上源码,大家拿回去就能自己跑起来玩和后续优化。

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Random;
/**
 * @Author derek_smart
 * @Date 202/6/26 15:11
 * @Description 贪吃蛇
 * <p>
 */
public class SnakeGame extends JPanel implements ActionListener {

    private static final int GRID_SIZE = 20;
    private static final int TILE_SIZE = 20;
    private static final int GAME_SIZE = GRID_SIZE * TILE_SIZE;
    private static final int DELAY = 100;
    private int score = 0;
    private int level = 1;

    private final ArrayList<Point> snake = new ArrayList<>();
    private Point food;
    private char direction = 'R';
    private boolean running = true;
    private Timer timer;

    /**
     * - 设置面板的首选大小和背景颜色。
     *    - 设置面板为可聚焦,以便接收键盘事件。
     *    - 添加键盘监听器以便捕获玩家的方向输入。
     *    - 调用 `startGame()` 方法来初始化游戏。
     */
    public SnakeGame() {
        setPreferredSize(new Dimension(GAME_SIZE, GAME_SIZE));
        setBackground(Color.BLACK);
        setFocusable(true);
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                switch (e.getKeyCode()) {
                    case KeyEvent.VK_LEFT:
                        if (direction != 'R') direction = 'L';
                        break;
                    case KeyEvent.VK_RIGHT:
                        if (direction != 'L') direction = 'R';
                        break;
                    case KeyEvent.VK_UP:
                        if (direction != 'D') direction = 'U';
                        break;
                    case KeyEvent.VK_DOWN:
                        if (direction != 'U') direction = 'D';
                        break;
                }
            }
        });
        startGame();
    }

    /***
     * - 清空蛇的身体部分的列表。
     *    - 将蛇的初始位置设置在游戏区域的中心。
     *    - 调用 `generateFood()` 方法来放置第一个食物。
     *    - 创建并启动一个计时器,以固定的延迟触发 `actionPerformed()` 方法。
     */
    private void startGame() {
        snake.clear();
        snake.add(new Point(GRID_SIZE / 2, GRID_SIZE / 2));
        generateFood();
        timer = new Timer(DELAY, this);
        timer.start();
    }

    /**
     *  使用随机数生成器在游戏区域内随机放置食物。
     */
    private void generateFood() {
        Random random = new Random();
        int x = random.nextInt(GRID_SIZE);
        int y = random.nextInt(GRID_SIZE);
        food = new Point(x, y);
        increaseLevel(); // 增加游戏级别
    }

    private void increaseLevel() {
        if (score % 5 == 0 && score != 0) { // 每得到5分,游戏级别增加
            level++;
            if (timer.getDelay() > 40) { // 保证游戏速度不会太快
                timer.setDelay(timer.getDelay() - 10);
            }
        }
    }

    /**
     *  - 负责绘制游戏的图形界面。
     *    - 如果游戏正在运行,绘制食物和蛇的身体。
     *    - 如果游戏结束,调用 `gameOver()` 方法来显示游戏结束信息。
     * @param g
     */
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        drawScoreAndLevel(g); // 绘制得分和级别
        if (running) {
            g.setColor(Color.RED);
            g.fillRect(food.x * TILE_SIZE, food.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);

            for (Point p : snake) {
                g.setColor(Color.GREEN);
                g.fillRect(p.x * TILE_SIZE, p.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
            }
        } else {
            gameOver(g);
        }
    }

    private void drawScoreAndLevel(Graphics g) {
        g.setColor(Color.WHITE);
        g.drawString("Score: " + score, 5, 15);
        g.drawString("Level: " + level, 5, 30);
    }
    /**
     * - 负责处理蛇的移动逻辑。
     *    - 根据当前方向更新蛇头的位置。
     *    - 调用 `checkCollision()` 来检查是否发生碰撞。
     *    - 调用 `checkFood()` 来检查蛇头是否与食物重合。
     */
    private void move() {
        for (int i = snake.size() - 1; i > 0; i--) {
            snake.set(i, new Point(snake.get(i - 1)));
        }
        Point head = snake.get(0);
        switch (direction) {
            case 'L':
                head.x--;
                break;
            case 'R':
                head.x++;
                break;
            case 'U':
                head.y--;
                break;
            case 'D':
                head.y++;
                break;
        }
        checkCollision();
        checkFood();
    }

    /**
     *    - 检查蛇头是否触碰到游戏边界或自身的身体。
     *    - 如果发生碰撞,设置游戏状态为结束并停止计时器。
     */
    private void checkCollision() {
        Point head = snake.get(0);
        if (head.x < 0 || head.y < 0 || head.x >= GRID_SIZE || head.y >= GRID_SIZE) {
            running = false;
        }
        for (int i = 1; i < snake.size(); i++) {
            if (head.equals(snake.get(i))) {
                running = false;
            }
        }
        if (!running) {
            timer.stop();
        }
    }

    /**
     *    - 检查蛇头是否与食物位置重合。
     *    - 如果重合,将食物位置添加到蛇的身体中,并生成新食物。
     */
    private void checkFood() {
        Point head = snake.get(0);
        if (head.equals(food)) {
            score++; // 增加得分
            snake.add(new Point(food));
            generateFood();
        }
    }

    /**
     *   - 当游戏结束时调用,显示游戏结束的文本信息。
     * @param g
     */
    private void gameOver(Graphics g) {
        g.setColor(Color.WHITE);
        g.drawString("Game Over", GAME_SIZE / 2, GAME_SIZE / 2);
        g.drawString("Score: " + score, GAME_SIZE / 2, GAME_SIZE / 2 + 20);
        g.drawString("Level: " + level, GAME_SIZE / 2, GAME_SIZE / 2 + 40);
    }

    /**
     *   - 实现了 `ActionListener` 接口的方法,定时触发。
     *    - 如果游戏正在运行,调用 `move()` 方法并重绘面板。
     * @param e
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        if (running) {
            move();
            repaint();
        }
    }

    /**
     *   - 创建游戏窗口,并添加 `SnakeGame` 面板到窗口中。
     *     - 设置窗口的关闭操作、大小、位置,并使其可见。
     * @param args
     */
    public static void main(String[] args) {
        JFrame frame = new JFrame("贪吃蛇");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.add(new SnakeGame(), BorderLayout.CENTER);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

1719388649849.png

时序图:

sequenceDiagram
    participant User as 玩家
    participant Main as 主函数
    participant SnakeGame as 贪吃蛇游戏
    participant Timer as 计时器
    participant Food as 食物

    User->>Main: 运行游戏
    Main->>SnakeGame: 创建游戏实例
    SnakeGame->>SnakeGame: 初始化游戏(startGame)
    SnakeGame->>Food: 生成食物(generateFood)
    SnakeGame->>Timer: 启动计时器(timer.start)
    loop 每个时间周期
        Timer->>SnakeGame: 触发事件(actionPerformed)
        SnakeGame->>SnakeGame: 移动蛇(move)
        SnakeGame->>SnakeGame: 检查碰撞(checkCollision)
SnakeGame->>SnakeGame: 检查食物(checkFood)
        SnakeGame->>SnakeGame: 绘制游戏界面(repaint)
        User->>SnakeGame: 输入控制指令
        SnakeGame->>SnakeGame: 改变方向
    end
    SnakeGame->>User: 显示游戏结束(gameOver)
    User->>Main: 关闭游戏

时序图中,描述了玩家与 SnakeGame 类的交互流程:

  • 玩家运行游戏,主函数创建 SnakeGame 实例。
  • 游戏初始化,生成食物,并启动计时器。
  • 在游戏的主循环中,计时器每个时间周期触发 actionPerformed 事件。
  • 游戏移动蛇,检查碰撞和食物,然后绘制游戏界面。
  • 玩家输入控制指令,改变蛇的移动方向。
  • 当游戏结束时,显示游戏结束信息。
  • 玩家关闭游戏。

上班无聊打开玩玩还是可以的。

后续优化和建议

可以从以下几个方面进行:

1. 代码重构和模块化

分离关注点:将游戏逻辑、渲染逻辑和输入处理分离到不同的类或方法中,提高代码的可读性和可维护性。
使用MVC模式:采用Model-View-Controller (MVC) 设计模式,将游戏状态(模型)、游戏界面(视图)和用户交互(控制器)分离。

2. 游戏逻辑增强

增加难度级别:随着得分的增加,逐渐提高游戏速度或引入更复杂的地图。
实现蛇的旋转:允许蛇头旋转,使蛇的移动更加自然。
增加障碍物:在游戏区域内添加障碍物,增加游戏难度。

3. 用户界面改进

美化界面:使用图片或更复杂的图形来代替简单的颜色块,提升视觉效果。
增加动画效果:添加如蛇移动、吃食物等动画,增强游戏的感官体验。
响应式设计:确保游戏界面能够适应不同大小的屏幕。

4. 功能增加

暂停/恢复功能:允许玩家暂停游戏,并在暂停后恢复。
得分和生命系统:引入生命值的概念,蛇撞到自己或墙壁时失去生命值。
多人模式:支持本地或网络多人游戏。