简介:这是一份基于Java Swing实现的「大鱼吃小鱼」游戏完整源码包,面向Java初学者与游戏开发入门者,帮助读者通过一个可运行的小项目理解GUI编程、事件监听与动画渲染的完整流程。压缩包共1171个文件,约61.41MB,以715张png与27张gif图片素材为主,辅以jpg背景资源、xml与properties配置、java与class源码文件,以及工程元数据,覆盖游戏窗口搭建、鼠标事件处理、Graphics绘图、定时器重绘、游戏状态管理与图片资源加载等核心模块。代码结构清晰、注释详尽,便于对照学习JFrame与JPanel的职责划分,理解MouseListener与MouseMotionListener如何驱动大鱼跟随鼠标移动,以及如何借助Timer与repaint实现游动与吞噬动画。目前已有1454人学习下载,适合作为课程设计、期末大作业或二次开发的起点,读者可在此基础上扩展鱼类种类、障碍物与关卡逻辑,巩固继承与多态等面向对象思想。
1. 一个 JFrame 里塞进整片鱼塘:这份 Java 大鱼吃小鱼源码到底能跑出什么
很多人第一次看到java大鱼吃小鱼.zip会以为是个玩具,但真把包解开、用 IDEA 跑起来之后,你会发现它其实是一份相当完整的 Swing 桌面游戏骨架:JFrame 做窗口容器,JPanel 承担全部绘制与刷新,Timer 驱动主循环,玩家鱼、敌方鱼、碰撞判定、分数结算、游戏状态切换全都在一个可读性尚可的工程里。它解决的不是"教你写游戏",而是给你一个能直接改、能直接拆的 Java 面向对象练手项目——尤其适合刚学完 Java 基础、想找一个比控制台贪吃蛇更有画面感的实战案例的人。你拿到手就能改鱼的速度、换贴图、加道具,甚至把碰撞逻辑抽出来单独测。下面我按"能跑起来 → 看懂结构 → 改得动 → 不踩坑"的顺序拆一遍。
2. 把 zip 跑起来:环境、目录与第一次启动
2.1 环境准备与工程导入
这份源码是标准 Java SE 桌面项目,不依赖 Maven 或 Gradle,常见做法是直接用 IDEA 或 Eclipse 以"现有项目"方式导入。JDK 版本建议 8 到 17 之间,Swing 和 AWT 在这些版本里 API 稳定,不会出现javax.swing找不到的玄学问题。如果你机器上装了多个 JDK,记得在 IDEA 的 Project Structure 里把 SDK 和 Language Level 都锁到同一个版本,否则编译能过、运行时报UnsupportedClassVersionError的情况我见过不止一次。
导入后先别急着点运行,按这个顺序确认三件事:
# 1. 确认 JDK 版本,避免多 JDK 环境串味 java -version javac -version # 2. 进入解压后的源码根目录,看结构 cd java大鱼吃小鱼 ls -R # 3. 如果源码里带 .class 旧文件,先清掉,避免和新编译的冲突 find . -name "*.class" -delete第一段命令是确认java和javac指向同一个 JDK,多 JDK 环境下这俩经常不一致,是启动失败的常见原因。第二段列出目录,你大概率会看到src源码目录、images或res资源目录、可能还有.project、.classpath这类 Eclipse 残留文件。第三段清理旧.class很关键——有些打包者会把编译产物一起塞进 zip,如果 IDE 输出路径和它们重叠,会出现"改了代码没生效"的黑匣子现象。
2.2 入口类与启动参数
找到带main方法的入口类,通常叫GameMain、StartGame或MainFrame。运行前先看它有没有硬编码的资源路径,比如new ImageIcon("src/images/fish.png")这种相对路径。相对路径的基准是"工作目录",IDEA 默认是项目根目录,命令行java默认是你执行命令的目录,两者不一致就会报NullPointerException或图片全白。
public static void main(String[] args) { // 常见入口写法:先建窗口,再塞面板 JFrame frame = new JFrame("大鱼吃小鱼"); frame.setSize(800, 600); // 窗口尺寸,和面板绘制坐标强相关 frame.setLocationRelativeTo(null); // 居中,避免出现在屏幕外 frame.setResizable(false); // 关掉缩放,省得坐标系错乱 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(new GamePanel()); // 核心面板,所有绘制在这里 frame.setVisible(true); }这段是典型的 Swing 启动模板。setSize的宽高要和GamePanel里绘制背景、边界判定的常量对齐,否则鱼会游出可视区。setResizable(false)不是必须,但强烈建议保留——一旦允许缩放,paintComponent里的绝对坐标就和实际像素对不上,鱼会变形或碰撞错位。setLocationRelativeTo(null)是防止窗口跑到多显示器边缘外的后悔药。
如果运行后窗口一闪而过或直接抛异常,优先看控制台第一行堆栈,八成是资源路径或空指针,而不是 JDK 问题。
3. JPanel 绘制与主循环:游戏是怎么"动"起来的
3.1 paintComponent 与双缓冲
Swing 里所有画面都靠paintComponent(Graphics g)重绘,这份源码也不例外。它继承JPanel,重写该方法,在里面依次画背景、敌方鱼、玩家鱼、分数文字。理解这一点的价值在于:你想加任何视觉元素,都是往这个方法里加绘制调用,而不是去改窗口。
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 必须先调,清屏交给父类 g.drawImage(bgImage, 0, 0, getWidth(), getHeight(), null); for (EnemyFish fish : enemyList) { // 遍历敌方鱼集合 g.drawImage(fish.getImage(), fish.getX(), fish.getY(), null); } g.drawImage(player.getImage(), player.getX(), player.getY(), null); g.setColor(Color.WHITE); g.drawString("分数: " + score, 20, 30); }super.paintComponent(g)这行不能省,它负责清掉上一帧,否则画面会拖影。遍历enemyList时要注意并发——如果主循环在另一个线程里增删这个集合,绘制时可能抛ConcurrentModificationException。常见做法是把逻辑更新和绘制都放在 Swing 的 EDT 线程里,用Timer驱动,而不是自己开Thread。
3.2 Timer 主循环与帧率控制
游戏能动,靠的是一个反复触发的定时器。源码里大概率是javax.swing.Timer,每隔几十毫秒触发一次actionPerformed,在里面更新坐标、检测碰撞、调用repaint()。
Timer timer = new Timer(30, e -> { // 30ms 约等于 33 帧 player.move(); // 玩家位置更新 for (EnemyFish fish : enemyList) { fish.move(); // 敌方鱼移动 if (fish.getBounds().intersects(player.getBounds())) { handleCollision(fish); // 碰撞处理 } } repaint(); // 请求重绘,不是立即重绘 }); timer.start();Timer的第一个参数是间隔毫秒数,30 对应约 33 帧,是桌面小游戏的舒适区;调到 10 会更快但 CPU 占用上升,调到 60 会明显卡顿。repaint()只是"请求"重绘,真正执行由 Swing 调度,所以别在它后面写依赖画面已更新的逻辑。碰撞用Rectangle.intersects做矩形近似,简单够用,但鱼是圆的或带透明边时会有"看着没碰到却死了"的误差,这是后面避坑章要讲的点。
3.3 玩家控制与键盘监听
玩家鱼靠键盘移动,源码一般用KeyAdapter或KeyListener绑定到面板。注意焦点问题:面板必须setFocusable(true)并requestFocusInWindow(),否则按键没反应。
addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_UP: player.setDy(-speed); break; case KeyEvent.VK_DOWN: player.setDy(speed); break; case KeyEvent.VK_LEFT: player.setDx(-speed); break; case KeyEvent.VK_RIGHT: player.setDx(speed); break; } } @Override public void keyReleased(KeyEvent e) { // 松键归零,否则鱼会一直飘 player.setDx(0); player.setDy(0); } });keyPressed设速度、keyReleased归零,是保证"按住才动、松开就停"的标准写法。只写keyPressed不写keyReleased,鱼会朝一个方向一直游,这是新手最常见的翻车点。VK_UP这些是键码常量,别用字符比较,方向键没有可打印字符。
4. 碰撞、计分与难度:把玩法逻辑改到顺手
4.1 碰撞判定与大小关系
大鱼吃小鱼的核心规则是"大欺小":玩家比敌鱼大就吃掉加分,比敌鱼小就被吃游戏结束。判定通常分两步——先矩形相交,再比尺寸。
private void handleCollision(EnemyFish fish) { if (player.getSize() >= fish.getSize()) { score += fish.getScore(); // 吃掉,加分 enemyList.remove(fish); // 从集合移除 player.grow(2); // 玩家略微变大 } else { gameOver = true; // 被吃,结束 timer.stop(); // 停掉主循环 } }getSize()是自定义的尺寸字段,不是图片像素宽。enemyList.remove(fish)在遍历中直接删元素会触发并发修改异常,正确做法是用Iterator.remove()或先收集待删列表再统一删。player.grow(2)让玩家越吃越大,但要有上限,否则玩家鱼撑满屏幕,游戏失去难度曲线。
4.2 难度递增与敌鱼生成
一直刷同样的鱼很快就腻,源码里一般有个生成逻辑,按时间或分数提高敌鱼速度和数量。
private void spawnEnemy() { Random random = new Random(); int x = random.nextInt(getWidth() - 60); // 留出鱼身宽度 int size = 20 + random.nextInt(40); // 尺寸随机 int speed = 1 + score / 50; // 分数越高越快 enemyList.add(new EnemyFish(x, 0, size, speed)); }getWidth() - 60是防止鱼生成在右边界外。speed = 1 + score / 50是线性难度,简单直观;想更平滑可以改成1 + Math.log(score + 1)。生成频率由主循环里的计数器控制,别每帧都生成,否则瞬间满屏。
4.3 游戏状态与重开
游戏结束要有重开入口,否则只能关窗口重跑。常见做法是加一个状态字段和鼠标或按键监听。
if (gameOver) { g.drawString("游戏结束,按 R 重开", 300, 300); } // 在 keyPressed 里 if (gameOver && e.getKeyCode() == KeyEvent.VK_R) { resetGame(); // 清空集合、重置分数、恢复玩家尺寸 gameOver = false; timer.start(); }resetGame()要把enemyList、score、玩家位置和尺寸全部还原,漏掉任何一个都会出现"重开后鱼还在原地"或"分数没清零"的怪象。这是状态管理最容易漏的地方。
5. 避坑与排查:跑不起来、动不了、吃不准的常见问题
5.1 窗口空白或图片不显示
现象:程序能启动,窗口出来了,但一片灰白,鱼和背景都没有。 原因:图片资源路径错误,ImageIcon加载失败返回空图,drawImage画了个寂寞;或者资源没被复制到编译输出目录。 解决:把图片放在源码同级res目录,用getClass().getResource("/res/fish.png")这种基于 classpath 的方式加载,而不是相对文件路径。确认 IDE 的输出目录里确实有这些图片。
5.2 按键没反应
现象:窗口正常,鱼不动,按方向键毫无反应。 原因:面板没有获得键盘焦点,焦点被窗口或其他组件抢走。 解决:在面板构造里setFocusable(true),窗口可见后调用requestFocusInWindow()。如果还不行,改用KeyBindings(InputMap+ActionMap),它不依赖焦点,是 Swing 里更稳的键盘方案。
5.3 碰撞判定"看着没碰到却死了"
现象:玩家鱼和敌鱼明明还有一段距离,却触发了吃掉或死亡。 原因:用整张图片的矩形做碰撞,而鱼图带透明边距,实际可视区域比矩形小。 解决:把碰撞矩形按图片内边距收缩,比如new Rectangle(x + 10, y + 10, w - 20, h - 20);或者用圆形距离判定Math.hypot(dx, dy) < r1 + r2,对鱼这种圆润造型更准。
5.4 遍历集合时抛 ConcurrentModificationException
现象:吃掉一条鱼或生成新鱼时,控制台报并发修改异常,游戏卡死。 原因:在for-each遍历enemyList的过程中直接add或remove。 解决:删除用Iterator.remove(),或者把要删的鱼先放进临时列表,遍历结束后统一removeAll。生成新鱼也放到遍历之外。
5.5 多 JDK 环境下编译能过、运行报版本错误
现象:IDEA 里编译无报错,一运行就UnsupportedClassVersionError。 原因:javac和java指向不同版本的 JDK,编译产物版本高于运行环境。 解决:java -version和javac -version对齐;IDEA 里 Project SDK、Module SDK、Language Level 三处统一;命令行运行时显式用JAVA_HOME对应的java。
6. 进阶改造:把这份源码变成你自己的练手底座
跑通只是起点,这份源码真正的价值在于它足够小、足够完整,适合拿来练手。我一般会按三个方向改:第一是换资源,把images里的鱼图换成自己找的素材,顺便练一下图片缩放和透明通道处理;第二是加玩法,比如加个"加速道具"或"无敌三秒",这需要你在GamePanel里加状态字段、在paintComponent里加绘制、在主循环里加计时;第三是抽逻辑,把碰撞和计分从面板里拆成独立的GameLogic类,让JPanel只负责画,这样以后想换渲染方式(比如换成 JavaFX)时不用重写规则。
验证改造是否成功,有个简单办法:把主循环间隔从 30ms 改成 100ms,如果游戏逻辑依然正确、只是变慢,说明你的更新和绘制是解耦的;如果一改就乱,说明逻辑里混了帧率假设,得把"每帧移动多少"改成"每秒移动多少"再乘以时间差。
| 改造方向 | 涉及文件 | 关键注意点 |
|---|---|---|
| 换贴图 | 资源目录 + 加载代码 | 保持图片尺寸比例,透明通道用 PNG |
| 加道具 | GamePanel + 实体类 | 状态字段要能被 resetGame 清理 |
| 抽逻辑 | 新建 GameLogic 类 | 面板只保留绘制,逻辑不碰 Graphics |
| 调难度 | 生成与速度计算 | 用时间或分数驱动,别写死帧数 |
有个习惯我保持了很久:每次改完碰撞相关代码,都会故意把玩家鱼调到和敌鱼一样大,看边界情况是判吃还是判死。这个"等大"的临界点最容易暴露>=和>写反的问题,也是我早期翻车最多的地方。希望这份拆解能帮你少走点弯路,把这份java大鱼吃小鱼.zip真正用起来。
本文还有配套的精品资源,点击获取