简介:一份基于Java语言、在IDEA中开发的贪吃蛇游戏完整项目,定位为Java初学者的课程设计/实训参考。项目内包含全部源代码、可运行的打包jar文件,以及内容详实的项目分析报告,同时提供配套导入教程。功能覆盖背景音乐播放、账号注册登录、成绩排行榜、难度切换,支持空格控制、R键重开等交互,适合希望系统练习Swing界面编程、文件读写与事件监听的学习者。压缩包共49个文件,大小约93.06MB,以java源码、class编译文件、xml配置、wav音频、png图片、jar可执行文件及pdf报告为主,audio与data等目录分别用于存放音乐和玩家信息,结构清晰。已有205人下载学习,作为课设作业或练手项目均能直接运行,且基于源码可进一步扩展游戏逻辑与界面样式。
1. 一个Java课设的含金量:贪吃蛇为什么是IDEA里最该手写一遍的项目
大学 Java 课程设计里出镜率最高的题目就是贪吃蛇,但你去翻那些网上下载的 java 课程设计案例源码,会发现大量工程只有可怜的几个类,甚至 main 方法里堆着几百行面条代码,能跑但一问就露馅。这个标题给的交付物其实很完整:源码(能逐行读懂的工程结构)、jar 文件(打包完双击就能玩)、项目报告(答辩时拿得出手的文档)。我的建议是,哪怕你已经工作,也值得用 IDEA 从零把这只蛇写一遍。Swing 的事件模型、Timer 驱动的循环、网格坐标和像素坐标的换算,正是你在黑匣子里调界面程序时最容易吃亏的地方,而贪吃蛇恰好把这些点全部覆盖一遍。本文按“先定模型、再写循环、后补报告”的顺序,把整个流程拆开讲清楚,从新建工程到打包 jar,连带答辩前的避坑清单。
2. 拆解贪吃蛇的三层结构:先定坐标模型,再写循环和监听
2.1 网格坐标与像素坐标的换算:20x20 棋盘在 JPanel 上的映射
很多初学 Java 基础的人一上来就写 paintComponent,把蛇画得花花绿绿,结果移动逻辑是用像素坐标硬算的,蛇跑几圈之后方向判断就乱了。贪吃蛇这类格子游戏最稳的做法是先定网格坐标,绘图只负责把网格翻译成像素。
常见做法是定义三个常量:列数、行数和单格边长。我会把它们单独放在一个 GameConfig 类里,而不是散落在各个类中。
package com.snake.config; public class GameConfig { public static final int COLS = 20; // 棋盘列数 public static final int ROWS = 20; // 棋盘行数 public static final int CELL_SIZE = 25; // 单格边长(像素) public static final int TIMER_INTERVAL = 120; // 定时器间隔(毫秒) }这里三个参数决定了整个游戏的观感。COLS 和 ROWS 设成 20,配合 25 像素的格子,棋盘正好是 500x500,在笔记本屏幕上不用缩放就能完整显示。CELL_SIZE 调大格子更醒目,但同样窗口下棋盘变小;调小则画面更密。TIMER_INTERVAL 就是游戏速度的倒影,120 毫秒约等于每秒走 8 步,这个节奏对第一次接触键盘操作的人是友好的,高手可以改到 60 毫秒。
绘图时把网格坐标换算成像素的公式很简单:水平方向像素位置等于 col 乘以 CELL_SIZE,垂直方向等于 row 乘以 CELL_SIZE。绘制方块时,我会让每个格子往右下方缩 1 像素,也就是 fillRect 的宽高取 CELL_SIZE - 1,这样蛇身之间有一条细缝,视觉上能清楚分辨每一节,不至于糊成一根长条。碰撞检测则完全在网格坐标系下完成,判断蛇头坐标是否落在棋盘边界之外,或者是否等于蛇身的某个节点坐标,全程不碰像素。
2.2 三个核心类各管一摊:Snake、Food、GamePanel 的职责边界
写这个项目最容易翻车的设计,是把蛇身数组、食物坐标、计分逻辑、碰撞检测全部塞进 JPanel 里。代码能跑,但后面加暂停、加难度档位时,你会在一个几百行的方法里来回找变量。我一般会把核心逻辑拆成三个类,职责边界先划清楚再动手。
| 类 | 负责的事情 | 不负责的事情 |
|---|---|---|
| Snake | 蛇身坐标序列、当前方向、转向合法性、移动、自我碰撞检测 | 不画图、不监听键盘、不关心食物位置 |
| Food | 持有食物坐标、随机生成位置、避免生成到蛇身上 | 不画图、不计分、不判断是否被吃到 |
| GamePanel | 游戏循环、撞墙判定、吃食物判定、计分、绘制画面、键盘监听 | 不保存游戏状态到文件、不控制窗口标题 |
这种拆分最大的好处是逻辑层和表现层分离。Snake 类可以在完全不依赖 Swing 的情况下被 JUnit 直接测试,这在后面第 6 章会派上大用场。项目报告的系统设计章节也有话可写,一张类职责表格配一段“低耦合”的说明,比贴一堆代码有说服力得多。
Snake 类的实现里,蛇身存储我用的是 ArrayDeque,而不是 ArrayList。原因在于蛇每走一步,头部要插入一个新坐标,尾部要移除一个旧坐标,ArrayDeque 对两端操作的时间复杂度都是 O(1),ArrayList 则在头部增删时需要整体搬移元素。游戏每 120 毫秒就要做一次这样的操作,虽然数据量小看不出差距,但这属于“一开始就用对容器”的习惯。
package com.snake.core; import java.awt.Point; import java.util.ArrayDeque; import java.util.Deque; public class Snake { public enum Direction { UP, DOWN, LEFT, RIGHT } private final Deque<Point> body = new ArrayDeque<>(); private Direction direction = Direction.RIGHT; private boolean alive = true; public Snake(int initLength) { for (int i = 0; i < initLength; i++) { body.addFirst(new Point(10 - i, 10)); } } public Direction direction() { return direction; } public boolean isAlive() { return alive; } public void turn(Direction newDir) { // 禁止180度转向,防止蛇反向钻进自己身体 if (direction == Direction.UP && newDir == Direction.DOWN) return; if (direction == Direction.DOWN && newDir == Direction.UP) return; if (direction == Direction.LEFT && newDir == Direction.RIGHT) return; if (direction == Direction.RIGHT && newDir == Direction.LEFT) return; direction = newDir; } public Point head() { return body.peekFirst(); } public Point nextHead() { Point h = body.peekFirst(); return switch (direction) { case UP -> new Point(h.x, h.y - 1); case DOWN -> new Point(h.x, h.y + 1); case LEFT -> new Point(h.x - 1, h.y); case RIGHT -> new Point(h.x + 1, h.y); }; } public boolean contains(Point p) { return body.contains(p); } public void move(boolean grow) { body.addFirst(nextHead()); if (!grow) { body.pollLast(); } } public int size() { return body.size(); } }这段代码里有几个关键点值得展开。turn 方法的四行 if 判断是贪吃蛇的保命逻辑:当蛇头正朝右时,玩家按左键应该被忽略,否则蛇会在下一帧直接反方向吞掉自己的身体。nextHead 方法是整个游戏循环的地基,它只计算“蛇头下一步会在哪个格子”,而不真正改变蛇身状态,这样外部可以在蛇移动之前先判断这一步撞没撞墙、吃没吃到食物。move 方法的 grow 参数控制的是蛇身长度是否增加,吃到食物时传入 true,头部前进、尾部不动,长度自然加一;没吃到时尾部也前进,蛇身就像履带一样整体平移。
2.3 游戏循环的心脏:javax.swing.Timer 怎么驱动移动、碰撞、重绘
Swing 程序里最关键的一条规则是:所有界面更新必须在事件分发线程(EDT)里完成。如果你用 while(true) 里套 Thread.sleep 的方式驱动游戏,主线程不断调用 repaint,轻则画面闪烁,重则整个窗口假死。常见做法是改用 javax.swing.Timer,它每隔固定时间往 EDT 的任务队列里投递一个 ActionEvent,回调 actionPerformed 方法,天然线程安全。
package com.snake.ui; import com.snake.config.GameConfig; import com.snake.core.Food; import com.snake.core.Snake; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class GamePanel extends JPanel implements ActionListener { private Snake snake; private Food food; private Timer timer; private int score = 0; private boolean running = true; public GamePanel() { setPreferredSize(new Dimension( GameConfig.COLS * GameConfig.CELL_SIZE, GameConfig.ROWS * GameConfig.CELL_SIZE)); setBackground(Color.BLACK); configureKeyListener(); startGame(); } private void startGame() { snake = new Snake(3); food = new Food(snake); running = true; score = 0; timer = new Timer(GameConfig.TIMER_INTERVAL, this); timer.start(); } @Override public void actionPerformed(ActionEvent e) { if (!running || snake == null) return; Point next = snake.nextHead(); // 先做碰撞预判,再真正移动蛇身 if (isOutOfBounds(next) || snake.contains(next)) { running = false; timer.stop(); repaint(); return; } boolean grow = next.equals(food.position()); snake.move(grow); if (grow) { score += 10; food.respawn(); } repaint(); } private boolean isOutOfBounds(Point p) { return p.x < 0 || p.x >= GameConfig.COLS || p.y < 0 || p.y >= GameConfig.ROWS; } }这里把每帧的逻辑分成四步:先根据当前方向算出下一格的坐标;再判断这个坐标是否越界或撞到蛇身,如果是就直接结束游戏;随后判断这一格是不是食物,决定蛇要不要增长;最后调用 repaint 触发界面重绘。这个顺序不能乱,如果先 move 再判断碰撞,蛇已经把头部移到非法位置了,视觉上会看到蛇头“穿墙”的那一帧画面,非常掉价。
定时器的间隔就是游戏难度本身。把 TIMER_INTERVAL 从 120 改成 80,速度立刻提升;要用 JSlider 做难度档位时,只需要调用 timer.setDelay(新间隔),然后 timer.restart() 让新值立刻生效,不需要重新创建 Timer 对象。这里特别提醒一句:不要在 startGame 里 new 一个新的 Timer 而不先 stop 掉旧 Timer,否则两个定时器同时在 actionPerformed 里驱动同一只蛇,你会看到蛇头一帧走两格,撞墙判定直接失灵。
3. 在 IDEA 里跑通并打包:从新建工程到可双击的 JAR 文件
3.1 IDEA 工程初始化:社区版就够用,JDK 版本别选太新
网上各种 idea 安装教程会引导你下载旗舰版,还要琢磨怎么激活,实际这个项目用 IntelliJ IDEA 社区版已经完全覆盖,JetBrains 官网直接下载,安装包自带 JBR(JetBrains Runtime),不需要额外配置 Java 环境变量。如果你之前装的是 JDK 8 或者 JDK 11,那就更省事。JDK 17 也可以,Swing 在 17 上没有任何兼容性障碍,只是要留意新建工程时不要勾选使用 module-info.java,默认类路径模式下完全不用管模块导出问题。
新建工程的步骤就三步。打开 IDEA,点击 New Project,左侧选 Java,Name 填 SnakeGame,Build system 保持 IntelliJ 默认,Project SDK 下拉选择本机已有的 JDK,最后点 Create。工程创建后默认会有个 src 目录,右键 src 新建五个 Java 类文件,包名我习惯用 com.snake.config、com.snake.core、com.snake.ui 三层,分别放配置、核心逻辑、界面代码。
这个包名结构看起来多敲了几个字,但对后面写项目报告很有用,需求分析和系统设计章节可以直接引用包名说明模块划分。而且打包 jar 时,Main-Class 需要写全限定名,包路径越规范越不容易写错。
3.2 逐个文件实现:Final 类的完整代码与关键参数
前面 2.2 已经给出了 Snake 类的完整实现,这一节把剩下的 Food 和 GameFrame 补上。Food 类设计了一个很关键的方法 respawn,它必须在蛇身范围内生成不重叠的食物坐标。用 do-while 循环配合 contains 判断是一般做法,尽量保证随机位置不与蛇身冲突。
package com.snake.core; import com.snake.config.GameConfig; import java.awt.Point; import java.util.Random; public class Food { private final Random random = new Random(); private final Snake snake; private Point position; public Food(Snake snake) { this.snake = snake; respawn(); } public Point position() { return position; } public void respawn() { int x; int y; do { x = random.nextInt(GameConfig.COLS); y = random.nextInt(GameConfig.ROWS); } while (snake.contains(new Point(x, y))); position = new Point(x, y); } }这里最值得说明的是 do-while 的退出条件。random.nextInt 生成的是 0 到 COLS - 1 之间的整数,正好对应网格坐标的合法区间。循环一旦检测到生成的坐标已经落在蛇身上,就重新生成一次。当蛇很短时,这个循环一般一两次就能退出;但蛇很长、几乎占满棋盘时,随机碰撞的概率极高,可能出现长时间空转。理论上蛇身占满全部格子时 do-while 会陷入死循环。这个极端情况理论上存在,实际游戏中几乎触发不了,因为蛇没长到那个程度游戏早就撞墙结束了。如果你追求严谨,可以在循环里加一个尝试上限,比如最多重试 200 次,超出后把食物放在当前棋盘第一个空位上。
GameFrame 是程序的入口窗口。Swing 的规则是窗口创建和显示都要放在 EDT 线程里,所以 main 方法里用 SwingUtilities.invokeLater 包了一层。setResizable(false) 很重要,允许用户拉伸窗口会让面板的实际尺寸偏离你算好的 500x500,导致棋盘变形、绘制错位。
package com.snake.ui; import javax.swing.*; public class GameFrame extends JFrame { public GameFrame() { setTitle("贪吃蛇 - Java Swing"); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setResizable(false); add(new GamePanel()); pack(); setLocationRelativeTo(null); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new GameFrame().setVisible(true)); } }pack() 方法会根据面板的 preferredSize 自动调整窗口到刚好容纳内容的大小,所以 GamePanel 构造器里 setPreferredSize 那行必须写对。把 add(new GamePanel()) 放在 pack() 之前,顺序不能反。键盘监听也应该加在 GamePanel 上而不是 JFrame 上,并且要在面板上用 setFocusable(true) 确保它能获得焦点,否则你按方向键时系统根本不知道把事件发给谁。
3.3 打包成 JAR:Artifact 配置、Main-Class 与命令行验证
IDEA 打包 jar 不依赖 Maven 或 Gradle,用内置的 Artifact 机制就行。打开 File -> Project Structure,切到 Artifacts 标签页,点左上角的加号,选择 JAR -> From modules with dependencies,Main Class 弹窗里选 com.snake.ui.GameFrame,确定后 IDEA 会自动生成一个名为 SnakeGame.jar 的打包方案。右侧面板底部有个 Include in project build 的复选框,勾上它,以后每次 Build 都会自动产出 jar,省得手动操作。
打包动作在菜单栏 Build -> Build Artifacts -> Build 完成。产物输出在工程根目录的 out/artifacts/SnakeGame_jar/ 下面,双击 SnakeGame.jar 就能直接运行。如果双击没反应,第一步不是怀疑打包配置,而是打开终端手动执行验证:
java -jar SnakeGame.jar这条命令会把真正的异常信息打到终端里。最常见的两种错误:一是 ClassNotFoundException: com.snake.ui.GameFrame,说明 Artifact 的 Main-Class 填错了全限定名;二是 UnsupportedClassVersionError,说明你打包用的 JDK 版本比运行环境的 JDK 版本新,比如用 JDK 17 编译的 class 放到只有 JDK 8 的机器上跑,天然不兼容。解决方式要么统一两侧的 JDK 版本,要么打包时把 Project SDK 降到目标机器同样的版本。
标题里同时给了源码和 jar 文件,说明需要交付一个能直接运行的产物,而不是只有工程目录。打包成功后,还有一个不可忽视的小问题:JAR 包运行的当前工作目录可能与 IDE 里不一致。如果游戏有存档功能,用相对路径写文件会导致 IDE 里跑出一个分数、双击 jar 跑出另一个分数。这个问题在 4.5 里会细说。
4. 贪吃蛇最容易翻车的五个坑:方向反转、食物穿模、JAR 包打不开
4.1 按左键蛇反向钻进自己身体:180 度转向的判定漏洞
现象:蛇正在向右移动,你按了一下左方向键,蛇没有立刻死,而是先反向走了一格,下一格才撞上自己的身体,游戏结束。原因:键盘监听器里直接拿新方向覆盖了当前方向,没有判断两个方向是否正好相反。解决:把转向合法性判定放在 Snake.turn 方法里,也就是 2.2 节中那四个 if 判断。这条规则要放在方向赋值之前,一旦检测到 180 度转向就静默忽略,而不是报错或切换方向。
血泪经验是,不要为了“手感更灵活”而去掉这层限制。有人为了让蛇能快速连续转向,把判断改成允许 180 度但把蛇尾那一节留给玩家,这种“容错尾”的做法在蛇身长度大于 3 时依旧会撞死自己,得不偿失。
4.2 食物刷在蛇身上:随机坐标没做去重检测
现象:食物偶尔生成的位置和蛇身某一节完全重合,玩家看到食物却被蛇身盖住,走过去也吃不到。原因:生成食物时只调用了 random.nextInt,没有检查坐标是否在蛇身上。解决:用 do-while 循环把 snake.contains 作为重新抽随机的条件,也就是 3.2 节 Food.respawn 的实现。另一个隐藏问题在初始化顺序上:Food 构造器接收 Snake 对象时,Snake 必须先完成蛇身坐标的初始化,否则 contains 查到的是一条空蛇,食物生成后会落在蛇头上。
4.3 双击 JAR 包没反应,命令行却能跑:Swing 异常被系统吞掉
现象:双击 jar 文件画面上什么都没弹,但打开终端执行 java -jar 却运行正常,或者终端里才看得到异常堆栈。原因:Windows 关联的是 javaw.exe,这个可执行文件不挂控制台,所有的 System.err 输出全部丢失,程序如果一启动就抛异常,你什么提示都看不到。解决:双击没反应的第一动作永远是开终端跑一次 java -jar 看报错。如果是环境变量没配导致 java 命令找不到,那就要先配置 JAVA_HOME 和 Path。这个习惯能帮你省下大量瞎猜的时间,所谓 jar 包打不开的灵异事件,八成都能在堆栈第三行里找到答案。
4.4 游戏越玩越卡:多个 Timer 同时驱动同一只蛇
现象:游戏运行中暂停再继续,或者在某次重开之后,蛇的移动速度突然变快,甚至一帧走两格。原因:每次重开调用 startGame 时都 new 了一个 Timer,旧的 Timer 没有 stop,两个定时器以相同间隔向 EDT 派发 actionPerformed,导致蛇的移动状态在一帧里被推进两次。解决:要么在 startGame 开头判断 timer 不为空时先 timer.stop(),要么用 timer.setDelay 和 timer.restart 复用同一个 Timer 对象。调试时可以在 actionPerformed 第一行加个计数器打印帧率,如果每秒调用次数超过 1 除以间隔毫秒数,基本就是 Timer 堆积。
4.5 最高分记录读不到:相对路径受工作目录影响
现象:在 IDEA 里运行游戏,最高分能存也能读;双击 jar 运行,存档路径完全失效。原因:代码写的是 new FileOutputStream("highscore.dat"),相对路径基于 System.getProperty("user.dir"),在 IDEA 里它指工程根目录,在命令行里它指终端当前目录,在双击 jar 时它往往指向你双击文件的那个资源管理器目录,三个地方互不共享。解决:不要用相对路径存配置文件,改成用户主目录下的固定位置,或者干脆放在系统临时目录。用 Path.of 拼路径时,IDE 旧版本可能不兼容 NIO 的 Path.of,稳妥做法是依赖 java.io.File,兼容性最好。
File dataFile = new File(System.getProperty("user.home"), ".snake_highscore.txt");这样无论从哪个入口运行,存档都落在用户主目录下,行为完全一致。如果你坚持把存档放进 jar 包旁边的目录,可以用启动脚本传入参数,但那属于后话,课设阶段直接放 user.home 最省心。
5. 把项目报告写出专业感:需求分析、系统设计、测试记录一整套骨架
5.1 报告结构:六个章节的顺序与篇幅分配
项目报告是标题里的第三个交付物。我见过太多人把报告写成“我做了个贪吃蛇,用到了继承和多态”,这种充其量算使用说明。一份能撑住答辩的项目报告,建议分为以下六章,总篇幅控制在 8 到 12 页。
| 报告章节 | 核心内容 | 建议篇幅 |
|---|---|---|
| 需求分析 | 功能性需求和非功能性需求 | 1 - 2 页 |
| 系统设计 | 总体架构、类职责、关键流程 | 2 - 3 页 |
| 核心代码说明 | 挑选 3 到 4 个关键方法详解 | 3 - 4 页 |
| 测试记录 | 测试用例表、边界情况、测试结果 | 1 - 2 页 |
| 功能扩展说明 | 难度档位、暂停、存档等 | 1 页 |
| 总结与展望 | 遇到的问题、改进方向 | 半页 |
需求分析不能只写“做一个贪吃蛇游戏”。要拆成具体的、可验证的条目:支持键盘方向键控制蛇头移动;吃到食物后蛇身长度加一且分数加十;蛇头撞墙或撞自身时游戏结束并显示得分;提供暂停与继续功能。每条需求后面标注对应实现位置,老师看到的是你真有设计意识。非功能性需求写清楚游戏主循环刷新频率可调、界面响应延迟保持在 EDT 任务队列内即可。
5.2 核心代码说明的写法:挑关键方法解释设计意图,不要全文粘贴
报告里最常见的错误是把 500 行源码整体贴进去,老师扫一眼就知道是凑页数。正确写法是挑最有含金量的方法,每段配图配字。我一般会选四个点:Snake.turn 的转向合法性判断、GamePanel.actionPerformed 的游戏循环主逻辑、Food.respawn 的随机位置去重、GameFrame.main 的 EDT 启动方式。
每个方法下面写三段话:一段说这个方法解决了什么问题,一段说核心参数的含义和取值范围,一段说如果改掉某个判断条件会发生什么。以 actionPerformed 为例,你要写清楚先预判后移动的顺序是为了避免蛇头穿墙渲染,还要说明 TIMER_INTERVAL 与游戏难度之间的关系。这种“设计决策 + 参数说明 + 影响分析”的写法,能让报告在答辩时直接变成你的提词器。
5.3 三个性价比高的功能扩展:难度档位、暂停继续、历史最高分
如果做完基础版还有时间,优先加下面三个扩展。它们的共同特点是改动范围小、答辩演示效果好、报告里有得写。
| 扩展功能 | 核心实现 | 预计工作量 |
|---|---|---|
| 难度档位选择 | JComboBox 绑定时间间隔,调用 timer.setDelay 切换 | 1 小时 |
| 暂停与继续 | 键盘 P 键切换,暂停时 timer.stop | 半小时 |
| 历史最高分 | Properties 文件存入 user.home,游戏结束时比对 | 1 小时 |
难度档位实现起来最简单,在 GamePanel 里加一个 setSpeed(int level) 方法,内部把间隔换算成毫秒值。换算公式不要做成线性变化,推荐用固定档位表,例如 120、90、60 三档,每档对应一个难度描述。用 JSlider 会有连续取值的问题,你还得额外做范围钳制,JComboBox 只有三个固定值,天然不会越界。
暂停功能有个容易忽略的细节:暂停时要同时 stop 定时器和记录当前状态,恢复时 restart。如果只 stop 定时器,绘图事件还会继续触发,蛇虽然不动但画面依旧重绘,暂停前后状态难以区分。我在课程设计里见过同学按暂停后蛇还会动一下才停下,就是因为 actionPerformed 里没判断暂停状态,定时器 stop 到真正停摆这一帧之间的延迟造成了“偷步”。
6. 最后一招:用 JUnit 把蛇的逻辑层从 GUI 里剥出来测,答辩时没人敢说你水
前面 2.2 节把 Snake 设计成不依赖 Swing 的纯逻辑类,就是为了这一章。GUI 代码没法做自动化测试,但 Snake 的移动和转向规则完全可以。在 IDEA 里选中 Snake 类,按 Ctrl+Shift+T 选择 Create New Test,测试库选 JUnit5,IDEA 会自动在 test 目录下生成同包名的测试类。下面这份测试覆盖了三个最容易出逻辑错误的场景。
package com.snake.core; import org.junit.jupiter.api.Test; import java.awt.Point; import static org.junit.jupiter.api.Assertions.*; class SnakeTest { @Test void 向右时按左不应改变方向() { Snake snake = new Snake(3); snake.turn(Snake.Direction.LEFT); assertEquals(Snake.Direction.RIGHT, snake.direction()); } @Test void 头部坐标应随方向前进一格() { Snake snake = new Snake(3); Point head = snake.nextHead(); assertEquals(new Point(11, 10), head); } @Test void 吃到食物后身体长度加一() { Snake snake = new Snake(3); snake.move(true); assertEquals(4, snake.size()); } }第一个测试验证的是 180 度转向被正确忽略,第二个验证 nextHead 的坐标计算,第三个验证 grow 参数传 true 时蛇身长度变化。运行方式很简单,测试类左侧的绿色三角点一下,IDEA 底部的测试面板会打出通过结果。这三个测试跑绿之后,你再想改 Snake 类的转向规则或移动逻辑,可以立刻知道有没有改出回归问题,不需要每次手动开游戏试半天。
我自己做这类课设时养成了习惯,任何一次调整逻辑都先跑一遍测试再碰界面。转向这个看似简单的方法,我改错过两次:一次为了支持快速转向取消了反向限制,结果蛇走着走着会自己撞死;一次把判断条件写反,按上下变成按左右。两次都是靠这份测试兜底才在答辩前被发现。等以后面试被问到事件分发机制时,这段测试代码就是你面试题之外的实物证据,比背 java 面试八股文管用得多。希望帮到你。
本文还有配套的精品资源,点击获取