Unity RTS游戏开发实战:拆解单位调度、寻路与命令系统的工程陷阱

发布时间:2026/10/14 16:06:59
Unity RTS游戏开发实战:拆解单位调度、寻路与命令系统的工程陷阱
做RTS插件分享这么多年我经手过不少Unity即时战略项目从毕业设计里的单机小Demo到商业项目里动辄上百单位的战场表现。每次有人问我“RTS游戏是不是很难做”我第一反应都是难的不在渲染和战斗而在那一整套“单位调度体系”。选人框、寻路避让、命令列队、阵营关系这几个东西任何一个没做好玩家上手三分钟就能感觉到“僵”。市面上的RTS Starter Kit类插件本质上就是想把这一整套体系打包好让开发者在Unity里直接落地不用从头用Mesh网格、A*寻路和射线检测去拼一个“能玩”的原型。这篇就围绕RTS Starter Kit这套完整的RTS游戏开发工具包讲讲里面那些核心系统到底怎么拆、怎么配、怎么调哪些地方是坑哪些地方能走捷径。这个工具包适合谁适合两类人一是想快速验证RTS玩法的独立开发者先跑通核心循环再做差异化二是正经做过RTS但被单位逻辑折磨过一轮的团队拿它当底层底座在上面替换美术资源和数值规则省掉最枯燥的工程部分。不管你是第一天接触Unity还是已经有些经验只要目标是“让一群单位听指挥、打起来、跑得动”这篇都值得看完。1. RTS游戏开发的核心痛点与这个工具包的整体思路1.1 为什么那么多RTS项目死在半路先聊点实在的。RTS游戏英文Real-Time Strategy即时战略核心是“同时管理多个单位和资源在实时战场上决策”。听起来不难但Unity里默认提供的能力差得太远。你放一个角色控制它移动很简单可一旦单位数量上到几十个事情就复杂了单位之间不能互相踩头需要有碰撞和避让逻辑大批单位同时移动性能会崩Draw Call和CPU寻路计算双重压力选中单位不是点一下就完要支持框选、Ctrl加选、区域选择命令不只是“走到那”还要有“移动、攻击、采集、巡逻、停止”这套命令队列阵营关系、视野战争迷雾、建筑队列、资源点……这些都是Unity原生没有的。单独做一个功能不难难的是把它们像齿轮一样咬合在一起。很多项目死在第一步单位寻路和命令交互总是抖框选范围不对点了地面单位不理你。这中间消耗的时间不是几周是以月计的。而这类工具包的价值就是把这一整套别人已经淌过一遍血的逻辑做成可复用的模块。1.2 RTS Starter Kit 的定位不是“完整游戏”是“完整底架”先降个预期。市面上叫RTS Kit的东西不少但它们的定位通常不是“一个做好了的游戏”而是“一个把RTS通用逻辑做完的工程基底”。你拿到手能跑起来一个包含单位选择、移动攻击、阵营冲突、简单UI的Demo。你的工作不是从零开始写单位状态机而是在这个地基上盖自己的房子。这套工具包的核心是几个顶层模块我按重要程度排一下模块作用难度权重单位控制系统左键选择、框选、命令下达、命令队列最核心出错立刻暴露寻路与移动系统多点寻路、单位间避让、移动动画同步最难调性能瓶颈常在这阵营与管理层玩家、AI、中立阵营的归属和互动关系表面简单扩展时麻烦生产与资源建筑建造队列、单位训练、资源增减偏逻辑但最容易和UI耦合相机与交互层RTS视角控制、屏幕边缘滚动、鼠标命中检测体验关键影响“手感”1.3 工具包的整体架构逻辑这套东西是怎么串起来的我看过一些RTS套件的源码优秀的那一版架构上都有一个共性把“单位逻辑”和“指令逻辑”彻底分开。单位本身只负责“接收命令并执行”不负责“命令从哪里来”。指挥层则统一管理点击、框选、目标检测再把结果转化为“命令对象”下发。这就很聪明。比如你要加一个新单位“工程车”不需要改指挥层只需要让工程车实现统一的命令接收接口它就能响应移动、采集、建造这些操作。这种面向接口的设计后期扩展起来非常省事。另一个关键点是“引用关系”。维护单位列表的通常是阵营管理器而不是Unity的FindObjectsOfType。每个单位出生时注册到所属阵营死亡时注销。选中、寻路、攻击判定都通过阵营管理器查询列表这样既能控制数量又能做性能优化比每次都遍历场景所有物体要快得多。2. 核心系统拆解与实操要点2.1 选中系统别小看框选这里有大学问选中的手感直接决定玩家对游戏的第一印象。点一下选一个单位拖一个框选一群单位按住Shift追加选择鼠标点击地面下达移动命令——这套交互逻辑看着简单实操里每个点都是细节。先说单位选中。刚拿到的工具包一般默认用Physics.Raycast打在Collider上这里第一个坑就是Collider的层级千万不要把UI的Collider和场景Collider混在一起否则点击UI时射线穿过UI点到地面单位会原地乱走。我自己的习惯是给UI单独一个Layer然后在射线检测的LayerMask里排除掉。框选实现上核心是“屏幕坐标矩形转世界坐标范围”。过程大概是鼠标按下时记录屏幕起点拖动时记录终点形成一个矩形把矩形的四个角分别用Camera.ScreenPointToRay投射到地面得到一个世界空间下的四边形然后遍历阵营单位列表把单位位置用坐标点转屏幕坐标判断是否在矩形内最后统一加入选中列表。这里有个小优化不要用相机出来的射线去找每个单位的Collider那样成本太高。用碰撞体范围做粗略判断就够了比如用Collider.bounds的中心点加半径判断是否在框内。实测单位数量几百的时候每帧做这个也不会卡。再提醒一个细节框选方向。玩家从右下角往左上角拖框还是从左往右拖在传统RTS里是有语义差别的——有的游戏设定从左往右必须完全包含单位才选中从右往左只要碰到就选中。工具包一般默认都是“碰到就算”。如果你要做严谨的RTS手感这个方向判定逻辑值得自己再补一版。2.2 命令队列与右键交互需要精心设计状态RTS里右键是最忙的按键右键地面是移动右键敌人是攻击右键资源是采集右键建筑是进入载具。这套“根据不同命中选择不同行为”的逻辑通常用一个命令解析器来处理。命令队列的实现我推荐是“每个单位持有一个命令队列Queue”新命令默认入队如果按住Shift则是追加队尾按Alt或者右键某个按钮则是取消当前命令。这和RTS老玩家心里默认的交互是一致的。但这块也是bug高发区。常见问题是单位执行完一条命令后没有正确触发“下一命令”卡在原地发呆。排查思路很简单——确认单位的“当前命令”完成时有没有把队列头弹出。很多套件里命令是一个对象不是枚举好处是不同命令可以携带不同数据坏处是状态机切换时容易丢失“完成回调”。我自己会在每个命令的Execute方法里明确设置“完成条件”到达距离阈值、攻击目标死亡等并在Update里检查完成后再通知状态机切到下一个命令不要依赖Animator的Event去驱动逻辑那个时机非常不可靠。2.3 寻路与避障性能与效果的双重考验RTS寻路Unity自带的NavMesh可以做基础网格寻路但RTS里需要的是“多单位移动时互相避让”。默认NavMesh的RVO局部避障在Unity新版本里依然存在但说实话几十个单位挤在一个路口时表现还是不够自然会出现抖动、停停走走。工具包里的寻路常见方案是两层结构全局寻路用NavMesh或A*网格得到一条路径局部避让用分离力Separation或RVO算法在移动时不断微调方向避免单位重叠。我实操中的经验是不要让所有单位同时计算完整的寻路路径而是给每个单位一个寻路间隔比如0.2到0.5秒之间随机错开。这样视觉上几乎没有区别但CPU消耗能降下一大截。尤其在单位数量高的时候效果明显。另外路径平滑是必须做的。原始NavMesh路径点非常“折线”直接移动单位会显得很僵硬。工具包一般会有平滑算法比如基于Catmull-Rom样条或简单拉直可视线段。如果套件没有自带我建议你一定补上不然单位走路像机器人再好的美术也救不回来。2.4 阵营系统与可视战斗逻辑构建一个可靠的战场规则阵营RTS里面通常有几个玩家控制的己方Player敌方Enemy中立的野生单位或资源点Neutral还有可能存在的友军AI。阵营关系决定了谁能攻击谁、谁能治疗谁、谁的视野能共享。工具包里的阵营管理器一般维护一个阵营Id列表单位在初始化时注册进对应列表。战斗逻辑检测目标时先看目标归属哪个阵营再根据阵营之间的“敌对/友好/中立”关系来决定是否可以攻击。这种设计很干净但要注意一点同一个阵营里如果为了做“不同队伍红蓝对抗”需要把阵营Id做得更粗粒度否则局内分两队就无从下手。战斗逻辑里还有一个核心细节攻击前摇和伤害判定时机。如果攻击动画播放到一半就结算伤害玩家会觉得“我的手还没碰到他他就掉血了”如果结算时机太晚又会有“我明明先出手但对方先死”的不公感。这个平衡没有普适公式只能靠实际手感去调一般工具包默认用动画事件触发伤害我建议你改成“命令执行后经过固定延迟再结算”这样更容易保持一致的手感。3. 实战上手从导入到搭建一局可玩的RTS3.1 导入工具包之后的初始配置清单拿到RTS Starter Kit第一步不是急着打开场景看Demo而是先建一个空场景自己过一遍依赖。原因很简单Demo场景里绑了许多测试资源和脚本直接基于它改后面会带着一堆冗余内容。我建议这么干新建场景删除默认的Directional Light以外的物体从工具包的“核心/Prefabs”里拖入EventSystem、CameraRigRTS相机、GameManager、PlayerManager确认Physics Layer已经配好至少要有“Ground”“Unit”“Building”“UI”四个层打开Project Settings里的Physics确认Layer Collision Matrix不要让UI层参与任何物理碰撞把Gravity那一项的“Unit”层设为不接收重力如果你用单位是Kinematic而非Dynamic这步可以忽略。这里特别说下碰撞矩阵。我见过有人把单位设成普通Rigidbody带重力结果单位莫名其妙从山坡上滑下去。RTS里单位一般不模拟物理重力而是采用Kinematic刚体加自身移动逻辑这样可以完全掌控运动轨迹不受物理微扰动。工具包默认大多是Kinematic但如果你用旧版Demo可能还有遗留的Dynamic刚体记得统一改成Kinematic。3.2 搭建一个能被“选中”和“移动”的基础地图地图这块工具包一般自带Demo地形但要做自己的场景需要注意几点。地面必须有Collider而且它所在的Layer要设置为Ground。没有Collider你的射线检测就永远无法命中地面单位就没法移动。Collider类型用MeshCollider或TerrainCollider都可以但要注意MeshCollider如果网格很大性能会受影响建议用Terrain Collider或分割成多个BoxCollider。编写一个简单的平整地面测试场景新建Plane缩放成合适大小放在原点给Plane一个物理材质摩擦力设成0防止单位移动时受到异常摩擦力其实Kinematic单位不太受影响但预防一下Layer设为Ground拖入工具包里的一个测试单位例如Marine预制体放到Plane上设置单位的阵营为Player然后Play测试。此时你点单位能选中右键地面单位能移动过去说明最基础链路已经通了。如果点击地面没反应先检查EventSystem里有没有PhysicsRaycaster组件主相机上有没有PhysicsRaycaster。这个组件是UGUI与物理射线兼容的关键少了它点击就会被UI层吞掉。3.3 配置单位与建筑调整成本数量出生点工具包里的建筑和单位预制体一般会带一个自定义组件叫UnitComponent或者BuildingComponent。这里面有几个关键字段Cost单位造价BuildTime建造时间Health生命值AttackDamage攻击力AttackRange攻击范围MoveSpeed移动速度ArmorType护甲类型有扩展玩法才需要默认可以忽略。你新做一个单位比如“重型坦克”直接复制一个现有单位预制体改模型和数值再挂上对应的UI图标和头像就能在建造菜单里出现。前提是你把新预制体拖进了“UnitDatabase”或“BuildingDatabase”的列表中。这里我要强调一下“出生点”的概念。生产单位时需要指定一个生成位置。工具包一般提供两种从建筑旁边的自动找空地生成或者玩家手动指定的集结地。集成时容易忽略的是“出生点被占位”的问题。如果建筑门口生产队列连续出5个单位而建筑周围都是障碍物生产会卡住。解决办法很简单——工具包一般有生成位置检测逻辑如果位置被占用会尝试周围螺旋搜索空地。自己写的话别用固定偏移点要写一个“从近到远逐步外扩的环形搜索”。3.4 调试运行一局五分钟的实机验证流程配置完至少一个建筑和一个单位后跑一局最简单的对战自己生产单位用框选拉一队兵右键敌方单位看它们是否交火。我建议的验证流程是这样第一步生产测试 控制玩家资源数值给自己比如1000起步资金然后点击建筑生产5个步兵确认队列数量、建造进度条、资源扣减都正常。第二步移动寻路 框选五个步兵全部右键到地图另一侧一个狭窄路口观察是否有单位卡住、抖动、原地转圈。把速度调高到默认的两倍看是否会有单位飞出地图或者穿墙。第三步战斗测试 放置一个敌对单位选我方单位右键攻击观察攻击目标是否持续、目标死亡后是否自动寻找最近敌人、被攻击单位是否作出反击。这套流程跑完你基本能判断这个工具包是否适合你的项目继续深挖。如果连这五个步都调不顺那说明套件里核心逻辑还有依赖问题越早排查越好。4. 常见问题与排查技巧实录4.1 单位不响应命令先看“层”和“事件”再查逻辑这个堪称RTS插件最常被问的问题。我的排查顺序按高概率排检查PlayerManager里的当前玩家是否拥有该单位很多工具包在初始化时只把标签是“Player”的单位归到玩家控制如果你新建的单位忘了设置标签或阵营命令系统就会“看不到”它。检查鼠标射线是否打中了Ground层如果场景里有UI的Image挡住了地面或者主相机上没有PhysicsRaycaster射线根本到不了地面。检查命令队列是否被“停止”命令清空如果之前测试时按了停止快捷键命令队列为空单位自然不会动。检查单位的Animator是否处于Idle且CanMove标志被禁用有些工具的UnitMovement组件依赖动画状态机一旦状态机被打断单位就锁死。这几步做完90%的问题能定位。剩下10%是工具包自身bug那就要看源代码比如命令ID冲突、事件监听没有取消注册等问题。4.2 框选时误选到UI或建筑很多RTS项目的UI是叠在游戏画面上的框选时如果鼠标点到了UI本意是拖动地图框选结果系统把它当成UI按钮点击直接导致框选失效。排查方法检查EventSystem里的第一个Raycaster是不是“Graphic Raycaster”如果是并且放在主Canvas上那所有UGUI元素都会消耗点击。我建议是给UI Canvas单独建立一个EventSystem或者用“UI层不参与物理检测”来规避而不是去改UI元素的Raycast Target属性。一个更稳的做法是在框选开始时先检测鼠标是否在UI上调用EventSystem.current.IsPointerOverGameObject()如果是则跳过框选逻辑。工具包有的默认没做这个判断你需要自己加上这是RTS操作手感的基础保障。4.3 寻路卡顿和单位“互相表演堵车”单位多了移动时必然互相堵路。有几个有效的优化技巧降低寻路频率不是每帧寻路而是每0.3秒对“距离路径终点较远的单位”重新寻路一次对单位分组离得很近的一小组单位移动到大致同一目标方向时可以让它们共用一个“组路径”组内成员只微调局部避让放宽碰撞体尺寸RTS不想让单位完美贴合用比视觉尺寸小20%的碰撞体对移动更友好直接禁用单位之间的碰撞只保留“单位与障碍物”的碰撞单位之间的物理碰撞完全交给避障逻辑处理。这个在很多商业RTS里是常用做法因为物理碰撞是刚体计算而避障是逻辑计算前者成本高后者可控。另外如果你发现单位在目标点附近像“布朗运动”一样来回乱走多半是停止条件的“到达距”设置得太严格。检查MoveCommand里ArriveDistance这个参数默认值建议设为单位半径的1.5倍太小数会让单位一直尝试“精确对位”。4.4 性能瓶颈排查瓶颈往往不在渲染而在“逻辑Tick”RTS游戏的性能瓶颈恐怕不是Draw Call而是Update里的逻辑Tick。工具包默认给每个单位单独一个Update单位数量几百的时候每一帧都要算寻路、避障、攻击判定压力非常大。我常用的方案是把单位逻辑更新迁移到自定义的Manager里做批量帧率控制。比如UnitManager里维护一个单位列表每帧只“真正更新”其中一部分单位像动画等可以用普通Update但寻路和攻击不用每帧执行。这样总CPU开销能降到原来的五分之一。另一个隐蔽的性能坑是频繁创建和销毁命令对象。鼠标右键一下地面每个单位就new一个MoveCommand几百个单位就是几百个对象。GC压力不小。工具包如果没做对象池你可以自己做一个CommandPool回收命令对象。我看到很多开源项目里就是用结构体struct来存命令数据避免GC这个思路很值得参考。5. 踩坑后的心得与扩展建议老实说工具包能节省我大概一个半月的基础代码工作。但套件归套件真正决定RTS游戏好不好玩的还是上面那些“手感”细节——选框方向、攻击判定时机、单位碰撞宽度、寻路响应速度。我做过一个模拟项目单位数量拉满到300同屏战斗跑起来帧率还能稳住。当时最花时间的不是搭建逻辑而是反复调“框选-移动-攻击”这条链路的手感。如果你准备深入改造套件我强烈建议把命令系统抽出来单独理解。很多后续玩法的扩展都建立在命令上技能命令、建造命令、巡逻命令、载具命令都是“命令对象”的不同实现。只要命令解析器写得干净加一个新命令就跟加一个类一样简单不用动单位主逻辑。还有一个方向值得关注RTS的战争迷雾和视野系统。工具包一般只提供最基础的视野遮挡如果你要做类似经典RTS的战争迷雾需要自己结合RenderTexture来做雾效遮罩或者用2D TileMap方案存视野数据。这块没有一劳永逸的工具至少要预留一个接口让单位能被查询“当前是否可见”否则后面的动态生成、小地图、寻路逻辑都会受影响。最后训练AI也是一个巨大的扩展点。很多RTS工具包自带的AI逻辑极其简单无非是“攒一波兵然后A过去”。真要做有策略感的AI需要自己搭建决策树或行为树并且在单位层面支持“区域集结”“撤退百分比”“集火目标”这类指令。如果套件里这些接口都没有建议趁早期改造而不是等AI写到一半才发现踢不动。按我这几年的习惯拿到一个RTS工具包我不会急着堆功能而是先花一周把它的命令系统、阵营关系和寻路参数吃透。因为RTS这个品类表面拼的是画面和数值底层拼的永远是单位调度和交互手感。把底座理清楚后面的一切都是水到渠成。这个工具包是不是“完整”不重要重要的是你拿到它之后能不能用它快速进入“验证可玩性”的阶段——对我来说它确实做到了。