资讯动态

Java课设贪吃蛇:Swing+LinkedList实现与答辩加分指南

发布时间:2026/10/26 8:33:45 来源:尧图企业网站定制
简介这份资源是面向高校学生与Java初学者的贪吃蛇小游戏课程设计完整方案包含可直接运行的源代码与配套课设报告适合需要完成Java课设、课程作业或入门练手的人群。压缩包共38个文件约25.66MB涵盖java源码、class编译文件、xml配置、png图片素材、mp4运行录屏以及docx课设报告等源码、素材与文档一应俱全方便对照理解游戏逻辑与界面实现。目前已有119人学习下载可作为课设参考与排错借鉴。资源界面风格保持原始朴素贴近学生实际水平便于顺利通过课程考核同时附带的录屏能直观展示运行效果报告文档则提供现成的写作框架帮助读者快速梳理贪吃蛇的蛇身移动、食物生成、碰撞检测与分数统计等核心模块节省从零搭建的时间。1. 贪吃蛇小游戏Java课设为什么它是检验面向对象基本功的最佳靶子很多同学拿到 Java 课设题目时第一反应是去搜「贪吃蛇源码」找到一份能跑的代码改个名字就交上去。但答辩时老师问一句「你的蛇身是怎么存储的」就露馅了。贪吃蛇这个题目之所以被大量高校选为课设不是因为它简单而是因为它恰好覆盖了 Java 基础中最核心的几个能力面向对象编程、集合框架、事件驱动、线程调度和图形界面。它小到可以一个人两周做完又大到每一层都能拉开差距。这篇文章面向正在做 Java 课设的本科生也面向想用一个小项目把 Java 基础串起来的自学者。我会从架构设计讲到可运行的代码再到参数怎么调、坑在哪最后给出答辩时能加分的进阶方向。你跟着走完手里会有一个自己能讲清楚每一行逻辑的项目而不是一份连自己都看不懂的源码。2. 架构选型Swing 定时器 双向链表为什么这样搭最稳2.1 技术栈选择Swing 还是 JavaFXTimer 还是游戏循环做 Java 课设的贪吃蛇GUI 框架的选择是第一道分叉路。常见做法有三种Swing、JavaFX、或者控制台字符画。控制台版本虽然简单但答辩时视觉效果差老师很难给高分。JavaFX 是较新的方案但从 JDK 11 开始 JavaFX 被移出标准库需要额外引入依赖对于课设环境很多机房还在用 JDK 8来说配置成本高容易在演示时翻车。Swing 虽然老但它是 JDK 自带的不需要任何额外依赖javax.swing和java.awt直接可用在 JDK 8 到 JDK 21 上都能跑。对于课设场景Swing 是最稳的选择。游戏循环的实现方式同样关键。很多教程用Thread.sleep()配合while(true)来做主循环这在课设里能跑但会带来线程安全问题——游戏逻辑线程和 Swing 的事件分发线程EDT同时操作游戏状态容易出现画面撕裂或者ConcurrentModificationException。更稳的做法是用javax.swing.Timer它把定时回调放在 EDT 上执行天然避免了并发问题。代价是精度稍低但对于贪吃蛇这种 100~200 毫秒一帧的游戏来说完全够用。蛇身的存储结构是答辩高频问题。用ArrayListPoint是最直观的但每次移动都要remove(0)再add()在蛇很长时效率低。用LinkedListPoint作为双向链表头部插入和尾部删除都是 O(1)更符合贪吃蛇的语义。我一般会推荐LinkedList并且在答辩时主动说出这个选型理由老师会觉得你真的想过。2.2 项目结构五个类各管什么一个结构清晰的贪吃蛇项目我建议拆成五个类每个类职责单一类名职责关键方法Snake蛇的数据模型身体坐标、方向、增长move()、grow()、checkSelfCollision()Food食物的生成与位置管理generateRandom()、isEatenBy()GamePanel游戏画布绘制、键盘监听、碰撞检测paintComponent()、actionPerformed()GameFrame主窗口组装面板、菜单栏构造函数Main程序入口main()这样拆的好处是Snake和Food是纯数据模型不依赖 Swing可以单独写单元测试GamePanel负责所有和界面相关的逻辑GameFrame只做窗口组装。答辩时老师问「你的业务逻辑和界面耦合了吗」你可以直接指出这个分层。2.3 核心数据模型蛇的移动与增长逻辑先看Snake类的实现。这里用LinkedListPoint存储蛇身Point是java.awt.Point直接复用 JDK 自带的坐标类。import java.awt.Point; import java.util.LinkedList; public class Snake { // 蛇身索引 0 是蛇头 private LinkedListPoint body; // 当前方向 private Direction direction; // 待增长节数吃到食物后 1 private int pendingGrow; public enum Direction { UP, DOWN, LEFT, RIGHT } public Snake(int startX, int startY) { body new LinkedList(); // 初始长度 3水平排列 body.add(new Point(startX, startY)); body.add(new Point(startX - 1, startY)); body.add(new Point(startX - 2, startY)); direction Direction.RIGHT; pendingGrow 0; } // 每帧调用一次根据方向计算新蛇头 public void move() { Point head body.getFirst(); Point newHead new Point(head.x, head.y); switch (direction) { case UP: newHead.y - 1; break; case DOWN: newHead.y 1; break; case LEFT: newHead.x - 1; break; case RIGHT: newHead.x 1; break; } body.addFirst(newHead); // 如果没有待增长移除尾部否则保留尾部实现增长 if (pendingGrow 0) { pendingGrow--; } else { body.removeLast(); } } public void grow() { pendingGrow; } // 检查蛇头是否撞到自己跳过蛇头本身 public boolean checkSelfCollision() { Point head body.getFirst(); for (int i 1; i body.size(); i) { if (body.get(i).equals(head)) { return true; } } return false; } // 方向切换禁止 180 度掉头 public void setDirection(Direction newDir) { if (isOpposite(newDir, this.direction)) { return; } this.direction newDir; } private boolean isOpposite(Direction a, Direction b) { return (a Direction.UP b Direction.DOWN) || (a Direction.DOWN b Direction.UP) || (a Direction.LEFT b Direction.RIGHT) || (a Direction.RIGHT b Direction.LEFT); } public LinkedListPoint getBody() { return body; } public Point getHead() { return body.getFirst(); } public Direction getDirection() { return direction; } }这段代码有几个关键设计点需要说清楚。第一pendingGrow机制吃到食物时不是立刻在尾部加一节而是记一个「待增长」计数下一帧移动时保留尾部这样蛇的增长和移动是同步的不会出现视觉上的突变。第二setDirection里的反向判断如果蛇正在向右走玩家按左键直接忽略。如果不做这个判断蛇会瞬间掉头撞到自己游戏直接结束这是新手最常见的翻车点。第三checkSelfCollision从索引 1 开始遍历跳过蛇头自身否则蛇头永远等于自己一移动就判定碰撞。2.4 游戏面板绘制、定时器与键盘响应GamePanel是整个游戏的中枢它继承JPanel并实现ActionListener由javax.swing.Timer驱动。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.Random; public class GamePanel extends JPanel implements ActionListener { // 网格尺寸20x20 格每格 25 像素 public static final int GRID_SIZE 20; public static final int CELL_SIZE 25; public static final int PANEL_SIZE GRID_SIZE * CELL_SIZE; private Snake snake; private Food food; private Timer timer; private boolean gameOver; private int score; private int speedMs 150; // 初始速度毫秒/帧 public GamePanel() { setPreferredSize(new Dimension(PANEL_SIZE, PANEL_SIZE)); setBackground(Color.BLACK); setFocusable(true); initGame(); addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { handleKey(e.getKeyCode()); } }); } private void initGame() { snake new Snake(10, 10); food new Food(GRID_SIZE); food.generateRandom(snake.getBody()); gameOver false; score 0; speedMs 150; if (timer ! null) { timer.stop(); } timer new Timer(speedMs, this); timer.start(); } private void handleKey(int keyCode) { switch (keyCode) { case KeyEvent.VK_UP: snake.setDirection(Snake.Direction.UP); break; case KeyEvent.VK_DOWN: snake.setDirection(Snake.Direction.DOWN); break; case KeyEvent.VK_LEFT: snake.setDirection(Snake.Direction.LEFT); break; case KeyEvent.VK_RIGHT: snake.setDirection(Snake.Direction.RIGHT); break; case KeyEvent.VK_R: if (gameOver) initGame(); break; } } Override public void actionPerformed(ActionEvent e) { if (gameOver) return; snake.move(); Point head snake.getHead(); // 撞墙检测 if (head.x 0 || head.x GRID_SIZE || head.y 0 || head.y GRID_SIZE) { gameOver true; timer.stop(); repaint(); return; } // 撞自己检测 if (snake.checkSelfCollision()) { gameOver true; timer.stop(); repaint(); return; } // 吃食物检测 if (head.equals(food.getPosition())) { snake.grow(); score 10; food.generateRandom(snake.getBody()); // 每吃 5 个食物提速一次最低 60ms if (score % 50 0 speedMs 60) { speedMs - 10; timer.setDelay(speedMs); } } repaint(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 画食物 g.setColor(Color.RED); Point fp food.getPosition(); g.fillOval(fp.x * CELL_SIZE, fp.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); // 画蛇 LinkedListPoint body snake.getBody(); for (int i 0; i body.size(); i) { Point p body.get(i); if (i 0) { g.setColor(Color.GREEN); // 蛇头亮绿 } else { g.setColor(new Color(0, 150, 0)); // 蛇身深绿 } g.fillRect(p.x * CELL_SIZE, p.y * CELL_SIZE, CELL_SIZE - 1, CELL_SIZE - 1); } // 画分数 g.setColor(Color.WHITE); g.setFont(new Font(Arial, Font.BOLD, 14)); g.drawString(Score: score, 10, 20); // 游戏结束提示 if (gameOver) { g.setColor(Color.WHITE); g.setFont(new Font(Arial, Font.BOLD, 28)); g.drawString(Game Over, PANEL_SIZE / 2 - 90, PANEL_SIZE / 2); g.setFont(new Font(Arial, Font.PLAIN, 16)); g.drawString(Press R to restart, PANEL_SIZE / 2 - 70, PANEL_SIZE / 2 30); } } }actionPerformed是每帧的回调执行顺序很重要先移动蛇再检测碰撞再检测吃食物最后重绘。如果把吃食物检测放在移动之前会出现蛇头还没到食物位置就判定吃到的 bug。速度递增的逻辑是每得 50 分提速 10 毫秒最低 60 毫秒这个参数可以根据演示需要调整——太快了老师看不清太慢了显得没难度。paintComponent里画蛇身时用CELL_SIZE - 1而不是CELL_SIZE是为了在格子之间留 1 像素的缝隙视觉上蛇身有分节感不会糊成一条。这是个小技巧但答辩时能体现你对细节的把控。2.5 食物生成随机位置不能和蛇身重叠Food类看起来简单但有一个容易忽略的坑随机生成的位置可能落在蛇身上。如果食物生成在蛇身下面玩家看不到以为游戏卡了。import java.awt.Point; import java.util.LinkedList; import java.util.Random; public class Food { private Point position; private final int gridSize; private final Random random new Random(); public Food(int gridSize) { this.gridSize gridSize; } // 生成食物确保不与蛇身重叠 public void generateRandom(LinkedListPoint snakeBody) { int maxAttempts 1000; for (int i 0; i maxAttempts; i) { int x random.nextInt(gridSize); int y random.nextInt(gridSize); Point candidate new Point(x, y); if (!snakeBody.contains(candidate)) { position candidate; return; } } // 极端情况蛇几乎占满整个网格遍历所有空格 for (int x 0; x gridSize; x) { for (int y 0; y gridSize; y) { Point candidate new Point(x, y); if (!snakeBody.contains(candidate)) { position candidate; return; } } } // 真的没有空格了说明玩家赢了 position null; } public Point getPosition() { return position; } }这里用了「随机尝试 兜底遍历」的策略。随机尝试 1000 次在蛇身不长时几乎瞬间命中当蛇很长、空格很少时随机尝试效率急剧下降所以加了一个遍历所有格子的兜底逻辑。position为null表示蛇已经占满整个网格理论上玩家赢了可以在GamePanel里加一个胜利判定。3. 从零跑起来环境配置、编译命令与三个必调参数3.1 JDK 环境配置与项目编译课设环境最常见的问题是 JDK 版本不一致。Swing 在 JDK 8 到 JDK 21 上都能用但如果你用了var关键字或者List.of()这类 JDK 9 的语法在 JDK 8 的机房机器上就编译不过。我的建议是课设代码统一用 JDK 8 兼容语法这样在任何环境都能跑。环境配置步骤确认 JDK 已安装命令行执行java -version能看到版本号即可。如果提示「不是内部或外部命令」需要配置JAVA_HOME和PATH环境变量。确认javac可用执行javac -version。有些机器只装了 JRE 没装 JDKjava能跑但javac不存在。如果机房有多个 JDK 版本用where javaWindows或which javaMac/Linux确认当前用的是哪个。编译和运行命令# 编译所有 Java 文件到 out 目录 javac -encoding UTF-8 -d out src/*.java # 运行主类 java -cp out Main-encoding UTF-8这个参数很容易被忽略。如果你的代码里有中文注释或者中文字符串在 Windows 默认 GBK 编码下编译会报「编码 GBK 的不可映射字符」。加上这个参数就解决了。-d out把编译产物放到out目录保持源码目录干净。如果你的项目用 IDEA 或 Eclipse直接新建项目、把五个类放进src目录、右键运行Main即可。但答辩时老师可能要求你现场用命令行编译所以上面两条命令要记牢。3.2 三个必调参数速度、网格、初始长度贪吃蛇的手感几乎完全由三个参数决定答辩演示前一定要调好参数位置推荐值影响speedMsGamePanel150ms 起步越小越快低于 60ms 人眼跟不上GRID_SIZEGamePanel20网格越多蛇的活动空间越大难度越低初始蛇长Snake构造函数3太长开局就难太短没有贪吃蛇的感觉speedMs是最关键的。我见过有同学设成 50ms蛇跑得飞快老师还没看清就撞墙了演示效果极差。150ms 是一个舒服的起步速度配合每 50 分提速 10ms 的递增逻辑既有挑战性又不至于失控。GRID_SIZE设成 20 意味着 20x20 的网格每格 25 像素窗口大小 500x500在 1080P 屏幕上大小合适。如果你想让游戏更简单把GRID_SIZE调到 15蛇的活动空间小了但食物更容易碰到想更难就调到 25。初始蛇长 3 是经典设定。有些教程设成 1蛇只有一个头看起来不像蛇设成 5 以上开局就容易撞到自己。3 节刚好能看出蛇的形态又不会开局就死。3.3 用 IDEA 打包成可执行 JAR课设提交时老师通常要求提交一个能直接双击运行的 JAR 包。用 IDEA 打包的步骤File→Project Structure→Artifacts→→JAR→From modules with dependencies。Main Class选择Main。勾选Build on make点击OK。Build→Build Artifacts→Build在out/artifacts/下生成 JAR。命令行打包的方式# 编译 javac -encoding UTF-8 -d out src/*.java # 打包指定主类 jar cfe SnakeGame.jar Main -C out . # 运行 java -jar SnakeGame.jarjar cfe三个参数分别是c创建、f指定文件名、e指定入口类。-C out .表示切换到out目录并把所有内容打包进去。打包后双击 JAR 就能运行前提是机器上有 JRE。注意如果 JAR 双击没反应大概率是文件关联问题不是代码问题。在命令行用java -jar SnakeGame.jar运行如果报错会打印堆栈信息根据错误定位。4. 避坑与排查五个让课设翻车的经典问题4.1 蛇按了反方向键直接自杀现象蛇向右移动时玩家快速按了左键蛇瞬间掉头蛇头撞到蛇身游戏结束。原因setDirection没有做反向判断或者判断逻辑有漏洞。有些实现只在按键时判断但忽略了「同一帧内连续按两个键」的情况——比如蛇向右走玩家先按上再按左如果两次按键都在同一帧内被处理蛇会先转向上再转向左而左相对于上是合法的但相对于原来的右是反向的。解决在setDirection里做反向判断如 2.3 节代码所示并且确保方向切换只在每帧移动时生效。更严格的做法是维护一个「下一方向」变量在move()执行时才真正应用方向变更这样同一帧内的多次按键只有最后一次生效。4.2 食物生成在蛇身上玩家找不到现象游戏运行中分数不再增长画面上看不到食物蛇一直在跑。原因Food.generateRandom只做了一次随机没有检查是否与蛇身重叠。当蛇很长时随机位置落在蛇身上的概率增大。解决用 2.5 节的「随机尝试 兜底遍历」策略。另外可以在paintComponent里加一个调试模式把食物位置用不同颜色标出来方便排查。4.3 游戏越玩越卡蛇长了之后明显掉帧现象开局流畅蛇长到 30 节以上后画面开始卡顿。原因paintComponent里每次都创建新的Color对象new Color(0, 150, 0)蛇越长创建越多触发频繁 GC。另外如果用ArrayList并且每次remove(0)在蛇很长时是 O(n) 操作。解决把颜色对象提成static final常量避免重复创建。蛇身用LinkedList替代ArrayList。paintComponent里只做绘制不做任何对象创建。4.4 键盘监听失效按方向键没反应现象游戏窗口打开了蛇在动但按方向键完全没反应。原因JPanel没有获得焦点。Swing 的键盘事件只会发给拥有焦点的组件。如果面板没有requestFocusInWindow()或者焦点被其他组件比如菜单栏抢走了键盘事件就到不了面板。解决在GamePanel构造函数里调用setFocusable(true)和requestFocusInWindow()。如果窗口里有其他可聚焦组件在游戏开始时手动调用gamePanel.requestFocusInWindow()。另一个常见原因是用了KeyListener但面板被其他透明组件覆盖检查一下布局。4.5 打包成 JAR 后图片资源加载失败现象在 IDEA 里运行正常打包成 JAR 后蛇和食物的图片显示不出来报NullPointerException。原因用了new File(res/snake.png)这种方式加载资源。JAR 打包后资源文件在 JAR 内部不再是文件系统上的路径File找不到。解决用getClass().getResourceAsStream(/res/snake.png)从 classpath 加载。如果不想处理资源加载干脆用纯色块绘制如本文代码不依赖外部图片打包永远不会出问题。课设阶段纯色块完全够用还省去了资源管理的麻烦。5. 答辩加分项用状态模式和单元测试把课设变成作品5.1 用状态模式重构游戏状态切换基础版本的贪吃蛇用boolean gameOver管理状态简单但扩展性差。想加「暂停」「开始菜单」「关卡切换」时boolean就不够用了。用状态模式可以把游戏状态抽成独立的类每个状态负责自己的行为。// 状态接口 public interface GameState { void handleInput(GamePanel panel, int keyCode); void update(GamePanel panel); void render(GamePanel panel, Graphics g); } // 运行状态 public class RunningState implements GameState { Override public void handleInput(GamePanel panel, int keyCode) { if (keyCode KeyEvent.VK_SPACE) { panel.setState(new PausedState()); return; } panel.getSnake().setDirection(/* 根据 keyCode 映射方向 */); } Override public void update(GamePanel panel) { // 移动蛇、检测碰撞、吃食物 } Override public void render(GamePanel panel, Graphics g) { // 绘制游戏画面 } } // 暂停状态 public class PausedState implements GameState { Override public void handleInput(GamePanel panel, int keyCode) { if (keyCode KeyEvent.VK_SPACE) { panel.setState(new RunningState()); } } Override public void update(GamePanel panel) { // 暂停时不更新逻辑 } Override public void render(GamePanel panel, Graphics g) { // 先画游戏画面再叠加「已暂停」文字 } }GamePanel里持有一个GameState currentStateactionPerformed里调用currentState.update(this)paintComponent里调用currentState.render(this, g)键盘监听里调用currentState.handleInput(this, keyCode)。这样加新状态只需要新增一个类不用改现有代码符合开闭原则。答辩时老师问「你的设计模式用在哪」这就是一个具体的落点。5.2 给 Snake 类写单元测试Snake和Food不依赖 Swing可以直接用 JUnit 测试。课设里加几个单元测试答辩时是很大的加分项因为它证明你的代码是可验证的不是「跑起来就行」。import org.junit.Test; import static org.junit.Assert.*; import java.awt.Point; public class SnakeTest { Test public void testMoveRight() { Snake snake new Snake(10, 10); snake.setDirection(Snake.Direction.RIGHT); snake.move(); assertEquals(new Point(11, 10), snake.getHead()); assertEquals(3, snake.getBody().size()); // 没吃食物长度不变 } Test public void testGrow() { Snake snake new Snake(10, 10); snake.grow(); snake.move(); assertEquals(4, snake.getBody().size()); // 增长后长度 1 } Test public void testNoReverse() { Snake snake new Snake(10, 10); snake.setDirection(Snake.Direction.RIGHT); snake.setDirection(Snake.Direction.LEFT); // 反向应被忽略 assertEquals(Snake.Direction.RIGHT, snake.getDirection()); } Test public void testSelfCollision() { Snake snake new Snake(10, 10); // 手动构造一个自撞场景蛇头向右移动后撞到身体 snake.setDirection(Snake.Direction.DOWN); snake.move(); snake.setDirection(Snake.Direction.LEFT); snake.move(); snake.setDirection(Snake.Direction.UP); snake.move(); assertTrue(snake.checkSelfCollision()); } }这四个测试覆盖了移动、增长、反向拦截、自撞检测四个核心逻辑。跑通它们你对蛇的行为就有信心了。答辩时如果老师质疑某个逻辑你可以说「这个行为有单元测试覆盖」比口头解释有力得多。5.3 我踩过的那些坑做贪吃蛇课设我最大的教训是不要一开始就写界面。我第一次做的时候上来就写GamePanel结果逻辑和绘制混在一起改一个碰撞检测要翻半天代码。后来学乖了先把Snake和Food写成纯数据类用main方法打印坐标验证逻辑确认没问题再接界面。这个顺序能省掉大量调试时间。另一个血泪经验是答辩前一定要在机房环境实测。我在自己电脑上用 JDK 17 跑得好好的到机房 JDK 8 上因为用了List.of()直接编译报错。后来所有课设代码我都坚持用 JDK 8 兼容语法写再也没翻过车。最后一个习惯给每个类写清楚注释尤其是move()和actionPerformed()这种核心方法每一步在做什么都标出来。答辩时老师翻你代码看到注释清晰、命名规范印象分直接拉满。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑