☰
植物大战僵尸 Java+Swing 实战:从游戏循环到对象池与状态机设计
2026/10/2 2:40:52 网站建设 项目流程

简介:这是一份基于Java+Swing开发的“植物大战僵尸”游戏复刻项目,适合Java初学者、游戏编程爱好者作为综合练手案例,用于理解桌面应用界面构建与游戏循环设计。资源共148个文件,约3.77MB,包含19个Java源文件、38个class编译文件、64个png与24个jpg图片素材,以及classpath、project等工程配置,基本覆盖从代码编写、界面绘制到资源加载的完整项目结构。项目涉及多线程调度(如子弹移动、僵尸移动线程)、碰撞检测、游戏模式管理、存档读写等典型游戏开发环节,通过Controller、Plant、Grid等核心类可直观学习对象划分与交互方式,图片素材可直接替换以定制界面。结合已有2336人学习的规模,这是一份便于对照调试、性价比较高的入门级游戏源码参考。

1. 植物大战僵尸 Java+Swing:一个把 Java 基础课变成实战项目的切入点

很多人第一次听到"植物大战僵尸 java+swing"这个组合时,第一反应是"Swing 不是做桌面表单的吗,拿来做游戏会不会太勉强"。实际上这个选题恰恰是 Java 课程设计里最常见也最适合练手的项目:Swing 负责界面渲染和事件响应,Java 核心语法负责游戏逻辑,两者拼起来就是一个完整的"面向对象 + 多线程 + 集合框架"实战现场。它适合两类人:一类是刚学完 Java 基础、想找一个不依赖游戏引擎就能跑通的项目做课程设计;另一类是工作后用 Java 做业务系统做腻了,想回头补一下 GUI 编程和游戏循环这类基本功的老手。这个项目能解决的问题很具体:怎么用 Swing 的定时重绘机制做游戏循环,怎么用集合管理大量游戏实体,怎么在单一线程模型下让植物、僵尸、子弹各自推进且不互相干扰。难点不在"写得出功能",而在"让画面不闪、逻辑不卡、对象不泄漏",这才是本篇要拆开讲的重点。

2. Swing 游戏的主循环与双缓冲:先让画面不闪,再说游戏性

2.1 Swing 绘制机制:paintComponent 与重绘的本质

Swing 组件绘制都集中在paintComponent(Graphics g)方法里,游戏画面本质上就是在这个方法里不断重画。JPanel被调用repaint()后,Swing 的事件分发线程(EDT)会安排一次重绘,但我们不直接调用paintComponent,而是通过repaint()发出请求,由 Swing 内部合并绘制请求,避免每次都立刻重画。

常见做法是:自定义一个GamePanel继承JPanel,重写paintComponent,在其中遍历所有植物、僵尸、子弹并绘制到画布上。游戏循环放一个 60 帧的定时器里,每次 tick 更新游戏状态并触发repaint()。这样做的原因是 Swing 不是线程安全的,所有 UI 操作必须在 EDT 上执行,而Swing Timer的回调正是在 EDT 上触发,天然规避了跨线程访问组件的风险。

public class GamePanel extends JPanel { private Game game; public GamePanel(Game game) { this.game = game; setPreferredSize(new Dimension(900, 600)); setBackground(new Color(0, 0, 0)); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 绘制背景,避免上一帧残留 drawBackground(g); // 绘制植物、僵尸、子弹 game.draw(g); // 绘制阳光数量、冷却状态、波次信息 drawUI(g); } }

逻辑说明:super.paintComponent(g)这句不能省,它负责用背景色清空画布,否则上一帧画面会残留。game.draw(g)是所有实体的统一绘制入口,把逻辑与绘制解耦。参数上,GamePanel持有的Game对象是核心逻辑类,包含植物列表、僵尸列表、子弹列表和阳光计数,绘制时只读取状态,不修改状态。绘制与逻辑分离是后面所有扩展的前提。

2.2 用 Swing Timer 驱动游戏循环:60 帧下的 delay 取值

游戏循环有两种实现方式:一种是Swing Timer,一种是javax.swing.Timer之外的Thread + while(true)循环。课程设计里九成会用Swing Timer,原因是它直接在 EDT 上执行,不需要额外处理线程同步。但如果你打算做更复杂的逻辑,比如网络对战或后台 AI 计算,就应该把游戏逻辑放到独立线程,用volatile boolean控制循环,再通过SwingUtilities.invokeLater把绘制请求派发回 EDT。

我一般建议课程设计先用Swing Timer跑通全流程。60 帧对应 delay 约 16 到 17 毫秒,取 16 毫秒会略快,17 毫秒更接近标称 60fps。这个值不要设得太小,Swing 绘制能力有限,10 毫秒以下重绘频率超过实际处理能力反而会在 CPU 占用上白白消耗。

public class GameController { private Timer timer; private Game game; private GamePanel panel; public void start() { timer = new Timer(16, e -> { game.update(); // 逻辑更新 panel.repaint(); // 请求重绘 }); timer.start(); } public void stop() { if (timer != null) { timer.stop(); } } }

逻辑说明:game.update()是每秒固定调用约 60 次,所有游戏实体的位置、冷却、碰撞检测都在这里完成。panel.repaint()只是请求重绘,Swing 会在空闲时执行实际的paintComponent。参数说明:Timer的 delay 是 tick 间隔,单位毫秒,16 意味着大约 62.5 ticks/秒;如果机器性能较差,可调为 20 到 25 毫秒换来更低的 CPU 占用。另一个隐式参数是Timer的初始延迟setInitialDelay,默认与 delay 相同,游戏启动瞬间会有一个 tick 立即触发,这符合我们的预期,不需要特殊处理。如果游戏里有"准备阶段"或"开场动画",可以在构造时传入initialDelay来控制首个 tick 的延迟。

2.3 双缓冲与脏矩形:避免画面闪烁的三个做法

Swing 的JPanel默认已经启用了双缓冲,在大多数情况下你不需要自己实现。但如果你直接绘制大图片或频繁更新整块区域,仍然可能出现闪烁或撕裂。常见的三个处理手法:第一,不要在paintComponent里做耗时操作,比如磁盘 IO、图片缩放、复杂计算,这些操作会阻塞 EDT 导致画面卡顿;第二,背景图预先加载并缩放到合适尺寸,不要每次绘制都重新缩放;第三,如果画面局部变化频繁,可以只重绘变化区域,用repaint(x, y, width, height)代替无参repaint()。

// 只重绘发生变化的小区域,减少绘制负担 panel.repaint(plant.getX() - 10, plant.getY() - 10, plant.getWidth() + 20, plant.getHeight() + 20); // 如果画面大范围变动(如新一波僵尸进场),再全量重绘 panel.repaint();

逻辑说明:repaint(x, y, w, h)是重载方法,参数是裁剪区域的坐标和尺寸,Swing 只对该区域进行绘制。x是左上角横坐标,y是左上角纵坐标,width和height是区域宽高。这里向外扩展 10 像素是为了覆盖实体移动时的边缘残影。参数说明:裁剪区域如果设置得比实际变化范围小,画面边缘会出现拖影;比实际范围大则失去性能优化意义。实践中最稳的做法是:普通 tick 用全量repaint(),仅当游戏实体数量非常多且画面大面积静止时才考虑脏矩形优化,否则优化收益远低于调试成本。Swing 的双缓冲机制由 UI 管理器对JPanel默认开启,不需要手动创建BufferedImage再整体绘制,除非你要做复杂的滚动地图或画面特效。

3. 植物与僵尸的碰撞与调度:把实体建模成可维护的对象池

3.1 用 Tiles 锁定单元格:从绝对坐标到网格坐标

植物大战僵尸的场地天然是网格:植物种在格子(Cell)里,僵尸在行上行走。如果直接用像素坐标判断"该种哪里"、"僵尸在哪一行",代码很快会变成一堆魔法数字。更合理的做法是引入网格坐标系统:把屏幕划分成固定行列的格子,每个格子有确定的像素矩形。点击位置通过坐标换算确定落在哪个格子,再对格子进行操作。

一个常见的参数配置是 9 列 5 行,场地从 (80, 100) 开始,每个格子宽 80、高 100。这样可以算出:列索引 =(x - offsetX) / cellWidth,行索引 =(y - offsetY) / cellHeight。注意要加上边界判断,点击位置在场地外时索引会越界。

public class GridMap { public static final int COLS = 9; public static final int ROWS = 5; public static final int CELL_WIDTH = 80; public static final int CELL_HEIGHT = 100; public static final int OFFSET_X = 80; public static final int OFFSET_Y = 100; public static Point getCellIndex(int mouseX, int mouseY) { if (mouseX < OFFSET_X || mouseY < OFFSET_Y) { return null; } int col = (mouseX - OFFSET_X) / CELL_WIDTH; int row = (mouseY - OFFSET_Y) / CELL_HEIGHT; if (col >= COLS || row >= ROWS) { return null; } return new Point(col, row); } public static Rectangle getCellBounds(int col, int row) { return new Rectangle(OFFSET_X + col * CELL_WIDTH, OFFSET_Y + row * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT); } }

逻辑说明:getCellIndex返回值是java.awt.Point,这里用x存储列索引、y存储行索引,避免了新建类。坐标换算的核心是"减去偏移量再除以格子尺寸",整数除法自动向下取整。参数说明:CELL_WIDTH = 80和CELL_HEIGHT = 100是典型取值,具体尺寸要跟背景图对齐。如果你的背景图是 900x600 且左侧有信息栏,OFFSET_X应等于信息栏宽度,OFFSET_Y应等于顶部工具栏高度。如果发现点击格子与实际种植位置偏差大,通常是OFFSET_X/OFFSET_Y与背景图错位,而不是公式错误。

3.2 对象池管理子弹与僵尸:避免频繁 new 和 GC 抖动

僵尸游戏里,子弹的创建和销毁非常频繁。豌豆射手每秒发射约 1.5 颗子弹,每颗子弹存活时间不到 2 秒,如果每次发射都new Bullet(),销毁时再从链表移除,短时间内会产生大量短命对象,触发频繁 GC,导致游戏卡顿。课程设计规模下不一定能肉眼观察到,但这是排查卡顿的常见原因。

我用ArrayList存活动实体,把待移除对象标记后统一清理,避免在遍历时直接删除。对子弹这种高频创建的对象,可以用一个空闲池来复用。

public class BulletPool { private final Deque<Bullet> idlePool = new ArrayDeque<>(); public Bullet obtain(int x, int y, int row) { Bullet b = idlePool.poll(); if (b == null) { b = new Bullet(); } b.reset(x, y, row); return b; } public void recycle(Bullet b) { idlePool.push(b); } }

逻辑说明:obtain先从空闲池取对象,池空则新建;取到对象后调用reset恢复初始状态。recycle把不再使用的子弹放回池中,以便后续复用。参数说明:Deque在这里用ArrayDeque,push/poll 是栈操作,取出的总是最近回收的对象,逻辑简单。这个池的价值在子弹数量超过 300 时开始体现,GC 压力明显下降。一个容易踩坑的点是:回收时必须重置对象的引用和状态,比如子弹的targetZombie引用、damage数值、存活标记,否则复用时会产生"子弹带着上一轮的伤害值继续打人"这类怪异问题。

僵尸对象本身创建频率不高,但数量多时同样要注意:僵尸死亡动画结束后,应当从活动列表移除,而不是仅仅标记为死亡。很多半成品项目只把僵尸标记为"死",却忘了把它从列表中移除,结果就是僵尸越来越多,update 遍历越来越慢,最终卡死。

public void update(List<Zombie> zombies, List<Bullet> bullets) { Iterator<Zombie> it = zombies.iterator(); while (it.hasNext()) { Zombie z = it.next(); z.update(bullets); if (z.isDead() && z.isRemovable()) { it.remove(); // 死亡动画播放完毕,安全移除 } } }

逻辑说明:isRemovable表示死亡动画已经播完,此时才可以移除。直接remove会中断遍历,所以必须用Iterator。参数说明:如果僵尸死亡到移除之间需要播放动画,可以设一个deathTimer,在Zombie内部累计帧数,达到阈值后返回isRemovable为 true。若不关心死亡动画,可以在isDead()为 true 时立即移除,但要注意Zombie内的update不要再去访问已经移除的对象。

3.3 状态机控制僵尸行为:行走、攻击、死亡三态迁移

僵尸的行为不是"一路向前走",而是由状态驱动:正常行走、攻击植物、被减速、死亡。用布尔标志位写的话,代码会膨胀成if (walking && attacking && frozen)这类的组合爆炸。一个简单的枚举状态机就能把行为整理清楚。

public enum ZombieState { WALKING, ATTACKING, DYING, REMOVED }

僵尸update里根据状态决定行为:WALKING时向前移动并检测前方是否有植物;ATTACKING时停止移动、播放攻击动画、按攻击间隔对植物造成伤害;DYING时播放死亡动画并转入REMOVED。状态之间的迁移条件要仔细设计,尤其是"攻击中植物被干掉"要能自动回到WALKING。

public void update(Game game) { switch (state) { case WALKING: x -= speed; Plant target = game.getPlantAt(x - attackRange, row); if (target != null) { state = ZombieState.ATTACKING; this.target = target; } break; case ATTACKING: // 目标丢失时回到行走 if (target == null || target.isDead()) { state = ZombieState.WALKING; break; } attackTimer--; if (attackTimer <= 0) { target.takeDamage(attackDamage); attackTimer = attackInterval; } break; case DYING: deathTimer--; if (deathTimer <= 0) { state = ZombieState.REMOVED; } break; default: break; } }

逻辑说明:WALKING每次把x减去speed,然后检测前方attackRange范围内是否有植物。一旦锁定植物,进入ATTACKING并缓存目标。ATTACKING时不再移动,按attackInterval帧数执行一次攻击。参数说明:attackRange通常取值 20 到 40 像素,attackInterval在 30 到 60 帧之间,attackDamage设 20 到 30 比较合理。注意target在僵尸进入攻击前应该保持为 null,攻击结束后也要置空,否则僵尸会追着已经消失的目标进入死循环。

这套状态机的好处是后续加"减速"或"冰冻"时不用改动主干逻辑,只需要在WALKING分支里判断是否被减速即可。状态机的核心价值在于把"这帧该干什么"变成确定性决策,而不是散落在各种 if 里。

4. 避坑:Swing 游戏最常见的五处翻车现场

4.1 界面闪烁与残留残影

现象:游戏运行时画面闪烁严重,移动的植物和僵尸身后拖着残影。

原因:paintComponent里没有调用super.paintComponent(g),旧画面没有被清空,新画面叠在旧画面上。或者主循环更新逻辑和绘制逻辑在同一个 tick 内有序执行但绘制频率过高,低于屏幕刷新率时会产生视觉撕裂。

解决:第一,paintComponent方法第一行务必调用super.paintComponent(g);第二,标准 60 帧用 16 毫秒 timer 即可,不要为了流畅而无限调小 delay;第三,背景图如果很大,加载后用BufferedImage缓存缩放结果,不要每帧重新drawImage缩放。Swing 默认为JPanel开启双缓冲,不需要额外再创建离屏画布。

4.2 遍历集合时 ConcurrentModificationException

现象:游戏运行几分钟后崩溃,控制台抛出ConcurrentModificationException,位置在update中遍历僵尸列表时。

原因:在for (Zombie z : zombies)循环体内直接调用zombies.remove(z),或逻辑线程与 EDT 同时访问同一个集合。Swing Timer的回调在 EDT 上执行,不存在多线程问题,但如果在game.update()内部移除元素,就会触发这个异常。

解决:所有"遍历中删除"统一改用Iterator的remove方法,或先收集待删除对象再统一移除。多线程场景下,不要在 EDT 之外的线程修改任何 Swing 相关组件和集合,必须通过SwingUtilities.invokeLater包一层。

4.3 网格点击错位与"种不下去"

现象:鼠标点击后总是选中错误的格子,或者点击合法的格子却提示无法种植。

原因:坐标换算时没有考虑OFFSET_X和OFFSET_Y,或者网格的CELL_WIDTH与背景图上格子的实际宽度不一致。另一个常见原因是没有做边界校验,点击场地外区域时算出负数索引或超出行列数的索引,存入数组时抛ArrayIndexOutOfBoundsException。

解决:把GridMap里的常量打印出来,和背景图逐一对比。调试时可以在GridMap.getCellIndex返回 null 的分支打印一条日志,确认点击位置落在哪个坐标区间。种植前检查格子是否已有植物,以及阳光数是否足够,不要依赖 UI 层的置灰逻辑,逻辑层必须再校验一次。

4.4 僵尸越来越多,性能越来越差

现象:游戏进行到中后期,僵尸数量超过 100 后明显卡顿,或者每波僵尸刷新后帧率从 60 掉到 20 多。

原因:比较隐蔽的是僵尸死亡后没有被移出活跃列表,update 每帧还在为所有"死掉"的僵尸做状态判断。另一个原因是子弹对象没有回收,每秒产生大量短命对象导致 GC 频繁。

解决:按 3.2 节的做法,用Iterator移除僵尸,用BulletPool复用子弹。如果还卡,检查paintComponent里是否每帧都创建了新的Font、Color或ImageIcon,这些对象会被丢弃并在下一次 GC 时回收,频繁创建会直接拖慢绘制速度。把ImageIcon的创建移到类初始化阶段,绘制时直接引用。

4.5 游戏暂停或切窗口后逻辑还在跑

现象:切出窗口或点击暂停按钮后,游戏还在后台推进,回来发现植物已经种满、阳光已经爆棚。

原因:Timer没有在窗口失活或暂停时停止。Swing Timer启动后无条件持续触发,直到调用stop()。

解决:监听窗口事件,在窗口最小化或失去焦点时调用timer.stop(),恢复时调用timer.start()。暂停按钮同理,但要注意暂停时记录当前游戏时间,恢复后重新设置Timer的初始 delay,避免暂停损耗被当作游戏时间。

frame.addWindowListener(new WindowAdapter() { @Override public void windowIconified(WindowEvent e) { controller.stop(); } @Override public void windowDeiconified(WindowEvent e) { controller.start(); } });

逻辑说明:windowIconified在窗口最小化时触发,windowDeiconified在恢复时触发。controller是GameController实例,start/stop方法内部管理Timer。参数说明:直接调用timer.stop()不会丢失游戏状态,恢复后Timer从头开始计时,所以不需要在恢复时做额外的状态补偿,除非你希望精确计算游戏内耗时。

5. 资源与扩展:从能跑到能看的最后一公里

5.1 图片与音频资源的加载路径规范

Swing 项目里最常见的资源加载错误是使用绝对路径,比如new ImageIcon("D:\\images\\sunflower.png")。换一台机器代码就炸,交课程设计时评阅老师也会直接扣分。正确做法是把资源放进src/main/resources(Maven 结构)或与源码同级的resources目录,通过类加载器读取。

图片加载必须做一次缓存,避免每次创建植物都重新读文件。一个全局的ResourceManager用Map<String, Image>保存已加载图片,重复请求时直接返回缓存。

public class ResourceManager { private static final Map<String, Image> IMAGE_CACHE = new HashMap<>(); public static Image loadImage(String path) { Image image = IMAGE_CACHE.get(path); if (image != null) { return image; } try { URL url = ResourceManager.class.getResource(path); if (url == null) { throw new IllegalArgumentException("资源不存在: " + path); } image = new ImageIcon(url).getImage(); IMAGE_CACHE.put(path, image); return image; } catch (Exception e) { e.printStackTrace(); return null; } } }

逻辑说明:getResource是相对 classpath 查找,资源路径以/开头表示 classpath 根路径。ImageIcon的getImage返回Image对象,只加载一次,后续绘制复用。参数说明:注意getResource返回的路径中,目录结构要和构建输出一致。如果你用 IDE 直接运行,资源目录要标记为 "Resources Root",否则运行时会找不到文件,表现就是图片不显示但程序不报错。加载音频同理,Java 原生支持 WAV 格式,AudioSystem.getClip()打开,循环播放的背景音乐要控制好资源释放,否则第二次进入游戏时可能播放重叠。

5.2 让游戏"像"游戏:冷却、阳光、特殊植物

"能跑"和"能玩"之间差着三个东西:植物冷却时间、阳光数值平衡、特殊植物机制。冷却时间的实现是每个植物格子记录"上次种植时间"或"剩余冷却 tick",每次update减一,冷却未归零时禁止种植。这个逻辑要放在Game类里,而不是 UI 层。

public class PlantSlot { public static final int COOLDOWN = 300; // 300 tick = 约5秒 private int remainingCooldown; public boolean canPlant() { return remainingCooldown <= 0; } public void plant() { if (!canPlant()) { return; } remainingCooldown = COOLDOWN; } public void update() { if (remainingCooldown > 0) { remainingCooldown--; } } }

逻辑说明:COOLDOWN常量表示冷却总 tick 数,300 tick 配合 60 帧每秒正好 5 秒。plant方法里先做校验再重置冷却,避免 UI 上绕过限制。参数说明:不同植物的冷却时间应该不同,向日葵可以短一些,樱桃炸弹应长一些。更好的做法是让PlantSlot根据植物类型读取冷却配置,比如从Map<PlantType, Integer>里取,而不是写死在常量里。阳光数值平衡没有统一标准,但课程设计要求不高的话,可以参考一个简单模型:向日葵生产阳光间隔 10 秒左右,普通豌豆射击间隔 1.3 秒到 1.5 秒,僵尸血量 200 左右、移动速度约每秒 40 像素。按这些基础参数测试,第一波僵尸在开局 60 秒后刷出,玩家有足够时间建立防线,又不至于太无聊。

5.3 植物类型与行为差异的实现:策略模式

不同植物的行为本质上就是三个差异点:生产阳光还是射击子弹、攻击间隔、攻击范围。用switch (plantType)散落在各处,后期加一个新植物要改四五处代码。更整洁的做法是把"行为"抽象成接口或抽象类,每种植物一个子类。

常见的课程设计实现是:Plant基类持有位置、血量和冷却状态,子类覆写update。Sunflower生产阳光,Peashooter在行方向上搜索僵尸并发射子弹,WallNut不攻击只提供高血量。创建植物时用简单工厂根据类型返回对应子类实例。这个模式的成本是类数量变多,但后续新增植物只需要加一个子类,不需要动Game类里的逻辑分支。如果你只打算做五个以内的植物种类,用switch也能应付,但想往简历上写"可扩展设计"的话,策略模式明显更有说服力。框架代码不多,核心就是Plant的update用多态分发。

6. 性能验证与后续演进:把 Swing 项目做成简历里的作品集

项目跑通之后,沉下心做两件事:验证性能、留下可持续迭代的台阶。

性能验证不用上 JProfiler 那类重型工具,三个土办法足够撑起课程设计答辩:第一,在GameController里统计最近 120 帧的平均耗时,打印到控制台或画在画布左上角;第二,观察游戏运行五分钟后的 GC 日志,确认没有频繁 Full GC。在 JVM 启动参数加-Xlog:gc就能看到G1 Young Generation Pause的频次,如果每秒超过两次,说明对象创建太疯狂,优先查子弹和临时字符串;第三,做一个"极限测试模式",每次刷 200 个僵尸挤满屏幕,看帧率是否还能维持 25 帧以上,低于 20 帧说明绘制层有瓶颈。

public class FpsMeter { private long lastTime = System.nanoTime(); private double avgFrameTime = 0; private int frameCount = 0; public void tick() { long now = System.nanoTime(); long elapsed = now - lastTime; lastTime = now; avgFrameTime = avgFrameTime * 0.9 + elapsed * 0.1; frameCount++; if (frameCount % 120 == 0) { double fps = 1_000_000_000.0 / avgFrameTime; System.out.printf("avg fps: %.1f, frameTime: %.2fms%n", fps, avgFrameTime / 1_000_000); } } }

逻辑说明:avgFrameTime使用 EMA 平滑,权重取 0.9 保留历史、0.1 吸收新值,避免单帧抖动影响判断。每次 tick 更新一次,每 120 帧打印一次。参数说明:System.nanoTime()是纳秒级时间戳,适合做帧耗时统计;1_000_000_000.0 / avgFrameTime把纳秒耗时转成帧率。如果打印结果显示平均耗时高于 35 毫秒,优先优化绘制;高于 60 毫秒则逻辑更新也可能有问题,需要逐步注释掉game.update()里的分支定位瓶颈。

后续演进有两条主线,我个人的习惯是都做一点:存档系统。植物大战僵尸没有存档就不是完整体验,但这恰恰是 Swing 项目里最容易被忽略的部分。用Properties保存关卡进度、阳光数量和已解锁植物,读档时重新构建游戏状态。做法很笨但很可靠,不引入序列化和 JSON 库。另一个方向是做"无尽模式",僵尸波次数据从文件读取,用List<List<ZombieSpawnInfo>>描述每波僵尸的种类、数量和间隔,游戏逻辑读取配置驱动波次,这样关卡设计彻底与代码解耦。视图层和数据层分离后,你想把Swing换成JavaFX,只需要重写GamePanel和事件处理,Game和Zombie可以原封不动搬走。

我自己的习惯是:把一个 Swing 游戏项目做成"程序能跑、逻辑清晰、设计上有话讲"三个层次。第一个层次靠的是把代码写完跑通,第二个层次靠的是一致性好、命名规范、集合使用正确,第三个层次靠的是状态机和策略模式这类设计思想。如果答辩时老师问"为什么用状态机而不是 if",你能说出"因为行走和攻击的迁移条件是互斥的,用 if 会产生组合爆炸",这套项目就已经超出课程设计均值了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询