NPC 行為 · 從 FSM 到 LLM

NPC 行為:狀態機、行為樹、GOAP 與 AI 驅動的 NPC

做自己的 NPC 行為

NPC 怎麼決定下一步要做什麼?狀態機、行為樹、GOAP、效用 AI 與 LLM 驅動的 NPC——用瀏覽器裡可即時互動的 3D 範例逐一說明。

✦遊戲
做一個 NPC 行為

NPC 行為就是那套邏輯,把一個模型、一條血條、一顆碰撞膠囊變成看起來「有想要的東西」的東西。它反覆問同一個問題,通常是每一帧、或固定 tick 問一次:就我目前所知的世界狀態,我下一步該做什麼?這一頁的其他東西——有限狀態機、行為樹、GOAP、效用 AI、LLM 驅動的意圖——都只是這個問題的不同答案,代價不同,失敗模式也不同。

我們做的 NPC 是在 Neta Studio 的 3D 遊戲建構器裡、跑在瀏覽器分頁裡的。這個限制決定了下面所有建議。沒有專用伺服器,沒有獨立的物理執行緒,也沒有預算讓規劃器每個 tick 搜尋 40 毫秒。如果一個技術沒辦法壓到便宜到能在中階筆電上跟渲染並行,我們就降級它,或把它移出熱路徑。以下各節講的是我們實際出貨的東西,以及每種做法在哪裡會壞。

快速回答:NPC 怎麼做決定

NPC 的決定方式是:按排程執行一個決策函式,讀取一小塊世界狀態(與目標的距離、視線、目前血量、最後已知位置),回傳一個動作交給移動與動畫系統執行。排程跟邏輯本身一樣重要。決策跑在 100 毫秒到 250 毫秒的 tick 上,移動與碰撞每帧跑,成本只有每帧做決策的零頭,而且在大多數情況下玩家根本分不出來。

四種經典模型的差別在於複雜度放在哪裡。狀態機把決策編碼成具名狀態之間的轉移。行為樹把決策編碼成選擇器與序列節點的階層。GOAP 把決策編碼成一個目標加上一組有前置條件與效果的動作,再搜尋出一份計畫。效用 AI 把決策編碼成對每個候選動作打分的函式。LLM 驅動的 NPC 把高階意圖編碼成自然語言,再把結果交回上面其中一種比較便宜的模型。

實務上你會把它們混著用。效用評分器挑目標,行為樹或狀態機執行目標,navmesh 移動身體,語言模型只碰對話與意圖。這一頁剩下的部分會逐層拆開講,並點名你會撞到的具體失敗模式。

狀態機(FSM)

有限狀態機是最便宜、也最有用的模型,對大多數 NPC 來說是正確的第一步。你定義一組狀態——Idle、Patrol、Chase、Attack、Flee、Stunned——以及它們之間的轉移條件。每個 tick 你檢查從當前狀態出發的轉移,有一條成立就切換。成本與你檢查的轉移數量成正比,而那個數量很小。狀態就是一個變數,因此要序列化、除錯、顯示在編輯器疊層上都非常容易。

失敗模式是震盪。一個 NPC 在離玩家 5.1 公尺處從 Patrol 轉到 Chase,移動到 4.9 公尺,一條對稱的轉移又把它送回 Patrol。它就在邊界上永遠抖個不停。解法是遲滯:5.0 公尺進 Chase,6.5 公尺才離開。同樣的問題會出現在視線檢查上——玩家在柱子後面左右橫移,檢查結果就閃爍——解法也一樣:要求條件連續成立 N 個 tick,或加一個記憶計時器,最後一次看到目標後讓 NPC 繼續 Chase 兩秒。

第二個失敗模式是轉移糾纏。五個狀態要十條轉移;十個狀態要將近四十五條。每個新狀態都得接到所有能到達它的狀態上,而且條件彼此重疊,很難推理。當你的轉移表放不進一個畫面,那就是該換行為樹的訊號。

一個巡邏守衛的具體 FSM:Idle(等 2 秒)→ Patrol(走到下一個路徑點)→ Alert(轉向聲音來源,1.5 秒)→ Chase(以跑步速度移動到最後已知位置)→ Attack(距離 2 公尺內且冷卻結束)→ Search(在最後已知位置附近遊走 8 秒)→ Patrol。七個狀態、大約十幾條轉移,而 Search 裡的那個記憶計時器,就是讓它看起來像在思考、而不是在發抖的關鍵。

  • 單一變數的狀態當前狀態就是一個 enum。除錯就是讀一個值,不用從堆疊裡反推意圖。
  • 每個閾值都要做遲滯任何可能雙向切換的距離或計時器,都需要不同的進入值與離開值,否則 NPC 就會在邊界上震盪。
  • 轉移數量預算狀態數盡量壓在十個以內,再多表格就維護不動了。超過就換行為樹,或拆成子狀態機。

行為樹

行為樹用階層取代轉移表。葉節點是動作(MoveTo、PlayAnimation、Wait)與條件(IsPlayerVisible、HealthBelow)。真正做事的只有兩種複合節點。選擇器依序試它的子節點,第一個成功就回傳成功——這就是你的優先順序清單。序列依序執行它的子節點,第一個失敗就失敗——這就是你的逐步程序。

規模上的優勢是可組合性。加一個新行為,就是在選擇器底下加一棵子樹,而不是把一個新狀態接到所有能到達它的狀態上。優先順序就寫在子節點的順序裡,比一組互相重疊的布林條件好讀太多。子樹可以重複使用:同一條「調查噪音」分支,守衛、狗、平民都能用。

行為樹不會自動變快。天真的實作會在每次 tick 時重新求值整棵樹,所以你要把 tick 掛在 100–250 毫秒的決策排程上,讓當前正在跑的動作每帧驅動移動。長時間執行的動作需要一個「執行中」狀態讓樹記住,否則每個 tick 動作都從頭開始,NPC 就會一直滑回路徑起點。多數內建行為樹的引擎——Unity 的 Behavior 套件、Unreal 內建的樹、或自己用 TypeScript 寫的小型版本——都用回傳值 Running 處理這件事,但這也正是 NPC 卡頓時你要第一個去查的地方。

一個戰鬥 NPC 的選擇器結構,依優先順序排列:(1) 血量低於 25%,序列 [Flee to cover, Heal];(2) 玩家可見且距離 2 公尺內,序列 [Face player, Attack];(3) 玩家可見,序列 [MoveTo 走位點, Attack];(4) 有最後已知位置,Search;(5) Patrol。選擇器會挑第一個條件成立的 branch。調整那幾個子節點的順序,就是你不動程式碼也能調攻擊性的方法。

  • 選擇器節點第一個成功的子節點就回傳成功。用在 fallback 優先順序上:先試最特定的行為,再試下一個,最後才是預設行為。
  • 序列節點第一個失敗的子節點就失敗。用在有順序的程序上——如果第一步沒發生,第二步就沒意義。
  • 執行中狀態長時間動作必須回傳 Running,並從中斷處繼續。如果它立刻回傳 Success,樹每個 tick 都會把它重啟,移動就會壞掉。
  • 黑板一個共用的鍵值儲存,放樹會讀寫的事實——目標位置、警戒等級、冷卻到期時間。保持它小、保持它有型別。

GOAP(目標導向動作規劃)

GOAP 把模型反過來。你不描述什麼時候該轉移,而是描述動作做什麼、需要什麼,再讓規劃器搜尋出一條能達成目標的動作序列。每個動作有前置條件、效果與成本。「Attack」需要 HasWeapon 和 InRange,並設定了 TargetDead。「GetWeapon」需要 WeaponAvailable,並設定了 HasWeapon。「MoveToTarget」需要 TargetKnown,並設定了 InRange。規劃器從目標出發反向推,找出效果能滿足目標、成本最低的動作鏈。

當世界狀態很豐富、正確順序又不明顯時,這就划算了。一個 NPC 需要先斷開視線、換彈、重新移位、再交火,會產出一份設計師從來沒明寫的計畫,因為規劃器是從前置條件圖裡推導出順序的。在量產上也划算:新增一個道具或技能,就是新增一個動作定義,而不是去改每一台該知道這件事的狀態機。

代價是搜尋。天真的規劃器會一直展開動作組合直到找到計畫,隨著動作與狀態變數增加,這會爆炸。標準做法是加啟發式——在狀態圖上做 A*,用對目標的距離估計——再加上嚴格的展開上限。在我們自己的瀏覽器情境裡,計畫是在決策 tick 上算的,上限幾百個節點展開,超過就退回快取的計畫或一條簡單的行為樹分支。誠實的界線在這裡:當有意義的動作序列很多時,GOAP 值得那個搜尋成本;當 NPC 永遠只做三件事時,它就是浪費的額外開銷。

  • 前置條件與效果每個動作者宣告執行需要什麼成立、以及它改變了什麼。規劃器是對這些宣告推理,不是對程式碼推理。
  • 從目標反向搜尋從想要的狀態出發,找出效果能產生它的動作,再對它們的前置條件遞迴。通常比正向搜尋便宜。
  • 展開上限限制每次規劃呼叫的節點展開數。超過就退回——絕不讓規劃器卡住一個帧。
  • 計畫快取只在世界狀態有實質變化時重新規劃。靜態情境直接重用上一份計畫,成本為零。

效用 AI

效用 AI 對每個選項打分,取最高的。每個候選動作者有一個函式,把世界狀態映射成數字,通常正規化到 0 到 1。「Flee」在血量低、敵人近的時候分數高。「Attack」在目標在範圍內、彈藥大於零時分數高。「Wander」在沒什麼事在催的時候拿個中等分數。NPC 取 argmax,可以再加一點隨機擾動,讓同分時結果不確定。

這是最能產出意外角色感的模型,因為行為是從曲線之間的交互作用湧現出來的,不是從明寫的規則。兩個評分權重略有不同的 NPC,不用任何分支程式碼就能有明顯不同的行為,這讓它天生適合做性格變化。它也是執行時最好調的模型:把權重開在除錯面板上,遊戲跑著就能拖。

失敗模式是效用平坦或吵雜。如果每個動作都拿 0.4,NPC 看起來優柔寡斷、不停切換。如果最高分每個 tick 在兩個接近的值之間翻來翻去,你就得到跟爛狀態機一樣的震盪。標準解法是承諾視窗:一旦選定動作,就至少維持固定時間——500 毫秒是個合理的預設——除非最高分超出已承諾動作的分數一個有意義的幅度。曲線也很重要。血量的線性反應曲線,會做出一個每場戰鬥都在同一個時刻逃跑的 NPC。指數或 logistic 曲線做出的閾值會跟著局勢移動。

  • 每個動作各自的評分函式每個動作把世界狀態映射成 0 到 1 的效用。讓它們彼此獨立,才能只調一個而不動到其他。
  • 承諾視窗一旦選定,就維持該動作一段最短時間。避免在分數接近的選項之間逐 tick 翻來翻去。
  • 可調權重把權重即時開出來。兩個只有權重不同的 NPC 讀起來就是兩個角色,這是最便宜的性格系統。
  • 反應曲線線性曲線會造出固定閾值。Logistic 曲線讓 NPC 跟著局勢反應,而不是跟著一個魔法數字。

導航與碰撞

決策邏輯挑目的地。導航把身體送過去。navmesh 是一塊三角化的可走表面;尋路在它的多邊形圖上跑 A*,結果是一串 NPC 要沿著走的多邊形邊。我們是在載入時從關卡幾何建好再快取的,因為每帧重建是讓瀏覽器遊戲帧率掉下去最簡單的一種方式。

操縱處理最後幾公尺與逐刻的移動。Seek 和 Arrive 產生前向向量,分離力把 NPC 推離附近的代理,障礙閃避讓路徑繞過門或另一個角色這類動態阻擋物。操縱每帧跑;尋路只在目的地改變或路徑失效時跑。每個 NPC 每帧都重算路徑,是常見而且可以避免的成本。

碰撞是阻止 NPC 穿牆的那道關卡。navmesh 讓路徑留在可走地面上,但操縱與物理還是可能把角色推進幾何裡,所以角色控制器會在移動之後對碰撞體解穿透。在我們的等角世界——Mini World 和 The Cyclops' Island——大部分看得見的 NPC bug 都出在這裡:一條穿過比角色半徑還窄的縫的路徑,或一個在轉角把代理推進牆裡的操縱向量。在 navmesh 上把代理半徑設對,並讓分離力低於控制器的推開強度,就能解掉大多數問題。

1

01

在載入時從靜態幾何烘焙 navmesh 並快取。不要每帧重建。

2

02

代理半徑與高度要對齊角色實際的碰撞膠囊,不是模型的包圍盒。

3

03

只在目的地改變、路徑被阻擋、或 NPC 偏離路徑超過一個閾值時才跑尋路。

4

04

操縱、分離、障礙閃避每帧跑,並對分離力設上限。

5

05

移動之後對碰撞體解碰撞,再把角色貼回 navmesh 上。

6

06

在除錯疊層上把路徑與 navmesh 畫出來並寫 log——這些 bug 通常是空間問題,不是邏輯問題。

LLM 驅動的 NPC

語言模型改變的是意圖與對話這一層,不是導航層。尋路、操縱、碰撞完全照舊,因為一個要花上百毫秒才回應的模型,沒辦法驅動一個每帧都要動的身體。模型能做的是:根據玩家的話決定哪個目標合理、用角色口吻生成回應文字、在情況模糊而手寫規則只會產出很平的東西時,在高階選項之間做選擇。

可行的架構要把兩種速度分開。即時反應跑在狀態機或效用評分器上——攻擊、逃跑、找掩護——因為它們得在一兩個帧內發生。深思熟慮的意圖則透過模型以慢得多的節奏跑,也許每次互動一次、或每幾秒一次,它的輸出是一道結構化指令,給快層去執行:把目標設成 Follow、把對玩家的態度設成 Hostile、設定對話台詞。模型永遠不直接碰位置。

誠實的極限是延遲、成本與不確定性。回應時間長到 NPC 必須在輸入一到就用一個手勢或一句短的罐頭台詞回應,否則讀起來就是壞掉。成本與你呼叫的頻率成正比,所以要批次、要快取。而且因為輸出不確定,你必須在它進到遊戲之前先用 schema 驗證——一段解析失敗的自由文字回應,應該退回一句預設台詞,而不是讓整個對話系統崩掉。把模型當成一個藏在有型別介面後面的不可靠服務,整合就撐得住。

  • 有型別的指令輸出模型回傳的是結構化意圖,不是讓遊戲程式碼去解析的散文。用 schema 驗證,格式不對就拒絕。
  • 兩種速度的迴圈反應交給本機的快邏輯,深思熟慮交給慢的模型呼叫。永遠不要把一次網路往返放在移動路徑上。
  • 立即回應輸入一到就播一個手勢或一句短罐頭台詞,等模型回應再換掉。用來蓋掉延遲。
  • 優雅退回呼叫失敗或逾時,就退回狀態機的預設反應。NPC 絕不能卡在那裡等模型。
Ashenmere——在決鬥中讀對手的決策。
The Cyclops' Island——等角格線上的導航與碰撞。

走進它所屬的那個世界

精灵表是一個無處可站的角色。最後一步是別人沒提供的:把你剛做好的表,放進一個有人能走來走去世界裡——一座小島、一顆行星、一個下雨的街角。這個網站上的每個世界,都是從一句描述做出來,並發布成一個你能玩的頁面。

回到 3D 遊戲建構器

全部 31 個完成的 3D 世界、遊戲與場景都在同一頁上——每一個都能玩。

平台:Neta Studio頁面:NPC 行為:狀態機、行為樹、GOAP 與 AI 驅動的 NPC接著讀:/explore/guide/what-is-a-non-player-character · /explore/guide/what-are-npcs · /explore/guide/ai-npc
×