恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Java实战:植物大战僵尸完整源码解析,覆盖面向对象与Swing游戏开发
首页
资讯中心
/
Java实战:植物大战僵尸完整源码解析,覆盖面向对象与Swing游戏开发
Java实战:植物大战僵尸完整源码解析,覆盖面向对象与Swing游戏开发
发布时间:2026/8/27 4:18:47
简介在Java学习进阶阶段如何将继承、多态、接口与集合等核心知识真正落地是许多开发者面临的共同困惑。以经典塔防游戏植物大战僵尸为实践载体通过Swing GUI框架完整还原游戏核心玩法深度示范了面向对象设计在游戏场景中的具体应用。项目涵盖状态机建模、AABB碰撞检测、多线程调度、资源加载与性能优化等关键技术从基础概念到工程实践层层递进。借助Maven构建与打包即可生成可独立运行的JAR包为游戏开发入门者提供了一套可复用的完整方案。本文基于实际源码展开既能帮助理解Java核心机制也能为简历项目积累优质素材。 事情得从去年秋天说起。当时我在整理自己这几年写过的项目代码翻到一个用Java写的植物大战僵尸突然就想起来当初为了把它跑起来光JLayer音频库就折腾了一整晚。很多人一开始学Java都会卡在“面向对象到底有什么用”这个问题上而植物大战僵尸恰好是一个能把继承、多态、接口、集合这些Java核心概念用明白的项目。这篇博文就围绕这个完整源码项目展开包括核心系统设计、类结构拆解、碰撞检测和状态机如何实现、图片素材怎样适配和打包以及我调试过程中踩过的坑和最终复盘出来的经验希望能给打算用Java做游戏练手的朋友提供一套可以直接参考的方案。1. 项目整体设计与思路拆解1.1 为什么选植物大战僵尸作为Java练手项目植物大战僵尸这个游戏有着天然的面向对象教学属性。它有两种主要角色植物和僵尸。植物又分为向日葵、豌豆射手、坚果墙、樱桃炸弹等不同类型僵尸也分为普通僵尸、路障僵尸、铁桶僵尸等。这些角色之间有公共行为又有各自特有行为非常适合用继承和多态来做。比如可以抽象出一个Plant基类所有植物继承这个类并重写行为方法。从Java核心技术覆盖角度看这个项目能带动的东西非常多集合框架管理场景中的植物列表、僵尸列表、子弹列表多线程游戏主循环刷新、阳光掉落动画、僵尸攻击动画文件IO读取配置文件、保存最高分GUI编程使用Swing或JavaFX构建游戏界面处理鼠标点击和键盘事件事件驱动模型按钮监听、点击放置植物、菜单操作我当时做这个项目时用的JDK是8因为Java 8的稳定性和生态兼容性最好Swing在Java 8下运行流畅而且市面上大多数Java面试对Swing也不做强制要求但作为理解事件驱动模型很有帮助。如果你用的是JDK 11或17这个项目也基本不需要改动就可以直接编译运行。1.2 项目目录结构和资源组织方式源码项目的目录组织方式直接决定了项目的可维护性。我的项目采用了标准的分层结构plants-vs-zombies/ ├── src/ │ ├── com/pvz/ │ │ ├── main/ # 游戏入口 │ │ ├── model/ # 实体类植物、僵尸、子弹、阳光 │ │ ├── view/ # 绘制与界面类 │ │ ├── controller/ # 事件处理与游戏控制器 │ │ ├── service/ # 游戏逻辑服务 │ │ └── util/ # 工具类 ├── resources/ │ ├── images/ # 图片素材 │ ├── audio/ # 音频素材 │ └── config/ # 关卡配置 ├── lib/ # 外部依赖库 └── out/ # 编译输出目录这个结构参考了MVC模式Model-View-Controller即模型-视图-控制器把数据、界面显示和业务逻辑分开。这样做的直接好处是写游戏逻辑的时候不用管界面怎么画改UI样式的时候也不会碰到业务代码。很多人写小游戏喜欢把所有逻辑堆在一个类里结果就是上千行的代码纠缠在一起后面改一个功能都要翻半天代码所以从项目开始就保持清晰的分层受益是长远的。1.3 为什么用Swing而不是JavaFX或游戏引擎这个话题我常被问到。现在市面上游戏开发主流是用Unity、Godot等引擎为什么我还要用Java的Swing来写一个游戏我的回答分三层第一Swing是JDK自带的GUI框架不需要额外安装任何东西这对Java学习者来说门槛最低。你用Unity做植物大战僵尸需要安装引擎、配置环境、学习引擎API这些都在和学习Java核心知识抢时间。第二Swing的绘图机制够直观。它基于AWT的Graphics2D类提供了drawImage、fillRect、drawOval等方法足够实现2D游戏的渲染。你可以直观地理解“一帧一帧重绘”的游戏渲染流程。第三用Swing写游戏反而能逼你更深入地理解Java核心机制。比如游戏主循环里的repaint()调用会触发paintComponent()方法这里涉及Swing的事件调度线程EDTEvent Dispatch Thread。再比如动画效果需要控制线程休眠时间这涉及多线程同步问题。这些是游戏引擎封装好的你学不到这些底层的Java知识点。当然如果你后面想做一个真正商业化的游戏用Unity是合理的。但作为Java学习者Swing项目是理解“用Java做游戏”这件事的必经之路。2. 核心类设计与面向对象实践2.1 植物类型继承体系的搭建植物是整个游戏的基石。我先定义了一个Plant抽象类它包含了所有植物的公共属性名称、生命值、所在行、所在列、冷却时间、阳光消耗等。然后通过继承派生出不同功能的植物类型。public abstract class Plant { protected String name; protected int hp; protected int row; protected int col; protected int sunCost; protected long cooldown; // 冷却时间毫秒 protected long lastShootTime; // 上次射击时间 public Plant(int row, int col, int hp, int sunCost) { this.row row; this.col col; this.hp hp; this.sunCost sunCost; } public abstract void update(); // 每种植物每帧执行的逻辑 public void takeDamage(int damage) { this.hp - damage; if (this.hp 0) { System.out.println(name 被摧毁了); } } public int getRow() { return row; } public int getCol() { return col; } }基于这个基类我实现了四类典型植物Peashooter豌豆射手具有攻击能力update方法中需要检测同行的僵尸是否出现在射程内如果出现且冷却时间已到就向僵尸方向发射一颗子弹。Sunflower向日葵生产阳光的植物update方法中通过计数器控制每8秒生成一个阳光对象。WallNut坚果墙纯防御型植物没有攻击能力但有非常高的生命值update方法中只做动画帧切换和受击检测。CherryBomb樱桃炸弹一次性爆发植物种下后经过短暂的引信时间就会爆炸对3x3范围内的所有僵尸造成伤害。这里是体现Java多态最典型的场景。在游戏主循环中我只需要维护一个List 调用每个植物的update()方法Java运行时就会根据对象的实际类型调用对应的重写方法无需在循环中写大量的if-else来判断这个植物是豌豆射手还是向日葵这对代码的可维护性和可扩展性是巨大的提升。2.2 僵尸的有限状态机实现僵尸的行为比植物复杂得多。它有三种基本状态行走、攻击、死亡。这三种状态之间的转换遵循固定的规则非常适合用有限状态机FSM来建模。我给僵尸设计了一套完整的状态实现public enum ZombieState { WALKING, // 行走中 ATTACKING, // 攻击中 DEAD // 死亡 } public class NormalZombie { private ZombieState state; private int hp; private int attackDamage; private int row; private float x; private float speed; public void update() { switch (state) { case WALKING: x - speed; // 检测前方是否有植物 Plant target findPlantInFront(); if (target ! null) { state ZombieState.ATTACKING; } break; case ATTACKING: // 攻击目标植物 Plant plant findPlantInFront(); if (plant null) { state ZombieState.WALKING; // 植物没了继续走 } else { attack(plant); } break; case DEAD: // 死亡动画播放完成后移除 break; } } }枚举加状态模式的好处很明显它把每个状态下的行为封装起来状态之间的转移明确且可控不会出现僵尸一边攻击一边走的逻辑。在实际开发中我也尝试过用if-else的布尔变量组合来判断状态但状态多了以后就变得混乱不堪后来才用枚举重构。2.3 子弹和碰撞检测的处理方案子弹的移动和碰撞检测是这个游戏的技术核心之一。子弹和僵尸都是不断移动的对象需要实时判断它们是否重叠。最简单高效的碰撞检测方式是矩形碰撞检测AABB即轴对齐包围盒原理是判断两个矩形是否相交。我是这样实现的public boolean isCollided(GameObject obj1, GameObject obj2) { Rectangle rect1 obj1.getBounds(); Rectangle rect2 obj2.getBounds(); return rect1.intersects(rect2); }具体到豌豆子弹和僵尸的碰撞需要注意几个细节子弹检测范围不要和僵尸的真实图片一样大。我实际用的是图片范围的80%左右因为原版图片中僵尸图片边缘有透明区域如果拿整个矩形来检测子弹还没碰到僵尸的实体就会消失看起来很奇怪。每个子弹每帧检测一次碰撞即可。如果每帧检测多次会消耗性能如果检测间隔太长子弹可能会直接穿过僵尸。所以帧率稳定很关键我采用60FPS的主循环频率一帧大概16.6毫秒在这个频率下子弹不会穿模。还要处理不同行的碰撞。豌豆射手发射的子弹只能攻击同一行的僵尸所以碰撞前要判断子弹所在的行和僵尸所在的行是否一致不一致直接跳过检测。这个简单的30行代码的判断可以避免绝大多数的碰撞判断计算。2.4 游戏状态机与场景切换植物大战僵尸的完整流程不只是关卡内战斗还有开始菜单、关卡选择、暂停、游戏结束等多个场景。我定义了一个GameState枚举public enum GameState { START_MENU, // 开始菜单 SELECT_LEVEL, // 关卡选择 PLAYING, // 游戏中 PAUSED, // 暂停 GAME_OVER, // 游戏结束 WIN // 通关 }游戏主类中维护一个GameState类型的变量程序启动后进入消息循环根据当前状态调用不同的update和render方法。每一次状态切换比如玩家点击“开始游戏”按钮就把当前状态从START_MENU改成PLAYING同时重新初始化关卡数据。这种设计模式在游戏开发中非常常见。它把游戏流程切分成离散的、互相独立的状态每个状态只关注自己的逻辑。我从这个项目中学到的一个有价值的思路是把一个复杂的全局逻辑拆成多个独立状态可以让整个代码的逻辑变得非常好梳理。3. 关键技术实现与实操步骤3.1 游戏主循环的实现与帧率控制游戏主循环是整个游戏的心脏所有逻辑更新和画面重绘都在这里完成。我的实现方式是用一个while循环配合Thread.sleep来控制帧率public class GameLoop implements Runnable { private static final int FPS 60; private static final long FRAME_TIME 1000 / FPS; private boolean isRunning; private GamePanel gamePanel; Override public void run() { while (isRunning) { long startTime System.currentTimeMillis(); gamePanel.updateGameLogic(); // 更新所有游戏对象状态 gamePanel.repaint(); // 请求界面重绘 long elapsedTime System.currentTimeMillis() - startTime; long waitTime FRAME_TIME - elapsedTime; if (waitTime 0) { try { Thread.sleep(waitTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } }这个循环有两个要点需要注意。第一updateGameLogic和repaint的顺序不能颠倒。必须先更新逻辑再重绘画面否则玩家会看到一帧延迟画面上元素的坐标永远是上一帧的样子。这就好比先算好了拍照时每个人应该站的位置再按下快门而不是先拍一张再调整站位。第二Thread.sleep的时间不能固定写死。因为逻辑更新和绘制本身也需要时间所以要减去已经执行过的时间否则实际帧率会低于60FPS。我见过有人直接写Thread.sleep(16)毫秒最终实际帧率只有50FPS左右因为忽略了处理逻辑需要的时间。3.2 图片素材的加载、切割与适配策略图片素材是游戏视觉表现的关键。我拿到的图片素材是一整张雪碧图Sprite Sheet也就是把游戏的所有图片元素拼在一张大的PNG图里。为了在游戏中使用这些素材我需要用Java的ImageIO配合BufferedImage完成图片的切割和加载。切割的关键方法private BufferedImage loadAndCrop(String path, int x, int y, int width, int height) { try { BufferedImage fullImage ImageIO.read(new File(path)); BufferedImage cropped fullImage.getSubimage(x, y, width, height); return cropped; } catch (IOException e) { e.printStackTrace(); return null; } }这里有一个必须注意的坑getSubimage返回的图片和原始大图共享数据缓冲区这意味着如果你后续对切割出来的图片做修改比如缩放或变色会影响原图甚至其他同源的切割图。正确做法是复制一份BufferedImage safeCopy new BufferedImage( cropped.getWidth(), cropped.getHeight(), BufferedImage.TYPE_INT_ARGB ); Graphics2D g2d safeCopy.createGraphics(); g2d.drawImage(cropped, 0, 0, null); g2d.dispose();图片素材的命名规范也值得参考。我给每张图片定义的格式是类型_动作_序列号.png比如pea_shooter_idle_1.png、zombie_walk_3.png。这个格式让我在代码里通过字符串拼接就能加载图片不用每个图片手动写一行代码。缩放处理方面因为我拿到的图片素材是高清重制的分辨率偏大直接用会导致界面放不下或比例失衡。我封装一个ScaleUtil工具类用Graphics2D的drawImage配合AffineTransform实现平滑缩放public static BufferedImage scale(BufferedImage source, double ratio) { int newWidth (int) (source.getWidth() * ratio); int newHeight (int) (source.getHeight() * ratio); BufferedImage scaled new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d scaled.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.drawImage(source, 0, 0, newWidth, newHeight, null); g2d.dispose(); return scaled; }注意这里必须设置RenderingHints否则缩放后的图片边缘会有严重的锯齿。高性能和高质量之间有取舍因为双线性插值比默认缩放耗时但在这个游戏场景下图片不大、数量不多完全不用担心性能问题。3.3 多线程处理阳光掉落和僵尸生成这个游戏有几个异步的系统阳光自然掉落、僵尸自动生成、植物攻击。如果全放在主循环的update里顺序执行代码会变得臃肿而且一些需要独立计时的逻辑会互相干扰。我用了好几个ScheduledExecutorService来管理这些独立逻辑。比如阳光生成的逻辑ScheduledExecutorService sunScheduler Executors.newScheduledThreadPool(2); // 每隔8秒在随机位置生成一个自然阳光 sunScheduler.scheduleAtFixedRate(() - { if (gameState GameState.PLAYING) { int randomX 80 new Random().nextInt(800 - 160); int randomY 100 new Random().nextInt(400); suns.add(new Sun(randomX, randomY)); } }, 3, 8, TimeUnit.SECONDS);再比如僵尸的生成我设计了一个按波次生成的逻辑每波僵尸之间间隔一定时间而且不同波次的僵尸数量和类型不同。用一个定时任务检查当前时间是否达到下一波生成时间点到了就生成指定数量的僵尸。这里要提醒一个重要的线程安全问题ScheduledExecutorService的线程池中运行的任务和Swing事件分发线程EDT中的GUI操作分属不同线程。如果你在定时任务里直接修改sunList或zombieList而同时主循环的update也在遍历这个列表就会出现ConcurrentModificationException异常。我的解决方案是引入线程安全的集合类型使用CopyOnWriteArrayListprivate ListSun suns new CopyOnWriteArrayList(); private ListZombie zombies new CopyOnWriteArrayList();CopyOnWriteArrayList是写时复制机制读取时不加锁写入时复制整个底层数组适合读多写少的场景。游戏里的对象列表恰好满足这个特征因为每帧的update和paint都是读操作只有少量时间点会新增或移除对象。虽然CopyOnWriteArrayList的内存开销高一些但在这个项目规模下完全可接受换来的是线程安全性。3.4 关卡数据配置化从硬编码到JSON我刚开始写的时候关卡数据全是硬编码在代码里第几关、多少只僵尸、间隔多久、阳光倍率是多少都写在代码里。后来要调整难度发现每改一个数字都要重新编译非常痛苦。后来我把关卡配置提取到资源文件夹的JSON文件里{ level: 1, background: day_drive.png, availablePlants: [sunflower, peashooter, wallnut], spawnWaves: [ { time: 15, count: 2, type: normal }, { time: 30, count: 3, type: normal }, { time: 50, count: 2, type: normal }, { time: 70, count: 1, type: cone, hpBonus: 100 } ], sunSeed: 50, startSun: 150 }然后写一个LevelConfigLoader工具类用Jackson库把JSON映射成Java对象ObjectMapper mapper new ObjectMapper(); LevelConfig config mapper.readValue(jsonFile, LevelConfig.class);这样做的好处是调试平衡性时只需要改JSON文件再重启游戏不用改代码重新编译。这个思路延伸到后端开发中也是一样的配置和代码分离是工程化的基本功。虽然对于小游戏来说这有些过度设计但如果想把它做成一个完整的项目放进简历这个设计点能让你在面试时多说五分钟。3.5 音效播放的正确姿势一个没有声音的植物大战僵尸就像一个默片电影玩起来缺少反馈感。所以我在项目中加入了背景音乐和音效。Java播放音频最常用的方案是JLayer库专门支持MP3格式的播放。我先封装了一个AudioManager单例类public class AudioManager { private static AudioManager instance; private Player player; private AudioManager() {} public static AudioManager getInstance() { if (instance null) { instance new AudioManager(); } return instance; } public void playBGM(String mp3Path) { if (player ! null) { player.close(); } new Thread(() - { try { FileInputStream fis new FileInputStream(mp3Path); BufferedInputStream bis new BufferedInputStream(fis); player new Player(bis); player.play(); } catch (Exception e) { e.printStackTrace(); } }).start(); } }这里有个非常重要的问题播放音乐不能放在主线程中否则音乐播放会阻塞游戏主循环导致画面卡顿。所以每次播放都新开一个专门的线程。音效的触发点包括种植植物、发射豌豆、僵尸被击中、樱桃炸弹爆炸、获得阳光等。为了避免同一时间重复播放太多音效导致声音混乱我实现了一个简单的声音池限制最多同时播放4个短音效新音效会顶替掉最旧的音效。4. 调试过程和性能优化实录4.1 内存溢出和GC问题排查游戏跑了一段时间后我遇到了一个典型问题内存占用不断攀升最终导致游戏卡顿甚至崩溃。当时Swing控制台打印了很多OutOfMemoryError的信息。排查思路是这样展开的第一步我先检查是不是图片加载的问题。我用JVisualVM工具JDK自带的性能监控工具观察了堆内存变化曲线发现内存上升有明显的阶梯状特征每一波上升后会有暂时的平台期然后继续上升。这排除了单一事件导致的泄漏更像是某个列表在持续累积对象。第二步检查是不是某个List只增不减。我浏览了所有add操作目标锁定在sunList和bulletList。发现问题了我写的逻辑是“阳光存在12秒后自动消失”但在某些情况下这个定时移除逻辑没被触发。第三步找到罪魁祸首。看代码才发现在游戏暂停时所有对象的update方法都停止了但ScheduledExecutorService里生成新僵尸的任务用的是独立线程池不受暂停状态控制。所以暂停期间僵尸还在不停生成僵尸列表越积越长。修复方式是在生成僵尸的定时任务前面加一层暂停状态的判断——如果当前GameState不是PLAYING就直接跳过生成。4.2 帧率波动的优化手段交付之前我发现游戏帧率波动很严重人少的时候跑满60FPS僵尸多起来以后降到30FPS以下。用Profiler跑了一轮后找到几个性能瓶颈第一大瓶颈是Swing每次repaint都会重绘整个界面。我优化为脏矩形重绘技术就是说只有当某个区域发生了变化才重绘该区域而不是整个画布。但如果全屏都在变化植物在攻击、僵尸在移动、子弹在飞这个优化效果就有限。所以更有效的优化是减少无用区域的绘制比如背景层是静止的可以单独绘制到一个BufferedImage上每次repaint时直接把这个背景图刷上去省去每次重新加载背景图案的开销。第二大瓶颈是碰撞检测的嵌套循环复杂度。我原来是三层循环每个子弹依次检测所有僵尸、每个植物检测所有僵尸复杂度是O(N*M)僵尸多了就会指数级变慢。优化方案是把僵尸按行分桶——因为游戏地图固定是5行我把僵尸分别放进5个List每行的碰撞检测只遍历同一行的僵尸直接砍掉了大部分无效检测。第三个优化点是图片资源的重复加载。我原来每次动画帧切换都重新从磁盘读图片文件后来改成了启动时把所有需要的图片全部加载到内存用ConcurrentHashMap缓存起来运行时只做Map.get不再访问文件IO。这个改动对性能提升效果最明显因为磁盘IO比内存慢好几个数量级。4.3 从Java基础到项目落地的完整步骤如果是想从零开始完整跑通这个项目对Java基础还不太熟的朋友我建议按照下面这个顺序来操作第一步确认环境。JDK版本建议用8或者11IDE用IDEA社区版足够。在命令行输入java -version和javac -version能正常输出版本号才说明环境没问题。如果提示找不到命令多半是JAVA_HOME环境变量没有配置好。第二步导入项目。把整个源码文件夹放入IDEA的工作区选择“Open”IDEA会自动识别Maven或Gradle结构。如果没有构建工具直接添加src目录为源代码根目录。第三步理解入口。找到main包下的GameApplication类里面只有一行new MainFrame().setVisible(true)。所有的启动逻辑都是从这一行开始的这也是典型的Java GUI入口写法。第四步从改参数开始熟悉。不要一上来就通读全部代码先找到PVZConstants类里面有各种游戏参数的常量定义比如阳光初始值、豌豆伤害、僵尸移动速度等。改几个数值运行游戏看看效果感受参数对游戏体验的影响。4.4 常见疑难杂症速查表症状可能原因解决方案运行后报 ClassNotFoundException: javazoom.jl.player.Player缺少JLayer音频库把jlayer-1.0.1.jar引入项目依赖游戏画面不刷新或闪烁忘记调用repaint或没有super.paintComponent在paintComponent方法第一行调用super.paintComponent(g)ConcurrentModificationException在遍历列表时同时修改列表使用CopyOnWriteArrayList或倒序遍历删除图片显示不出来图片路径使用了绝对路径或中文路径改用相对路径通过ClassLoader.getResource方式加载CPU占用100%游戏主循环没有sleep无限空转加入Thread.sleep或ScheduledExecutorService控制帧率点击放置植物没反应鼠标坐标没有换算到网格坐标检查坐标偏移量用点击位置除以单元格宽高得到行列号4.5 Maven构建与打包的完整配置到这个阶段代码已经可以正常运行了接下来就是构建和打包。我推荐用Maven管理项目pom.xml中需要配置的内容包括JDK编译版本、项目依赖Jackson、JLayer和打包插件。其中maven-assembly-plugin可以把所有依赖的jar包和资源文件打成一个可执行的完整jar包。我实际用的打包配置build finalNamepvz-java-edition/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.3.0/version configuration archive manifest mainClasscom.pvz.main.GameApplication/mainClass /manifest /archive descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs /configuration executions execution idmake-assembly/id phasepackage/phase goals goalsingle/goal /goals /execution /executions /plugin /plugins /build执行mvn clean package之后在target目录下就能看到pvz-java-edition-jar-with-dependencies.jar文件。由于这个jar包是通过assembly插件把所有依赖都打进去了可以直接双击运行或者用java -jar pvz-java-edition-jar-with-dependencies.jar启动不需要外部依赖库。需要注意的是resources目录下的图片和音频会被自动打包进去在代码里读取时注意不能用new File(images/xx.png)这种方式因为jar包内的文件不是一个真实磁盘文件必须用ClassLoader来读取InputStream is getClass().getClassLoader().getResourceAsStream(images/pea_shooter.png); BufferedImage img ImageIO.read(is);这个坑很多人会踩我第一次打包后启动就黑屏排查了半天才发现是这个原因。5. 项目总结与后续扩展建议整一套代码完成下来我的体会是用Java做植物大战僵尸真正锻炼的不是“会写Java语法”而是把一门语言综合运用起来解决实际问题的能力。从划分模块、设计类结构、实现状态机到优化性能、处理线程同步、适配资源这些都是真实项目开发中每天都会遇到的事情。如果已经跑通了基础版本后续可以试着这样扩展第一种扩展增加新植物和新僵尸。比如增加寒冰射手它的子弹会减速僵尸。在已有框架下你只需要新写一个类继承Plant重写update方法然后在卡牌选择界面加上这个植物即可。这能实际体会到开闭原则的好处。第二种扩展把Swing界面换成JavaFX。JavaFX的动画系统和CSS样式支持比Swing丰富得多特别是过渡动画和绑定属性。移植过程中你会更深入地理解视图层和数据模型的关系。第三种扩展做联机对战。用Socket或者Netty实现双人对战模式一个人放植物另一个人控制僵尸进攻。这涉及网络通信、状态同步、协议设计是从单机游戏跨入联网游戏的一次很好实践。第四种扩展把项目改造成Spring Boot后台加Web前端的形式。用RESTful API管理关卡配置和玩家分数前端用Canvas重新实现渲染层。这个方向更贴近现代Web开发对以后找工作帮助更大。我写这个项目的目标从来不是为了做一个完美平衡的商业游戏而是通过它把一个学完Java基础的人带到一个“能独立完成项目”的台阶上。如果你正处在Java学习的中期阶段看完这些思路可以打开源码跑起来对照着改代码、调参数踩几次坑之后你对Java的理解会和只看书完全不同。这也是我最初写这套源码时最想达到的效果。本文还有配套的精品资源点击获取