そうだ、ゲームを作ろう

現状や学んだことなど記録するブログ。間違ってたらごめんね

AIで色々悩んでみた:BehaviorTree編

ここのところAIを中心に学習していたので復習と備忘録を兼ねて記事にしてみる。

まずはUE4のAIシステム全体の僕的な見取り図をどーん。

f:id:wvigler:20191114111324p:plain

いい加減に書いた図なので細かいことは気にしないでください。

一見複雑にも見えますが、まずは一番上の方を見ていきましょう。PlayerPawnBPをPlayerControllerBPが操っています。更にそれを操作しているのはコントローラーから入力であり、その向こうには人間の脳があります。これがプレイヤーキャラクターの動く仕組みです。簡単ですね。

ControllerまではAIもそんなに変わりません。EnemyPawnBPをAIControllerBPが操っています。しかしその後にコントローラーからの入力はなく、人間の脳をシミュレートするためのシステムがあります。そのためAIControllerBPはPlayerControllerBPといくつか違いがあります。

 

・BehaviorTree

まずはRun_Behavior_TreeノードによるBehaviorTreeとの関連付けが追加されています。

f:id:wvigler:20191114115633p:plain

これはAIの挙動を一種の樹形図にして示したもので人間の思考回路にあたります。詳しくは

historia.co.jp

ここなどを参考にしてください。

BehaviorTreeを組む時に気をつけるべきは必ず「優先度の高い行動を左側に設置する」ことです。後でDecoratorなどにより、このBehaviorTreeの流れをある程度無視するような処理が出てきますが、それらも優先度の高い行動が左側に設置されていることが大前提になっています。
条件式を弄って無理やり優先度の高い行動を右側に持ってくる、ということもできなくはないですが、フローは分かりにくくなるしちょっと行動を追加するだけで不安定になっていいことがありません。

 

・Composites

さてBehaviorTreeは基本的にRootノードから始まり、SequenceとSelectorというノードによってフローを制御しています。これらはCompositeと呼ばれます。Sequenceは左側から順番に子ノードを実行し、Successが返された場合次の子ノードへ移行し、Failureが返された場合、以降の子ノードの処理をキャンセルしてFailureを親ノードに返します。

というと複雑ですが、ざっくり言えば「一連の処理」を司るノードです。人間は机の上のものを取る際、「腕を伸ばし」「手を広げて」「物を持つ」という「一連の処理」をしています。Sequenceはそれらの動きをまとめて「机の上のものを取る」という処理を作っています。

 

次にSelectorですがこれは名前の通り子ノードのうちいずれかを選択するノードです。処理としては左側から順番に子ノードを実行し、Failureが返された場合次の子ノードへ移行して、Successが返された場合には、以降の子ノードの処理をキャンセルしてSuccessを親ノードに返します。

こちらもざっくりと言えば「意思選択」を司るノードといえるでしょう。残っている選択肢の中で常に優先度の高い行動を成功するまで続ける、それがこのノードの処理です。

 

さて、あくまで今紹介したこの基本機能において、という前提ですが実はSequenceとSelectorはSuccessとFailureが入れ替わっただけで全く同じ動作をしています。しかし、じゃあ入れ替えて組んでやろう、などという妙な考えは起こさないほうがいいです。Decoratorなど他のノードとの兼ね合いにおいて問題が発生します。

しかし人間にとっては全く思考を挟まない「一連の処理」と思考によって決定している(と思っている?)「意思決定」がプログラム上では殆ど同じ処理で表現されている、というのは大変興味深いですね。更に踏み込んで言えばBooleanにおいてはSequenceは「AND」、Failureは「OR」を表現しているものです。

 

・Task

それはともかくとして、これらCompositeは所詮プロセスに過ぎません。いくら繋げてもPawnやControllerは何も処理をしません。なので実際に行う処理を書くノードが必要になります。それがTaskです。

f:id:wvigler:20191114135142p:plain

TaskはObjectの一種です。BehaviorTreeでは一番下位の”葉”ノードとして振る舞います。ControllerやPawnへのアクセスを持っており、それを通じて行う処理そのものと、処理が終わった際にSuccessとFailure、どちらを返すのかを記述します。

f:id:wvigler:20191114131556p:plain

上の画像ではExecutionPinを上に接続すればSuccessを返し、下に接続すればFailureを返します。もちろん条件式で分岐させることも自由です。EventもTick処理やAbortされた時の処理など複数用意されています。

 

・DecoratorとService

DecoratorとServiceはいずれもCompositeにくっつける形で実装するノードです。これらのノードはCompositeやTaskと違いSuccessやFailureを返すことはありません。見た目が大分違うので分かりにくいですが、DecoratorもServiceも基本的にはTaskの亜種という認識の方が分かりやすいと思います。

ちなみに一番上の見取り図ではTaskとPawn、Controllerは繋がっていますが、DecoratorとServiceは繋がっていません。これは視認性が落ちるために省略したもので、実際にはDecoratorもServiceもPawn、Controllerにアクセス可能です。

Decoratorは基本的には条件式を伴ったTaskと同じで、条件分岐を司ります。ここまではTaskと一緒ですが、このノードは基本的にObserverAbortsというEnumパラメーターを持ちます。要素が上位ノードによって変化し、Sequenceの場合はNoneとSelf、Selectorの場合には更にLowerPriorityとBothが追加されます。

f:id:wvigler:20191114145709p:plain

まずNoneの場合は実行タイミングに処理される普通の条件式として機能します。条件を満たしていれば下位ノードが実行され、満たしていなければ実行されません。他の場合についてですが、選択するとそれぞれSelfは自分の下位ノード、LowerPriorityは自分の下位ノードよりさらに優先順位の低いノード、Bothの場合はその両方が青く光ります。

f:id:wvigler:20191114150426p:plain

この青く光った領域においてDecoratorは条件式の”監視”を行います。監視領域において監視している条件に変化が起こった場合、即座にDecoratorは処理を”奪って”条件式を判定しなおします。もし条件式を満たした場合にはやはり下位ノードが実行されます。

BehaviorTreeの場合、Taskのみだと条件式はSelectorが実行されたタイミングでしか判定できません。それを解決するためにこのような機能が備わっています。これにより優先順位の低い行動を行っている際にある条件が加わった場合、いつでも優先順位の高い行動に移行する、といった動作が可能になります。

 

Serviceは自らの下位ノード実行中に一定時間で定期的に呼び出されるノードです。ブラックボードやPawn、Controllerの各種変数の書き換えが主な役目です。Decoratorと比較すると簡単だと思いますが、独自の要素としてIntervalとRandomDeviationがあります。その名の通りIntervalは呼び出される定期の時間、RandomDeviationはランダムにそこに加算される数値を示しています。

 

はじめはAI関連全てを一括で書く予定だったのですが、長くなりすぎ…orz

流石にこのペースでは冗長すぎるので、何回かに分割して書くことにしました。

間違いなどご指摘等ありましたらぜひお願いいたします。