首页 / 资讯中心 / 文章详情

Java Swing实战:大鱼吃小鱼源码拆解与二次开发指南

Java Swing实战:大鱼吃小鱼源码拆解与二次开发指南 ★ FEATURED ARTICLE
简介这份资源是面向Java初学者与Swing游戏开发入门者的「大鱼吃小鱼」完整项目源码包围绕JFrame与JPanel搭建游戏窗口、鼠标事件监听驱动大鱼跟随、Graphics绘图与Timer重绘实现动画、游戏状态管理与图片资源加载等核心环节展开可帮助读者理解Java GUI游戏的基本架构与设计思路。压缩包共1171个文件以715张png、27个gif、20张jpg等图片素材为主辅以java源码、class字节码、xml与properties配置及工程元数据文件整体约61.41MB目录结构清晰便于按模块查阅与调试。目前已有1454人学习下载。代码注释较为详尽读者可据此掌握事件处理、图像渲染与状态切换的实现方式并在此基础上扩展鱼种、障碍物与关卡设计适合作为课程设计或二次开发的实践底本。1. 从一份 Java 小游戏源码说起大鱼吃小鱼能跑出什么前阵子有个做后端的朋友想练手图形界面翻出一份 Java 写的「大鱼吃小鱼」小游戏源码问我这玩意儿除了玩还能学到什么。我拆完发现它其实是个被低估的 JFrame JPanel 实战样本窗口生命周期、双缓冲绘图、键盘事件监听、定时器驱动的游戏循环、碰撞检测、对象池式的鱼群管理全塞在几百行里。对刚学完 Java 语法、想搞懂「一个能动的程序到底怎么组织」的人来说它比一堆控制台练习题有用得多。这份源码适合两类人一是想照着跑通第一个图形化项目的初学者二是想拿它当骨架改出自己小游戏的开发者。下面我按「能干什么、怎么跑、怎么改」的顺序把这份源码拆开讲。2. 项目结构与技术选型JFrame 窗口加 JPanel 画布的分工2.1 为什么是 Swing 而不是别的这份源码用的是 Java 标准库自带的 Swing核心就两个类JFrame负责窗口外壳标题栏、关闭按钮、尺寸JPanel负责窗口里的画布所有鱼、背景、分数都画在这块画布上。选 Swing 的理由很实在——零额外依赖JDK 装好就能编译运行不用配 Maven 仓库、不用管前端构建工具。对教学和二次开发起步来说这是最省心的路径。游戏循环没有用多线程硬怼而是用javax.swing.Timer定时触发重绘。常见做法是每 16 毫秒触发一次约 60 帧在actionPerformed里更新所有鱼的位置、判断碰撞、再调repaint()。这个设计的好处是更新逻辑跑在事件调度线程EDT上和 Swing 的绘图线程天然一致省掉了一堆线程同步的麻烦。代价是如果某一帧逻辑太重会卡顿但对这个体量的游戏完全够用。2.2 目录与类的职责划分源码解压后是一个标准的 Java 工程结构没有用构建工具直接javac编译即可。典型布局如下BigFishEatSmallFish/ ├── src/ │ ├── Main.java # 程序入口创建窗口 │ ├── GameFrame.java # 继承 JFrame装配游戏面板 │ ├── GamePanel.java # 继承 JPanel游戏主循环与绘图 │ ├── Fish.java # 鱼的基础属性与行为 │ ├── PlayerFish.java # 玩家控制的鱼继承 Fish │ ├── EnemyFish.java # 敌方鱼继承 Fish │ └── GameConfig.java # 常量配置窗口尺寸、速度、初始大小 └── res/ └── images/ # 鱼、背景的图片资源这个划分的关键在于Fish作为基类抽出共性坐标 x/y、宽高、速度、图片、移动方法PlayerFish和EnemyFish各自扩展。玩家鱼多一个「跟随鼠标或键盘移动」的逻辑敌方鱼多一个「从屏幕边缘生成、单向游动、出界回收」的逻辑。GameConfig把魔法数字集中管理改难度时只动一个文件这点在二次开发时特别省事。2.3 游戏主循环的骨架GamePanel是整个项目的心脏它同时实现了ActionListener接收定时器事件和KeyListener接收键盘输入。核心骨架大致是这样public class GamePanel extends JPanel implements ActionListener, KeyListener { private Timer timer; private PlayerFish player; private ListEnemyFish enemies new ArrayList(); private int score 0; public GamePanel() { setPreferredSize(new Dimension(GameConfig.WIDTH, GameConfig.HEIGHT)); setBackground(Color.CYAN); setFocusable(true); // 关键让面板能接收键盘事件 addKeyListener(this); initGame(); timer new Timer(16, this); // 约 60 帧 timer.start(); } private void initGame() { player new PlayerFish(GameConfig.WIDTH / 2, GameConfig.HEIGHT / 2); spawnEnemy(); // 初始生成若干敌方鱼 } Override public void actionPerformed(ActionEvent e) { updateGame(); // 更新位置、碰撞、生成 repaint(); // 触发 paintComponent } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 先画背景再画敌方鱼最后画玩家鱼层级顺序 for (EnemyFish f : enemies) f.draw(g); player.draw(g); g.setColor(Color.BLACK); g.drawString(Score: score, 10, 20); } }这里有几个参数值得说清楚。Timer(16, this)的 16 是毫秒延迟数值越小帧率越高但 CPU 占用也越高改成 33 就是 30 帧画面会明显变顿。setFocusable(true)这行是血泪经验——不加的话面板拿不到键盘焦点按方向键毫无反应新手最容易在这里翻车。paintComponent里必须先调super.paintComponent(g)否则上一帧的画面不会被清掉鱼会拖出一条残影。3. 核心模块拆解鱼的对象模型、碰撞检测与输入响应3.1 Fish 基类与继承体系Fish基类把「一条鱼」的公共状态收拢在一起子类只关心差异部分。一个合格的基类大概长这样public abstract class Fish { protected int x, y; // 左上角坐标 protected int width, height; // 碰撞盒尺寸 protected int speedX, speedY; protected Image image; public Fish(int x, int y, int width, int height) { this.x x; this.y y; this.width width; this.height height; } public abstract void move(); // 子类各自实现移动策略 public void draw(Graphics g) { g.drawImage(image, x, y, width, height, null); } // 矩形碰撞检测供游戏面板调用 public Rectangle getBounds() { return new Rectangle(x, y, width, height); } }move()声明为抽象方法是因为玩家鱼和敌方鱼的移动逻辑完全不同玩家鱼根据按键状态改变速度敌方鱼则是固定方向匀速游动。getBounds()返回Rectangle后面碰撞检测直接调Rectangle.intersects()比手写坐标比较清晰得多。width/height同时承担「绘制尺寸」和「碰撞盒尺寸」两个角色简单直接但如果图片有透明边碰撞会偏大这是后面避坑章节要说的点。3.2 玩家鱼的控制与边界约束玩家鱼继承Fish核心是响应键盘并把自己限制在窗口内public class PlayerFish extends Fish { private boolean up, down, left, right; public PlayerFish(int x, int y) { super(x, y, 60, 40); this.speedX 5; this.speedY 5; } public void setDirection(boolean up, boolean down, boolean left, boolean right) { this.up up; this.down down; this.left left; this.right right; } Override public void move() { if (left) x - speedX; if (right) x speedX; if (up) y - speedY; if (down) y speedY; // 边界钳制防止鱼游出画布 x Math.max(0, Math.min(x, GameConfig.WIDTH - width)); y Math.max(0, Math.min(y, GameConfig.HEIGHT - height)); } }setDirection由GamePanel的keyPressed/keyReleased调用把「按键状态」和「移动计算」解耦。speedX/speedY是每帧位移量5 意味着 60 帧下每秒移动 300 像素想调手感就改这两个值。边界钳制用Math.max/min一行搞定注意减的是width/height而不是 0否则鱼会有一半身子卡在窗口外。3.3 敌方鱼的生成、游动与回收敌方鱼的关键是「源源不断但不无限堆积」。常见做法是在GamePanel里维护一个ListEnemyFish每次更新时检查是否需要生成新鱼同时把游出屏幕的鱼移除private void updateEnemies() { IteratorEnemyFish it enemies.iterator(); while (it.hasNext()) { EnemyFish f it.next(); f.move(); if (f.getX() -f.getWidth() || f.getX() GameConfig.WIDTH) { it.remove(); // 出界回收避免列表无限增长 } } if (enemies.size() GameConfig.MAX_ENEMIES Math.random() 0.02) { spawnEnemy(); // 概率生成控制密度 } }用Iterator而不是增强 for 循环是因为遍历中要remove用 for-each 会抛ConcurrentModificationException这是新手高频翻车点。Math.random() 0.02是每帧 2% 的生成概率配合MAX_ENEMIES上限双重控制既保证画面不空也不至于鱼满为患。生成位置一般在屏幕左侧或右侧外方向随机这样鱼是「游进来」而不是「凭空出现」。3.4 碰撞检测与吃鱼判定碰撞检测放在updateGame里玩家鱼和每条敌方鱼做矩形相交判断再根据体型决定谁吃谁private void checkCollisions() { Rectangle playerBox player.getBounds(); IteratorEnemyFish it enemies.iterator(); while (it.hasNext()) { EnemyFish f it.next(); if (playerBox.intersects(f.getBounds())) { if (player.getWidth() f.getWidth()) { // 玩家更大吃掉加分并长大 it.remove(); score 10; player.grow(2); } else { // 玩家更小游戏结束 gameOver(); return; } } } }判定依据是宽度比较player.getWidth() f.getWidth()时算玩家赢。grow(2)让玩家鱼每次吃鱼宽度加 2形成「越吃越大、能吃的鱼越来越多」的正反馈。gameOver()里通常停掉timer并弹一个对话框或切换到结束画面。这里要注意return的位置——一旦判定失败就立刻退出避免同一帧继续处理后面的鱼导致状态错乱。4. 跑起来与调参编译、资源路径与难度手感4.1 从零编译运行的完整步骤这份源码没有用 Maven 或 Gradle走的是最朴素的javacjava流程。假设 JDK 已经装好8 及以上都行在项目根目录执行# 1. 编译所有源文件到 out 目录 javac -encoding UTF-8 -d out src/*.java # 2. 运行注意把资源目录带上 classpath java -cp out:res MainWindows 下 classpath 分隔符是分号命令要改成java -cp out;res Main。-encoding UTF-8不能省源码里有中文注释或字符串时不指定编码在部分系统上会编译报错。-d out把.class文件统一输出到out目录保持源码目录干净。如果图片资源是用getClass().getResource(/images/fish.png)这种方式加载的那res目录要作为 classpath 的一部分且代码里的路径要以/开头相对于 classpath 根。如果用的是new ImageIcon(res/images/fish.png)这种相对路径那就要在项目根目录下运行路径才对得上。两种方式选一种别混用混用是资源加载失败的头号原因。4.2 关键参数怎么调GameConfig里集中了一批可调参数改难度基本就是改这里参数名典型值作用调大的效果WIDTH / HEIGHT800 / 600窗口画布尺寸视野更大鱼显得更小TIMER_DELAY16帧间隔毫秒数值越大越卡PLAYER_SPEED5玩家每帧位移操作更灵敏但更难控MAX_ENEMIES15同屏敌方鱼上限难度上升性能压力增大SPAWN_RATE0.02每帧生成概率鱼更密集GROW_STEP2每次吃鱼增长量成长更快节奏更爽调手感有个经验先把TIMER_DELAY固定住别动只调速度和生成率因为帧率一变所有基于「每帧位移」的参数含义都跟着变。想做出「前期轻松后期紧张」的曲线可以让SPAWN_RATE随分数递增而不是写死一个常数。4.3 图片资源加载的两种写法资源加载是这类项目最容易出问题的地方两种主流写法各有适用场景// 写法一classpath 相对路径配合 -cp out:res 使用 Image img new ImageIcon(getClass().getResource(/images/fish.png)).getImage(); // 写法二文件系统相对路径需在项目根目录运行 Image img new ImageIcon(res/images/fish.png).getImage();写法一更健壮打包成 jar 后依然有效因为资源被塞进了 classpath写法二在 IDE 里跑没问题一旦打成 jar 就找不到文件。我一般推荐写法一代价是编译运行命令里必须带res目录。如果getResource返回 null先检查路径开头的/有没有漏再检查res是否真的在 classpath 里这两步能解决九成的「图片加载不出来」。5. 避坑与排查那些让游戏跑不起来的细节5.1 按键没反应焦点没给对现象窗口出来了鱼也在动但按方向键玩家鱼纹丝不动。原因通常是JPanel没有拿到键盘焦点或者焦点被窗口里的其他组件抢走了。解决分两步构造面板时调setFocusable(true)并requestFocusInWindow()如果面板被加进了JFrame确保没有别的可聚焦组件比如按钮抢焦点。另一个隐蔽原因是用了KeyAdapter但没addKeyListener监听器压根没注册上。5.2 画面闪烁或拖影双缓冲与 super 调用现象鱼移动时拖出一条残影或者整个画面疯狂闪烁。原因一是paintComponent里忘了调super.paintComponent(g)旧帧没被清掉原因二是直接在JFrame上重写paint而不是在JPanel上重写paintComponent。Swing 的JPanel默认开启双缓冲只要老老实实在paintComponent里画、并且先调 super闪烁基本不会出现。如果还闪检查是不是在actionPerformed里直接调了paint(g)而不是repaint()。5.3 遍历时删元素抛异常并发修改现象游戏运行几秒后抛ConcurrentModificationException程序崩溃。原因是在增强 for 循环里对List做了remove。解决就是前面代码里用的Iterator用it.remove()而不是list.remove(f)。这个坑在「吃鱼」和「出界回收」两处都会踩凡是「边遍历边删」的场景一律上Iterator。5.4 鱼越玩越多导致卡顿回收逻辑缺失现象玩一两分钟后帧率明显下降任务管理器里 CPU 占用飙升。原因是敌方鱼只生成不回收List无限膨胀每帧要遍历的对象越来越多。解决是在updateEnemies里判断鱼是否完全游出屏幕注意是x -width或x WIDTH不是x 0出界就remove。同时用MAX_ENEMIES给同屏数量封顶双保险。5.5 碰撞判定「明明没碰到却死了」碰撞盒与图片不一致现象玩家鱼和敌方鱼看着还有距离却判定被吃。原因是width/height既当绘制尺寸又当碰撞盒而图片本身带透明边距实际可见的鱼比矩形小。解决有两个方向一是把碰撞盒单独抽出来比绘制尺寸略小一圈比如各缩 10%二是改用圆形或更精细的碰撞检测。对这个体量的游戏缩一圈矩形是最省事且效果够用的做法。6. 二次开发把这份骨架改成你自己的小游戏拆完这份源码我最想说的是它真正的价值不在「大鱼吃小鱼」这个玩法而在那套「窗口 定时器 对象列表 碰撞检测」的骨架。想改成别的游戏改动集中在三个地方。第一是对象模型把Fish换成Bullet、Enemy、PowerUp继承体系照搬move()各自实现getBounds()保留。第二是生成与回收策略spawnEnemy里的随机位置和概率换成按关卡配置的波次生成回收条件从「出界」扩展到「被击中」或「超时」。第三是状态机现在只有「进行中」和「结束」两个状态加一个GameState枚举MENU / PLAYING / PAUSED / GAME_OVER在actionPerformed里按状态分发更新逻辑暂停功能就是停掉 timer 再恢复。验证改造是否成功我习惯用一套最小回归清单改完先确认窗口能开、鱼能动、按键有响应、吃鱼加分、被吃结束这五条过了再谈新玩法。性能上盯两个指标——同屏对象数和每帧耗时前者靠上限封顶后者可以在actionPerformed首尾打时间戳超过 16 毫秒就说明逻辑太重得把碰撞检测从 O(n) 优化成空间分区或者干脆减少对象数。有个具体的技巧值得单独提把GameConfig里的常量改成从properties文件读取这样调难度不用重新编译改完重启就生效迭代速度快一个量级。我当初第一次改这类项目时把所有参数硬编码在各个类里调一次手感要改五六个文件、重新编译五六次折腾一下午才想明白该集中管理。从那以后我每次接手这种小游戏源码第一件事就是把散落的魔法数字收进一个配置类再谈功能。希望这份拆解能帮你少走那段弯路把这份源码真正跑起来、改起来。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站