始めに
ちょっと昔に敵を作っていた時にBehaviorDesignerというものを知りました。
正直、モンスターを作るのって滅茶苦茶大変だしやること多いしデバッグも大変だしで難儀な工程だと思います。
しかし、それでも作っていると楽しかったりします。
雑談が長くなっても仕方がないので、このBehaivor Designerを使って簡単な敵の挙動を作成します。
ちなみにですが、今回の内容は部活のLTでも発表しました。
目標
今回作成する敵は次のような挙動を目指します。
- プレイヤーを探す
- プレイヤーを見つけると追いかける
- プレイヤーを見つけていない場合、ステージを巡回する
Behavior Treeについて
Behavior DesignerはBehavior Treeモデルを構築してプログラム作成するが出来ます。Behavior Treeって?となる方は次のサイトを参考にして下さい。
個人的にBehavior Treeについて簡単に解説すると4つのノードAction・Conditional・Decorator・Compositeを使用してゲーム木を構築するって感じです。 また、ノードの実行結果として成功と失敗があり、これらが条件判定に使用されます。ゲーム木は深さ優先探索で実行されます。
Actionは歩く、攻撃するなどの行動や計算等を指します。 (図のLog、Log Value等)
Conditionalはif文に相当し、その時の状況や条件を判断します。 (図のBool Conditional等)
Decoratorはwhile文やfor文に相当します。今回使ってません。
CompositeはSelector、Sequence等があります。これについては後程解説致します。
これらの要素で作られたゲーム木を深さ優先探索で実行します。
Selector、Sequenceについて
これらノードは複数のノードを子として持つことが可能で、それを左から一つずつ実行していきます。
大きな違いが以下になります。
- Selectorは子のノードが一つでも成功すれば実行を終了し成功、すべて失敗したら失敗を返す。
- Sequenceは子のノードが一つでも失敗したら実行を終了して失敗、すべて成功したら成功を返す。
他のブログではSelectorがOR、SequenceがANDに相当すると解説されています。この方が理解しやすいと思います。
実際に作ってみる
プレイヤーを追跡する
初めにただプレイヤーを追いかけるだけのAIを作成します。Unityの機能、NavMeshを使用して簡単に作成できます。
まずはテキトーにステージを作ってプレイヤーと敵を作ります。
では、敵にBehaviorDesignerをつけてSetDestinationだけを付けます。
加えて実行が終了しても実行し続けるよう設定します。
最後にPlayerの座標を設定して完了です。
結果が次の動画になります。
[Unity]BehaviorDesignerを使用した追跡
プレイヤーを探す
次にプレイヤーを探す処理を加えて行きます。プレイヤーを探す処理は主に2つあります。
- プレイヤー方向ベクトルと敵の前方向ベクトルの内積が一定以上
- 敵位置からプレイヤー方向へRayを飛ばす
内積を計算することによってプレイヤーが敵の前方にいるかを判定します。
次にRayCastを使用することによって、障害物等に遮られることなくプレイヤーを直に見ることが出来るかを判定します。
この仕組みをBehavior Designerで作成します。
ノードの"Look For Player"、"Check Dot"、"Check Ray"はSequenceノードです。ここでは分かり易くするために名前を変更しています。
SequenceノードなのでCheck DotもしくはCheck Rayが失敗した場合はゲーム木の実行を終了します。
Check Dotでは以下の流れを行っています。
- direction = Player.transform.position - transform.positionでプレイヤー方向ベクトルの計算
- direction = direction.normalize()で正規化(ベクトルの大きさを1にする)
- Vector3.Dot(transform.forward, direction)で前方向ベクトルとプレイヤー方向ベクトルの内積を計算
- 内積が一定以上かを条件判断
今回は内積が dot > 0.6 としています。
Check Rayでは以下のようになっています。
- プレイヤー方向にRayを飛ばし、当たったオブジェクトを取得
- オブジェクトのタグが "Player" であるか条件判断
結果が次の動画になります。
ステージを巡回する
では最後に敵を巡回させていきます。ただ、Behaivor Designerの既存のノードで実装するのは大変なので自作ノードを作成しました。
作成では次のサイトを参考にしています。
作成したノードの実装が次のスクリプトになります。
using System.Collections; using System.Collections.Generic; using UnityEngine; using BehaviorDesigner.Runtime; using BehaviorDesigner.Runtime.Tasks; using UnityEngine.AI; public class Patrol : Action { [SerializeField] private List<Transform> m_patrolTransforms; [SerializeField] private float m_changeSqrDistance; [SerializeField] private BehaviorTree m_behaviorTree; [SerializeField] private NavMeshAgent m_navMeshAgent; [SerializeField] private float m_patrolSpeed; private int m_nowPositionIndex; private Vector3 m_nowPatrolPosition; // Tick毎に呼ばれる public override TaskStatus OnUpdate() { if (Vector3.SqrMagnitude(m_nowPatrolPosition - transform.position) < m_changeSqrDistance) { m_nowPositionIndex = (m_nowPositionIndex + 1) % m_patrolTransforms.Count; m_nowPatrolPosition = m_patrolTransforms[m_nowPositionIndex].position; } Debug.Log(m_nowPositionIndex); m_navMeshAgent.SetDestination(m_nowPatrolPosition); // 成功 return TaskStatus.Success; } public override void OnStart() { m_navMeshAgent.speed = m_patrolSpeed; SharedVariable variable = m_behaviorTree.GetVariable("Finish_Searching"); bool? finish_Searching = variable.GetValue() as bool?; if (finish_Searching != true) return; m_behaviorTree.SetVariableValue("Finish_Searching", false); Debug.Log("Check Patrol Position"); var navMesh = m_behaviorTree.GetComponent<NavMeshAgent>(); var position = transform.position; var minDistance = Vector3.SqrMagnitude(m_patrolTransforms[0].position - position); m_nowPositionIndex = 0; // 一番近い巡回地点を取得 for (int i = 1; i < m_patrolTransforms.Count; ++i) { var tempDistance = Vector3.SqrMagnitude(m_patrolTransforms[i].position - position); if (tempDistance < minDistance) { minDistance = tempDistance; m_nowPositionIndex = i; } } // 次の巡回地点を設定 m_nowPositionIndex = (m_nowPositionIndex + 1) % m_patrolTransforms.Count; m_nowPatrolPosition = m_patrolTransforms[m_nowPositionIndex].position; m_navMeshAgent.SetDestination(m_nowPatrolPosition); } public override void OnBehaviorComplete() { } }
あまり参考になるかは怪しいプログラムですが、簡単に解説します。
OnStartメソッドでは移動速度をゆっくりにして、直前までプレイヤーを追いかけていたかを判定しています。
もし直前まで追いかけていた場合は一番近くの巡回地点を取得して、その次の巡回地点を次の目的地に設定しています。
一番近くの巡回地点ではなく、その次の巡回地点を目的地に設定する理由はより自然に巡回ルートに戻るためです。
OnUpdateでは目的地まである程度近づいたら、次の巡回地点を目的地に設定しています。
自作ノードが完成したのでさっそくゲーム木に組み込みます。
一番上にSelectorノードを追加して、プレイヤーが見つかった場合は追跡、見つからなかった場合は巡回としています。
また、プレイヤーを追いかける場合と巡回するときの移動速度を変更するノードと追跡している状態を設定するノードを追加しています。
結果が次の動画です。
Behavior Designerを使用した探索・追跡・巡回
考察
簡単に探索、追跡、巡回を行うAIを作成しましたが、動画を見て頂けたら見失ったらすぐに諦めてしまうということがわかります。さらにAIらしい挙動を目指すには見失った場所まで追いかけるアルゴリズムを追加するべきでしょう。
最後に
Behaivor Designerを使っての感想ですが、 簡単に凄いAIがすぐに作れるわけではないと感じました。
しかし、ビジュアルプログラミングはとても分かり易く、実装していてとても楽しかったです。
良いAIを作るためにはモデルの知識もそうですが、探索アルゴリズムや実装についての学習に加えて敵を作る経験を重ねることが大事かもしれません。
今回の学習からBehavior Designerか自作Behavior Treeで敵を作る経験とそれを部活で共有することを目標にしていきたいです。
参考
今回は以下のサイトを参考にさせて頂きました。
Behavior DesignerでAIを組んでみる - Qiita
Behavior Designer ナビゲーションでプレイヤーを追いかけるNPC作りに挑戦!(セールじゃないよ) - Unity AssetStoreまとめ
【Unity】Behavior Designerの4種類のTaskを理解して基本的な使い方を学ぶ - LIGHT11
【Unity】Behavior Designerをサクッと使う!Behavior Treeを簡単ビジュアルスクリプティング - LIGHT11