ビヘイビアベースAIのワーク資料
AI設計ワーク
ビヘイビアツリー入門
優先順位つきの木構造で敵AIを考える
1. この資料で学ぶこと
この資料では、敵AIの行動を「木構造」で整理する方法を学びます。
前回までに、AIを状態で分ける方法を扱いました。
PatrolState
ChaseState
AttackState
SearchState
これは、Stateパターンを使ったAIです。
Stateパターンでは、
今どの状態か?
を中心に考えます。
今回扱うビヘイビアツリーでは、
今できる行動の中で、どれを優先するか?
を中心に考えます。
2. この資料のゴール
この資料のゴールは、次の通りです。
1. ビヘイビアツリーが何をするものか理解する
2. SelectorとSequenceの役割を理解する
3. Success / Failure / Running の意味を理解する
4. 優先順位つきの行動選択を木構造で表せるようにする
5. 最後に、C++ではどのような形になるかを少し見る
今回は、最初からC++コードをたくさん書くことはしません。
まずは、
木を読む
木を考える
木を設計する
ことを中心に進めます。
3. Stateパターンの復習
Stateパターンでは、敵AIを状態ごとに分けます。
Patrol
↓ プレイヤーを見つけた
Chase
↓ 攻撃範囲に入った
Attack
↓ プレイヤーを見失った
Search
↓ 探索時間が終わった
Patrol
この考え方では、
今はPatrol
今はChase
今はAttack
のように、AIが現在の状態を持ちます。
4. Stateパターンが分かりやすい場面
Stateパターンは、次のようなAIでは分かりやすいです。
巡回する
↓
見つけたら追跡する
↓
近づいたら攻撃する
↓
見失ったら探索する
↓
戻る
行動の流れが一本道に近い場合は、Stateパターンで整理しやすいです。
5. Stateパターンで困る場面
敵の行動が増えてくると、Stateだけでは整理しづらくなることがあります。
例えば、敵に次のような行動を入れたいとします。
HPが少ないなら逃げる
攻撃できるなら攻撃する
プレイヤーが見えているなら追跡する
音が聞こえたら調べる
仲間が攻撃されていたら助ける
何もなければ巡回する
このようなAIでは、
どのStateから逃げるStateに移動するのか
どのStateから助けるStateに移動するのか
攻撃中でもHPが少なくなったら逃げるのか
追跡中に音が聞こえたらどうするのか
のように、状態遷移が増えて複雑になります。
6. 行動を優先順位で考える
このような場合は、状態よりも優先順位で考えると整理しやすくなります。
例えば、敵AIの行動を次のように決めます。
1. HPが少ないなら逃げる
2. 攻撃できるなら攻撃する
3. プレイヤーが見えているなら追跡する
4. 音が聞こえたら調べる
5. 何もなければ巡回する
この場合、上にある行動ほど優先順位が高いです。
逃げる
攻撃する
追跡する
調べる
巡回する
の順番で、できるかどうかを調べます。
7. ビヘイビアツリーとは
ビヘイビアツリーとは、
条件と行動を木構造で整理するAI
です。
次のように、木の形で行動を並べます。
Root
└ Selector
├ HPが少ないなら逃げる
├ 攻撃できるなら攻撃する
├ 見えているなら追跡する
├ 音が聞こえたら調べる
└ 巡回する
この木では、上から順番に行動候補を調べます。
8. ビヘイビアツリーの中心
ビヘイビアツリーの中心は、次の考え方です。
上から順番に調べる
できる行動を見つける
見つかったら、それを実行する
つまり、
今できる中で一番優先度が高い行動を選ぶ
ための仕組みです。
9. ノードとは
ビヘイビアツリーでは、木の1つ1つの部品をノードと呼びます。
例えば、次のようなものがノードです。
HPが少ない?
攻撃範囲内?
プレイヤーが見えている?
逃げる
攻撃する
追跡する
巡回する
Selector
Sequence
ノードには、大きく分けて次の種類があります。
条件ノード
行動ノード
制御ノード
10. 条件ノード
条件ノードは、条件を調べるノードです。
例:
HPが少ない?
攻撃範囲内?
プレイヤーが見えている?
音が聞こえた?
条件ノードは、基本的に「はい」か「いいえ」を返します。
ビヘイビアツリーでは、これを次のように表します。
はい → Success
いいえ → Failure
11. 行動ノード
行動ノードは、実際に何かをするノードです。
例:
逃げる
攻撃する
追跡する
調べる
巡回する
行動ノードは、行動の結果を返します。
行動が終わった → Success
行動できなかった → Failure
まだ行動中 → Running
12. Success / Failure / Running
ビヘイビアツリーでは、ノードが次の3つの結果を返します。
| 結果 | 意味 |
|---|---|
| Success | 成功。条件を満たした。行動が完了した |
| Failure | 失敗。条件を満たしていない。行動できない |
| Running | 実行中。行動がまだ終わっていない |
13. 条件ノードの例
プレイヤーが見えている?
このノードは、次のように考えます。
見えている
→ Success
見えていない
→ Failure
条件ノードでは、基本的に Running は使いません。
条件を調べるだけなので、
Success
Failure
のどちらかになります。
14. 行動ノードの例
追跡する
このノードは、次のように考えます。
プレイヤーに追いついた
→ Success
追跡できない
→ Failure
まだ追いかけている途中
→ Running
行動ノードでは、1フレームで終わらない行動が多いので、Running が出てきます。
15. Selectorとは
Selectorは、
上から順番に候補を試すノード
です。
次のような木を考えます。
Selector
├ 攻撃する
├ 追跡する
└ 巡回する
Selectorは、上から順番に見ます。
攻撃する
↓ できなければ
追跡する
↓ できなければ
巡回する
16. Selectorのルール
Selectorのルールは次の通りです。
子ノードが Failure
→ 次の子ノードを見る
子ノードが Success
→ そこで止まる
子ノードが Running
→ そこで止まる
つまり、Selectorは、
最初にSuccessまたはRunningになった行動を選ぶ
ノードです。
17. Selectorと優先順位
Selectorでは、上にある行動ほど優先順位が高くなります。
Selector
├ 逃げる
├ 攻撃する
├ 追跡する
└ 巡回する
この場合の優先順位は次の通りです。
1. 逃げる
2. 攻撃する
3. 追跡する
4. 巡回する
上の行動ができるなら、下の行動は見ません。
18. Sequenceとは
Sequenceは、
上から順番に実行し、全部成功したら成功するノード
です。
次のような木を考えます。
Sequence
├ 攻撃範囲内?
└ 攻撃する
これは、
攻撃範囲内なら攻撃する
という意味です。
19. Sequenceのルール
Sequenceのルールは次の通りです。
子ノードが Success
→ 次の子ノードを見る
子ノードが Failure
→ そこで止まる
子ノードが Running
→ そこで止まる
全部の子ノードが Success になったとき、Sequence全体も Success になります。
20. Sequenceの例
Sequence
├ プレイヤーが見えている?
└ 追跡する
これは、
プレイヤーが見えているなら追跡する
という意味です。
動きは次のようになります。
プレイヤーが見えている?
→ Failure
→ 追跡しない
プレイヤーが見えている?
→ Success
追跡する
→ Running
→ 追跡中
21. SelectorとSequenceを組み合わせる
ビヘイビアツリーでは、SelectorとSequenceを組み合わせて使います。
Root
└ Selector
├ Sequence
│ ├ HPが少ない?
│ └ 逃げる
│
├ Sequence
│ ├ 攻撃範囲内?
│ └ 攻撃する
│
├ Sequence
│ ├ プレイヤーが見えている?
│ └ 追跡する
│
└ 巡回する
これは、次の意味です。
HPが少ないなら逃げる
そうでなければ、攻撃範囲内なら攻撃する
そうでなければ、プレイヤーが見えているなら追跡する
そうでなければ、巡回する
22. なぜSuccess / Failure / Runningが必要なのか
Selectorは、子ノードの結果を見て、次に進むかどうかを決めます。
Failure
→ この候補はできない。次を見る
Success
→ この候補で完了。ここで止まる
Running
→ この候補を実行中。ここで止まる
つまり、Success / Failure / Running は、
木を進めるための合図
です。
23. 優先順位と結果のつながり
次のツリーを考えます。
Root
└ Selector
├ Sequence
│ ├ 攻撃範囲内?
│ └ 攻撃する
│
├ Sequence
│ ├ プレイヤーが見えている?
│ └ 追跡する
│
└ 巡回する
優先順位は次の通りです。
1. 攻撃
2. 追跡
3. 巡回
攻撃できる場合
攻撃範囲内?
→ Success
攻撃する
→ Success
Selectorはここで止まります。
攻撃できたので、追跡や巡回は見ない
攻撃できないが、プレイヤーが見えている場合
攻撃範囲内?
→ Failure
攻撃はできないので、Selectorは次を見ます。
プレイヤーが見えている?
→ Success
追跡する
→ Running
追跡中なので、Selectorはここで止まります。
追跡中なので、巡回は見ない
プレイヤーが見えていない場合
攻撃範囲内?
→ Failure
プレイヤーが見えている?
→ Failure
巡回する
→ Running
攻撃も追跡もできないので、最後に巡回します。
24. Stateパターンとの違い
Stateパターンは、
今どの状態か
でAIを整理します。
ビヘイビアツリーは、
今できる行動の中で、どれを優先するか
でAIを整理します。
25. if文との関係
ビヘイビアツリーは、考え方としては if 文に似ています。
例えば、次の木は、
Root
└ Selector
├ 攻撃できるなら攻撃
├ 見えているなら追跡
└ 巡回
次の if 文に近いです。
もし攻撃できるなら、攻撃する
そうでなければ、見えているなら追跡する
そうでなければ、巡回する
ただし、ビヘイビアツリーでは、条件と行動をノードとして分け、木構造で整理します。
そのため、行動が増えたときに、優先順位を見やすくできます。
26. ここまでのまとめ
ビヘイビアツリーは、条件と行動を木構造で整理するAI
Selectorは、上から順番に候補を試す
Sequenceは、条件と行動をセットにしやすい
Success / Failure / Running は、木を進めるための合図
上にある行動ほど優先順位が高い
Failureなら次の候補を見る
SuccessまたはRunningなら、そこで止まる
27. ワーク1:Stateパターンとの違いを整理する
次の表を埋めなさい。
| 項目 | Stateパターン | ビヘイビアツリー |
|---|---|---|
| 中心の考え方 | ||
| 行動の選び方 | ||
| 図の形 | ||
| 向いているAI | ||
| 複雑になりやすい場面 |
28. ワーク2:優先順位を読み取る
次のビヘイビアツリーを見て、行動の優先順位を書きなさい。
Root
└ Selector
├ Sequence
│ ├ HPが少ない?
│ └ 逃げる
│
├ Sequence
│ ├ 攻撃範囲内?
│ └ 攻撃する
│
├ Sequence
│ ├ プレイヤーが見えている?
│ └ 追跡する
│
└ 巡回する
優先順位:
1.
2.
3.
4.
29. ワーク3:状況から行動を選ぶ
ワーク2の木を使って、次の状況で選ばれる行動を答えなさい。
状況A
HPは十分ある
攻撃範囲内である
プレイヤーは見えている
選ばれる行動:
理由:
状況B
HPは十分ある
攻撃範囲外である
プレイヤーは見えている
選ばれる行動:
理由:
状況C
HPが少ない
攻撃範囲内である
プレイヤーは見えている
選ばれる行動:
理由:
状況D
HPは十分ある
攻撃範囲外である
プレイヤーは見えていない
選ばれる行動:
理由:
30. ワーク4:条件ノードと行動ノードに分ける
次の行動候補を、条件ノードと行動ノードに分けなさい。
| 行動候補 | 条件ノード | 行動ノード |
|---|---|---|
| HPが少ないなら逃げる | ||
| 攻撃範囲内なら攻撃する | ||
| プレイヤーが見えているなら追跡する | ||
| 音が聞こえたら調べに行く | ||
| 何もなければ巡回する |
31. ワーク5:SelectorとSequenceを使って表す
次の行動候補を、Sequence の形で表しなさい。
攻撃できるなら攻撃する
Sequence
├
└
プレイヤーが見えているなら追跡する
Sequence
├
└
音が聞こえたら調べに行く
Sequence
├
└
32. ワーク6:if文を木構造に変換する
次の考え方を、ビヘイビアツリーの形に直しなさい。
もしHPが少ないなら逃げる
そうでなければ、攻撃範囲内なら攻撃する
そうでなければ、プレイヤーが見えているなら追跡する
そうでなければ、巡回する
ビヘイビアツリー:
Root
└ Selector
33. ワーク7:戻り値を考える
次のノードは、どのようなときに Success、Failure、Running を返すべきか考えなさい。
攻撃範囲内?
Success:
Failure:
Running:
追跡する
Success:
Failure:
Running:
攻撃する
Success:
Failure:
Running:
34. ワーク8:敵AIのビヘイビアツリーを設計する
次の仕様の敵AIを、ビヘイビアツリーで設計しなさい。
敵AIの仕様
・HPが少ないときは逃げる
・攻撃範囲内なら攻撃する
・プレイヤーが見えているなら追跡する
・最後に見失った場所が分かるなら調べに行く
・何もなければ巡回する
ビヘイビアツリー:
Root
└ Selector
35. ワーク9:Stateパターンだと複雑になる理由を説明する
ワーク8の敵AIをStateパターンだけで作ると、どのような問題が起こりそうか説明しなさい。
説明:
36. ワーク10:ビヘイビアツリーのメリットをまとめる
次の文章を完成させなさい。
ビヘイビアツリーでは、行動の候補を __________ の順番に並べる。
Selectorは、上から順番にノードを実行し、
__________ なら次の候補を見る。
__________ または __________ なら、
今回はその行動を選んだことになる。
そのため、ビヘイビアツリーは、
行動の優先順位や割り込みが多いAIを整理しやすい。
37. 最後に:C++で書くと最小構造はどうなるか
ここまでは、概念と木構造を中心に考えました。
最後に、C++で書く場合の最も簡単な形を確認します。
ビヘイビアツリーでは、ノードが結果を返します。
enum class BehaviorResult
{
Success,
Failure,
Running
};
ノードの親クラスは、次のような形になります。
class Enemy;
class BehaviorNode
{
public:
virtual ~BehaviorNode() = default;
virtual BehaviorResult Tick(Enemy& enemy) = 0;
};
Stateパターンの Update() と似ています。
違いは、Tick() が結果を返すことです。
StateのUpdate()
→ 処理をする
BehaviorNodeのTick()
→ 処理をして、Success / Failure / Running を返す
38. C++の条件ノード例
class CheckAttackRangeNode : public BehaviorNode
{
public:
BehaviorResult Tick(Enemy& enemy) override
{
if (enemy.CheckAttackRange())
{
return BehaviorResult::Success;
}
return BehaviorResult::Failure;
}
};
このノードは、
攻撃範囲内ならSuccess
攻撃範囲外ならFailure
を返します。
39. C++の行動ノード例
class AttackNode : public BehaviorNode
{
public:
BehaviorResult Tick(Enemy& enemy) override
{
enemy.Attack();
return BehaviorResult::Success;
}
};
このノードは、
攻撃を実行してSuccess
を返します。
40. C++のSelectorの考え方
BehaviorResult SelectorNode::Tick(Enemy& enemy)
{
for (int i = 0; i < childCount_; ++i)
{
BehaviorResult result = children_[i]->Tick(enemy);
if (result == BehaviorResult::Success)
{
return BehaviorResult::Success;
}
if (result == BehaviorResult::Running)
{
return BehaviorResult::Running;
}
// Failureなら次の子を見る
}
return BehaviorResult::Failure;
}
このコードは、次の意味です。
子ノードを上から順番に見る
Failureなら次を見る
Successならそこで止まる
Runningならそこで止まる
全部FailureならSelectorもFailure
41. C++のSequenceの考え方
BehaviorResult SequenceNode::Tick(Enemy& enemy)
{
for (int i = 0; i < childCount_; ++i)
{
BehaviorResult result = children_[i]->Tick(enemy);
if (result == BehaviorResult::Failure)
{
return BehaviorResult::Failure;
}
if (result == BehaviorResult::Running)
{
return BehaviorResult::Running;
}
// Successなら次の子を見る
}
return BehaviorResult::Success;
}
このコードは、次の意味です。
子ノードを上から順番に見る
Successなら次を見る
Failureならそこで止まる
Runningならそこで止まる
全部SuccessならSequenceもSuccess
42. 最終まとめ
ビヘイビアツリーは、C++の難しいコードから入るよりも、まず木構造で考えると理解しやすくなります。
重要なのは次の点です。
条件と行動をノードに分ける
上にある行動ほど優先順位が高い
Selectorは、成功する候補を上から探す
Sequenceは、条件と行動をセットにできる
Success / Failure / Running は、木を進めるための合図
C++では、各ノードをクラスにして、Tick()で結果を返す
Stateパターンを理解しているなら、ビヘイビアツリーは次のように考えるとよいです。
Stateパターン
→ 今どの状態かを管理する
ビヘイビアツリー
→ 今できる行動を優先順位で選ぶ
No Comments