跳到正文
Hacker News· kkukshtel·· 5 天前AI 评分34

如何为游戏开发打造 AI harness

How to engineer an AI harness for game development

AI 导读

一位拥有近 15 年经验的游戏程序员提出,LLM 在游戏开发上表现糟糕,根源在于游戏中大量非代码资产对模型不透明,而 Three.js 等"一切皆代码"的工作流之所以可行,正是因为资产可用简单几何体表示。他主张为游戏开发专门设计 AI harness,而非依赖通用 LLM 工作流。

正文 · AI 翻译

harness

我于 9/12/26 在卡尔加里的 Indie Rodeo 活动中做了一个 45 分钟的演讲,题为 “Your Game Is A Harness”。在演讲后与很多人交流之后,我觉得最好能更广泛地分享我在那里提出的想法,以一种不仅限于参会者、能让更多人欣赏的形式呈现。

另外,因为我在这个博客上经常谈论 AI,人们会以为我用 AI 写作。这不是真的!以下所有内容都是 100% 由人类撰写的,我写这篇文章时基本上只参考了我展示的幻灯片(这不是转录稿)。尽管我很喜欢用 AI 来写代码和工具,但我也从 “老派方式”写作中获得类似的乐趣,尤其是因为我没有任何理由试图让这个过程的任何部分变得更高效。

那么,废话不多说,我向你们呈现我人生中第一次将演讲博客化。请欣赏!


引言

[我对自己和自己的作品做了简短介绍,这里省略,因为你正在我的网站上]

我想谈谈在游戏开发中使用 LLM。不是那种在 Twitter 上看起来很酷的一次性演示,而是真正的开发。那种你每天工作八小时、每周五天等等的开发。

在深入之前,我觉得有必要先对 LLM 做个基准设定。或者至少讲一些基础知识,这样我接下来讲的内容才说得通。我明白对很多人来说这些是复习,所以如果你只想看开发循环相关的内容,请向下滚动到下一节。

我使用 LLM 进行开发工作的旅程始于我做的一个情绪板网站,叫 mood.site。那还是在任何 “智能体”出现之前,但我想测试一下当时的 ChatGPT 是否真的能写代码。这个过程很大程度上就是我把代码盲目地复制粘贴到 ChatGPT 的网页界面里。这个过程很糟糕,但我对结果感到惊讶。我建成了这个网站!它跑起来了!我仍然需要自己验证相当多的东西,但这足以让我转变观念。如果我没有 LLM 可用,我不可能以最终花费的时间(大约 2-3 周的夜晚,总共可能 20 小时左右)做出那个网站。我可能今天还在构建第一个版本。

从那以后,mood 在某种程度上成了我探索 LLM 相关东西的试验场(而且本身也是一个还算成功的项目!),并激励我启动了许多使用类似技术栈的其他项目。此外,LLM 变得更好了,尤其是有了 Claude Code 和后来的 Claude 模型。根据我自己的经验,我觉得现在对于基于 Web 的技术栈,几乎没有什么是我认为 LLM 不能做的。

那么很容易就会说 LLM 能做一切,因为很多软件都是 Web 软件。但每当我尝试用 LLM 构建非 Web 的游戏东西时,都会得到一个响亮的结论:

Screenshot 2026-09-19 at 2.00.12 PM

“LLM 不擅长游戏”

每次我尝试用 LLM 做游戏相关的事情,它都会崩溃。也许这是我自己的偏见。我做了近 15 年的游戏程序员,我觉得我懂这些东西是怎么运作的。我一眼就能嗅出 “糟糕的”游戏代码。但说到 Svelte 网页应用,我就明显没那么熟悉了,而且倾向于认为 LLM 生成的代码都是 “好的”。也许它实际上就是垃圾。但确实有理由相信 Svelte 和 Web 技术栈的东西是高质量的,部分原因是网上有大量高质量且源码公开的 Web 项目。相比之下,对于游戏,我猜可用来训练的数据要少好几个数量级。

不仅如此,游戏里还有大量东西并不是代码。游戏不只是把静态图片和文字放进一个布局里。游戏里有无数东西是代码难以表示的。而且在此之上,Web 的所有文字布局那套东西它都有,但这只是游戏需要处理的另外 100 个系统中的一个子集。

这就是为什么很多人一直在用 Three.js 做游戏的部分原因——他们可以轻松地把资源表示为简单的方块图元,跳过游戏通常需要的大量资源生成/适配工作和处理。结果就是,很多这类东西都是粗制滥造的,因为除了 LLM 的中位数水平之外,它们缺乏任何真正的作者声音。

不过,也许他们说得有道理。

Screenshot 2026-09-19 at 2.01.19 PM

一次性 Three.JS 工作流(以及 LLM 在构建 Web 应用方面的熟练度)的一个关键理念是,一切都是代码。把东西当作代码,LLM 就很容易对它进行推理、做出修改等等。

Screenshot 2026-09-19 at 2.02.41 PM

然而对于传统游戏,情况并非如此。或者至少不是全部真相。游戏显然有代码,但还有很多其他东西不是代码。这意味着游戏的一些关键组成部分对 LLM 来说实际上是不透明的。如果你的游戏依赖于对这种不透明东西的有效操控和变换,那么 LLM 在尝试处理你的游戏时就会崩溃。

Screenshot 2026-09-19 at 2.09.12 PM

我觉得很多游戏开发社区可能不久前试了试 LLM,然后直接撞上了这个问题。 “LLM 很糟糕/没用/等等”这种情绪(这里有个巨大的前提,我只谈能力本身,不谈可能终结社会的潜在影响)就来自于一头撞上了这些限制。

Screenshot 2026-09-19 at 2.11.28 PM

这就是说,从结构上讲,LLM 不擅长阅读游戏。至少在其默认状态下是这样。

LLM 基础

让我们进一步谈谈原因,以及我们能做些什么。

对任何 LLM 服务的任何调用/提示都分解为三个基本组成部分。系统提示、助手提示和用户提示:

系统提示: 一项服务所有用户共享的通用文本。这通常描述 LLM 本身的基本情况,比如 “你是 Opus 5,一个由 Anthropic 创建的智能大型语言模型……”。这并不是说这些不重要(它们很重要),而是说它们与我们的用例不太相关。你可以在这里看到一些已发布(和被泄露)的模型系统提示示例。

助手提示词:这算是一个辅助提示词部分,用来帮助引导 AI 理解它正在回应什么。你可以把它基本理解为模型与你提示词之间的基本 “交互”向量,它代表了模型基于某些输入对用户的 “回应”。通常你不需要考虑这一点。

用户提示词:这才是真正关键的部分。你输入到任何 AI 工具中的几乎所有内容都会放在这里。任何提交的聊天、代码等都会被塞进这一部分。当人们谈论 “上下文”时,他们很大程度上是在谈论 “提示词的用户(和助手)部分中存储了多少 token”。

人们很容易以为每次聊天都是上述内容的 “全新”版本,但实际发生的是,当你在某个会话中与模型交互时( “会话”的边界因你使用的服务而异——可以是关闭前的单个 Claude Code 会话、网页托管模型侧边栏中可访问的 “聊天”等等),你之前发送给模型的每条消息都会在你每次发送消息时重新提交。这有点像玩老式的操场游戏:轮到你时,你必须再次说出其他人的内容,然后再加上你自己的内容,让下一个人继续添加。

Screenshot 2026-09-24 at 8.35.14 AM

上下文与上下文污染

随着你一遍又一遍地这样做,你会积累起你所处会话的 “上下文”。

Screenshot 2026-09-24 at 8.35.51 AM

这一切都很好,但这里需要注意的一个关键点是,你实际上只能处理上下文中的 “最新”内容(即你提交的新消息)。之前消息中的内容会被一遍又一遍地提交给模型。因此,你要确保上下文中的内容是 “好的”,否则你可能会陷入退化场景,上下文因早期 “坏”内容不断影响新工作而受到污染。

Screenshot 2026-09-24 at 8.47.56 AM

因此,管理上下文是与 LLM 协作的关键事项,鉴于这些限制,一种名为 “上下文工程”的新工程实践已经出现。上下文工程要问的是 “我们如何将最佳信息放入上下文,同时尽可能减少污染”。

这里有一篇很棒的博客文章,讲的是 “长上下文失败的方式”,其中对此有更多探讨,但我会在这里强调关键部分:

Screenshot 2026-09-24 at 8.50.21 AM

上下文工程本身是一个非常大的话题,超出了本次演讲/博客的范围,但我们将在这里使用它的一些原则,来探索如何帮助 LLM 更好地处理游戏。

换句话说,让 LLM 与游戏良好协作是一个上下文问题,而不是与游戏或 LLM “本身是什么”(或 “不是什么”!)有内在关联的事情。

Screenshot 2026-09-24 at 8.52.19 AM

不过,这里的复杂因素是,游戏很古怪。或者至少可以说:我们没有真正的模板来构建游戏。我们有游戏引擎,但实际上几乎没有任何标准化工具,部分原因是每款游戏所呈现的需求都各不相同。

Screenshot 2026-09-24 at 8.56.06 AM

这对游戏来说极其独特(在我看来),但它在其他技术领域中也表现得足够明显,以至于出现了一个新概念,目的是让智能体能够有效地 “处理”你的任何工作成果:Harness。

关于 Harness 概念的一些描述

Harness 的总体思路是,它是一些固定的行动模式,智能体针对某些定义明确(或宽松)的预期输入来采取这些行动。你可能已经在使用像 Claude Code 这样的 harness,但这些可以被增强、扩展、提炼、抽象等,以更好地支持你正在尝试做的特定工作。最近这个领域也有大量动作,许多 harness 被构建出来用于开发各种各样的东西:

Screenshot 2026-09-24 at 8.59.31 AM

这些全都指向的关键点是: “我怎样才能更好地理解我正在构建的东西,并在开发过程中引导这一过程”。Harness 可以整合诸如用浏览器迭代、遥测、性能分析,甚至完整的SDLC工作,比如创建工单、文档等。

然而,这些都没有做好的一件事是真正与你的游戏对话。你的游戏有点像一个黑箱,而LLM能看到的大部分只是写出来的代码,正如我们之前谈到的,这其实还不够。我们知道 LLM 已经擅长处理代码,所以并不是某个 harness 本身就能解决我们一开始遇到的 “游戏不只是代码”的问题。

这里真正需要发生的是,我们能够把游戏中的全部东西拿出来,并尝试把它变成LLM能够处理的东西。我们需要撬开你游戏的黑箱,让其中更多部分能够被LLM直接处理。

Screenshot 2026-09-24 at 9.06.06 AM

Screenshot 2026-09-24 at 9.06.24 AM

在演讲版本中,我有一张非常搞笑的幻灯片,上面说这里的箭头就是本次演讲的实际内容。这个笑话在博客形式里远没有那么好笑,但我还是把它留在这里以传后世:

Screenshot 2026-09-24 at 9.06.38 AM

Screenshot 2026-09-24 at 9.07.28 AM

所以让我们谈谈我们能做什么。

让游戏对AI智能体可读

这里有一些非常具体、可操作的事情,你可以为你的游戏做,让它对LLM更可读、更可操作。

在开始之前简单说一句,这里也值得指出,这些技巧中有很多对游戏开发总体上都很有好处。它们会因AI而得到一些超级增强,但做其中任何一件事都可能让你开发自己的游戏总体上变得更好。

固定随机性

贯穿整个演讲/文章的一个主题是确定性的概念。基本上,就是你能多准确地复现游戏中的一个非常特定的状态。总体目标是,这应该非常容易,而且非常精确。在制作的最后阶段才做这件事会非常困难,而且很可能需要在最后关头对你的游戏进行根本性修改。

相反,如果你在写下第一行代码之前就假设这个约束,并把它作为一个目标坚持下去,你就会为更有成效的终局铺平道路,而不会发现自己要对抗熵和意大利面式代码。

从这里开始其实很简单。在游戏开始时,创建一个单一的种子化随机源。确保任何对非确定性事物的调用都使用这个随机源。这意味着你的游戏将能够确定性地重建游戏中的几乎任何状态。如果你的代码在30个地方调用 Random.Float 或其他什么来决定某些游戏行为,你知道第30次调用每次都会是一样的。简单。

截图能力

为什么要有可复现的状态?原因很多,但对于一个智能体来说,想象一下你正在反复迭代,试图纠正某些东西。智能体能够一遍又一遍地从零开始到达相同的状态,而不需要经历状态复制失败,这一点很重要。

而一旦你到达了那里,你怎么知道呢?你如何评估那些变化?

嗯,首先,截图!大多数引擎都有一些内置的截图 API 或功能:

Screenshot 2026-10-01 at 12.22.01 PM

我还建议添加轻松切换 UI 层、游戏层等开/关的能力,这样你可以减少视觉杂乱,这些杂乱会分散 LLM 对你试图处理的层的注意力(即,如果你试图修复某个 UI 样式的东西,就不要让游戏内容位于 UI 之下)。

在智能体更改之间为你的游戏拍摄前后对比图,是让智能体能够理解发生了什么变化的好方法。

……或者真的是这样吗?

Screenshot 2026-10-01 at 12.15.58 PM

当你为一次更改拍摄两张截图(之前/之后)时,LLM,即使它是多模态的(即具有视觉能力),很可能不仅很难理解更改是什么,而且很难理解它所做的更改是否让它更接近自己的最终目标。

这种来自图像数据的噪声是它自己形式的上下文腐化:天真地把截图扔给模型可能会混淆它们自己的进展感,让它们和你都感到困惑。

一个视觉例子在这里会有帮助:想象你截取了这个会议主页的截图:

Screenshot 2026-10-01 at 12.18.48 PM

你要求模型 “把骷髅变成海盗”。假设如果(对于文本 LLM 来说,不太可能)这可以作为注释应用到图像上(Google Stitch 做到了这一点),你的图像将连同你的标签一起传递给模型提供商,但然后还有所有你并不试图处理的其他东西(下面绿色的部分)。

Screenshot 2026-10-01 at 12.20.46 PM

这意味着绿色部分的所有东西都类似于 “上下文污染”。这是 LLM 正在处理的数据,以及你请求的更改,但它不仅仅是在思考你请求的内容,而是在思考整个图像。

你需要让 LLM 只思考需要更改的内容。你如何判断什么发生了变化?

图像差异对比!

Screenshot 2026-10-02 at 9.32.48 AM

如果你拍摄两张图像并 “差异对比”它们,就像你对代码更改所做的那样,你可以轻松看到发生了什么变化。结合巧妙的裁剪使用,你可以让 LLM 通过定量度量理解更改后的某些输出与您提供的输入/目标图像有多接近。

幸运的是,你不必自己做这个。有一个非常好的免费 命令行图像差异对比工具叫做 odiff,它正是做这个的。

Screenshot 2026-10-02 at 9.35.12 AM

给定两张不同的图像,odiff 找到两者之间的 “差异”,并可以将该差异输出为一张 “差异”图像:

Screenshot 2026-10-02 at 9.36.25 AM

odiff 还能给你一个量化的差异值,比如 “85% 相似”,LLM 非常擅长读取这个值,然后可以将其用作每个 agent 回合中评估的稳定基线,因为它努力减少差异/提高相似度。

换句话说,在你工作时,这些 agent 截图 -> 进度差异检查成为持续 agent 评估的基础,朝着你的任务目标前进:

Screenshot 2026-10-02 at 9.40.29 AM

然而,这显然不是全部。游戏远不止其视觉元素。 “进度”在这里分解成它自己的小内循环:

Screenshot 2026-10-02 at 9.47.22 AM

这个内循环很可能是 agent 花费大部分时间的地方,在回到截图比较/差异之前自行进行多个回合(假设你的更改有一些视觉影响)。

这里的目标是能够使每个更改成为达到期望结果所需的最小更改量,并且这些更改能够被快速验证。你想要一个极其稳定、 “无聊”的基线,可以在近乎隔离的情况下进行评估,以确保在其之上构建的任何东西都不会产生广泛的技术债务(也就是,意大利面条式代码)。所以让我们更多地谈谈确定性状态。

Screenshot 2026-10-02 at 9.53.45 AM

确定性状态

上图中的其余项目也在某种程度上涉及确定性状态,但我想谈谈实现它的具体策略和技术,因为它们对其他一切都会至关重要。

有一个简单的问题可以问,以了解/知道你对确定性状态的接近程度:你能多容易地保存和加载游戏中的精确状态?或者换句话说,状态复现最重要的部分是从一开始就有一个高效保存和加载游戏的策略。

保存/加载通常是业余开发者会推迟到游戏末期的事情,但这是一个严重的错误,正如许多经验更丰富的开发者会告诉你的。很容易把保存/加载看作是一个 “功能”,你可以在游戏开发的某个时候添加,但保存和加载,如果在游戏非常开始时就被利用,并与一起编程,可以成为开发超级力量,拥有几乎数不清的好处。

让我们谈谈这样做的一种策略,以及为什么它使所有开发都更容易。

Screenshot 2026-10-02 at 10.00.47 AM

我们将在这里小游一下 CS 风格的编程理论。大多数花大量时间在游戏的面向对象类型架构上的人会发现,在项目接近尾声时,类似上面的陈述 “树是狗”是真的。他们看着自己的代码,以及游戏中事物实际做什么,并意识到,这两个看似非常不同的对象,有着独立的 OOP 层次结构,在一天结束时,从代码的角度来看,实际上或多或少是同一类 “东西”。

树是一个有网格和碰撞体的对象。狗是一个有网格、碰撞体,并且能移动的对象。 “移动”不是本质上 “狗”的东西(游戏中的其他东西也可能移动),因此树和狗基本上只是具有某种游戏表示的组件的组合。从代码的角度来看,它们实际上是相同的。

一开始这可能有些反直觉,因为它在感觉上是错的,但这里有一个解锁脉轮的洞见,一旦你把握住它,游戏编程就会变得容易得多,而不再是当你试图在游戏中维持一个强 OOP 风格的对象模型时,那种不断与抽象之墙对抗的艰难爬坡。

事物驱动开发

相反,如果你接受我提出的这个想法并加以实践,那么从游戏一开始你就可以假定一切实际上都是同一种东西(甚至可以使用类型 Thing)。如果你在代码中定义 Thing,你会得到类似这样的东西:

Screenshot 2026-10-02 at 10.17.15 AM

你可以看到 Thing 上有一些 stat 类型的值,以及来自 isAlive 标志的共享的 “存活”概念。除此之外,事物的 “类型”只是一个枚举值,基于该类型,实现代码可以采取某些行动。

而事实证明,AI 真的很喜欢这个。

Screenshot 2026-10-02 at 10.18.21 AM

使用这种类型的架构,你的对象模型就有了一个单一事实来源。相反,如果你有 100 个类、100 种不同类型的对象,AI 可能需要理解所有这些(以及它们之间的关系)才能采取行动。这会填满你的上下文,并使得推理如何融入该模式变得混乱——我(AI)应该扩展某个类吗?改变某个类的类型以更好地捕捉这个新事物?创建一个新类?很难说!

使用单一对象类型,就没有歧义。AI 每次都能确切知道在哪里进行更改。

不仅如此,而且(回到保存/加载),拥有单一对象类型使得保存和加载容易得多:

Screenshot 2026-10-02 at 10.20.44 AM

当你只有一种对象类型时,反序列化/序列化是微不足道的——它是单一类型,所以只有一条进路/一条出路。而使用多种不同类型(以及可能的继承树),你很可能会陷入奇怪的竞态条件反序列化路径,需要以奇怪的方式链接对象和子对象树,并应对诸如 “多态反序列化”之类的概念。了解这些东西并在需要时使用它们是好事,但在游戏中这里可是有龙的。

那么游戏就只是一个巨大的 Thing 列表。保存就是通过一条路径保存所有这些事物,加载也一样,只是反过来。你需要做的任何转换都可以在反序列化后的枚举类型值上进行。这很好!

我也远不是第一个谈论这个的人,所以如果你想了解更多,我强烈推荐 Wookash 播客的一些剧集,他在其中与 Dreams 的一些创作者谈论了一个非常类似的系统:

通过使用这样的系统,立即重现状态相当容易,并且你避开了保存和加载的所有困难。这里还有更多复杂性,我确实推荐上面那个演讲作为进一步阅读,但基本思想是一样的——将所有内容捕获为单一对象类型。状态本身随后就可以被同一系统轻松捕获。没有什么能阻止你在每个结构上也有一个 Turn Number 字段,以及一个 GameManager 类型。让这个对象做字面意义上的所有事情。而且不,它并不像你可能认为的那样耗费内存。

可寻址状态

一旦你有了这样的系统,它也会让游戏的其他部分变得更容易。

Screenshot 2026-10-02 at 10.31.00 AM

如果你只有一种对象类型,那么捕获状态变化就非常容易,因为游戏中的任何变化想必都针对那唯一一种对象类型。你可能想直接修改这些值,但采用这种 “胖结构体”方法,你可以非常轻松地实现所谓的 “命令模式”,从而获得更多好处:

Screenshot 2026-10-02 at 10.32.01 AM

命令模式的核心思想是:你捕获对对象本身的更改,不是通过松散的函数调用,而是通过一个(理想情况下可序列化的)对象。这些命令随后会经过某个处理类,由该类对对象本身进行更改,而不是由你直接修改。这种间接层级一开始可能又显得奇怪,但请听我解释。

想象你有一个初始状态。玩家执行了一些改变状态的操作,这些操作表现为命令。这导致某个最终状态,而 bug 就在那里出现。

现在假设你想尝试复现这个问题。一种做法是,你尝试与玩家沟通,弄清楚他们做了什么,然后在你自己的游戏副本中尝试做同样的事。

或者,你可以取初始状态,然后 “回放”那些序列化的命令,它们精确捕获了玩家所做的操作。值得注意的是,这不一定需要你亲自玩游戏,或者编写专门的函数调用来改变状态:你只需回放命令即可。甚至不需要你来做,一个 AI 代理也可以运行它们,因为它会不断尝试评估其更改如何影响状态。

Screenshot 2026-10-02 at 10.38.21 AM

更进一步,你可能会注意到,这些命令实际上也不必由游戏本身来编写。你可以轻松地在文本编辑器中将命令串联起来(或由 AI 编写),然后在运行中的游戏实例上回放它们,无需编译!

Screenshot 2026-10-02 at 10.40.32 AM

命令为 AI(以及你)提供了一个类型化的接口来与你的游戏交互,有效地使其变得 “可操控”,并且不需要每次更改或尝试与游戏交互时都使用某种专门的处​​理逻辑或反射(然后再编译这些更改)。AI 喜欢这样!

再稍微谈谈无需编译的更改:你越能让游戏在不编译的情况下进行更改,就越好。这不仅仅是一种荣誉勋章,尤其对于代理循环而言——它能在无需等待任何编译时间的情况下进行测试的次数越多,就意味着它能更快地行动,并在更紧凑的循环中进行更改和验证。

关于数据管理

这恰好与我个人的一个痛点相契合:数据管理。让我们回到胖结构体的例子:

Screenshot 2026-10-03 at 1.15.51 PM

当我说 “数据”时,我指的是你的游戏用来描述事物当前状态的静态、操作性数据。在上面的例子中,我会将 Type、attackDamage、walkSpeed 和 boatSpeed 都视为 “数据”。这些是对象不会改变的固有特征。事物可以修改这些数据并与之交互,玩家可能永远不会看到这些具体的数字,但它们描述了游戏中某些事物存在的基本形态。

如果你在为你的游戏制作内容,你可能会直接写出很多结构体,根据你所知道的内容来设置它们的属性。这些更改随后会被编译进游戏中。如果你想改变某个东西的基础行走速度,你需要么进行更改并重新编译,要么编写一些函数绑定来赋值并且可以从外部调用(想想控制台命令)。

所有这些选项都会给AI代理带来变动。特别是在你进行编译的场景中,你把测试基本数值变化置于编译之后。

更好的做法是让你的所有数据外部于游戏,而不是以代码形式表示。分配这些数据只是反序列化数据、将其应用到正确的对象上,然后完成!所以更改数据只是在您选择的数据源中进行更改,在游戏中设置一些基本的反序列化,并将其与对象创建结合起来。完成!这也允许你(或代理,或其他团队成员等)能够通过一个构建来迭代你的游戏!他们甚至不一定需要编辑器访问权限。

Screenshot 2026-10-03 at 2.17.15 PM

对于Cantata来说,这正是我们的工作流程。团队中所有非程序员(约7人)都获得了游戏构建版本和我们自定义的数据编辑器Depot。这个工作流程如此有效,我现在正把Depot变成一个成熟的产品:

img

团队成员在Depot中进行更改,游戏构建版本处理反序列化的更改,这样它们就能在一个循环中工作,而不需要我制作新的构建版本。Depot是我在这里的首选工具,但其他团队使用Google Sheets或他们自己的自定义工具。

AI代理可以被教导使用你自己的工具,然后可以像其他团队成员一样,自行测试数据更改,而不需要不断重建游戏。你能让代理循环越紧密,就越好!这一切都是为了尝试消除循环瓶颈。

Screenshot 2026-10-03 at 2.18.01 PM

这引出了另一个核心想法。这有点像一个兔子洞,所以请耐心听我说。

独立模拟/黑盒

在开发游戏时,无论是在引擎中还是使用一些自定义工具自行开发,通常情况是游戏本身的逻辑与视觉状态转换、UI调用等交织在一起。这不是任何人的错,但作为默认模型是合理的:某些动作发生,你更新UI,也许改变一些视觉效果等。

然而,在把所有这些放在一起时,尤其是在引擎中,你开始意识到,要开始尝试测试任何东西或重现某人的bug,你需要让整个游戏运行并做它的事情。模型必须加载,声音设置,场景加载等。你游戏的模拟需要拥有 “整个游戏”。当事情开始出错时,就很难区分什么在出错以及为什么:是模拟?模型?场景设计?

这里更好的工作方式是,你游戏的模拟位于引擎实际运行逻辑的外部。所谓外部,我指的是字面意义上作为它自己的DLL/库。游戏消费模拟及其结果,但本身并不 “拥有”模拟。

相反,引擎通过 “命令”与模拟通信,这与Web编程中客户端和服务器的工作方式并无不同。

Screenshot 2026-10-04 at 8.43.30 AM

这些命令可以(也应该!)采用我们之前介绍的先前命令理念的形式:

Screenshot 2026-10-02 at 10.32.01 AM

当引擎将命令传递给模拟核心时,模拟会响应并将更新后的状态传回引擎。然后引擎解释更新后的状态(或与上次接收到的状态的差异),并据此行动。

这听起来有点抽象,所以这里有一个更具体的例子:

想象一下你的游戏中有东西在移动。这种移动可能涉及几个不同的方面——动画、模型、音效,以及关于移动开始、持续、停止的逻辑,可能还有物体移动时视觉等中间更新。然而,移动从根本上 “是”某物响应某些玩家输入从点A到点B的过程。

其理念是引擎为某个Thing(记住,胖结构体!)发送移动请求,模拟独立决定这是否是有效的移动目标(引擎也可以在提交命令之前专门过滤这一点)。然后模拟解析移动,并向引擎返回一个更新后的状态对象,显示对象确实移动了。引擎可以通过与先前状态进行差异比较来理解移动发生了,或者其他过于复杂无法在此完全展开的方式(记录缓冲命令并在模拟接受时对其采取行动等)。但关键在于引擎是 “愚蠢的”——它只对从模拟得到的东西采取行动,并不质疑它。模拟是真相的来源:如果模拟说某物移动了,引擎就移动该对象,毫无疑问。

这种间接层次可能看起来过于抽象,但当你将模拟核心与引擎分离时,这意味着任何东西都可以向你的核心发送命令:

Screenshot 2026-10-04 at 9.18.05 AM

这里最大的直接好处是网络编程变得远远更容易——因为你的游戏被设置为只消费状态/差异,它们实际来自哪里并不重要。它们可以来自网络、LLM,任何东西!

还有更简单的用例。游戏开发中的测试通常有点难对付,但我认为其中很多原因是因为状态/逻辑/数据往往耦合得太紧密,以至于很难测试游戏,因为它需要太多的脚手架。

然而,利用这样的架构可以让你轻松测试游戏的模拟,完全不需要引擎。你可以用纯C#项目、使用标准C#工具快速进行测试,不需要任何引擎运行你的代码。AI喜欢这个!

Screenshot 2026-10-07 at 9.52.56 PM

这个理念广义上被称为 “黑盒游戏架构”。它的版本以前就存在过,但几年前Brian Cronin在Roguelike Celebration上的这个演讲深入探讨了他如何为Monster Train利用这一技术的技术细节:

我还与Solium Infernum背后的团队在Game Data Podcast中交谈过,他们描述了类似的技术:

当你利用这样的东西时,它就成了游戏开发的超能力。你可以开始真正把游戏当作一个技术对象来处理,将其与实际玩家体验抽象开来。你可以直接在模拟本身上运行模拟、假设、测试等,并能避开所有通常困扰那些不使用此类架构策略的相关工作的状态问题。

Screenshot 2026-10-10 at 12.42.23 PM

回到上面的图表,我们已经走到了飞轮的接近末端。我们设定了一些确定性的输入参数,有办法以结构化的方式修改该状态从而避免编译,然后可以在真空中验证我们自己的更改。

影响评估

然而,我们如何真正知道这些更改会按我们想要的方式影响实际游戏呢?这就是 “影响评估”的理念。

Screenshot 2026-10-10 at 12.46.40 PM

我将在这里聚焦于一组相对狭窄的选项,但工作室在这个领域可以部署更多针对自己游戏更具体的工具。像测试这样的东西属于这里的一般领域,但也可以运行诸如固定玩法场景来弄清平衡影响等。这些都很重要,但它们是游戏特定的,因此很难在本篇文章的范围内直接提取。

然而,所有游戏都共有的一个关注点是性能。传统上,性能分析一直是一项相对手动的工作,需要开发者使用分析工具,无论是 Unity/Unreal 等编辑器内置的工具,还是通过像 dotMemory 这样的 IDE 工具:

Screenshot 2026-10-10 at 12.49.36 PM

这些工具是为人类消费而设计的——它们期望你深入调用栈和图表等,以找到你正在寻找的任何瓶颈。因此,也许并不意外的是,AI 并不真正喜欢使用这样的工具:它们对智能体来说不易读(不过值得一提的是,Unity 确实 在这方面做了大量工作,使分析器能更好地被智能体读取!)。

AI 真正喜欢的是处理文本(而且是大量的文本),因此能够将原始捕获数据转储为文本的分析工具 REALLY 有助于加速 AI 迭代。这里有几个选项,但我越来越喜欢的是(免费的!)工具 samply:

Screenshot 2026-10-10 at 12.53.50 PM

Samply 可以轻松接入并针对你自己的项目运行,并将捕获输出转储到控制台。AI 智能体知道如何阅读 samply 的源代码和文档,以帮助它弄清楚如何在此评估自己的更改,因此捕获和迭代循环可以有效地自主执行。AI 喜欢这个!

落到实处

当我们将所有这些工具和技术结合在一起时,类似这样的东西就出现了(而这只是一个版本!):

Screenshot 2026-10-10 at 12.55.44 PM

有了我在这里勾勒的面向数据、二进制隔离、无需编译的开发版本,一个智能体回合可以在几秒钟内跑完这WHOLE个循环。它使迭代比基线朴素方法显著更快,并且在每一步都帮助 AI 更好地理解你的游戏以及你试图做的事情。因为整个循环高效且快速,它可以针对每一个更改运行,而不是只留到它 “重要”的时候才运行。

稍微重新表述一下,你可以看到游戏驱动的测试框架是什么样子:

Screenshot 2026-10-10 at 1.03.19 PM

剧透:它就是你的游戏!任何游戏都能够做到这一切,这意味着任何游戏都可以轻松成为自己的测试框架。或者,正如我们在开头所说的:

Screenshot 2026-10-10 at 1.04.16 PM

感谢阅读!

后记

在展示之后再来写这篇文章很有意思。我仍然坚持我说过的很多内容,但我正在发布一个项目,在其中我也没有用到这些。我认为我在这里针对的规模是更大的项目。我正在发布的东西非常小,所以我在任何 harness 相关的东西上真的不需要做太多。

然而,当我继续推进我正在酝酿的一个更大的游戏时,这些内容中的很多都绝对相关,我非常感谢所有这些工具。

如果你把上述策略应用到自己的游戏中,我很想听听你的经验,以及哪些有效、哪些无效!随着我继续制作游戏和工具,我显然在这个领域思考了很多,所以很期待听到其他人在做什么。

再次感谢阅读,下次再聊!

发布于 2026 年 10 月 10 日。

来源:Hacker News · kylekukshtel.com