Utility Theory (Beta)
簡介
效用理論 AI 透過基於數學模型的決策架構來運作。在建立效用理論代理 (UTAgent) 時,設計者的工作通常是建立和調整「回應曲線」(Response Curves),這些曲線定義了執行一組動作的效用,從而生成所需的機器人行為。
Bot SDK 的實作分為以下核心概念:
- 考慮: 這是主要使用的節點類型,定義了代理應該 考慮 執行的事情。它封裝了生成 分數(即效用值) 所需的大部分相關資料、以及如果該考慮被選為最有用時要執行的動作,以及許多其他額外方面,如排名 (Ranking)、動量 (Momentum) 和冷卻時間 (Cooldown)。
範例: 「考慮戰鬥」評估代理目前開始與其他實體戰鬥的效用。「考慮逃跑」評估由於危險(低生命值、技能冷卻等)而逃離戰鬥的效用。
- 回應曲線: 建模一個回應曲線(或效用曲線)的實例,該曲線輸出一個分數值。它接收一個輸入參數,用於評估曲線的特定點。它使用 Unity 的 AnimationCurve 編輯器,讓使用者能夠根據輸入值表達分數的行為。它可以用來表達效用分數隨時間線性增加、指數增加,或者隨時間減少、在特定區間保持恆定等。一個考慮節點可以包含多個回應曲線實例,這些實例會被組合以生成最終的分數結果;
範例: 「考慮治療」有一個回應曲線,其分數值隨時間線性下降,並接收當前實體標準化的生命值作為輸入,這意味著當實體生命值接近1
時,該考慮的相關性較低。
- 動作: 定義當一個考慮節點被選中/更新/未被選中時要執行的一系列順序動作。這些動作負責實際改變遊戲狀態,作為代理行為的一部分。
範例: 「考慮施放火球」是被選中的考慮節點,然後執行施放火球法術的動作,這可能涉及初始設置、順序更新和資料清理。
還有其他實作細節會補充這些內容:排名 (Ranking)、承諾 (Commitment) 和內嵌考慮 (Nested Considerations)。這些將在接下來的章節中解釋。
優點與缺點
以下是使用 Bot SDK UT 時需要考慮的優缺點列表:
- 優點:
- 沉浸式行為: 考慮節點被自由評估和選擇。與狀態機 (State Machines) 和行為樹 (Behaviour Trees) 不同,效用理論文件 不需要 使用者嚴格定義代理執行的每一種可能性。考慮節點是鬆散的,UTAgent 可以按照它認為最有用的任何順序執行它們。這使得代理在執行方式上更加「有創意」,使用者無需預先定義這些方式;
- 平滑的決策空間: 由於使用回應曲線來定義效用分數,決策過程比狀態機轉換和行為樹分支中的真/假分支更加平滑。決策的語義發生了變化。
- 缺點:
- 較難定義順序: 使用 UT 時,定義要執行的邏輯順序並不容易。在狀態機中,定義一系列應該發生的狀態並嚴格控制其邏輯和時間非常容易,而 UT 編輯器並不容易實現這一點,儘管可以定義動作和內嵌考慮的順序,因此表達某些類型的遊戲動作可能更困難,儘管這些順序可以委託給其他系統;
- 較低的可預測性: 由於 UTAgent 可以按照最有用的順序執行考慮節點,它可能會執行使用者未預期或不希望的行為,並且可能不容易找出原因;
- 需要平衡多個變量: 考慮節點是包含大量資訊的大容器。它們表達的內容量,為了增加使用自由度和最佳化,使得它們有點塞滿了使用者可能需要考慮的多個值。不僅僅是建模回應曲線,還有其他概念,如排名 (Rank)、動量 (Momentum) 和冷卻時間 (Cooldown),這些都可以幫助最佳化代理執行,這意味著一些維護工作會集中在微調這些變量上。
在以下影片中,您可以查看 UT 編輯器的概述以及其在簡單遊戲範例中的使用範例:
建立效用理論文件
在 Bot SDK 視窗中,點擊New Document
按鈕並選擇Utility Theory (UT)
:

為 AI 文件選擇一個名稱。此文件是一個可指令碼物件,僅包含編輯器端所需的 XML,與 Quantum 模擬無關。它不需要包含在構建中。
為此 AI 文件選擇的名稱也將是用於模擬中實際更新實體 AI 的 Quantum 資產的名稱,因此建議選擇一個有意義的名稱。

建立一個新的 UT 文件會自動填充一個非常基本的考慮節點:

考慮節點
這是此編輯器中的主要節點類型。它包含了定義效用分數、要執行的動作以及是否有其他內嵌考慮節點的所有相關入口點。
在考慮節點中,可以定義特定考慮節點評估的回應曲線。這些曲線的評估結果會在[0..n]
範圍內生成值。如果有多個回應曲線,它們會相乘,結果就是執行該考慮節點的效用。
預設下,所有考慮節點都會被評估,這意味著回應曲線會根據定義的輸入生成輸出值。分數最高的考慮節點將被選為在該特定幀中執行的節點。
選中的考慮節點可能在下一幀的 UTAgent 更新時就已經不同,但有一些額外的機制可以幫助減少決策的「抖動」,當 UTAgent 需要在多幀中執行相同的考慮節點時。
讓我們分析一個考慮節點的結構:
基礎值

基礎分數: FP 基礎值,總是 加到回應曲線的結果中。使用此值為考慮節點提供固定的效用值;
冷卻時間: 當考慮節點被選中並開始執行時,冷卻時間定義了它將被跳過/忽略的秒數;
冷卻時間取消動量: 如果為
true
,冷卻時間會強制考慮節點被跳過,無論該考慮節點是否有動量正在應用。如果為false
,冷卻時間僅在動量結束後應用;動量值: 如果考慮節點被選中,其排名值會增加到此欄位定義的值。使用它來定義哪些考慮節點應該可能在更多幀中繼續執行,因為這會增加它們的絕對效用值。更多資訊請參閱排名主題;
動量衰減: 如果考慮節點處於動量狀態(即其排名值因動量值而增加),其排名值每秒會減少此欄位定義的值。使用它來表達動量減少的快慢;
使用內嵌動量: 強制考慮節點鏡像任何內嵌考慮節點的動量值,以便在內嵌上下文仍然相關時繼續執行一組考慮節點,即使父考慮節點沒有任何動量值。
基礎值並非必須使用,但它們確實提供了一些額外的功能,這些功能可能非常有用。
動作

動作是從代理上下文中實際改變遊戲狀態的主要方式。將角色從 A 點移動到 B 點、使用技能、掃描等都可以在動作中完成。
效用理論的動作與 HFSM 中使用的動作相同,並且有共享的文檔,可以透過點擊這裡存取。
雙擊動作區域以編輯動作集。可用的序列包括:
- 進入時: 當考慮節點被選為最有用並開始執行時執行;
- 更新時: 當考慮節點仍然被選為最有用時,每幀執行;
- 退出時: 當考慮節點在前一幀被選中,但在當前幀未被選中時執行;
排名

在選擇考慮節點時,有兩個主要分數會被比較:
- 絕對效用: 具有最高排名值的考慮節點絕對優先於排名值較低的考慮節點,後者在計算分數時會被完全忽略;
- 相對效用: 這是已經解釋過的分數,即從回應曲線評估的分數。
來自排名的絕對效用幫助「靜音」其他考慮節點,這對 UT 文件的組織以及 最佳化代理性能非常有益,因為它避免了評估不必要的曲線。
排名值計算為整數。舉例來說:考慮 4 個考慮節點(A、B、C 和 D)。假設它們的排名值如下:
- A = 0;
- B = 1;
- C = 2;
- D = 2;
當代理更新時,考慮節點 C 和 D 具有絕對優先權,它們的回應曲線將被評估和比較。考慮節點 A 和 B 將被忽略。
排名始終在運行時定義,並且可以在每一幀中更改,可以透過兩種方式定義:在排名容器中或在承諾容器中(更多資訊請參閱下一個主題)。
排名容器節點接受任何繼承自AIFunctionInt
類的節點的輸入。實現一個函數,該函數根據遊戲狀態返回所需的排名值。以下是一個範例:
C#
namespace Quantum
{
[System.Serializable]
public unsafe class AgentPriority : AIFunction<int>
{
public EAgentPriority DesiredPriority;
public override int Execute(Frame frame, EntityRef entity, ref AIContext aiContext)
{
// Get an agent-specific component
var agentData = frame.Unsafe.GetPointer<AgentData>(entity);
// Compare the agent current priority with the priority this AIFunction checks
// If the agent is currently prioritizing it, then increase the Rank of the Consideration to 10
// If the priority is something else, set it to 0 instead
if(agentData->Priority == DesiredPriority)
{
return 10;
}
else
{
return 0;
}
}
}
}
另一個範例是實現一個IsInDanger
函數,該函數讀取遊戲狀態/黑板值以識別代理當前是否處於危險中,即是否有敵人靠近它,或者是否有視線。如果檢測到危險,返回排名值10
,這意味著具有此排名的考慮節點現在具有非常高的絕對優先級。如果沒有危險,則返回0
。
要存取排名容器,請雙擊排名區域。
動量
動量值用於在考慮節點被選中時設置其排名值(類似於「進入時」邏輯)。
其主要目的是允許增加考慮節點的重要性,僅當它被選為比其他節點更有用時,並保持其相關性更長時間,減少 UTAgent 變得「抖動」的機會,即在代理應該嘗試花更多時間只做一件事的情況下改變選中的考慮節點。
例如,這對於讓代理在許多幀中追蹤目標而不是頻繁停下來做其他事情非常有用。
PS: 由動量生成的排名值 優先級高於 動態計算的排名值。
現在考慮節點處於動量狀態,它什麼時候會恢復正常?有兩種主要方法可以減少它:
- 透過設置
Momentum Decay
值,可以指定一個值,該值將用於每秒減少動量的排名值; - 也可以使用 承諾 節點取消動量的排名值,這些節點返回布林值,可以用於指定 何時 應該取消動量。這在動量不應隨時間衰減,而是透過某些特定的遊戲邏輯(例如「目標不再可到達」)時特別有用。要建立承諾函數,請繼承
AIFuncionBool
並實現其Execute
方法。當返回值為true
時,表示應該取消動量。
C#
namespace Quantum
{
[System.Serializable]
public unsafe class SampleCommitment : AIFuncionBool
{
public override int Execute(Frame frame, EntityRef entity, ref AIContext aiContext)
{
return false;
}
}
}
要存取承諾節點,請雙擊承諾區域。
這個想法是,您可能希望 維持一個高的排名值,直到某些條件完成。例如,當代理由於考慮節點跟隨另一個角色時,可能希望在追蹤期間將其排名增加到一個高值,將動量衰減設置為零,並新增一個承諾節點,該節點檢查代理是否仍然可以到達其目標(可能是一個簡單的距離檢查)。如果目標太遠,則返回true
會將考慮節點的排名重新設置為零,增加代理執行其他更有用的事情的機會。
也就是說,並非必須使用這些技術中的任何一種,或者獨家使用它們。可以為考慮節點新增動量,並同時擁有自然的動量衰減 和 承諾函數。
回應曲線

這是此 AI 技術的核心。決策完全基於定義曲線、評分、相乘和組合結果,以決定在特定幀中做什麼最有用。
我們重新使用 Unity 的AnimationCurve
系統來定義曲線,然後將其編譯為其確定性版本,稱為FPAnimationCurve
。
在建立曲線時,最重要的是使用能夠正確表達所需評估的曲線。這是否是一些僅在非常接近特定值時才有用的行為?效用是否線性增長?指數增長?是否應該在特定範圍內為零,然後在某一點後開始線性增加?等等。
建立您自己的曲線,從預設定中選擇曲線,並建立新的預設定。
一個非常重要的概念 是曲線的 Y 軸(即結果分數)應該被標準化(即在 0..1 之間)。這至關重要,因為曲線的結果是相乘的,所以需要保持比例,否則曲線的結果將無法真正相互比較,這將破壞 UT 技術的原則。
以下是 Bot SDK 範例中使用並儲存為預設的回應曲線的一些範例圖片:

要為考慮節點定義更多回應曲線,請雙擊曲線區域以進入曲線圖。
使用右鍵點擊在Utility
部分建立新的回應曲線。

點擊曲線以打開其編輯器視窗。

使用哪條曲線的決定完全取決於代理的特定需求。它取決於 將插入該曲線的輸入是什麼,以及這應該如何反映在「效用」值上。
舉例來說:
考慮治療: 假設一個代理的最大生命值為 10,並且僅當其生命值低於 5 時才希望治療。對於大於 5 的值,效用曲線的結果應為零,以便它專注於執行其他考慮節點。然後,對於低於零的值,治療的效用應該非常快地增加。這可以透過以下曲線來表達:
考慮攻擊: 假設一個代理僅在場景中至少有一個敵人才希望攻擊。無論是一個、兩個還是十個敵人。這是一個「二元閾值」曲線,它立即從零變為一。儘管這減少了我們使用曲線時的一些表達能力,但它仍然可以有用。曲線可能如下所示:
通常(但不是必須),一個考慮節點需要多於一條回應曲線。繼續根據需要新增新的曲線。請注意,新增更多曲線可能需要處理更多輸入,這會增加開銷,但可以使用一些策略來最小化必要的輪詢量。
可以從根視圖查看和編輯回應曲線:

回應曲線的輸入
輸入值由自定義用戶邏輯定義,因為這是非常遊戲特定的。它可以是來自實體元件的生命值,可以是存儲在黑板的資料,可以是從傳感器系統收集的資料等。
要建立自定義輸入類型,請建立一個繼承自AIFunction<FP>
的新類,並實現其Execute
方法。
C#
namespace Quantum
{
using Photon.Deterministic;
[System.Serializable]
public unsafe partial class InputEntityHealth : AIFunction<FP>
{
public override FP Execute(Frame frame, EntityRef entity, ref AIContext aiContext)
{
// Read the current health from a component from the agent entity
var health = frame.Unsafe.GetPointer<Health>(entity);
return health->Current;
}
}
}
儲存程式碼並在成功編譯後,輸入節點將可在視覺編輯器中使用。
要存取回應曲線節點,請雙擊考慮節點上的回應曲線區域。
連結考慮節點
可以將一個考慮節點連結到其他考慮節點,建立父子關係。
在這種情況下,僅在特定幀中父考慮節點被選為最有用時,子考慮節點才會被評估。當這種情況發生時,子考慮節點僅會與兄弟考慮節點競爭。
這主要有幾個原因有用:
- 它可以幫助性能最佳化。想像一個父考慮節點分析「戰鬥是否有用」,而子考慮節點是實際評估戰鬥選擇的節點。這些節點在戰鬥有用之前不會被計算;
- 仍然出於性能考慮,父考慮節點中包含的曲線隱式地為其所有子節點計算,因此不需要多次重新計算這些曲線;
- 它有助於以「上下文」方式組織考慮節點;
要連結考慮節點,請點擊其右上角的輸出插槽並將其連結到另一個考慮節點的輸入插槽:

編譯效用理論文件
為了在模擬中實際使用 UT,每次進行有意義的更改時,都需要編譯 AI 文件中的所有內容。
要編譯,有兩個選項:

- 左側按鈕用於僅編譯當前打開的文件;
- 右側按鈕用於編譯專案中的所有 AI 文件。
預設下,UT 文件將位於:Assets/QuantumUser/Resources/DB/CircuitExport/UT_Assets
。
此過程建立的主要資產類型是UTRoot
。

使用 UTRoot 資產
要使用建立的 UT 根資產,請使用類型為AssetRef<UTRoot>
的欄位對其進行引用,並透過frame.FindAsset()
載入它。
UT 編碼
UT 有一個名為UTAgent
的主要元件,可以以兩種不同的方式使用:
- 透過程式碼或直接在 Unity 的實體原型中將元件新增到實體中;
- 或者,在框架的全局變量中聲明
UTAgent
的實例;
UTAgent
元件有一個名為UtilityReasoner
的結構,它是代理使用的主要資料和邏輯中心。它存儲了所有必要的相關資料,用於評分考慮節點並選擇其中之一執行。
初始化和更新
以下是初始化和更新 UT 代理的程式碼片段:
C#
UTManager.Init(f, &utAgent->UtilityReasoner, utAgent->UtilityReasoner.UTRoot, entity);
UTManager.Update(f, &utAgent->UtilityReasoner, entity);
定義欄位值
在這裡找到更多關於在考慮節點、輸入、排名和承諾中設置欄位值的替代方案的資訊:定義欄位值。
AIParam
在這裡找到更多關於如何使用AIParam
類型的資訊,如果您想要更靈活的欄位,可以透過手動設置或從黑板/常量/配置節點中設置:AIParam。
AIContext
在這裡找到更多關於如何傳遞代理上下文資訊作為參數的資訊:AIContext。
BotSDKSystem
有一個類用於自動化一些過程,例如釋放黑板記憶體。在這裡找到更多關於它的資訊:BotSDKSystem。
視覺編輯器註釋
在這裡找到更多關於如何在視覺編輯器中建立註釋的資訊:視覺編輯器註釋。
更改編譯匯出文件夾
預設下,Bot SDK 編譯生成的資產將被放置在文件夾Assets/Resources/DB/CircuitExport
中。查看如何更改匯出文件夾:更改匯出文件夾。