用 AI 在 Unity 里做一款 RTS:最难的不是生成代码,而是证明它真的能玩

用 AI 在 Unity 里做一款 RTS:最难的不是生成代码,而是证明它真的能玩

|----------------------------------------------------------|
| 从程序化美术、运行时 UGUI,到 Windows 真实输入注入与异步测试协议:一次没有把失败藏起来的工程复盘。 |

我们在 Unity 中做出了一个包含采矿、生产、建造、战斗和胜负判定的 RTS 原型。但"功能接口能跑"与"玩家通过鼠标能完成一局"之间,隔着输入系统、编辑器生命周期、异步调度和测试可信度四道门。真正让进度停滞的,不是复杂的游戏算法,而是把排队当完成、把解析失败当业务失败、把弱证据当根因。本篇复盘既记录实现,也记录这些判断错误。

先说明交付边界:这是已打通核心交互的可玩原型,不是已完成发布验收的成品。 最新真实输入测试完成了选总部、造工兵、建兵营、等待施工、出步枪兵和自动接敌;整局主动进攻取胜尚未验收。最后新增的热重载索引恢复补丁,也仍待编辑器编译与运行验证。

一、起点:要做的不是一张"像游戏"的截图

需求很明确:充分使用 UI 绘图、布局、贴图、模型和编程能力,做一个可玩的 RTS,并提供需求、任务划分、实现方式与模拟试玩。

我们将原型命名为"铁线前线 / IRON FRONT",把范围压缩为一张平地地图、两个阵营、三种单位和三种建筑。工兵负责采矿与建造,步枪兵承担基础火力,坦克提供高血量和重火力;总部维持经济,兵营生产战斗单位,炮塔提供防御。胜利目标是摧毁敌方总部。

这里最重要的产品决策不是增加内容,而是保住闭环:

资源 → 生产 → 人口 → 部队 → 战斗 → 胜负 → 重新开始。

如果采集的资源没有用途,经济只是动画;如果生产的部队无法指挥,单位只是模型;如果只能用测试接口直接摧毁总部,胜利面板只是展示。一个可玩的原型,应当让这些环节由玩家操作真正串起来。

开发材料分为四份:需求文档、任务划分、实现方案和试玩报告。它们分别回答"做什么""按什么顺序做""怎样实现"和"有什么证据"。但后面的经历也证明:文档里写了验收,不代表验收的口径就正确。

二、技术选型:先让闭环成立,再为规模付费

本次环境是 Windows、Unity 2021.3.45f2c1、Built-in 渲染管线,使用 C# 编写工程逻辑,Node.js/Python 编排编辑器自动化,通过 Unity MCP 桥接执行操作。

2.1 游戏层与自动化层必须分开

系统可以拆成三个平面:

|----------|-----------------------------------------|---------------------|
| 平面 | 主要组成 | 职责 |
| 游戏运行平面 | RtsGame、RtsUnit、RtsBuilding、RtsAI | 维护经济、状态机、生产、战斗与胜负 |
| 表现与交互平面 | RtsHudBuilder、RtsHUD、RtsInput、RtsCamera | 显示状态、接收玩家输入、把输入变成命令 |
| 自动化与观测平面 | 编辑器工具、NativeInput、RtsCheats、试玩脚本 | 构建场景、注入输入、读取证据、组织回归 |

其中 RtsCheats 是测试辅助入口,不是玩家玩法入口。调用它能证明业务逻辑,却不能替代对 EventSystem、鼠标拾取和 UI 可用性的验证。

最初为了快速推进,这条边界被弱化了:直接调用按钮回调通过后,便容易误以为"按钮测试通过"。实际上,那只证明了回调之后的半条链路。

这是一个单位规模较小、地图平坦的原型。移动先用直线推进,配合黄金角散点目标和周期性分离力;单位行为用有限状态机,敌方 AI 用周期性决策。

工兵的核心状态链是:空闲 → 前往矿点 → 采集 → 返回基地 → 交付 → 再次采集。战斗单位则在移动、攻击、空闲与死亡之间切换。

这些选择减少了烘焙、导航配置和框架搭建成本,但不能包装成无条件优势。没有全局寻路,意味着未来复杂障碍、狭窄路口和大部队拥堵都会成为瓶颈;两两分离的开销接近 O(n²),也不能靠一次小规模帧率采样证明可扩展性。

早期记录中有"17 单位、202 FPS"的样本。它只能说明当时那次场景采样,没有证明 100 个单位、多建筑、频繁特效或自动化连接下的长期性能。

2.3 数值需要一个真值源

单位造价、人口、血量、伤害、生产时间等集中在 RtsDefs。HUD、生产逻辑与 AI 读取同一份定义,减少"显示价格是 90,实际扣费却不是 90"的分歧。

双阵营经济也必须真正隔离。早期曾出现共用经济与人口重复计数,后续拆出按阵营访问的接口,并明确资源扣除、人口占用与单位生成各在哪个时刻发生。

这里可以提炼一个原则:状态变化应当只有一个结算位置,其他模块表达意图,不重复结算。

三、程序化美术与 UI:产物存在,不代表资源正确

模型主要由基础几何组合:坦克通过车体、履带与炮管表达轮廓,建筑用体块和阵营色区分功能。地面、岩石、金属、水晶使用程序化贴图,UI 面板、按钮与图标由绘图能力生成。

这条路线适合原型:统一风格、无外部素材依赖、可反复生成。代价是细节、动画与视听反馈仍然有限,不能把"有模型"当作美术完成。

3.1 图标线条消失:先验证渲染能力,而不是相信参数名称

绘图过程中,line 元素的描边表现与预期不一致:stroke_width 没有带来期望线宽,某些配置下线条甚至不显示。最终把炮管、枪身等斜向部件改成 polygon,通过顶点构成有面积的形状。

经验不是"所有绘图工具都不能画线",而是:能力描述与实际输出之间,必须有最小样张。 在批量生成五个图标前,先生成一条粗线、一个斜杆和一个圆角面板,可以更早发现问题。

3.2 九宫格变形:要检查导入后的纹理

部分源图尺寸不是 2 的幂,在本工程的导入设置下被 NPOT 重采样,圆角和九宫格比例因此失真。后来统一使用合适的 2 的幂尺寸,并在运行时创建 Sprite、设置 border、启用 Sliced。

这里要避免过度归纳:不是 Unity 的所有贴图都必须是 2 的幂,而是本次导入配置让非 2 的幂纹理发生了重采样 。更稳健的方案是同时校验源图尺寸、导入尺寸、npotScale、Sprite border 和最终控件显示,而不是只检查磁盘上存在 PNG。

3.3 运行时 UGUI:引用正确比布局漂亮更基础

HUD 通过代码创建标准 Canvas、Image、Text 和 Button,明确绑定 targetGraphic 与 onClick,使用缩放画布适配 GameView。

运行时构建可以降低手工拖引用的成本,却会把错误从编辑期推迟到运行期。于是需要装配断言:字体是否存在、按钮引用是否非空、背景是否拦截射线、EventSystem 是否唯一、装饰性 Image 是否应该关闭 raycastTarget。

顶部常驻的深色横条就是一个小但典型的问题:Toast 文字清空了,背景仍显示。修复并不复杂,却说明了"状态数据正确"和"用户看到正确反馈"是两个不同的验收对象。

四、游戏逻辑里的坑:规则必须区分来源和生命周期

4.1 AI 走到一半不打了:自动行为不能覆盖明确命令

早期敌方部队向玩家总部推进,走到一定距离就放弃。根因是自动索敌的牵引距离限制,也作用到了明确攻击命令上。

修复时加入 AttackOrdered 标记:自动索敌需要限制追击范围,明确攻击指令则不能被同一条规则无条件取消。

它背后的设计问题是命令优先级。未来更好的表达不是不断增加布尔值,而是给命令附带来源、优先级、目标和失效条件,例如"玩家命令""AI 战术命令""自主索敌"。低优先级行为不能悄悄覆盖高优先级意图。

4.2 单例恢复不等于状态恢复

域重载会清空静态字段,因此 RtsGame.I 等单例需要重新绑定。早期加上 OnEnable 自愈后,空引用异常减少了,但问题没有彻底解决。

后来观察到一种更隐蔽的状态:人口与总部引用还在,单位和建筑列表却变成了空列表。原因是运行时注册索引并没有随场景对象一起恢复。

这提示我们区分三类东西:

• 事实:场景里真实存在的单位、建筑、资源点。

• 持久状态:血量、经济、计时、生产进度等应保留的数据。

• 派生索引:用于快速查询对象的列表、字典与缓存。

派生索引应该能够从事实重建。最后已补充 OnEnable 扫描恢复逻辑,避免重复人口和经济结算;但该补丁的最终编辑器编译与运行验证没有拿到结论,不能写成已解决。

五、最耗时的部分:让鼠标真的进入 GameView

原有输入模拟只改写 Event.current。它不是能驱动 Play 模式 Input Manager 的完整输入后端,因此"调用成功"并不意味着游戏收到鼠标或键盘事件。

为此新增 NativeInput,使用 Windows user32 的 SendInput、SetCursorPos 等能力,驱动旧 Input Manager 与 UGUI EventSystem。这是平台相关方案,不是跨平台通用实现。

5.1 一次点击至少涉及四个坐标空间

场景对象在世界坐标;Camera.WorldToScreenPoint 输出 Unity 屏幕坐标,原点在左下;自动化约定 GameView 内容区左上为原点;Windows 光标使用桌面坐标。

换算关系可以概括为:

|--------------------------------------------------------------------------------------------|
| 世界点 → 相机屏幕点 (sx, sy) GameView 左上坐标 = (sx, 内容高度 - sy) 桌面坐标 = GameView 内容区原点 + GameView 左上坐标 |

GameView 窗口左上角不等于内容区左上角,因为还有标题栏、工具栏、边框及可能的留白。实测采用移动光标并回读 Input.mousePosition 反解原点;读数必须等游戏帧更新,不能 SetCursorPos 后立刻读取。

窗口拖动、布局变化、分辨率与 DPI 都可能让标定失效。因此标定应当是有版本和有效条件的数据,而不是永远可信的常量。

5.2 系统焦点与 Unity GUI 焦点不是一回事

滚轮最初没有作用。即使光标位于窗口内,GameView 没有获得编辑器 GUI 焦点时,滚轮仍可能被编辑器接收。补上窗口 Focus 后,才观察到相机高度随滚轮变化。

输入调试至少要同时看:桌面焦点、GameView 焦点、Application.isFocused、Input 采样值和 EventSystem 射线命中。只看其中一个,很容易得出错误结论。

5.3 分帧输入:编辑器 update 不等于游戏帧

鼠标按下和抬起若发生在同一个游戏采样周期,游戏可能完全看不到有效点击。拖拽还要求按下、若干移动、抬起保持顺序。

早期队列按编辑器 update 出队,每步隔一段最小时间。但主线程会被编译、资源导入和请求处理占用,固定延迟并不代表游戏已经消费事件。

最终修复方向是:整次输入操作串行展开;关键步骤之间检查游戏帧推进;提供 busy 与 pending_steps;调用方等 busy=false,再读取业务结果。指针落位等待的超时,也从真正开始执行时计算,而不是入队时就开始倒计时。

核心转变是:用可观测的完成条件,替代猜测性的等待时长。

六、为什么跑了半天:真正的问题是诊断系统不可信

这部分比某一个 API 的用法更值得保留。过程中多次给出了过早、甚至错误的根因判断,而反复长跑放大了代价。

6.1 非法 JSON,被误判成业务失败

Report 曾输出如下片段:

|---------------|
| "supply":3/20 |

它是人能看懂的表示法,却不是合法 JSON。Probe 中还有布尔值拼接成 True/False 的问题;合法 JSON 要求小写 true/false。

脚本解析失败后返回空对象,再用默认值判断"建造未开启"。于是一次观测失败,被翻译成一次业务失败;已经开启的建造模式又被脚本主动取消。

正确结果类型应当至少区分:请求失败、任务执行中、工具返回失败、JSON 解析失败、断言不满足、断言通过。它们不能全部折叠成一个 false。

6.2 同样是数字,按钮序号不是建筑类型

建造按钮是 cmd_3,但 BuildingKind.Barracks 的值是 1。脚本将按钮序号直接传给 FindSpot,造成选址类型错误。

这不是高深算法错误,而是典型的跨接口语义混淆。解决方向是传符号化名称或使用带类型的客户端封装,不再让裸整数跨边界流动。

6.3 读得太早,然后误以为点击丢失

输入接口返回的是"已经排队",脚本却立即读取选择数和按钮状态。结果经常是"刚框选还显示总部""刚点建造却显示未开启"。

随后有过一个错误修法:新操作前清空积压队列,补发抬鼠标。它看似能解除按住状态,实际上会取消合法输入,甚至截断本该执行的后续步骤。

后续撤掉这种做法,改成 FIFO 串行操作与显式完成状态。吞掉旧任务不是解决异步问题,而是在改变业务语义。

6.4 对局失败,不等于平衡失败

多局无人有效操作时,AI 在约两分钟内摧毁玩家总部。这可以证明失败路径可达,不能证明"玩家必输",也不能证明难度曲线合理。

过程中曾过早归因于 AI 过强,并调整首波、投入比例与生产节奏。实际上,自动化玩家连兵营都没稳定造出来。应先修操作链路,再在相同版本、相同策略下做对照测试。

类似地,工具之间的思考和等待时间,让游戏继续推进,不能仅凭几条日志就判定 timeScale 异常。要同时记录墙钟时间、游戏时间、timeScale、暂停状态和采样间隔。

七、测试全流程:每一层究竟证明了什么

|--------|-------------------------------|-----------------|-------------|
| 层级 | 本次使用的手段 | 能证明 | 不能证明 |
| 编译 | 工程重编译与编译状态查询 | 工程脚本通过当前编译 | 游戏可玩 |
| 场景装配 | RtsValidator 的 16 项断言 | 必需层级、组件和引用满足检查 | 所有运行时状态正确 |
| 业务集成 | RtsCheats 调真实工程方法 | 扣费、生产、战斗、结算逻辑可达 | 玩家点击链路可达 |
| 输入端到端 | SendInput + InputProbe + 状态断言 | 原生输入经过游戏交互产生结果 | 一整局必定可完成 |
| 画面检查 | GameView 截图 | 该帧的布局与视觉表现 | 长时间性能、输入成功率 |
| 完整产品验收 | 从新局到胜负再重开 | 玩家闭环及生命周期 | 仍需不同环境与策略覆盖 |

这里还要区分两条编译通道。code.execute 的 runtime 模式是 CodeDOM 内存编译,用于执行小片段;它能成功,并不代表 Assets 下整个工程完成编译。工程代码仍须通过专门的重编译与诊断状态确认。

直接注入代码读取字段也曾出现不可靠表现。我们因此把关键断言方法放进工程内的测试组件,再从外部调用实例方法,减少跨编译上下文观察状态的不确定性。

7.1 对早期"12/12 通过"的重新解释

早期报告记录了 12 项通过,包括经济、生产、建造、战斗、失败和胜利面板。但其中不少通过测试接口完成;胜利路径包含强制摧毁敌方总部。因此它们是阶段性的逻辑集成证据,而不是完整真人输入验收。

旧报告对点击限制的解释也已被后续实现推翻:早期输入空实现不能驱动游戏,不能据此断言系统天然不支持真实注入。技术复盘必须允许新证据修正旧结论。

7.2 最新真正跑通的流程

在修正诊断数据、建筑枚举与异步完成等待之后,play11 记录了:

|-----------|-----------------------------------------------|
| 游戏时间 | 行为或结果 |
| 约 15 秒 | 真实点击选中总部 |
| 约 24 秒 | 总部排产两名工兵 |
| 约 45 秒 | 真实点击放下兵营,正确扣除建造费用 |
| 约 50 秒 | 选中施工中的兵营 |
| 约 64 秒 | 等到可生产后,排产三名步枪兵 |
| 约 102 秒 | 对局仍运行,击杀 3、损失 0 |
| 后续约 156 秒 | 蓝方 10 单位、2 建筑,人口 10/25,总部 2753/3000,击杀 6、损失 0 |

这些证据足以说明核心输入生产闭环成立。战斗成绩主要来自单位接敌后的自动行为;机器人主动框选军队、攻击目标、推进并取胜,还没有完成严格验收。坦克生产尝试也不能算通过,后续状态已受到热重载问题影响。

八、下一次应该怎样排查:先缩小系统,再扩大测试

如果重新来一次,我会采用以下顺序,而不是连续跑十多分钟再看最后一行:

  1. 先测观测器:所有探针返回都经过 JSON 解析和字段类型断言,缺字段立即报错,不用默认值掩盖。

  2. 先测一个按钮:只验证"移到按钮 → Input 回读到位 → UI 命中正确 → 按下抬起 → 资源变化"。

  3. 再测一个建筑:框选工兵 → 开始预览 → 找合法位置 → 确认 → 扣费一次 → 施工完成。

  4. 再测一个单位:等待生产可用 → 入队 → 队列变化 → 单位生成 → 人口变化。

  5. 任何断言失败就停止,不自动按 Escape 继续下一轮,不把错误状态带到后面。

  6. 调整产品参数前固定版本与策略,至少比较相同条件下的结果。

  7. 最后才运行完整对局,并验证结束面板、重开、资源重置和输入释放。

每一步都应记录前置条件、操作 ID、完成条件、前后状态和证据路径。这样失败能定位到一条边,而不是归因于"Unity 好像不响应"。

对于异步请求,还应保留同一个 request_id 查询结论。网络超时只说明没有收到响应,不说明操作没有执行;盲目重试可能重复扣费、重复建造或重复切换播放模式。

九、从原型到产品:按风险排序,而不是按功能欲望排序

P0:先把可信度补齐

• 验证最后的热重载恢复补丁:对象索引恢复、人口不重复、生产队列保留、HUD 订阅恢复。

• 诊断数据统一使用 DTO 与序列化器,加入协议版本、游戏局 ID、帧号、游戏时间和请求状态。

• 输入完成从全局 busy 升级为每操作状态:accepted、running、completed、failed、cancelled;失败不再静默降级。

• 暂停、退出 Play、编译重载和异常退出时确保释放按键;为输入队列增加有语义的取消与清理,而不是随意丢步骤。

• 用真实输入完成至少一局失败、一局主动胜利,并验证重开。当前不能把这项写成已完成。

P1:补玩家能理解的反馈

• 建筑施工进度、生产队列、集结点与不可用按钮原因可视化。

• 建造失败说明具体原因:越界、与建筑重叠、压到矿点、无工兵或资源不足。

• 提供操作引导、单位快捷选择、反馈音效和基础动画;把"功能存在"变成"玩家知道怎么用"。

• 给录像、截图和测试报告打上版本与局 ID,避免不同构建的数据混在一起。

P2:再谈平衡与规模

• 平衡基于有效玩家策略,而不是无操作失败局;比较首个兵营时间、首支部队时间、首次交战和首次损失。

• 对地图加入障碍后再评估 NavMesh、网格 A* 或流场,依据单位规模与地图形态选择。

• 用空间分桶降低索敌和分离扫描成本,配合 Profiler 测量主线程、GC、UI 重建与特效生成。

• 真正实现对象复用、状态重置与死亡回收;不要因为生成入口叫 RtsPool 就宣称对象池已经完善。

P3:形成可持续交付的自动化平台

最终值得沉淀的不只是这款 RTS,而是"构建---运行---操作---观测---断言---归档"的稳定链路。它可以服务 UI 回归、编辑器工具验证和其他交互原型。

但系统级输入会占用真实鼠标与焦点,不适合无约束地与用户并行工作。后续需要独立测试桌面或专用机器、窗口与 DPI 变化检测、每次会话授权边界,以及可回放的输入与状态轨迹。业务测试可以走无界面逻辑通道,原生输入只保留给真正需要覆盖的端到端路径。

十、结语:AI 工程能力的上限,取决于闭环的可信度

这次工作最直观的成果,是一个能采矿、建造、生产和战斗的 Unity RTS 原型。更重要的成果,是把几条容易混淆的界线重新划清:

生成了代码,不等于工程编译;工具返回成功,不等于动作执行完;接口测试通过,不等于玩家流程通过;看见失败,不等于找到了根因;有胜利面板,不等于已经用玩家操作赢过。

AI 可以快速铺出大量实现,也能同样快速地产生大量似是而非的解释。如果没有可靠观测、状态契约和分层验收,速度会变成重复试错的放大器。

下一阶段不应该急着增加第四种单位,而应该完成最后一条没有闭合的路径:从干净开局开始,由真实输入建立经济、组织军队、摧毁敌方总部,再安全地重新开始。只有这条路径稳定可复现,才有资格讨论更大的地图、更聪明的 AI 和真正的产品化。

元信息与材料来源

• 目标读者:熟悉 Unity/C#、希望用 AI 自动化开发和测试交互产品的中高级开发者。

• 前置知识:Unity 生命周期、UGUI、基本状态机与异步请求。

• 技术环境:Unity 2021.3.45f2c1、Built-in、Windows user32、旧 Input Manager、UGUI、Node.js/Python 标准库桥接;桥接运行时具体版本未纳入本次统一验收。

• 文章性质:项目实战复盘,不是跨平台输入注入教程,也不提供未经验证的性能承诺。

• 标签:Unity、RTS、AI 编程、自动化测试、工程复盘。

• 参考材料:docs/rts/01-需求文档.md、02-任务划分.md、03-实现方案.md、04-试玩报告.md;项目源码 RtsGame、RtsUnit、RtsBuildPlacer、RtsCheats、NativeInput、GamePlayTool;play11.log 与后续状态探针记录。

相关推荐
vivo互联网技术16 分钟前
AI Agent 技术全景解析 —— 从提示词工程到高级智能体构建
人工智能·agent
武子康20 分钟前
H3-World 的动作能按时发生,为何还不能据此判断实时互动?
人工智能·深度学习·agent
师夷长技以自愈22 分钟前
手搓 AI Agent 第 4 篇:后厨扩招——AI 从此能读、能写、能改、能跑
人工智能
Dawson Zhu1 小时前
【LLM推理与Serving⑨】业务模型推理引擎开发:以BEV自动驾驶为例的专用推理实践
人工智能·语言模型·架构·aigc·agi
袁俪1 小时前
数字孪生协同
人工智能
海拥✘1 小时前
让 AI 修 AI 平台自己的 Bug:我用 SoduWork 给 SoduWork 提了一个 PR
人工智能·bug
挖掘狂人1 小时前
AI 把代码写得飞快,然后呢?聊聊我验收生成代码的一套流程
人工智能·程序员·ai编程
汤姆宝库1 小时前
推理服务器的调度器:连续批处理与请求生命周期
人工智能
Rocky Ding*2 小时前
【三年面试五年模拟】2026-11-05_月之暗面大模型算法岗一面、二面面经全解析
论文阅读·人工智能·深度学习·机器学习·aigc·强化学习·ai-native