打造 AI 鉤子:遊戲開發的實務指引
How to engineer an AI harness for game development
AI 鉤子 是一套讓 LLM 能夠直接操作遊戲狀態的架構,作者分享從「固定隨機」到「命令模式」的實作技巧,並示範如何利用影像差異、資料外部化等方式提升迭代速度。
文章提供實作技巧,說明如何透過 LLM 與遊戲開發流程結合,並列出可落實的步驟,對開發者提升迭代效率有參考價值。

I gave a 45 minute presentation entitled “你的遊戲是束縛” as part of Indie Rodeo in Calgary on 9/12/26. After speaking with a lot of people after my talk, I thought it would be good to more broadly share the ideas I presented there in a form that could be enjoyed by people beyond just those who attended the conference.
Also, because I talk about AI a lot on this blog, people assume I use AI to write. This is not true! Everything below is 100% human authored, with me writing the post with basically only the slides I presented as a reference (this is not a transcription). As much as I enjoy using AI to write code and tools, I get a similar enjoyment out of writing “舊有方式”, especially as I have no reason to try and make any part of this process more efficient.
So with that all out of the way, I present to you my first ever blogification of a talk I gave. Enjoy!
Introduction
[I give a small introduction to myself and my own work, that I’m omitting here, as you’re on my website]
I want to talk about using LLMs in game development. Not one-shot type demos that look cool on Twitter, but like real development. Where you’re working eight hours a day, 5 days a week, etc.
And before I get into it, I think it’s useful to level set about LLMs. Or at least get into some basics so the rest of what I talk about here makes sense. I recognize that for a lot of this people this will be review, so if you just want to get into the dev-loop stuff, scroll down to the next section.
我開始使用 LLM 進行開發工作的旅程,始於我建立的一個情緒板網站,名為 mood.site。這是在「agentic」之類事物之前的作品,但我想測試當時的 ChatGPT 是否真的能寫程式。整個過程就是我盲目地把程式碼複製貼上到 ChatGPT 的網頁介面。這個過程很糟,但我對結果感到驚訝。我建好了這個網站!它運作了!我仍需自行驗證相當多的細節,但這已足以說服我。若沒有 LLM 的協助,我無法在接近我實際所花的時間(約 2-3 週的深夜,總共大約 20 小時)內完成這個網站。我可能今天還在努力完成第一版。
從那時起,mood 成為我探索 LLM 的測試場(也是一個相當成功的專案),並激發我啟動許多使用相似技術堆疊的其他專案。此外,LLM 的能力已經提升,尤其是像 Claude Code 以及後續的 Claude 模型。根據我的經驗,我覺得現在對於網路為基礎的堆疊,LLM 不能 做的事情已經很少,甚至可能根本不存在。
很容易說 LLM 可以做 一切,因為許多軟體都是網路軟體。但每當我嘗試用 LLM 建構非網路遊戲 事物 時,得到的結論都是:

“LLMs 在遊戲上表現不佳
每當我嘗試將 LLM 用於遊戲相關的事物時,它就會崩潰。也許這是我的偏見。我擔任遊戲程式設計師將近 15 年,我想我懂運作原理。我能在方圓一英里內嗅出 「爛」遊戲程式碼的味道。但談到 Svelte 網頁應用程式時,我顯然沒那麼熟練,傾向於認為 LLM 產生的任何程式碼都是 「好」的。也許它實際上就是垃圾。但我們確實有理由相信 Svelte 和網頁技術堆疊的內容是高品質的,部分原因在於網上有大量公開原始碼的高品質網頁專案。相較之下,我猜遊戲的訓練資料少了好幾個數量級。
不僅如此,遊戲中有很多東西根本不是程式碼。遊戲不只是把靜態圖片和文字排版而已。遊戲中有大量無法輕易用程式碼表示的事物。而且除此之外,它還包含了網頁所有的文字排版內容,這只不過是遊戲需要處理的 100 種其他系統中的一個子集罷了。
這也是為什麼很多人一直用 Three.js 製作遊戲的部分原因——他們可以輕易地將資產表示為簡單的方塊形基本物件,並跳過遊戲通常所需的許多資產生成/符合工作與處理。結果,其中許多內容都是劣質品,因為它們缺乏除了 LLM 平均水準之外的任何真實創作者風格。
不過,也許他們的觀點有道理。

單次提示 Three.JS 工作流程(以及 LLM 擅長建構網頁應用程式)的一個核心概念是所有東西都是程式碼。透過將東西視為程式碼,LLM 就能非常輕鬆地進行推理、修改等等。

然而,對於傳統遊戲而言情況並非如此。或者至少這不是全部的事實。遊戲顯然有程式碼,但還有很多其他不是程式碼的東西。這意味著遊戲中有一些關鍵元件對 LLM 來說實際上是黑盒子。如果你的遊戲隨後依賴於對這些黑盒子內容的有效操控與轉換,那麼 LLM 在試圖處理你的遊戲時就會崩潰。

我想遊戲開發社群的許多人不久前可能試用過 LLM,然後就直接撞上了這個牆壁。「LLM 很爛/沒用/等等」這種情緒(在此要大肆保留附帶條件,純粹討論能力而非潛在的毀滅世界影響)就是直接撞上這些限制所產生的。

這就是說,在結構上,LLM 不擅長閱讀遊戲。至少在其預設狀態下是如此。
LLM 基礎
讓我們多聊聊為什麼會這樣,以及我們能做些什麼。
對任何 LLM 服務的任何呼叫/提示都可以分解為三個基本元件。系統提示詞、助理提示詞和使用者提示詞:
系統提示詞:所有服務使用者共用的通用文字。這通常描述了 LLM 本身的基礎,帶有類似 「你是 Opus 5,由 Anthropic 建立的智慧型大型語言模型……」的內容。這並不是說這些不重要(它們很重要),而是說它們與我們的使用情境關聯不大。你可以在這裡看到一些已發布(且外洩)的模型系統提示詞範例。
Assistant prompt(助理提示詞):這種類似於附加的提示詞區段,旨在協助引導 AI 瞭解它正在回應什麼。基本上,你可以將其視為模型與你提示詞的基本「互動」向量,並代表模型根據某些輸入對使用者所做出的「回應」。通常你不需要去思考這個。
User Prompt(使用者提示詞):這才是真正的主體。你輸入到任何 AI 工具中的大多數內容都會放在這裡。任何送出的對話、程式碼等都會被傾印到這個區段。當人們談論「上下文」時,他們主要是在談論「提示詞的使用者(與助理)部分儲存了多少 Token」。
可能會讓人覺得每一次對話都是上述內容的「全新」版本,但實際上的情況是,當你在特定的對話行程中與模型互動時(「對話行程」的界限會根據你使用的服務而有所不同——這可以是關閉前的單一 Claude Code 對話行程、網頁代管模型的側邊欄中可存取的「對話」等),你先前傳送給模型的每一個訊息,在你傳送訊息的每一個瞬間都會被重新提交。這有點像在玩舊的團康遊戲,在輪到你時,你必須再次說出其他所有人的內容,然後才能加上你自己的內容,讓下一個人接著加上去。

上下文與上下文汙染
當你一遍又一遍地這樣做時,你就會建立起你正在作業中的對話行程「上下文」。

這一切都很好,但這裡需要注意的關鍵點是,你實際上只能處理上下文中的「最新」內容(亦即,你送出一個新訊息)。先前訊息中的內容會一遍又一遍地被提交給模型。因此,你會希望確保上下文中的內容是「好的」,否則你可能會陷入惡化情境,因為早期「不好」的內容不斷影響新的工作,導致你的上下文遭到汙染。

因此,管理上下文是使用 LLM 時的關鍵事項,鑑於這些限制,一門稱為「上下文工程」的新工程實踐應運而生。「上下文工程」探討的是「我們如何能在盡可能少汙染的情況下,將最佳資訊放入上下文中」。
這裡有一個關於「長上下文失敗的方式」的精彩部落格文章,對此有更多的深入探討,但我在此凸顯其關鍵部分:

上下文工程本身是一個非常大的主題,超出了本次演講/部落格的範圍,但我們將在此運用其中的一些原則,來探討如何協助 LLM 更好地處理遊戲。
換句話說,讓 LLM 很好地配合遊戲是一個上下文問題,而不是本質上與遊戲或 LLM「是」(或「不是」!)如何運作有關的因素。

然而,這裡讓情況變得複雜的因素是,遊戲很奇怪。或者至少:我們沒有建立遊戲的真正範本。我們有遊戲引擎,但實際上沒有任何東西擁有標準化的工具,部分原因是每個遊戲展現的需求彼此各不相同。

這在遊戲中非常獨特(依個人之見),但它在其他科技領域中也經常出現,為了讓代理程式(Agent)能與你的工作成果有效 「協作」,一個新概念應運而生:驅動框架(Harnesses)。
關於驅動框架概念的一些描述
驅動框架的總體概念是,它是一組代理程式針對明確(或模糊)定義的預期輸入所應採取的特定動作模式。你可能已經在使用像 Claude Code 這樣的驅動框架,但這些框架可以透過增強、擴充、提煉、抽象化等方式,來更好地支援你正試圖執行的特定工作。最近這個領域也有非常多的動態,許多驅動框架被打造出來用於開發各種事物:

這些事物指向的關鍵點是: 「我該如何更好地理解我正在建構的內容是什麼,並在開發過程中引導該過程」。驅動框架可以結合諸如與瀏覽器互動、遙測、效能分析,甚至是完整的軟體開發生命週期(SDLC)工作,例如建立票證(Tickets)、檔案等。
然而,這些工具沒有一個能真正做好的事情,就是實際與你的遊戲進行對話。你的遊戲有點像是個黑盒子,LLM 能看見的大多數內容只有所編寫的程式碼,正如我們之前討論過的,這其實還不夠。我們知道 LLM 已經很擅長處理程式碼,所以並不是說某個驅動框架本身就能解決我們一開始遇到的「遊戲不只是程式碼」的問題。
這裡真正需要發生的是,我們能夠獲取遊戲中的所有素材,並試圖將它們轉化為 LLM 能夠處理的形式。我們需要開啟遊戲的黑盒子,讓更多內容能被 LLM 直接定址與存取。


在這次演講的版本中,我有一張非常搞笑的投影片,上面寫著這裡的箭頭才是這次演講的實際內容。這個笑話在網誌形式中完全發揮不出效果,但我把它留在這裡以供後人參考:


所以,讓我們來談談我們可以做些什麼。
讓遊戲對 AI 代理程式具備可讀性
以下是一些非常具體、可落實的事情,你可以為你的遊戲做這些事,讓它對 LLM 而言更具可讀性且更易於操作。
在開始之前先簡單離題一下,也值得在此指出,這些技巧中的許多點對整體遊戲開發都很有幫助。它們透過 AI 得到了強化,但執行這些操作可能會讓你自己開發遊戲的過程變得更好。
固定隨機性
貫穿這整個演講/文章的一個核心主題是確定性(Determinism)的概念。基本上,就是你能以多高的精確度在遊戲中重現一個非常特定的狀態。總體目標應該是非常簡單且非常精確。如果在製作的最後階段才做這件事會非常困難,而且可能需要在最後關頭對遊戲進行根本性的修改。
相反地,如果你在寫第一行程式碼之前就假設了這個限制,並將其作為目標堅持下去,你就是在為更有生產力的收尾階段鋪平道路,而不會發現自己在對抗熵增和義大利麵條式程式碼。
從這裡開始相當簡單。在遊戲開始時,建立一個單一的種子化隨機性來源。確保任何對非決定性事物的呼叫都使用這個隨機性來源。這意味著你的遊戲將能夠以決定性的方式重現遊戲中的幾乎所有狀態。如果你的程式碼在 30 個地方呼叫 Random.Float 或類似的東西來決定某些遊戲行為,你就會知道第 30 次呼叫每次都會是相同的。簡單。
截圖能力
為什麼要有可重現的狀態?原因有很多,但對於 AI 代理(agent)來說,想像一下你正在重複迭代嘗試修正某個問題。對於 AI 代理來說,能夠從頭開始一次又一次地到達相同的狀態,而不需要經歷狀態複製失敗的折騰,是非常重要的。
一旦你到了那裡,你怎麼知道?你該如何評估這些變更?
嗯,首先,截圖!大多數引擎都有一些內建的截圖 API 或功能:

我也建議新增輕鬆切換 UI 圖層、遊戲圖層等開關的功能,這樣你可以減少視覺雜訊,避免這些雜訊分散 LLM 對你試圖處理的圖層的注意力(也就是說,如果你想修正某個 UI 樣式問題,就不要讓遊戲內容位於 UI 下方)。
在 AI 代理進行變更的前後拍攝遊戲照片,是讓 AI 代理能夠理解發生了什麼變化的好方法。
……真的是這樣嗎?

當你在變更前後拍攝兩張截圖時,LLM 即使它是多模態的(即具備視覺功能),也很難不僅理解變化是什麼,而且很難理解它所做的變更是否讓它更接近其自身的最終目標。
來自影像資料的這個雜訊本身就是一種上下文腐敗(context rot):天真地將截圖丟給模型會混淆它們對進展的感知,讓它們和你在不知所措。
這裡有一個視覺範例:想像你為這個會議的首頁拍了一張截圖:

你要求模型 「讓骷髏變成海盜」。假設(使用文字 LLM 的話不太可能)這可以作為註解套用到圖片上(Google Stitch 就是這樣做的),你的圖片將連同你的標籤一起傳遞給模型提供者,但同時也會傳遞所有你不想處理的其他東西(如下方的綠色部分)。

這意味著綠色中的所有東西都類似於 「上下文汙染」。這是 LLM 除了你要求的變更之外還在處理的資料,它不只是在思考你要求的內容,還在思考整張圖片。
你需要讓 LLM 只思考需要改變的部分。你如何判斷改變了什麼?
圖片差異比較(Image diffing)!

如果你拿兩張圖片並對它們進行 「差異比較」,就像你對程式碼變更所做的那樣,你就可以輕鬆看出改變了什麼。結合巧妙的裁剪運用,你可以讓 LLM 以量化指標理解變更後的某些輸出與你提供的輸入/目標圖片有多接近。
幸運的是,你不需要自己動手做。有一個非常好的免費命令列圖片差異工具叫做 odiff,專門用來做這件事。

給定兩張不同的圖片,odiff 可以找出兩者之間的 「差異」,並將該差異輸出為 「差異(diff)」圖片:

odiff 也能提供像 「85% 相似」這樣的量化差異數值,這是 LLM 非常擅長閱讀的,接著它能在每個代理程式(agent)輪次中,將其作為穩定的評估基準,努力減少差異或提高相似度。
換句話說,當你在運作時,這些代理程式的螢幕截圖 -> 進度差異檢查,會成為針對你任何任務進行持續代理程式評估的基礎:

然而,這顯然還不夠。遊戲的內涵遠不止於視覺元素。這裡的「進展」可細分為它自己的小內迴圈:

這個內迴圈很可能是代理程式花費最多時間的地方,它會在回到螢幕截圖比對/差異檢查之前,自行執行多個輪次(假設你的變更具有某些視覺上的影響)。
這裡的目標是能夠讓每一次變更都達到預期結果所需的最小變更量,並能快速驗證這些變更。你會需要一個極度穩定、「無聊」的基準,可以在近乎隔離的狀態下進行評估,以確保在其之上建構的任何東西都不會產生廣泛的技術債(也就是義大利麵程式碼)。所以,我們來多聊聊決定性狀態(Deterministic State)。

決定性狀態
上面圖表中的其餘專案也在某種程度上處理了決定性狀態,但我想談談實現它的具體策略與技術,因為這對其他所有事情都至關重要。
要理解/知道你距離決定性狀態有多近,只需問一個簡單的問題:你在遊戲中能多輕易地儲存與載入確切的狀態?或者換句話說,狀態重現最重要的部分,就是擁有一個能從一開始就有效率地儲存與載入遊戲的策略。
儲存/載入通常是業餘開發者會拖到遊戲開發尾聲才做的事,但正如許多經驗豐富的開發者會告訴你的那樣,這是個嚴重的錯誤。人們很容易把儲存/載入視為一種可以在遊戲開發過程中隨時加入的「功能」,但如果能在遊戲的最初期就善用儲存/載入,並與其搭配進行程式設計,這可能會成為一種開發時的超級能力,帶來數之不盡的好處。
我們來談談實現這點的一種策略,以及為什麼它能讓所有開發過程變得更輕鬆。

我們將在這裡稍微涉足電腦科學風格的程式設計理論。大多數花費大量時間在遊戲物件導向型別架構的人會發現,在專案接近尾聲時,類似上面「樹是一隻狗」的說法竟然是真的。他們審視自己的程式碼以及遊戲中物件實際執行的功能,然後發現在程式碼的視角下,這兩個看似非常不同、擁有獨立 OOP 階層的物件,歸根結底其實或多或少是同一種「東西」。
樹是一個擁有網格(mesh)與碰撞體(collider)的物件。狗是一個擁有網格、碰撞體且會移動的物件。「移動」本質上並不是「狗」專屬的東西(遊戲中的其他東西也可能會移動),因此樹和狗基本上就只是具備某些遊戲表現形式的元件組合。從程式碼的角度來看,它們實際上是相同的。
這起初可能會讓人感到違反直覺,因為它感覺上不太對勁,但這裡有一個類似打通脈輪的見解;一旦你掌握了它,就會發現遊戲程式設計變得容易許多,不再需要像對抗一道抽象化的高牆那樣進行永無止盡的苦戰,而這種苦戰通常在你試圖於遊戲中維持強烈的物件導向程式設計(OOP)風格物件模型時發生。
「物件驅動開發」(Thing-Driven Development)
相反地,如果你採用我所提出的這個概念並付諸實踐,你從遊戲的一開始就可以假設所有東西其實都是同一個東西(甚至可以使用型別 Thing)。如果你要在程式碼中定義 Thing(物件),你的程式碼看起來會像這樣:

你可以看到一個 Thing 具備一些屬性型別的數值,以及透過 isAlive 旗標所共享的「存活狀態」概念。除此之外,物件的「型別」只是一個列舉(enum)值,而實作程式碼就可以根據該型別採取某些行動。
事實證明,AI 非常喜歡這樣。

透過這種架構,你的物件模型將擁有單一真實來源。相對地,如果你有 100 種不同型別的 100 個類別,AI 可能需要理解所有這些類別(以及它們之間的關係)才能採取行動。這會佔滿你的上下文(context),並讓 AI 難以推敲如何融入該模式——我(即 AI)應該擴充某個類別嗎?轉換類別的型別以更好地捕捉這個新物件?還是建立一個新類別?這很難說!
使用單一物件型別,就不會有任何模稜兩可之處。AI 每次都能準確知道該在哪裡進行變更。
不僅如此,(回到儲存/讀取的話題),擁有單一物件型別會讓儲存與讀取變得非常簡單:

當你只有一種物件型別時,反序列化/序列化就變得微不足道——它只有一種型別,所以只有一種進出的管道。如果有多種不同的型別(以及潛在的繼承樹),你很可能會陷入奇怪的競態條件(race condition)反序列化路徑中,需要以奇特的方式將物件與子物件樹連結起來,還要應付像是 「多型反序列化」這樣的概念。知道這些東西並在需要時使用它們固然很好,但對於遊戲開發來說,這裡可是充滿險阻。
那麼,遊戲就只是一個巨大的 Thing 列表。所謂儲存,就是透過單一路徑儲存所有這些物件;所謂載入,則是反向操作。任何你需要執行的轉型(casing)都可以在反序列化後的列舉型別值上進行。這真的很棒!
我也絕不是第一個談論這個概念的人,所以如果你想深入瞭解,我強烈建議你收聽 Wookash 播客(podcast)的其中幾集,他在節目中與 Dreams 的幾位創作者討論了非常類似的系統:
透過使用這樣的系統,要立即重現狀態會變得相當簡單,並且能避開儲存與載入的所有難題。這裡還有更多複雜性,我確實推薦上面提到的訪談作為延伸閱讀,但核心概念是相同的——將所有東西都捕捉為單一物件型別。如此一來,狀態本身也能輕易地被同一個系統捕捉。沒有什麼能阻止你在每個結構上都加上一個回合編號(Turn Number)欄位,以及一個型別為 GameManager 的 Type。讓這個物件包辦字面意義上的所有事情。不,它並不像你想像的那樣消耗大量記憶體。
可定址狀態(Addressable State)
一旦你擁有了這樣的系統,它也會讓遊戲的其他部分變得更容易。

如果你只有一種物件,要捕捉狀態的變更會非常簡單,因為遊戲中的任何變更大概都是針對該單一物件型別。你可能會想要直接更改這些數值,但透過這種 「肥大結構(fat struct)」的方法,你可以非常輕鬆地實作稱為 「命令模式(Command Pattern)」的東西,以獲得更多好處:

命令模式的概念是,你將物件本身的變更捕捉為一個(最好是可序列化的)物件,而不是鬆散的函式呼叫。接著,這些命令會透過某個處理類別傳遞,由該類別負責對物件本身進行變更,而不是由你直接操作。這種間接層級一開始可能會顯得有些奇怪,但請聽我娓娓道來。
想像你有某個起始狀態。玩家做出一些會改變狀態的動作,這些動作會以命令的形式呈現。這會導致某個發生了臭蟲的結束狀態。
現在假設你想要試著重現這個狀況。其中一種做法是,你試著與玩家溝通,找出他們做了什麼,然後在你的遊戲複本中嘗試做出相同的行為。
或者,你可以採用起始狀態,並 「回放」那些捕捉了玩家確切行為的序列化命令。值得注意的是,這不一定需要你親自玩遊戲或編寫專門的函式呼叫來變更狀態:你只需要回放命令即可。甚至不只是你,AI 代理程式也可以在它持續嘗試評估其變更如何影響狀態時,將這些命令重新執行。

再進一步思考,你可能會發現這些命令其實也不一定非得從遊戲本身建立不可。你可以輕鬆地在文字編輯器中將命令串聯起來(或是由 AI 建立),然後在執行中的遊戲例項上回放,完全不需要進行編譯!

命令賦予了 AI(以及你)一個帶有型別的介面來與你的遊戲互動,實際上使遊戲變得 「可操控」,且不需要讓遊戲的每一個變更或操作嘗試都依賴自訂的處理邏輯或反射(然後再編譯這些變更)。AI 很喜歡這樣!
多聊一下免編譯變更:你的遊戲越能做到在不需編譯的情況下進行變更,情況就會越好。這不只是為了追求某種榮譽勳章,特別是對於代理式迴圈(agentic loops)而言——它越能在不需要等待任何編譯時間的情況下進行測試,就代表它能夠以更緊密的迴圈更快地行動並進行/驗證變更。
關於資料管理
這與我個人的某些地雷完美契合:資料管理。讓我們回到肥大結構的範例:

當我說 「資料」時,我指的是遊戲用來描述事物本質的靜態、營運資料。在上面的範例中,我會將 Type、attackDamage、walkSpeed 和 boatSpeed 都視為 「資料」。這些是不會改變的物件特徵。雖然其他事物可以修改並操作它們,玩家也可能永遠不會直接看到這些數字,但它們描述了遊戲中某個事物存在的基準形貌。
如果你正在為遊戲製作內容,你可能會直接寫出許多結構(struct),並根據你所知的屬性來設定它們。接著,這些變更會被編譯進遊戲中。如果你想更改某個物件的基礎步行速度,你必須進行修改並重新編譯,或者寫一些能指定數值且可從外部呼叫的函式繫結(例如主控臺指令)。
所有這些選項都會為 AI 代理程式帶來額外的消耗。特別是在需要進行編譯的情境下,你等於是把測試基礎數值的變更擋在了編譯流程的後面。
更好的做法是將所有資料保留在遊戲外部,而不是以程式碼的形式呈現。指派這些資料只需要將資料反序列化、套用到正確的物件上,就大功告成了!因此,更改資料就只變成在您選擇的資料來源中進行修改、在遊戲中設定一些基本的反序列化,並將其與物件建立相結合。完成了!這也讓你能(或讓代理程式、其他團隊成員等)透過組建(build)來疊代開發遊戲!他們甚至不一定需要編輯器存取許可權。

對於 Cantata 而言,這就是我們的確切工作流程。團隊中所有非程式設計人員(約 7 人)都拿到了遊戲的組建以及我們自訂的資料編輯器 Depot。這個工作流程非常有效,以至於 我現在正將 Depot 打造成一個成熟的產品:

團隊成員在 Depot 中進行變更,而遊戲組建會處理反序列化後的變更,讓它們能夠在迴圈中運作,而不需要我頻繁製作新的遊戲組建。在這裡,Depot 是我的首選工具,但其他團隊也會使用 Google 試算表或 他們自訂的工具 等東西。
可以教導 AI 代理程式使用你自己的工具,接著它們就能像其他團隊成員一樣,自行測試資料變更,而不需要不斷重新建置遊戲。代理程式的迴圈越緊密越好!這一切都是為了消除週期瓶頸。

這帶出了另一個核心概念。這有點像個無底洞,請耐心聽我道來。
獨立模擬 / 黑箱
當在遊戲引擎中,或是用你自己的自訂工具開發遊戲時,通常遊戲本身的邏輯會與視覺狀態轉換、UI 呼叫等交織在一起。這並不是任何人的錯,作為預設模型這很合理:某個動作發生了,你更新 UI、可能改變一些視覺效果等。
然而,在將所有這些組合在一起時(特別是在引擎中),你會開始意識到,要想開始測試任何東西或重現某人的臭蟲,你需要讓整個遊戲執行並執行它的任務。必須載入模型、設定音效、載入場景等。你遊戲的模擬過程必須具備 「整個遊戲」。當事情開始出狀況時,就很難釐清是什麼壞了以及原因為何:是模擬過程?模型?還是場景設計?
這裡更好的工作方式是讓遊戲的模擬過程位於引擎實際運作邏輯的外部。所謂外部,字面上的意思就是它擁有自己的 DLL/程式庫。遊戲會消耗模擬及其結果,但本身並不「擁有」模擬。
相反地,引擎是透過 「指令」與模擬進行通訊,這與網頁程式設計中客戶端和伺服器運作的方式相當類似。

這些指令可以(而且應該!)採用我們稍早介紹過的前一個指令概念的形式:

當引擎將指令導向模擬核心時,模擬核心會回應傳回給引擎的更新狀態。接著,引擎會解讀更新狀態(或與上次接收到的狀態進行差異比對後的狀態)並據此執行動作。
這聽起來有點抽象,以下是一個更具體的範例:
想像遊戲中有東西正在移動。該移動很可能是幾個不同事物的組合——動畫、模型、音效,以及關於移動開始、持續、停止的邏輯,可能還包含當物件移動時針對視野等事物的中間更新。然而,移動的本質「究竟是什麼」,其實就是回應某種玩家輸入後,從 A 點移動到 B 點。
這個概念是,引擎針對某個「東西」(還記得肥大的 struct 吧!)傳送移動請求,模擬核心會獨立決定這是否為一個合法的移動目標(引擎也可以在提交指令前特別過濾這點)。接著,模擬核心會解析該移動,並將一個顯示物件確實已移動的更新狀態物件回傳給引擎。引擎可以透過與前置狀態進行差異比對,或是透過其他在這裡無法完全深入探討的方式(例如記錄緩衝指令,如果模擬核心接受了它們就執行等),來理解移動已經發生。不過關鍵在於,引擎是「笨拙的」——它只根據從模擬核心收到的內容來執行動作,而不會質疑它。模擬核心是唯一的真相來源:如果模擬核心說某個東西移動了,引擎就會移動該物件,毫無疑問。
這種間接層級可能會讓人覺得過於抽象,但當你的模擬核心與引擎分離時,這意味著任何東西都可以向你的核心傳送指令:

這裡帶來最大的立即好處是,網路程式設計變得非常簡單——因為你的遊戲設定為只消耗狀態或差異值,它們實際從哪裡來並不重要。它們可以來自網路、LLM,或是任何東西!
還有更簡單的使用案例。遊戲開發中的測試通常有點難以處理,但我認為很大一部分原因是因為狀態、邏輯與資料通常緊密耦合,導致要測試遊戲非常困難,因為需要太多的鷹架程式碼。
然而,善用這樣的架構能讓你輕鬆測試遊戲的模擬,完全不需要引擎。你可以使用純 C# 專案快速跑完測試,運用標準的 C# 工具,而不需要任何引擎來執行你的程式碼。AI 很喜歡這樣!

這個概念大致被稱為「黑箱遊戲架構(Blackbox Game Architecture)」。它的版本以前就存在過,但幾年前 Brian Cronin 在 Roguelike Celebration 中的這場演講,深入探討了他如何將這項技術應用於《怪物列車(Monster Train)》:
我也和 Solium Infernum 背後的團隊在 Game Data Podcast 中聊過,他們描述了類似的技術:
當你善用這樣的技術時,它就會成為遊戲開發的超能力。你可以開始真正把遊戲當作一個技術物件來處理,將其從實際的玩家體驗中抽象出來。你可以直接在模擬本身上執行模擬、假設、測試等,並且能避開所有通常會困擾未使用此類架構策略之相關工作的狀態問題。

回到我們上面的圖表,我們已經來到了飛輪的尾聲。我們設定了一些確定性的輸入引數,擁有了以結構化方式修改該狀態並能避免編譯的方法,然後可以在一個孤立環境中驗證我們自己的變更。
影響評估
然而,我們究竟要如何知道這些變更確實如我們所願地影響了實際的遊戲呢?這就是 「影響評估」的概念。

我將在這裡聚焦於相對狹隘的一組選項,但工作室在這個領域還可以部署更多針對其自身遊戲的工具。諸如測試之類的東西屬於這裡的一般領域,但也包括執行諸如固定遊戲玩法情境以找出平衡性影響等操作。這些都很重要,但由於具有遊戲專屬性,因此很難在此文章的範疇內直接萃取出來。
然而,所有遊戲共同關注的一點就是效能。傳統上,效能剖析(Performance Profiling)一直是一項相對手動的工作,需要開發者使用剖析工具,包括內建於 Unity/Unreal 等編輯器中的工具,或是透過像 dotMemory 這樣的 IDE 工具:

這些工具是為人類使用而設計的——它們期望你深入呼叫堆疊(Call Stack)和圖表等內容,以找出你正在尋找的任何瓶頸。因此,或許毫不意外的是,AI 其實不太喜歡使用這類工具:它們對代理程式而言是無法讀取的(儘管值得一提的是,Unity 確實在這裡做了大量工作,讓剖析器能更好地被代理程式讀取!)。
AI 真的很喜歡處理文字(而且是大量的文字),因此能夠將原始擷取資料以文字格式傾印出來的剖析工具,真的非常有助於加速 AI 的迭代。這裡有幾個選項,但我最喜歡的是(免費的)工具 samply:

Samply 可以輕鬆連線並針對你自己的專案執行,並將擷取輸出傾印到主控臺。AI 代理程式知道如何閱讀 samply 的原始碼與檔案,以協助它找出如何在此評估自己的變更,因此擷取與迭代的迴圈可以有效地以自主方式執行。AI 很喜歡這樣!
總結
當我們將所有這些工具和技術結合在一起時,這樣的情景便應運而生(而這只是其中一種版本!):

透過我這裡所描繪的這種具資料導向、二進位隔離且無需編譯的開發版本,代理程式的單次執行可以在幾秒鐘內跑完這整個迴圈。它使迭代速度比基準的簡易作法顯著更快,並且在每一步都有助於 AI 更好地理解你的遊戲以及你試圖達成的事。由於整個迴圈既高效又快速,它可以針對每一次變更執行,而不只是留到「重要」的時候才做。
從宏觀的角度重新審視這點,你可以看出遊戲驅動的測試框架(Harness)是什麼樣子:

暴雷:這就是你的遊戲!任何遊戲都能做到這一切,這意味著任何遊戲都可以輕易成為它自己的測試框架。或者,正如我們一開始所說的:

感謝閱讀!
後記
在發表這個主題之後寫下這些感想,過程相當有趣。我仍然堅持我所說的大部分內容,不過我目前正在推出一個完全沒有用到這些技術的專案。我覺得我這裡鎖定的規模確實是比較大型的專案。我要推出的東西非常小,所以在任何框架或工具方面,我真的不需要做太多準備。
不過,當我著手處理另一款我正在籌備的大型遊戲時,這裡的許多內容絕對都派得上用場,我也非常感謝所有這些工具。
如果你將上述這類策略應用在自己的遊戲中,我很想聽聽你的經驗,以及哪些有效、哪些沒效!隨著我繼續製作遊戲與工具,我(很顯然地)在這個領域思考了很多,所以我也很期待聽到其他人在做些什麼。
再次感謝你的閱讀,我們下次見!
發布於 2026年10月10日。
來源:Hacker News (official RSS) · kylekukshtel.com