尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

纯Java手写RTS游戏:从地图寻路到AI对战的完整实践

纯Java手写RTS游戏:从地图寻路到AI对战的完整实践 简介实时策略游戏RTS是算法、并发与软件架构的集大成者其核心挑战在于如何让数百个单位在动态战场中高效寻路、避让并执行指令。A*寻路算法作为经典路径规划方案通过启发式搜索平衡效率与最优性是理解游戏AI的基础。多线程渲染与逻辑分离则能保障游戏流畅运行避免界面卡顿。从工程实践看利用Java Swing原生API即可搭建完整的游戏框架无需依赖重型引擎这为学习设计模式与性能优化提供了极佳载体。这种综合项目不仅适用于课程设计或毕业设计更能帮助开发者深入理解面向对象、并发编程等核心技能。本文以一款纯Java实现的魔兽争霸风格Demo为例拆解其地图系统、单位状态机、寻路与AI设计带你体验从零构建RTS游戏的完整脉络。 这段时间我把《魔兽争霸》那种经典即时战略玩法用纯 Java 重新撸了一套从地图绘制、单位寻路、资源采集到人机对战全部源码放在一起大概有一万多行。这个项目我给它起名叫 warlock-java本质上是一个“能跑起来的 Java 版魔兽争霸 Demo”不是用引擎套壳而是从零手写核心逻辑。如果你正处在 Java 基础学完、想找个综合项目练手或者准备做课程设计、毕业设计这套源码应该能给你省下大量查资料的时间。项目整体是用 Java Swing 写的客户端没有依赖任何第三方游戏引擎地图、单位、技能、AI 全部用 Java 原生 API 完成。我当时做它的动机很简单很多人学完 Java 面向对象、集合、多线程之后并不知道这些东西组合起来能干什么而 RTS 游戏恰好把这几块用得最狠。这篇文章我会把这个项目的整体设计、核心模块、踩坑记录和源码运行方式全部拆开讲清楚希望对想用 Java 做游戏或理解 RTS 底层逻辑的人有帮助。1. 项目整体设计与技术选型1.1 为什么选 Java 写即时战略游戏如果你去搜Java 游戏开发八成结果都是推 LibGDX 或 JavaFX但我的看法是如果你想搞懂游戏逻辑本身而不是学引擎 API直接用 Swing 反而更合适。魔兽争霸这种 RTS 游戏的核心难点根本不在画面而在数以百计的单位同时移动、寻路、攻击时程序如何保持逻辑稳定和响应流畅。Java 在这方面的优势有三个第一面向对象的特性非常适合描述游戏里的单位、建筑、技能一个Unit类、一个Building类继承和多态用起来非常自然第二Java 自带SwingWorker、Timer和高精度时间戳处理游戏循环和异步任务很方便第三Java 的内存管理是自动的对于这种单机 Demo 级别的小型项目完全不需要考虑手动释放资源开发效率高很多。当然用 Swing 写游戏也有明显短板渲染性能上限不高。所以我在设计之初就明确了一个原则——画面能做多简单就做多简单把精力全部集中在游戏逻辑上。所有单位用简单的几何图形和图片素材代替地图用瓦片格子这样既保证了 60 FPS 的流畅度又不会陷入美术资源的泥潭。1.2 渲染方案选型Swing 还是 JavaFX我在项目初期其实纠结过 Swing 和 JavaFX。JavaFX 有更现代的图形管线支持 CSS 样式、Canvas高性能绘制理论上做游戏效果更好。但实际调研后发现JavaFX 在 JDK 8 之后被从标准 JDK 中分离出去很多用户运行环境没装 JavaFX最后还得额外打包运行时非常麻烦。Swing 则不同它从 JDK 1.2 开始就是 JDK 标配只要装了 JDK 就能跑对拿到源码就能运行这个目标来说Swing 的兼容性完胜。真正决定我用 Swing 的还有一个原因Swing 的Graphics2D类提供了非常完善的 2D 绘制 API旋转、缩放、透明度、抗锯齿都有绘制简单的 RTS 游戏画面绰绰有余。而且 Swing 的双缓冲机制是内置的JPanel默认启用双缓冲我能把精力放在逻辑而不是底层绘制上。后来事实证明这个选择没错。项目在普通笔记本上跑单位数量到 200 个左右时帧率依然能稳定在 60 FPS。这个过程里我也踩过一些坑比如窗口缩放时的重绘闪烁、paintComponent里做耗时操作导致界面卡死等这些我会在后面常见问题部分详细说。1.3 源码结构与模块划分这个项目的源码结构是典型的 MVC 分层我简单列一下核心包结构com.warlock ├── core # 游戏入口、主循环、GameState 管理 ├── map # 地图生成、瓦片渲染、视野系统 ├── unit # 单位类、单位状态机、单位工厂 ├── building # 建筑类、建造队列 ├── ai # 电脑玩家 AI、寻路算法 ├── render # 渲染器、镜头控制、粒子效果 ├── input # 鼠标键盘事件处理 └── util # 工具类、资源加载、常量定义这种划分方式对新手特别友好代码可读性很高。比如想改单位的移动逻辑只需要打开unit包想调 AI 难度直接看ai包里的逻辑不需要在几百个文件里翻来翻去。这也是我想强调的一个经验写项目之前先把模块边界划清楚比你多写几百行代码更重要。后面所有模块的讲解我都会按照这个结构来。2. 核心功能模块实现2.1 地图系统与迷雾视野地图是一个 RTS 游戏的骨架所有单位、建筑、寻路都在地图上运行。我采用的是非常经典的瓦片地图方案整张地图被划分为 64×64 的格子每个格子有三种状态可通行、不可通行、半通行如树林。地图数据用二维数组存储mapData[x][y]的值为 0 表示平地1 表示障碍物2 表示可破坏的树木。地图的初始化逻辑比较简单我用了简单的随机种子生成算法先随机生成地形再用平滑算法让障碍物聚集避免出现过于零碎的不可通行区域。生成后地图会经过一个连通性检查确保玩家基地和资源点之间始终有路可走否则就会出现单位寻路永远无法到达的 Bug。迷雾系统是 RTS 游戏的重要元素也是很多初学者容易忽略的模块。我的实现方式是维护一个visibleMap布尔数组每个单位每帧检查自己视野范围内的格子将这些格子标记为可见。绘制时只有可见区域的瓦片才会真正渲染到屏幕上未知区域显示黑色迷雾已探索但当前不可见的区域显示灰色。这个实现看起来简单但其实对性能影响很大如果遍历所有格子做可见性判断地图一大就会卡。我的优化思路是用矩形视野 圆形裁剪单位视野先算出一个 bounding box再对 box 内的格子做圆形距离判断把计算量控制在很小范围内。2.2 单位对象模型与状态机单位是 RTS 游戏的核心农民、步兵、骑兵、弓箭手每个单位有不同的属性、行为和动画状态。我用一个基类Unit定义了所有单位共有的属性位置、所属玩家、生命值、攻击力、护甲值、移动速度、视野范围、当前状态等。然后通过继承派生出Peasant、Infantry、Archer等具体单位类。状态机是我这个项目里比较得意的一部分。每个单位有IDLE、MOVE、ATTACK、GATHER、BUILD、DEAD六种状态状态切换由transitionTo(UnitState newState)方法统一管理。这样做的好处是不管玩家下达什么指令单位的行为逻辑都是可控的不会出现“单位一边移动一边攻击”这种混乱情况。状态切换的触发条件我都集中在Unit.update()方法里每帧调用一次代码结构非常清晰。比如农民采金矿的逻辑初始状态是IDLE玩家右键点击金矿后状态切换为MOVE移动到矿脉附近后自动切换为GATHER然后开始循环往复的“采金-送回基地”操作。送回基地实际是通过状态机再次切换到MOVE目标点变为基地位置。这个看似简单的循环如果状态管理做得不好会出现单位卡在资源点不动、或者反复横跳的问题我在调试时没少吃这个亏。2.3 建造系统与生产队列建造系统是 RTS 和普通动作游戏最大的区别之一。玩家选择农民后可以点击建造按钮选择建筑位置农民走到指定位置后开始施工施工期间建筑会显示进度条建造完成后建筑才真正生效。我的实现方案是建筑在建造期间其实是一个“半成品”对象它已经有了位置和占地面积但不能生产单位也不能攻击。这个对象的constructionProgress属性从 0 增长到 100进度和建造农民的个数有关农民越多建造越快。建造完成后半成品对象会转换成正式建筑对象这个转换我用了 Java 的接口设计所有建筑都实现Building接口半成品和正式品都实现这个接口只是内部行为不同。生产队列相对简单一些每个生产建筑比如兵营内部维护一个QueueUnitType玩家点击训练按钮就把单位类型加入队列兵营每间隔一段时间就从队列头部取出一个类型实例化对应的单位对象并放到兵营旁边的空地上。为了控制训练节奏我用了ScheduledExecutorService每 500 毫秒检查一次队列是否有任务避免用Thread.sleep阻塞游戏主线程。3. 寻路与战斗 AI 实现3.1 A* 寻路算法简述寻路是 RTS 游戏中最容易出问题的模块。我的方案是经典的 A* 算法地图上每个格子就是一个节点单位移动时先通过 A* 算法找出一条从起点到终点的路径然后沿着路径逐格移动。虽然 A* 不是最先进的寻路算法像 JPS、HPA* 这类算法在大规模地图上性能更好但对于 64×64 的地图A* 的实时性完全够用。核心代码大概是这个思路public ListNode findPath(int startX, int startY, int endX, int endY) { PriorityQueueNode openList new PriorityQueue(Comparator.comparingInt(n - n.f)); MapNode, Node cameFrom new HashMap(); MapNode, Integer gScore new HashMap(); SetNode closedList new HashSet(); Node start new Node(startX, startY); openList.add(start); gScore.put(start, 0); start.f heuristic(start, endX, endY); while (!openList.isEmpty()) { Node current openList.poll(); if (current.x endX current.y endY) { return reconstructPath(cameFrom, current); } closedList.add(current); // 遍历相邻节点... } return Collections.emptyList(); }A* 的启发函数我用的是曼哈顿距离因为地图上单位只能上下左右移动不能斜着走曼哈顿距离比欧氏距离更准确能减少无效节点扩展。实际测试下来64×64 地图上从地图一角到另一角的寻路耗时在 1 毫秒以内完全不会卡顿。有一个细节必须提一下寻路必须放在后台线程执行不能在游戏主循环里直接跑。因为寻路是计算密集型任务如果在地图复杂时直接在主线程调用界面会明显卡顿。我用的方案是CompletableFuture.supplyAsync()提交寻路任务任务完成后回到游戏线程更新单位的路径队列。3.2 多单位移动与避让RTS 游戏里玩家经常框选一队单位命令它们移动到某个地点。如果每个单位都独立寻路会出现一个严重问题所有单位都挤在同一条路径上互相卡住队形散乱。我采用的方案是“队列移动 射线避让”的组合策略。具体来说当玩家框选了多个单位下达移动命令时我并不是让每个单位单独寻路而是先计算出整个编队的中心点和目标点然后根据每个单位在编队中的相对位置偏移生成各自的目标点。这样单位到达目的地后会自然形成编队而不是挤成一团。偏移量的计算参考了魔兽争霸的菱形编队思路并排单位错位排列视觉效果更接近原版。避让逻辑则相对朴素每帧检查单位是否与周围其他单位发生碰撞如果碰撞则将单位移动方向偏移一定角度同时降低移动速度。这个方法在大规模战斗时会出现轻微的“推挤”现象但用起来没有大问题。真正让我头疼的是当大量单位同时挤在一个狭窄路口时会出现死锁单位互相卡住不动。最终我加了一个简单的“等待超时”机制如果一个单位连续 2 秒没有移动就重新寻路一次绕开拥堵区域。3.3 电脑玩家 AI 的开发策略电脑 AI 的设计是 RTS 游戏里最有趣也最耗时的部分。我实现了一个基于规则的 AI不涉及机器学习整体思路分成三个层次经济运营、军事扩张、进攻时机。经济运营方面AI 会每 5 秒检查一次自己的金矿和木材储量如果低于某个阈值就训练新的农民并派往资源点。初始状态下 AI 会训练固定数量的农民然后才开始造兵营。军事扩张方面AI 会优先保证自己有 2-3 个兵营在生产单位当兵力达到预设数量比如 12 个步兵时就开始进攻玩家基地。这个“兵力阈值”是 AI 最关键的设计参数。如果设得太低AI 会一直送兵玩家随便防守就能赢设得太高AI 又可能过很久都不进攻游戏没有对抗性。我调了几轮数值最后把阈值设成“初始 8 个每过 5 分钟增加 2 个上限”这样 AI 的进攻压力会随游戏时间逐渐增大给玩家一种“越打越难”的体验。AI 的进攻逻辑也很简单选择玩家基地中心点作为目标命令所有进攻单位移动到目标点附近进入攻击范围后自动攻击。为了让 AI 不那么笨我在进攻前加了一个判断如果 AI 当前兵力不足就取消进攻命令撤回基地继续训练直到兵力达到阈值。这个简单的判断让 AI 的行为看起来聪明了很多。4. 渲染表现与 UI 交互4.1 游戏主循环与双缓冲渲染游戏主循环是所有实时游戏的心脏。我的实现用的是标准的时间步长循环目标帧率 60 FPS每一帧时间为 16.67 毫秒。循环里做的事情按顺序是处理输入事件、更新游戏逻辑、渲染画面。while (running) { long start System.nanoTime(); handleInput(); updateGame(); renderGame(); long elapsed (System.nanoTime() - start) / 1_000_000; if (elapsed FRAME_TIME) { Thread.sleep(FRAME_TIME - elapsed); } }主循环跑在一个独立的线程里而渲染操作需要交给 Swing 的事件分发线程EDT执行。我这里的做法是游戏逻辑线程把需要绘制的场景数据保存到共享变量然后在 EDT 中通过repaint()触发重绘。绘制的核心代码放在JPanel.paintComponent()中使用Graphics2D绘制地图瓦片、单位、建筑、血条、技能特效等。双缓冲是 Swing 自带的能力JPanel默认启用。但有一个细节如果你直接在paintComponent里做复杂绘制比如遍历几百个单位并绘制图片仍然可能掉帧。我的优化思路是预渲染静态地图地图的底层瓦片在初始化时渲染成一张大图后续每帧直接通过drawImage绘制而不是逐格重绘性能提升非常显著。4.2 鼠标框选与指令系统RTS 游戏的操作核心是鼠标框选和右键指令。我实现了一套完整的输入处理机制鼠标左键按下时记录起始点拖动时绘制一个半透明选框松开左键时统计所有落在选框内的己方单位将它们选中。鼠标右键点击时根据当前选中单位类型和目标对象生成不同的指令。指令系统我设计成了命令模式每个指令都实现了Command接口包含execute()方法。移动指令、攻击指令、采集指令、建造指令都实现了这个接口。这个设计的好处是扩展性很强以后想加技能指令只需要实现一个SkillCommand类就行了。有个交互细节我得提一下右键点击敌人时选中的单位是攻击右键点击地面时是移动右键点击金矿时是采集。这个判断逻辑需要根据鼠标点击位置的游戏对象类型来路由。我用了一个简单的GameObjectRegistry记录地图上所有对象的 ID 和类型点击时通过位置查询最近的游戏对象再判断类型后生成对应指令。4.3 状态栏、血条与提示信息血条是 RTS 游戏中最常见的 UI 元素。我的实现是在每个单位的头顶上方绘制一个小矩形条背景是黑色半透明前景色根据血量比例变化绿色表示健康黄色表示中度伤害红色表示濒死。血条的绘制发生在paintComponent阶段单位绘制完成后再绘制血条保证血条永远悬浮在单位上方。为了不遮挡视野血条默认只在单位被选中或者血量不满时显示。这样画面看起来干净很多也减少了绘制量。状态栏方面我在屏幕底部设计了类似魔兽争霸的底部面板左侧显示选中单位的头像和属性右侧显示单位的技能按钮目前只有攻击、移动、停止三个功能性按钮。这个底部面板我用了单独的JPanel实现通过 Swing 的布局管理器放置在主窗口的 SOUTH 区域。还有个不容忽视的提示信息模块当单位完成建造、敌人进攻、资源不足时屏幕上会弹出淡入淡出的提示文本。实现方式很简单维护一个提示消息列表每条消息有存活时间绘制时根据剩余时间计算透明度。这个功能虽然简单但极大提升了游戏的完成度显得不那么半成品。5. 常见问题与排查心得5.1 游戏卡顿与帧率不稳定卡顿是每个游戏开发者都会遇到的问题。我在项目开发过程中遇到的第一个卡顿问题出现在地图迷雾开启之后整个画面帧率从 60 FPS 掉到了 20 FPS 左右。排查思路是先分清瓶颈在 CPU 还是 GPU。因为 Swing 是 CPU 渲染所以直接用 JProfiler 看了 CPU 占用发现耗时主要集中在地图可见性判断上。原因是我对每个瓦片都做了圆形视野判断而整个地图有 4096 个瓦片每个单位每帧都要遍历这些瓦片计算量直接爆炸。解决方案是把可见性判断从每帧执行改为每 200 毫秒执行一次并且每个单位只计算自己周围视野范围内的瓦片而不是遍历全图。优化之后帧率立刻回到了 60 FPS。这里我得到的经验是在游戏开发中减少无效计算比优化单次计算速度更重要。5.2 寻路卡死与单位抖动寻路卡死是最让我崩溃的问题。现象是单位移动到某个点附近后开始不停地左右抖动永远无法到达目标点。我最初怀疑是 A* 算法的问题后来打日志发现路径本身是正确的但单位在到达路径最后一个节点时因为浮点数精度问题始终差零点几像素无法到达终点。解决方案是引入一个“到达判定阈值”当单位距离目标点小于 5 个像素时直接判定为到达将状态切换为IDLE并且不再继续向路径终点移动。这个 5 像素的阈值就是解决抖动问题的关键。类似的问题还有单位绕障碍物时路径出现锯齿状我通过路径平滑处理每隔一个节点丢弃一个点解决了路径看起来更自然单位移动也更流畅。5.3 多线程资源竞争问题这个项目的游戏逻辑线程和渲染线程是分离的这就涉及并发访问共享数据的问题。最典型的问题是游戏线程正在更新单位位置时渲染线程同时读取单位位置画图导致画面上出现单位“瞬移”或者撕裂。我的解决方案是使用CopyOnWriteArrayList存储单位列表。这个类的写操作会复制整个底层数组读操作不需要加锁非常适合读多写少的场景。缺点也很明显单位数量多时写操作开销大。在游戏里单位数量一般不超过 300 个所以完全可接受。另外一个坑是 Swing 组件不是线程安全的我最初在游戏线程里直接调用了JLabel.setText()更新状态栏偶尔会出现界面不刷新或者报错。后来统一改成通过SwingUtilities.invokeLater()提交任务到 EDT问题解决。记住一条原则所有 Swing 组件的操作只能在 EDT 中执行不能在后台线程直接碰。5.4 资源文件加载路径问题很多新手拿到源码运行时最常见的问题是图片、音频资源加载失败程序报FileNotFoundException。原因很简单代码里写的相对路径依赖了当前工作目录。如果启动命令的目录和源码目录不一致就容易出错。我的解决方案是使用类路径加载资源把所有图片和音频文件放在src/main/resources目录下代码里用getClass().getResource(/images/unit.png)获取。这样无论从哪个目录启动只要 classpath 包含 resources 目录都能正确加载资源。这个方法也推荐给所有用 Java 做小工具的开发者比依赖文件系统的绝对路径靠谱得多。6. 源码运行与后续扩展方向6.1 运行环境与启动教程如果你下载了这套源码想直接跑起来需要准备以下环境JDK 8 及以上版本我开发时用的是 JDK 11没有用任何高版本特性JDK 8 也能正常编译一个 Java IDEIntelliJ IDEA 或 Eclipse 均可Maven 3.x项目里有pom.xml用 Maven 管理依赖和构建运行步骤非常简单用 IDEA 导入项目选择 Maven 项目方式。等待 Maven 下载完依赖依赖非常少主要是 JUnit 单元测试库。找到core包下的GameMain.java右键运行即可。如果你是命令行爱好者也可以手动编译mvn clean package java -jar target/warlock-java.jar游戏启动后默认进入主菜单点击“单人游戏”即可开始。你也可以在主菜单设置地图大小、初始资源等参数。整个项目没有依赖任何外部数据库或服务离线环境下也能正常运行。6.2 从这堆源码里能学到什么我觉得这套源码最值钱的地方不是具体代码而是里面体现的工程思维。比如命令模式在指令系统里的运用状态机在单位行为控制里的运用双缓冲和线程分离在渲染性能优化里的运用这些都是 Java 后端开发和游戏开发里非常通用的设计思想。即使你不做游戏开发把这些设计模式理解透了再看 Spring Boot 等后端框架的源码会轻松很多。另外一个收获是调试能力。做游戏和做后端应用最大的不同是游戏的问题通常很难通过日志定位比如单位卡住、路径不对、帧率不稳定这些都需要你肉眼观察和反复实验。我在这套源码的开发过程中学会了打帧率日志、做性能剖析、加临时调试输出这些经验在面试时讲出来比单纯背八股文有说服力得多。如果你正好在准备 Java 面试可以把这套项目作为“项目经验”来包装重点讲游戏对象模型设计、多线程渲染、A* 寻路优化这些亮点面试官一般都会感兴趣。6.3 后续可以怎么扩展这套源码虽然完成了核心玩法但离一个完整的 RTS 游戏还有不小的距离。我整理了几个扩展方向供有兴趣继续折腾的朋友参考增加战争迷雾的渐变效果让视野边界的过渡更自然。实现多楼层寻路支持空军单位和飞行生物。增加英雄单位带主动技能和装备系统。实现多人联机对战这部分可以用 Java NIO 或 Netty 做网络同步。把 Swing 渲染换成 JavaFX 或者 OpenGL比如 LWJGL画质能提升一个档次。我自己目前在扩展的方向是联机对战已经用 Netty 搭了一个简单的服务器框架客户端和服务器的通信协议也设计好了不过距离真正跑起来还有不少工作。后续有进展我也会继续发文章分享。写这套源码的过程中我最大的体会是很多人觉得 Java 做游戏是异端但实际上语言只是工具真正决定游戏品质的是你对逻辑的理解和工程的组织能力。我用 Swing 写出这个项目后对 Java 并发、设计模式、性能调优的理解都上了一个台阶这种收益远超出“学会一个游戏”本身。如果你也在学 Java建议找个类似的综合项目做一做你会发现那些在书本上抽象晦涩的概念突然都变得具体而清晰。本文还有配套的精品资源点击获取
返回列表