効用理論(ベータ版)
はじめに
効用理論のAIは、数理モデリングに基づいた意思決定アーキテクチャを与えることで動作します。効用理論エージェント(UTAgent
)を作成するには、一連のアクションについての効用を定義する「反応曲線」を作成・調整し、望ましいBotの振る舞いを生成します。
Bot SDKの実装は、以下のコアコンセプトに分かれています。
- Consideration(コンシダレーション、検討事項):主に使用されるノードで、エージェントが検討すべきことを定義します。スコア(効用値)を生成するための関連データ、このコンシダレーションが最も有用だった場合に実行するアクション、ランキング・モメンタム・クールダウンなどの他の多くの要素をカプセル化します。
例:「戦闘の検討」は、エージェントが他のエンティティと戦闘を開始することが現時点でどれだけ有用かを評価します。「逃走の検討」は、危険(ライフの低下、スキルのクールダウン状況など)により戦闘から逃走することがどれだけ有用かを評価します。
- Response Curve(反応曲線):スコア値を出力する反応曲線(効用曲線)のインスタンスをモデル化します。曲線の特定の点を評価するために、入力をパラメーターとして受け取ります。UnityのAnimationCurveエディターを使用して、入力に基づくスコアを編集することで、線形に増加・指数関数的に増加・時間経過とともに減少・特定区間では一定など、様々な挙動を表現できます。コンシダレーションは複数の反応曲線インスタンスを持つことが可能で、それらを組み合わせて最終的なスコアを結果として生成します。
例:「回復の検討」は、スコアが線形に減少する反応曲線を持ち、エンティティの現在ライフの正規化した値を入力として受け取ります。つまり、エンティティのライフが1
(最大ライフ)に近づくほど、コンシダレーションの関連性が低くなります。
- Actions(アクション):コンシダレーションが選択/更新/非選択された際に実行される一連のアクションを定義します。これらは、エージェントのアクションの一部として、実際にゲームステートを変更する役割を担います。
例:「ファイアボール詠唱の検討」のコンシダレーションが選択されると、ファイアボールの呪文を詠唱するためのアクションが実行されます。これには、初期設定・継続的な更新・データのクリアなどが含まれます。
他の詳細な実装として「Ranking(ランキング)」「Commitment(コミットメント)」「Nested Considerations(コンシダレーションの入れ子)」があります。これらは以降のセクションで説明します。
利点と欠点
Bot SDK UTを使用する際の利点と欠点は次の通りです。
- 利点:
- 没入型の挙動:コンシダレーションは評価され自由に選択されます。ステートマシンやビヘイビアツリーとは異なり、効用理論のドキュメントはエージェントが実行しうるあらゆる可能性を厳密に定義する必要がありません。コンシダレーションの規則はゆるく、
UTAgent
は最も有用だと判断した順に実行できます。実行したいことを事前に定義する必要がないという意味で、エージェントはより「創造的」になります。 - スムーズな意思決定空間:反応曲線を使用して効用スコアを定義するため、ステートマシンの遷移やビヘイビアツリーのブランチのような真偽の分岐に比べて、意思決定がスムーズになります。これによって、意思決定の意味論(セマンティクス)が変化します。
- 没入型の挙動:コンシダレーションは評価され自由に選択されます。ステートマシンやビヘイビアツリーとは異なり、効用理論のドキュメントはエージェントが実行しうるあらゆる可能性を厳密に定義する必要がありません。コンシダレーションの規則はゆるく、
- 欠点:
- シーケンスの定義が困難:UTを使用する場合、実行したいロジックのシーケンスを定義することは困難です。ステートマシンでは、ロジックとタイミングを厳密に制御した状態遷移シーケンスを簡単に定義できます。しかしUTエディターでは、アクションやコンシダレーションのシーケンスを定義できても、同様の制御は困難です。そのため、特定のゲームアクションを表現することが難しくなりますが、そうしたシーケンスは他のAIモデルに委譲できます。
- 予測が困難:
UTAgent
は最も有用な順でコンシダレーションを実行できるため、予期しない/望まないアクションを実行する可能性があり、その原因を特定するのが難しい場合があります。 - 調整すべき変数の数:コンシダレーションは大量の情報を含む巨大なコンテナです。表現することが多く自由度と最適化を高めるために、考慮すべき複数の値が詰め込まれています。反応曲線のモデル化だけでなく、ランク・モメンタム・クールダウンなどの概念がエージェントの実行の最適化に影響するため、変数の細かい調整が保守作業の一部に含まれます。
UTエディター概要とシンプルなゲームの例は、以下の動画をご覧ください。
ドキュメントの作成
Bot SDKウィンドウでNew Document
ボタンをクリックして、Utility Theory (UT)
を選択してください。

まず、AIドキュメントの名前を設定してください。このドキュメントはScriptableObject
で、エディター側でのみで使用されるXMLを保持し、Quantumシミュレーションには関連しません。そのため、ビルドに含める必要はありません。
このAIドキュメントに設定した名前は、生成されるQuantumアセット名にもなります。アセットは、シミュレーション内でエンティティのAIを更新するために使用されるので、意味のある適切な名前を選ぶと良いでしょう。

新しく作成したUTドキュメントには、基本的なコンシダレーションノードが置かれています。

The Consideration Node(コンシダレーションノード)
これはこのエディターの主要なノードタイプです。効用スコアの定義・実行するアクション・入れ子のコンシダレーションなど、関連要素すべてのエントリーポイントになっています。
各コンシダレーションでは、評価に使用される反応曲線を定義できます。評価結果の範囲は[0..n]
の値です。複数の反応曲線が存在する場合は、それらを乗算した結果がコンシダレーションの実行の効用になります。
デフォルトでは、すべてのコンシダレーションが評価され、反応曲線は定義された入力に基づいて値を出力します。特定のフレームにおいて、最も高いスコアのコンシダレーションが選択されて実行されます。
スコアが変動する場合、次のUTAgent
の更新で異なるコンシダレーションが選択される可能性があります。UTAgent
が同じコンシダレーションを複数のフレームに渡って実行したい場合は、意思決定の「揺らぎ」を軽減するためのメカニズムが役立つでしょう。
コンシダレーションの構造を詳細に見てみましょう。
基礎値

Base Score(基本スコア):反応曲線の結果に常に追加される
FP
値です。コンシダレーションに固定の効用値を与えるために使用します。Cooldown(クールダウン):コンシダレーションが選択されて実行を開始した後に、スキップ/無視される秒数を定義します。
Cooldown Cancels Momentum(クールダウンによるモメンタムのキャンセル):
true
の場合、モメンタムが適用されているかどうかに関わらず、クールダウンがコンシダレーションを強制的にスキップします。false
の場合、モメンタム終了後にのみクールダウンが適用されます。Momentum Amount(モメンタム量):コンシダレーションが選択された場合、このフィールドで定義された値だけランク値が増加します。複数のフレームに渡って実行したいコンシダレーションを定義するために使用します。これによって、絶対的な効用値が増加します。詳細な情報は「ランキング」トピックをご覧ください。
Momentum Decay(モメンタム減衰):コンシダレーションがモメンタムの影響を受けている(モメンタム量によってランクが増加している)場合、このフィールドで定義された値だけランク値が毎秒減少します。モメンタムの減少速度を表すために使用します。
Use Nested Momentum(入れ子のモメンタムを使用):親コンシダレーションにモメンタム値がない場合でも、入れ子のコンシダレーションに適用されているモメンタム値を強制的に反映させます。これによって、入れ子のコンシダレーションが実行されている間、親コンシダレーションの実行も継続できます。
基礎値の使用は必須ではありませんが、非常に便利な機能を追加できます。
Actions(アクション)

アクションは、エージェントからゲームステートを変更する主要な方法になります。キャラクターを移動する・スキルを使用する・スキャンを行うなど、様々な挙動をアクションで実行できます。
効用理論のアクションは、HFSMのアクションと使用方法は同じです。こちらの共通ドキュメントをご覧ください。
「Actions」部分をダブルクリックすると、アクション一式を編集できます。利用可能なシーケンスは次の通りです。
- On Enter:コンシダレーションが最も有用として選択され、実行が開始された際に実行されます。
- On Update:コンシダレーションが最も有用として選択されている間、毎フレーム実行されます。
- On Exit:コンシダレーションが以前のフレームで選択されていて、現在フレームで選択されなかった際に実行されます。
Ranking(ランキング)

コンシダレーション選択時、主に2つのスコアが比較されます。
- 絶対的効用:最も高いランク値を持つコンシダレーションは絶対的な優先度を持ち、ランク値が低いコンシダレーションはスコア計算時に完全に無視されます。
- 相対的効用:これは(既に説明した通り)反応曲線から評価されるスコアになります。
ランクに基づく絶対的効用によって、他のコンシダレーションは「ミュート」されます。これは、UTドキュメントの整理に有益であり、不要な反応曲線の評価を避けることでエージェントのパフォーマンスを大幅に向上できます。
ランク値は整数として計算されます。一例として、4つのコンシダレーション(A・B・C・D)を考えてみましょう。それぞれのランク値を次のように仮定します。
- A = 0;
- B = 1;
- C = 2;
- D = 2;
エージェントが更新されると、絶対的な優先度を持つコンシダレーションC・Dについて、反応曲線が評価・比較されます。コンシダレーションA・Bは無視されます。
ランクは常に実行時に定義され、毎フレーム変更可能です。これは、ランクコンテナかコミットメントコンテナ(詳細は後述)の2通りの方法で定義できます。
ランクコンテナコードは、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)
{
// エージェント関連のコンポーネントを取得
var agentData = frame.Unsafe.GetPointer<AgentData>(entity);
// 「エージェントの現在の優先度」と「AIFuctionでチェックする優先度」を比較する
// エージェントが優先されている場合は、コンシダレーションのランクを10に上げる
// そうでなければ、ランクを0に設定する
if(agentData->Priority == DesiredPriority)
{
return 10;
}
else
{
return 0;
}
}
}
}
別の例として、IsInDanger
関数の実装では、ゲームステート/Blackboard
の値を読み込んで、エージェントが現在危険な状態にある(敵が近くにいる、敵の視線上にいる)かを識別します。危険を検知した場合にランク値10
を返すことで、コンシダレーションの絶対的な優先度は非常に高くなります。危険がない場合は、単に0
を返します。
ランクコンテナを開くには、「Ranking」部分をダブルクリックしてください。
Momentum(モメンタム)
モメンタム値は、コンシダレーションが選択された際(On Enter
ロジックと同様)に、ランクを設定するために使用されます。
主な目的は、コンシダレーションが選択された場合にのみ重要度を高め、選択された状態を長時間維持することで、UTAgent
の「揺らぎ(コンシダレーションの選択が頻繁に変更される状況)」を低減し、エージェントが一つのことに集中できるようにすることです。
例えば、エージェントが他の行動に移るために頻繁に立ち止まることなく、ターゲットの追跡を長時間続けさせるのに便利です。
追記:モメンタムによって生成されるランク値は、動的に計算されるランク値より優先度が高いです。
モメンタムの影響を受けたコンシダレーションがいつ元に戻るのか?について、モメンタムを減衰する方法は2つあります。
Momentum Decay
値を設定すると、モメンタムのランクの減少値(毎秒)を指定できます。- コミットメントノードを使用して、モメンタムのランクをキャンセルすることも可能です。コミットメントノードは、モメンタムをキャンセルするタイミングを指定するブール値を返します。モメンタムが時間で減衰するのではなく、「ターゲットに到達不可能になった」などの特定のゲームロジックによってキャンセルする場合に特に便利です。コミットメント関数を作成するには、
AIFunctionBool
を継承してExecute
メソッドを実装してください。これがtrue
を返す場合は、モメンタムをキャンセルすることを意味します。
C#
namespace Quantum
{
[System.Serializable]
public unsafe class SampleCommitment : AIFunctionBool
{
public override int Execute(Frame frame, EntityRef entity, ref AIContext aiContext)
{
return false;
}
}
}
「Commitment」部分をダブルクリックすると、コミットメントノードを編集できます。
ある条件が満たされるまで、ランク値を高く維持したい場合もあるでしょう。例えば、エージェントがコンシダレーションに基づいて他のキャラクターを追跡する場合、追跡中はランク値を高くしてMomentum Decay
を0にした上で、コミットメントノードで「エージェントがターゲットに到達できるか」をチェックすると良いかもしれません。または単純な距離チェックで、ターゲットが遠すぎる場合はtrue
を返し、コンシダレーションのランクを0に戻すことで、エージェントにとって他の行動が有用になる確率を高めます。
ただし、これらのテクニックの使用は必須ではありません。コンシダレーションにモメンタムを追加し、自然なMomentum Decay
とコミットメント関数の両方を持たせることも可能です。
Response Curves(反応曲線)

効用理論AI技術における最重要要素です。曲線・スコア・結果の乗算と組み合わせに基づいて、すべての意思決定が行われ、あるフレームにおける最も有用な行動を決定されます。
曲線の定義にはUnityのAnimationCurve
を使用し、それを決定論的なFPAnimationCurve
にコンパイルします。
最も重要なことは、望ましい評価を正確に表現する曲線を作成することです。(特定の値に非常に近い場合にのみ有用な振る舞いなのか?有用性は線形に増加するのか?それとも指数関数的に増加するのか?一定の範囲は0でそこから線形に増加するのか?など)
独自の曲線を作成するには、プリセットの一つを選択して、新しいプリセットを作成します。
非常に重要なコンセプトとして、曲線のY軸(結果スコア)は正規化(つまり0~1の範囲に)する必要があります。これは極めて重要で、曲線の結果は乗算されるため、比率を維持しないと相互に比較できなくなり、UT技術の原理が損なわれてしまいます。
Bot SDKサンプルでプリセットとして保存され使用されている、反応曲線のサンプルは次の通りです。

コンシダレーションに対して複数の反応曲線を定義するには、「Response Curves」部分をダブルクリックして曲線グラフを開きます。
マウス右ボタンで開くUtility
セクションから、反応曲線を作成します。

曲線をクリックすると、エディターウィンドウが開きます。

どの曲線を使用するかは、エージェント固有の要件に完全に依存します。どの入力をどの曲線に与えるかによって、反映される「効用」値が変わります。
以下は実例です。
回復の検討:「最大ライフ10のエージェントが、最大ライフが5を下回った場合のみ回復を望むようになる」と仮定します。ライフが5以上の値では、曲線の結果は0となり、別のコンシダレーションの実行に集中します。そして、ライフが0に近づくと、回復の効用は急激に増加します。これを曲線で表現すると、次のようになります。
攻撃の検討:「1体以上の敵がいる場合のみ攻撃を望むエージェント」を仮定します。ここで、敵の数が1体か2体か、それとも10体かは問題ではありません。これは「二値化」曲線で、0から1へ即座に変化します。曲線の表現力が損なわれているものの、それでも便利です。曲線は次のように表現できます。
コンシダレーションが複数の反応曲線を持つことは一般的(ただし必須ではない)で、必要なだけ新しい曲線を追加してください。曲線を追加することで処理すべき入力が増えるためオーバーヘッドが発生しますが、ポーリングを最小限に抑える方法も存在します。
反応曲線は、ルートビューからも確認できます。

反応曲線の入力
入力値は、ゲーム固有のカスタムロジックによって定義されます。これには、エンティティのコンポーネントから取得できるライフ値、Blackboard
に保存されたデータ、センサーシステムから収集された情報などが該当します。
独自の入力型を作成するには、AIFuction<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)
{
// エージェントのエンティティのコンポーネントから現在ライフを取得する
var health = frame.Unsafe.GetPointer<Health>(entity);
return health->Current;
}
}
}
コードを保存し、コンパイルに成功すると、ビジュアルエディター上で入力ノードが使用可能になります。
反応曲線ノードを編集するには、コンシダレーションノードの「Response Curves」部分をダブルクリックします。
コンシダレーションのリンク
コンシダレーションを別のコンシダレーションにリンクして、親子関係を作成することが可能です。
あるフレームにおいて、親コンシダレーションが最も有用として選択された場合にのみ、子コンシダレーションが評価されます。その際、子コンシダレーションは兄弟のコンシダレーションとのみ比較されます。
これは主に以下のような理由で役立ちます。
- パフォーマンス最適化に寄与します。「戦闘の検討」について、親コンシダレーションでは戦闘すべきかを分析し、子コンシダレーションでは実際の戦闘方法を評価する場合を想像してみましょう。子コンシダレーションは、戦闘が有用になるまで計算されません。
- さらにパフォーマンス面では、すべての子コンシダレーションに対して親コンシダレーションの曲線は暗黙的に計算されるため、曲線を繰り返し再計算する必要がなくなります。
- コンシダレーションの「文脈」を整理するのに便利です。
コンシダレーションをリンクするには、右上の出力スロットをクリックして、別のコンシダレーションの入力スロットに接続してください。

ドキュメントのコンパイル
UTを実際にシミュレーションで使用するには、AIドキュメントをコンパイルする必要があります。これは、ドキュメントを変更するたびに行う必要があります。
コンパイルには、2つのオプションがあります。

- 左ボタン:現在開いているドキュメントのみをコンパイルする
- 右ボタン:プロジェクトのドキュメントすべてをコンパイルする
デフォルトでは、UTファイルはAssets/QuantumUser/Resources/DB/CircuitExport/UT_Assets
に置かれます。
このコンパイルプロセスで、UTRoot
アセットが作成されます。

UTRootアセットの使用
作成されたUTRoot
アセットを使用するには、AssetRef<UTRoot>
型のフィールドで参照を作成し、frame.FindAsset()
からロードします。
UTのコード実装
UTの主要なコンポーネントはUTAgent
で、基本的に2つの方法で使用できます。
- エンティティにコンポーネントを追加する(直接コードから追加する、またはUnity上のエンティティプロトタイプから追加する)
frame.Global
にUTAgent
インスタンスを宣言する
UTAgent
コンポーネントはUtilityReasoner
という構造体を持ち、ここからエージェントが使用するデータとロジックが取得できます。構造体は、コンシダレーションを評価したり、実行するコンシダレーションを選択したりするために必要なすべてのデータを格納します。
初期化と更新
次のスニペットは、UTAgent
の初期化と更新です。
C#
UTManager.Init(f, &utAgent->UtilityReasoner, utAgent->UtilityReasoner.UTRoot, entity);
UTManager.Update(f, &utAgent->UtilityReasoner, entity);
フィールド値の定義
ノードのフィールドに値を設定する様々な方法について、詳細な情報はフィールド値の定義をご覧ください。
AIParam
AIParam
型の使用方法について、詳細な情報はAIParamをご覧ください。この型は、手動設定やBlackboard
/Constant
/Config
ノードからの設定など、柔軟に定義可能なフィールドを実現するのに便利です。
AIContext
エージェントのコンテキスト情報をパラメーターとして渡す方法について、詳細な情報はAIContextをご覧ください。
BotSDKSystem
Bot SDKコンポーネントの追加/削除コールバック時、コンポーネントのメモリ初期化/解放などのプロセスを自動化するクラスです。詳細な情報はBotSDKSystemをご覧ください。
ビジュアルエディターのコメント
ビジュアルエディターでコメントを作成する方法について、詳細な情報はビジュアルエディターのコメントをご覧ください。
コンパイル出力フォルダーの変更
デフォルトでは、Bot SDKのコンパイルプロセスで生成されたアセットはAssets/Resources/DB/CircuitExport
フォルダーに置かれます。出力フォルダーを変更する方法については、出力フォルダーの変更をご覧ください。