AIとは何かを考えながらUE4で味方NPCを作成してみよう
最初に
今回記事にするUE4のバージョンは4.16です。
また、今回の記事で執筆する内容がベストプラクティスというわけではありません。あくまでも一例としてお考えいただくようお願い致します。内容に対して「もっと効率良いやり方がある」「この部分は違う」などありましたら、コメント頂けると幸いです。勉強させてもらいます。
記事内で実装方法を順序立てて説明していますが、途中でわからなくなったり、つまずいてしまった場合はお気軽に私の方へメンション飛ばしてください。また、出来るだけ一本道にするよう、関数などはあまり作っていません。実際の開発を行う際には関数等を作り見やすくしたほうがいいと思われます。
Twitter:https://twitter.com/Ev_rossam
mail:mio041100505@gmail.com
本記事は株式会社よむネコのプロジェクトの業務に携わる中で蓄積した知見となります。
公開に関してはよむネコの許諾を得ております。以下会社URLです。
本記事のゴール
今回は「UE4の操作方法にはある程度慣れてきたけど、AIって具体的にどうやって作っていったらいいんだろう」という方を対象としています。
執筆順番としては以下の通りになります。
- AIの根本的な考え方
- 知能の定義とは
- ゲームキャラのAIに必要な要素
- 意思決定アルゴリズム
- まとめ
- AI作成に使用する機能の紹介
- Behaviour Treeとは
- Task
- Composites
- Decorators
- Services
- ブラックボードとは
- ナビゲーションメッシュとは
- EQSとは
- AIPerceptionとは
- Behaviour Treeとは
- 味方AIの作り方を解説
- まずは棒立ち
- 簡易的なプレイヤー追従の実装
- データテーブルを使用してプレイヤーとの距離を調整
- 一定距離離れた場合にワープする
- 敵を発見する
- 敵への攻撃
- いい感じの位置に移動してから攻撃をする
- 今後の発展
- 最後に
- 参考資料
今回の記事にあるAIの説明部分の多くは「三宅陽一郎」さんの「人工知能の作り方」という本を参考にしております。
人工知能の作り方 ―「おもしろい」ゲームAIはいかにして動くのか
AIの根本的な考え方
知能の定義とは
- 自分の内部を外部から守ること。内部から湧きあがる欲求を環境において実現すること。
- 外部と内部の境界でその関係を調節するもの。
- 知能は身体と環境の関係を取り持つ。
- 一つ一つの形は違えど、共通する性質(「知能の原理」と呼ばれる)がある。
- 言葉を操り、記憶と推論の能力を有するもの。
- 環境に適応させながら身体を目的に向かって運動させるもの。
「自分の内部を外部から守ること。内部から湧きあがる欲求を環境において実現すること。」?
- 知能は環境と身体の境界に存在している。
- 境界から内部を守っている。
「外部と内部の境界でその関係を調節するもの。」?
- 知能が身体の状態を察知して、現在の状態を緩和するために様々な処置を行う。
「知能は身体と環境の関係を取り持つ。」?
- 環境が変われば知能も変化する。
- 身体の形状によって意識の形もある程度決定される。
「一つ一つの形は違えど、共通する性質(「知能の原理」と呼ばれる)がある。」?
- 植物がいい例。一つ一つの形は違っていても基本的な構造などは同じになっている。
ゲームキャラのAIに必要な要素
ゲームキャラクターのAIに必要な共通要素は以下の二つあります。
- リアリティ(知能シミュレーション)
- 演技
また、あった方がよりリアリティを出すことができる要素は以下の通りとなります。
- 反射
- 群れ制御
リアリティ
ゲーム世界で、あたかも人間のような知能を持つかのように行動するAIを自律型AIと呼びます。
敵が近くにいれば間合いを取りつつ攻撃したり、戦闘中にプレイヤーが遠ざかったら敵を置いてプレイヤーを追いかける、といったリアリティのある行動がAIには必要。
リアリティを持つためには、ユーザーが当たり前だと思っている行動をAIが積み重ねることが必要となります。
演技
時にはゲームキャラクターに感情表現の伴う行動をしてもらいたいことがあります。例えば敵を見つけて怒り状態になった時には威嚇行動を取ったり、プレイヤーが死亡した際には近くに駆け寄って悲しんだりという、まるで感情を持ったかのような行動を指します。これらの行動があることによって、ゲーム自体の盛り上がりが変化するので、余裕がある場合は声を入れるなどの実装をお勧めします。
反射
AIに人間のような反射行動を行わせることで、ゲーム内の環境に馴染ませることができます。例えば、
- 敵からの攻撃を反射的に避ける。
- HPが一定の割合以下になったら回復薬を使用する
などです。
群れ制御
NPCが2体以上存在している場合には、群れ制御のアルゴリズムとして、「レイノルズの群アルゴリズム」が有効です。このアルゴリズムを使用すると、NPC同士が衝突しなくなったり、オブジェクトを回避することができます。これは、お互いの位置関係から間接的に自分の位置を変化させていきます。
要はAIの行動として、「相手を認識している感じ」を醸し出すことがリアリティを引き出す際に重要となるのです。
意思決定アルゴリズム
人間は、世界から受けた情報を元に行動をとります。この情報を受けてから行動へ移す転換点は「意思決定」と呼ばれています。意思決定はAIにおいてもコアとなる機能です。意思決定で重要な点は二つあります。一つは意思決定方法のアルゴリズム、もう一つが意思決定をどの程度まで深く掘り下げて行うかという設定です。意思決定方法のアルゴリズムとして代表的なものは以下の通りとなります。
-
ルールベースAI:「もし~なら〇〇を行う」といったifがtrueの場合に実行。拡張性とモジュール性に優れる。ルール間の調整が面倒。全ジャンルに対応。(例:FF12)
-
ステートベースAI:現在の状態や行動を元にして設定されてある遷移条件を満たすことで、次の状態又は行動に移る。制御の切り替えが高速だが、拡張性があまりない。アクション向き。(例:アンチャーテッド 黄金刀と消えた船団)
-
ビヘイビアベースAI:「キャラクターの身体的行動のレベルでキャラクターの行動を考えること」。現在ゲームAIで最も広く使われている。ビヘイビアツリーはその一例。抽象的な行動の定義には不向き。アクション向き。(後述)
-
タスクベースAI:ある課題を解決するために、行うべき行動を一つ一つ順番に実行する。行動自体が分解されているので、行動の入れ替えや追加が楽。ただし学習コストが高い。全ジャンルに対応。(例:KILLZONE2)
-
ゴールベースAI:目標の達成を第一に考え、行動する。大きい目標を達成するために小さい目標を一つずつ達成するように行動する。行動の組み立てに柔軟性があるが反射系の行動は実行しづらい。戦略系やアクション向き。(例:クロムハウンズ)
-
ユーティリティベースAI:効用や見返りを数式や数値で表現し、それを元にして行動する。例えば身体状況として、空腹度>疲労度の場合、先に食事を行ってから睡眠をとるような感じ(睡眠より食事の方が身体にとって効用が大きい)。汎用性は高いが、数学的。全ジャンルに対応。(例:モンハン日記 ぽかぽかアイルー村)
-
シミュレーションベースAI:行動する前に様々な数学的手法で、どのように行動を行えば最も効率的かを探し当てる。当たりがついてから実行。滑らかな行動を取ることができるが、シミュレーションコストが高い。アクション向き。(応用例:囲碁AI)
これらのアルゴリズムには、それぞれ向き不向きが存在しています。
- ゲーム全体の状況を掴むには:「ステートベース」、「ユーティリティベース」。
- 細かな状況に対応する場合は:「ルールベース」、「ビヘイビアベース」。
- 明確なミッションを遂行させる場合は:「タスクベース」、「ゴールベース」。
- チーム協調に適しているのは:「ゴールベース」、「タスクベース」。
なので「戦闘」「待機」といった抽象的な行動は「ステートベース」等を使用して行動し、各状態の中では専用のビヘイビアツリーを用意するといったことが有効です。
AI作成に使用する機能の紹介
ゲームのキャラクターAIにアクションを起こさせるためには、ゲーム世界が持つ「物」や「事」を認識させる必要があります。UE4にはAIに対してゲーム世界を認識させる便利な機能がいくつもあります。そちらを簡単にご紹介します。ちなみにここで紹介する機能は後程、実際に味方NPCを実装する際にも使用していきます。
Character
UE4のエンジン自体でデフォルトとして存在しているクラスです。UE4でのゲームキャラクターAIにおいて、身体を担当します。具体的には、アニメーション等の見た目に関わる部分や衝突判定用のコリジョン部分を、この「Character」クラスで実装していくことになります。AIロジック自体は、後述する「AIController」クラスにお任せすることになります。
AIController
UE4のエンジン自体でデフォルトとして存在しているクラスです。AIのコアとなる機能で、AIに関する指示系統全般を担当します。AIControllerを「Character」又は、「Character」を継承しているクラスに持たせることで、キャラクターの脳(AI)として機能します。後述するビヘイビアツリーの実行開始命令も、このAIControllerから行います。
Behaviour Tree
現在のゲーム業界で最も多く使われている手法です。ビヘイビアとは「振る舞い、行動」という意味で、ビヘイビアツリーはその名の通り、ツリー構造となっています。
行動自体の実装を持つのは主に枝の末端にあるノード(葉の部分)です。それ以外の、末端にあるノードに繋がっている中間ノードは、基本的に末端ノードの実行順番を決める役割を担っています。
タスク(Task)
UE4ではツリーの末端、つまり葉部分であり、行動を司るノードのことを「タスク(Task)」と呼びます。タスクの下に他のノード類を接続することはできません。AIが実行する処理を細分化し、タスクとして定義することで、再利用性を高めることができます。また、タスクには後程説明する「デコレーター」をアタッチすることもできます。UE4の方で元から用意されているタスクも存在します。(公式リファレンス:Behavior Tree ノードのリファレンス:Task)
コンポジット(Composites)
タスクの実行順番を決める中間ノードです。種類としては、「Sequence」、「Selector」、「Simple Parallel」が存在しています。コンポジットには二種類の機能をアタッチすることができます(後述)。
Selector
Selectorノードに繋がれている子ノードを左から右へ、子ノードが成功するまで順番に実行していきます。子ノードのどれかが成功を返してきた時点で順番実行を中止し、Selectorノードは成功として終了します。逆に、Selectorノードの下に繋がれている子ノードが全て失敗した場合には、Selectorノードも失敗として終了します。
Sequence
Sequenceノードに繋がれている子ノードを左から右へ順番に実行していきます。Selectorノードとは違い、Sequenceノード下に繋がれている子ノードのいずれかが失敗を返してきた時点で順番実行を中止し、Sequenceノードは失敗として終了します。逆にSequenceノードの下に繋がれている子ノード全てが成功した場合には、Sequenceノードも成功として終了します。
尚、SelectorとSequenceの処理の流れについては以下のhistoriaさんが書いている記事中にあるものがとてもわかりやすいです。
[UE4] ビヘイビアツリー(BehaviorTree)の使い方 入門編
Simple Parallel
このノードでは、二つの子ノードを並列で実行することができます。二つの子ノードのうち、一つは必ずタスクノードとなりますが、もう一つはサブツリーとすることができます。繋がれている単一のタスクノードが成功か失敗か問わず、終了するまでの間、もう一つのサブツリーを実行し続けることが可能となります。これは、例えばプレイヤーの方に移動しながら攻撃を行う、等に役立てることができます。
サービス(Service)
コンポジットにアタッチ可能な二種類のうちの一つが、このサービスノードです。このノードは、アタッチされているコンポジットノード下の、子ノードが実行されている間、指定した秒毎にサービスノード内の処理を実行することができます(例えば0.5秒ごと等)。サービスノードは主に、ブラックボードのキーが持つ値を書き換える時などに使用されます。
なお、全ノードの一番上にある根は、「ルート(ROOT)」と呼ばれています。ルートはビヘイビアツリーに必ず一つだけ存在しており、親を持ちません。
UE4の「Behaviour Tree」は基本的に左から右へと処理が流れていき、最後の処理の時点で再びROOTへと戻ります。
デコレーター(Decorators)
プログラムでの「if文」のように、条件式として使用できるノードです。ノード内部で記述している処理が返すtrue/false判定によって、子ノードを実行するかどうかを判断します。このデコレーターノードはサービスと同じようにコンポジットノードやタスクノードに複数アタッチすることが可能です。
ROOT
なお、全ノードの一番上にある根は、「ルート(ROOT)」と呼ばれています。ルートはビヘイビアツリーに必ず一つだけ存在しており、親を持ちません。
UE4の「Behaviour Tree」は基本的に左から右へと処理が流れていき、最後の処理の時点で再びROOTへと戻ります。
ブラックボード
黒板のことを英語で「ブラックボード」と言いますが、UE4における「ブラックボード」は重要な役割を持っています。AI自身が収集したり分析した情報群を、このブラックボードという機能に記憶させておくことができるのです。また、記憶させた情報は関連するビヘイビアツリー全体で共有することができます。例えば、ビヘイビアツリー上に存在するタスクAIの攻撃ターゲットや移動時のゴール地点に使用することが可能です。
ナビゲーションメッシュ
AIが「歩ける場所」を示したデータを指します。これはパス検索という処理も同時に行い、AIが目的地とする場所までの最短距離を取ることができます。
EQS
Environment Query System(略してEQS)とは、「プレイヤーの近くが良い」「敵の向いてる方向とは逆方向が良い」といった複数の条件式を与えて、最もスコアが高いActorや位置情報(Vector)を求めてくれる機能です。この機能を使えば複雑な処理を作らなくても、自身から一番近い敵をターゲットにしたり、敵の背後に回って攻撃を行ったりが行えるようになります。
AIPerception
人間が視覚や聴覚を用いて世界の情報を取り入れるように、ゲーム中の世界とAIの知能を結びつけるためには、AIに視覚などのセンサー群を備え付ける必要があります。そのセンサー群の役割を果たすのが「AIPerception」です。視覚や聴覚、痛覚等が存在しています。特に、視覚では、「視野外にターゲットが出てしまい、見失った」「視線が何かのオブジェクトによって塞がれた」という「見失う」イベントが取得できたり、視認したターゲットに対して再度気付く距離の値や、知覚した情報を忘れる時間等が設定できます。
味方AIの作り方を解説
まずは棒立ちまで漕ぎ着ける
最初ですので、学習用の新規プロジェクトを立ち上げましょう。
「ThirdPerson」というテンプレートを使用して新規プロジェクトを作成してください。
出来上がったら、コンテンツ > ThirdPersonBP > Blueprints にて「Npc」というフォルダを作成します。
更に「Npc」フォルダ内に「NpcBase」というフォルダを作成しておきましょう。作成したら「NpcBase」フォルダ内へ移動しておいて下さい。
次に、NPCの基本クラスをC++で作成したいと思います。今回は特にC++で処理を書くことはありませんが、予め基本クラスをC++で作成しておくことで、今後作成したBPをnative化したい時に役立ちます。
何故C++への移行を考えた作りにするかはhistoriaさんが書いた記事が参考になります。
端的に言うと、デバッグもしやすくなり、C++でしか書けないような処理も実装できるようになるからです。また、C++で変数にメタ情報をつけつつ定義した場合、BPとは違い、エディタ上には明示的に表示しないようにもできます。プログラマー以外の人には勝手に触ってほしくない変数等を定義する時に便利です。
では、新規追加ボタンを押下し、「新規C++クラス...」を選択してみましょう。
「C++クラスを追加」というウィンドウが表示されたと思います。今回はNPCを作成するので、「Character」を選びましょう。
ファイル名は好きなように付けてください。今回の記事ではC++ファイルであることが分かるように接頭辞に「CPP」を付けて「CPP_NpcBase」としておきます。ただし、実際に開発を進める際には、まずNPCやEnemy、Playerのようなゲームキャラクター共通となる「CPP_CharacterBase」を作成することをお勧めしておきます。
「クラスを作成」を押下するとC++コードのコンパイル処理が走るので、茶をしばいて待ちましょう。
終了後にはエディタが開くので、問題なく作成されているか確認してください。
それでは早速、この基本クラスを継承したNPCのBPを作成しましょう。
まず、「BP」というフォルダを生成して、フォルダ下へ移動してください。
再度、新規追加ボタンを押下し、「ブループリント クラス」を選択します。
「親クラスを選択」ウィンドウが表示されました。このウィンドウではデフォルトで「よく使うクラス」が開かれていますが、今回使うのは「すべてのクラス」の方です。検索ボックスで先ほど作成したBaseクラスの名前の一部を入れてあげれば、すぐにカスタムクラスがヒットするはずなので、それを選択して下さい。
出来上がったブループリントに好きな名前をつけてください。今回の記事ではBPファイルであることが分かるように接頭辞に「BP」を付けて「BP_NpcBase」としておきます。作成したNpcのBPファイルをダブルクリックして開きましょう。
以下画像の通り、MeshとAnimationにデフォルトで入っている「ThirdPerson」のメッシュとマテリアルを複製し、Npcだとわかりやすい様にカラーを変えてセットします。Meshの位置や回転も元々のThirdPersonCharacterからデータを引っ張ってきて設定しておきましょう。また、CapsuleComponentのShapeにあるパラメータの「Capsule Half Height」を96、「Capsule Radius」を42に変更します。
ここまで出来たらコンテンツブラウザへ戻ってください。先ほど作成したBPフォルダがある階層に新しく「Controller」というフォルダを作成し、移動します。
作成したら、新規追加ボタンを押下し、「ブループリント クラス」を選択します。「親クラスを選択」ウィンドウが表示たら、全てのクラスで「AIController」を検索し、選択します。
この記事では作成後のBPに「BP_NpcBase_AIController」と名前を付けておきます。作成後に「BP_NpcBase」の方を開き、「クラスのデフォルト」ウィンドウで「Controller」と単語を入れます。Pawn>AIController Classにて、先ほど作成したAIControllerを設定します。
ここまで出来たら、コンパイル後に保存しておきましょう。この状態で「BP_NpcBase」をマップにD&Dして配置し、プレイしてみます。まだ棒立ちをしているだけですが、とりあえず存在させることが出来ました。
簡易的なプレイヤー追従の実装
それではNPCがプレイヤーを追従の実装を簡易的にやってみましょう。予めステージにはナビメッシュを引いておいてください。
「BP」「Controller」フォルダがある階層に「AI」というフォルダを作成します。
そのフォルダ下で新規追加ボタンから AI>ビヘイビアツリー と AI>ブラックボード の二つを追加します。今回の記事ではそれぞれ名前を「BT_NpcBase」(BTはビヘイビアツリーの略)、「BB_NpcBase」(BBはブラックボードの略)と付けておきます。
このBT_NpcBaseを基本としてNPCのAIを構築していきます。
まずは「BB_NpcBase」を開いてください。既に"SelfActor"というキーが存在していますね。ここで定義されているキーと値は「BT_NpcBase」全体で使用が可能となるので、とても便利です。以下の画像のように新しく、Player自体の参照を持つキーを作り、ビヘイビアツリーの中でプレイヤー追従用のタスクを作成してみましょう。
- 新しく「新規キー」を押下して、「PlayerActor」と名付けてください。
- KeyTypeは「Object」、BaseClassはActorに設定しておきます。
保存したら、「BT_NpcBase」を開いてください。ビヘイビアツリーウィンドウにて、ルートから以下の画像のように構築してみましょう。
- 「ROOT」から直接タスクを繋げることはできないため、中間ノードとして「Selector」を作成してください。
- UE4側でデフォルトとして用意されているタスク「Move To」を作成してください。この「Move To」タスクは、設定されているBlackboard Key(Actor又はVector)が持っている値に向かって移動する、というものです。
- 「Move To」タスクの"Blackboard Key"に、先ほど作成したPlayerActorを設定してください。こうすることで、NpcBaseの移動目的地がプレイヤー自身となります。
Acceptable Raduisは「目的地を円の中心と考えた時、半径何cm離れていた場合は移動をONにするか」という項目です。これについては後程調整を加えますので、今はこの数値のままにしておいてください。
あとは、AIController側のBPにて、「BT_NpcBase」と「BB_NpcBase」を使用するように記述するだけです。
「BP_NpcBase_AIController」を開いてください。イベントグラフに存在している「イベントBeginPlay」を使用して、ビヘイビアツリーとブラックボードの設定を行います。
- 「Use Blackboard」を用いて、使用するブラックボードを指定します。今回は「BB_NpcBase」。
- 「Set Value as Object」を用いて、「BB_NpcBase」で作成した「PlayerActor」キーに値を設定しましょう。このノードでは、ブラックボード上に登録されているキーの名前を"Name"型で指定してあげる必要があります。「Make Literal Name」でキーの名前を指定し、「Get Player Pawn」でプレイヤー自体の参照を値として渡してあげましょう。
- 最後に「Run Behaviour Tree」で作成した「BT_NpcBase」を実行します。
完了したら、コンパイルしてプレイを押下してみましょう。緑のNPCが延々と追従してくることを確認できます。
データテーブルを使用してプレイヤーとの距離を調整
NPC用に使用するデータ群をテーブルに格納して簡単に値を調整できるようにしてみましょう。こうしておくことによって、NPC自体に使用するデータを調整したい時にBPを触ることなく、数値等を編集することが可能になります。
今回はNPCがプレイヤーを追従する際に、近すぎず遠すぎずの距離を保ったまま移動するように調整してみましょう。
何故、距離を空けるのか
プレイヤーとNPCとの距離を設定する際に、ある程度間隔を空けた方がより自然に感じることができます。これは、人間が無意識に**「パーソナルスペース」**という空間を持っているからと考えられます。
**「パーソナルスペース」**とは社会心理学の用語で、誰もが持っている、「他人に近付かれると不快に感じる空間」のことです。例えば恋人ではない赤の他人が自分と接触するくらいに至近距離だった場合、基本的には不快に感じます。たとえ、友人だとしても一緒に歩くときは一定の距離を空けると考えられます。
そのためAIがプレイヤーを追従する際もある程度、距離を空けておいた方が無難にリアリティを出すことができると考えられます。特にVRゲームの場合はプレイヤーがAIとの距離に敏感になるので注意が必要です。
実装
それでは、「NpcBase」フォルダがある階層に、新しく「NpcTable」というフォルダを作成してください。
この「NpcTable」の直下に移動し、新規追加ボタンから ブループリント > 構造体 を選択し、作成します。今回の記事では、構造体(Struct)であることが分かるように接頭辞に「S」を付けて、「S_NpcData」と名前をつけています。
「S_NpcData」を開いてNPCとプレイヤーとの距離を設定する変数を宣言してみましょう。
- 「DistanceToPlayer」というfloat変数を用意します。
- デフォルト値は0のままでOKです。
- ツールヒントは記述しておくと、その変数を使用する際にユーザー側に、「この変数はどのように使用するか」を見せることができます。
次に、新規追加ボタンから その他 > データテーブル を選択し、作成を行います。「構造体を選択」ウィンドウでは先ほど作成した「S_NpcData」を選択し、OKを押下します。今回の記事では、データテーブルであることが分かるように接頭辞に「DT」を付けて、「DT_NpcData」としておきます。
「DT_NpcData」を開いて新しく行を追加してみましょう。
- 「行エディタ」ウィンドウの、"+"ボタンを押下して新規行を追加します。
- 「Row Name」に"NpcBase"と入力して、NpcBase専用のデータ群を定義します。後々Npcの種類が増えてきたときに、このデータテーブルに行を追加していけば、固有の数値を持たせることができます(例えば、ペット系味方NPCなら"DistanceToPlayer"を100にして近くに居させたり、あまり主人公を良く思っていないような味方NPCなら数値を500などにして離れて歩かせたり)。
- 今回は"DistanceToPlayer"を、300に設定しておきます。
ビヘイビアツリー上の「Playerから指定された距離の位置まで移動する」というタスクで、この「DistanceToPlayer」という数値を使用するためには、「BB_NpcBase」(ブラックボード)にキーを登録しておく必要があります。
- 「新規キー」ボタンを押下します。
- 名前を「DistanceToPlayer」に変更後、"Key Type"をFloatに設定します。
次に、ビヘイビアツリーで繋いでいる「Move To」タスクの部分を編集しましょう。現在使用している「Move To」タスクは簡易的な移動実装にはちょうど良いのですが、今回のように、外部で定義されている数値の分だけ目的地から距離を空けて追従する、というような事ができません。
なので、NPCの移動用に新規タスクを作成します。ビヘイビアツリーウィンドウの上部にある、「新規タスク」を押下して下さい。自動的に新規タスクのイベントグラフが開かれます。ここで、「BB_NpcBase」等がある階層に、新規作成したタスクが追加されています。今回の記事では、ビヘイビアツリーで使用するタスクであることが分かるように接頭辞に「BT_T」と付けて、「BT_T_MoveToTarget」としておきます。
それでは、作成したタスクを開いて移動処理を実装していきましょう。
- 関数のオーバーライドから「Receive Execute」を選択します。
- Owner Actorは「BP_NpcBase_AIController」、若しくはそれを継承するAIControllerとなる(はず)なので、「Cast To AIController」を繋ぎます。
- 出力されたAIControllerを「Move To Actor」に渡します。この時、"Goal"に先ほどキーとして定義した「PlayerActor」を渡すため、新しく「Blackboard Key Selector」の変数「Goal Actor Key」を作成します。この変数は予め「インスタンス編集可能」にチェックを入れておいて下さい。後程ビヘイビアツリー上から、「Goal Actor Key」に対応するブラックボードのキーを設定します。
- 「GoalActorKey」が参照する値(今回の場合、PlayerActor)を「Get Blackboard Value as Actor」で引き出し、「Move To Actor」の"Goal"にインプットします。
- 「GoalActorKey」と同様の手順で「DistanceToGoalKey」も作成し、「Get Blackboard Value as Float」で値を引き出します。値は「Move To Actor」の"Acceptance Radius"にインプットします。
- ビヘイビアツリーではタスクをカスタムする際に、タスク完了時には必ず「Finish Execute」を呼び出す必要があります。今回は「Move To Actor」から返却される値「EPathFollowingRequenstResult」が"Request Successful"であった場合、つまり目標地点に到達可能であるなら、Successを返す「Finish Execute」を、それ以外の場合は失敗を返す「Finish Execute」を呼び出します。この時、ビヘイビアツリー上の他の場所でも再利用ができるように、「EPathFollowingRequenstResult」の等価値を外部から設定できるように変数として宣言しておくと良いです。今回は「MoveToResult」と宣言し、宣言後は「インスタンス編集可能」にチェックを入れておきます。
タスクの編集が終わったら、ビヘイビアツリーウィンドウに移動しましょう。元々あった「Move To」タスクを削除し、新たに先ほど作成した「BT_T_MoveToTarget」タスクを接続します。ここで、「GoalActorKey」には「PlayerActor」キー、「DistanceToGoalKey」には「DistanceToPlayer」キーを設定しておきます。「MoveToResult」には"Request Successful"を設定します。
ここまで完了したら、「BP_NpcBase_AIController」のブループリントへと移動します。「イベントBeginPlay」の「RunBehaviourTree」の前に、作成したデータテーブル「DT_NpcData」から「DistanceToPlayer」の値を引き出して、ブラックボードで定義した「DistanceToPlayer」に設定しましょう。
- 作成したデータテーブルを取得します。「データテーブル行を取得」を押下してください。
- "Data Table"に「DT_NpcData」を、"RowName"には「NpcBase」を設定します。
- 「外側の列」というピンの上で右クリックし、「構造体ピンを分割」を押下してください。
- 表示された値を「Set Value as Float」で「DistanceToPlayer」キーの値として設定します。
コンパイル後にプレイしてみましょう。NPCが一定の距離を保ちつつ追従してくれるようになっています。
一定距離離れた場合に背後へワープする
プレイヤーとNPCが一定距離離れた場合に、プレイヤーの背後(又はカメラに映らない箇所)にワープする、という手法はどのようなゲームでもよく見かけます。BPでも簡単に実装が可能なので作成してみましょう。
まずはワープを実行するタスクを作成してみましょう。「新規タスク」を押下して下さい。今回の記事では新しいタスクに「BT_T_WarpTargetBack」と名付けておきます。
それではタスク内の処理を実装してみましょう。
- 「イベントReceive Execute」から出力される"Owner Actor"を「Cast To AIController」でAIControllerにキャストして下さい。
- 出力されたAIControllerから「Get Controlled Pawn」で、このAIController自体を所持しているActorを引き出せるようにします(出力されるPawnとは、対象となるコントローラーに関連付けられたActorの基本クラスを指します。つまり今回だと、BP_NpcBase_AIControllerに関連付けられているクラス、BP_NpcBaseが返却されます)。
- 「SetActorLocation」を呼び出します。ターゲットには先ほど「Get Controlled Pawn」で引き出したPawn値を設定します。新しい位置情報(New Locationのデータ)を渡すために、更に計算式を処理に加えていきます。
- 新しく「Blackboard Key Selector」の変数「GoalActorKey」と「WarpToDistanceKey」を作成します。この変数群は予め「インスタンス編集可能」にチェックを入れておいて下さい。「GoalActorKey」は目標地点とするアクターを、「WarpToDistanceKey」は目標地点からどれくらい離れた場所にワープするかの距離値を指します。「WarpToDistanceKey」のブラックボードへのキー登録は後ほど行います。
- 「GoalActorKey」から「Get BlackboardValue as Actor」で値を引き出した後、アクター自体の前方を表す単位ベクトルを「Get Actor ForwardVector」で取り出します。取り出したベクトル値は「WarpToDistanceKey」が持つFloat型の値と掛けてあげます。
- 「GoalActorKey」の値であるアクターから「GetActorLocation」でアクターが持つ位置情報を引き出し、計算後のベクトル値と足してあげましょう。こうすることで、アクターの位置から、先ほど計算したベクトル値分だけ離れた場所の位置情報を出力することができます。
- 先ほど計算した位置情報を「SetActorLocation」の"New Location"に渡してあげます。
- 「SetActorLocation」の"Sweep"にチェックを入れておきましょう。こうすることで、コリジョン衝突が検出され、例え"New Location"で渡した位置情報が壁の中だったとしても、壁の中にめり込むことは無くなるはずです。
- 最後に「Finish Execute」を成功として実行しましょう。
これでワープ自体を実行するタスクの実装は完了です。
次に、どの程度の距離があいたらワープタスクを実行するのか、という部分を実装していきます。この処理の実装には、デコレーターを使用していきましょう。「新規デコレーター」を押下して下さい。今回の記事では新しいデコレーターにデコレーターであることが分かるように接頭辞に「BT_D」を付けて「BT_D_CheckDistanceToTarget」と名付けます。
「BT_D_CheckDistanceToTarget」を開いて実装していきます。
- 関数タブで「Perform Condition Check」をオーバーライドします。
- 出力されている"Owner Actor"をAIControllerでキャストして、「Get Controlled Pawn」で関連付けられているアクター(BP_NpcBase)を出力します。
- 出力したアクターから「Get Distance To」という計算関数を呼び出します。これは入力として渡される二つのアクターの距離を計算して出力する処理となります。
- 今回はプレイヤーとNPC間の距離を測りたいため、「Get Distance To」のもう一つの入力部分にはPlayerActorを設定します。
- 次はワープをするかどうかの閾値を用意する必要があります。新しくブラックボードキーの変数として「WarpableDistanceKey」を宣言しておきましょう。ブラックボードへのキー登録は後ほど行います。
- 「>=」という条件式を用意し、一つ目の入力に「Get Distance To」の出力値を、二つ目の入力に「WarpableDistanceKey」の値を設定します。出力されるbool値をリターンノードに渡してください。
これでデコレータの実装は完了です。ここで、後回しにしていた「WarpToDistance」等の数値設定やキー登録を行いましょう。
まず、「S_NpcData」を開いて「WarpToDistance」と「WarpableDistance」をFloat型で登録しましょう。
そのあとに、「DT_NpcData」を開いてください。先ほど追加した項目が増えているので、値を設定していきます。「WarpToDistance」には-500を、「WarpableDistance」には1000と設定しておきます。「WarpToDistance」はプレイヤーの前方単位ベクトルに掛ける値なので、マイナスの数値にしておくことでプレイヤーの背後から-5mの位置を指すことができます。
そして、ブラックボードである「BB_NpcBase」にて、先ほどの二つの値を参照するキーを登録しておきます。
最後に、「BP_NpcBase_AIController」のBPにて、キーに値を登録する処理を追加しておきます。
これで事前準備が整いました。それではビヘイビアツリーの方で先ほど作成したデコレーターとタスクを使用してみましょう。
「BT_NpcBase」を開いてください。
- プレイヤーを追従しながら、距離があいた場合にワープをするかしないかの判定→trueならワープタスク実行、といったように並列で処理を行わせたいと思います。
- 元々あった「Selector」ノードに先ほど作成した「BT_T_WarpTargetBack」を繋ぎ、デコレーターとして「BT_D_CheckDistanceToTarget」をアタッチします。
- 「BT_T_WarpTargetBack」と「BT_D_CheckDistanceToTarget」のブラックボードキー変数をそれぞれ対応したものに設定します。
これで一通りの実装は終わりました。ただし、今のNPCの移動速度だとワープタイミングなどが確認しづらいので、「BP_NpcBase」のコンポーネント「CharacterMovement」の詳細ウィンドウで以下のように"speed"と検索し、"Max Walk Speed"の値を今の半分程度にしておきます。
それでは実行してみましょう。NPCがワープしてついてくるようになっているはずです!
敵を発見する
次は実際に敵をターゲティングするよう、実装していきたいと思います。
下準備として、Npcフォルダのある階層に新しくEnemyフォルダを作成してください。
その中に、Characterクラスを継承した「BP_EnemyBase」を作成しましょう(実際の開発では、EnemyBaseもCPPを作成して、そのCPPから継承したブループリントを作成していく流れとなります)。
また、Enemyであることがわかりやすいように、Npcの時と同様、専用のマテリアルとメッシュを用意しておきます。
あとは、Npcの時と同様に「BP_EnemyBase」にてメッシュや位置状態、コリジョンの大きさを設定してください。
作成できたらステージの適当な位置にD&Dで設置してください。
これで下準備は完了です。早速本題に取り掛かりましょう。
「BP_NpcBase_AIController」を開いてください。「コンポーネントを追加」を押下し、AIPerceptionを追加してください。
AIPerceptionの詳細ウィンドウで、視覚を持たせるように設定します。
あとは視界に何かが入ってきたときに発火されるイベント、「On Perception Updated」を追加しましょう。「+」を押下してください。
今回は視界に入ってきたオブジェクトが「BP_EnemyBase」にキャストして成功するか否かで敵味方の判別をしていますが、AIPerception自体の"Sense"設定にある"Detection by Affilliation"をC++を使用つつ任意に設定していくことで、キャストの必要がなくなります。その際には以下のブログが参考になるので確認してみてください(ただし英語です)。今回は全てにチェックマークを付けておいてください。
AI Perception in Unreal Engine 4 – How to Setup
ここから視覚に入ったオブジェクトに対する処理を追加していきたいところですが…、まずは処理のためのデータを用意しておく必要があります。今回は構造体「S_NpcBase」に敵を追従する際にどの程度間合いを空けるかを示すFloat型の「DistanceToEnemy」を追加しておきます。
そのあとに、「DT_NpcData」にて、値の設定を行いましょう。今回は350に設定しています。
次に、敵を見つけた際にビヘイビアツリー上で共有が行えるように、ブラックボード「BB_NpcBase」に敵を値とするキー「EnemyActor」を追加しておきます。この時、「AttackTargetActor」をObject(BaseはActor)にし、ついでにMove系タスクで間合い値を参照できるように「DistanceToEnemy」も追加しておきます。
ここまで出来たら、「BP_NpcBase_AIController」を開いてイベントとデータ格納の処理を追加していきましょう。
1.BeginPlayの処理の中に、新しく「DistanceToEnemy」を設定するようの処理を追加。
次に、先ほど追加したイベント「On Perception Updated」から処理を繋いでいきます。
- 先ほどブラックボードにて登録したキー「EnemyActor」に値が設定されているかどうかを「IsVaild」で調べます。設定されていない場合には、正常に視覚が動作しているかを確認する処理に進みます。
- 「On Perception Updated」からは発見したアクター群の配列が出力されているため、一つ一つ「BP_EnemyBase」であるかどうかを確認していく必要があります。ただし、一つでもEnemyを見つけることができたらその後の処理は行う必要がないため、途中でループ処理を中断できる「ForEachLoopWithBreak」を呼び出します。
- "Array Element"として出力されているActorがどのように発見されたか知ることができる「Get Actors Perception」という関数があります。この関数にActorを渡し、ターゲットとして自身のコンポーネントであるAIPerceptionを繋げます。
- 出力された"info"というデータを分解し、"Last Sensed Stimuli"という配列の0番目(つまり視覚)を取り出します。
- これを更に分解し、要素の最後に存在している"Successfully Sensed"(感知が成功しているか)をブランチに繋ぎ成功していれば、次は敵を登録する処理に移っていきます。
- 感知されたActorが「BP_EnemyBase」であれば、ブラックボードのキー「EnemyActor」に設定を行います。
- 設定後は、もう検索ループを掛ける必要がないので、ノードを先ほどの「ForEachLoopWithBreak」にある"Break"へと繋ぎます。
これでAIController側での処理は完了です。
最後にビヘイビアツリーの方を編集していきましょう。「BT_NpcBase」がある階層に新しく戦闘用のビヘイビアツリーを作成してください。今回の記事では「BT_AttackNpcBase」と名付けておきます。
それでは「BT_NpcBase」を開いてください。
- ルートと既存のSelectorの間に新しくSelectorノードを挿入してください。
- プレイヤー追従タスクを子ノードとして持つSelectorの方に、デコレーターである「Blackboard Based Condition」をアタッチしてください。このノードは指定したブラックボードのキーに値が設定されているか/いないかで処理を通すかどうか決めることができます。
- アタッチした「Blackboard Based Condition」の設定を「EnemyActor」が設定されていなければプレイヤー追従機能を実行するように編集していきます。"Key Query"を"Is Not Set"に変更し、確認するキーを「EnemyActor」に設定しておきましょう。
- 新しく足したSelectorの左側には新しく作成した戦闘用のビヘイビアツリーを実行させるようにタスク「Run Behaviour」を追加しましょう。"Behaviour Asset"には「BT_AttackNpcBase」をせて地してください。また、タスクには先ほどと同じように「Blackboard Based Condition」をアタッチしてください。今度は「EnemyActor」が設定されていれば処理を通すように設定しておきましょう。
次に、「BT_AttackNpcBase」を開いてください。ここに編集を行っていきます。
- ルート下にSelectorをつなげてください。
- Selector下には以前作成した、移動用のタスク「BT_T_MoveToTarget」を繋げましょう。
- 「BT_T_MoveToTarget」のブラックボードキーを対応するものに変更してください。
ここまで出来たら完了です。早速プレイしてみましょう!Npcが敵を見つけていない間はプレイヤーを追従してきますが、敵を見つけた瞬間に敵の方へ走っていくようになりました。
敵への攻撃
敵をターゲティングするところまで出来上がりました。次は実際に攻撃を行わせてみましょう。ここはさっくり実装していきたいと思います。
まず、攻撃を行わせるには攻撃処理を持つ「Attack」という関数を用意しておきたいですよね。ではAttackの内部処理自体はどこに書くべきか。私としてはインターフェースブループリントを作成し、その中に「Attack」という関数を用意して、AIControllerに継承し、内部処理をAIControllerで用意した方が良いのではと考えています。
それでは、作成してみましょう。
BPフォルダ下に 新規追加 > ブループリント > ブループリントインターフェース を選択します。名前は、インターフェースであることが分かるように接頭辞に「IBP」と付け「IBP_ReactionableCharacter」としておきます。
新しく関数を追加し、「Attack」と名前付けを行います。
次に、「BP_NpcBase_AIController」に作成したインターフェースを追加します。
画面上部のメニューにある、「クラス設定」を押下し、詳細ウィンドウ > インターフェース を確認してください。そこに「追加」ボタンがあるので、検索窓に「IBP」等と打ち込んで「IBP_ReactionableCharacter」を選択しましょう。
そうすると、左の「マイブループリント」ウィンドウのインターフェースに、「Attack」という関数が追加されます。
ここで攻撃時の具体的な処理を実装していくこととなります。今回は、Npcの現在位置から真っすぐに飛んでいく弾を実装してみましょう。
まず、EnemyやNpcフォルダがある階層に新しく「Weapon」というフォルダを作成します。
更にその中に、「Shot」というフォルダを用意しましょう。
その中で、新規作成からActorを継承したブループリント「Ball」を追加しておきます。
Ballを開いてみましょう。このままの状態ではスタティックメッシュが追加されていないため、可視化することができません。なので コンポーネントを追加 > 球 を選び、拡大・縮小の値を0.5へ設定しておきます。
さて、このクラスをただ単にSpawnさせるだけだと、全く動かず、その場に留まり続ける謎の玉になってしまいます。この玉を前方へと真っすぐ飛ばすために実装してみましょう。
- イベントTickからノードを引っ張り、「SetActorLocation」へと繋ぐ。
- 玉自身の位置を取得する「GetActorLocation」と玉の前方ベクトルを取得する「GetActorForwardVector」を呼び出す。
- 「GetActorForwardVector」から出力される値を2倍にして、「GetActorLocation」の出力値と足す。
- 足した値を「SetActorLocation」の"New Location"に入力する。
これで、毎フレーム前方に2cmずつ動く玉ができました。
さて、このActorを、「BP_NpcBase_AIController」のAttackでスポーンさせるように実装しましょう。
-
"Class"にはBallクラスを設定する。また、"Spawn Transform"ピンを右クリックし「構造体ピンを分割」を選択して分割する。
-
そのままBallを作成すると、Npcの身体の中でBallが作成され、Ball自体が持つコリジョンによって、Npcが後ろへ押されてしまう。なので、Npcの身体よりも少し前でBallが作成されるように位置を調整する必要がある。
-
NpcBase自体を出力する「Get Controlled Pawn」を呼び出す。
-
出力されたNpcBaseをターゲットとした、「Get Actor Forward Vector」と「Get Actor Transform」を呼び出す。
-
「Get Actor Transform」の値を分割して、"Rotation"と"Scale"の値はそのまま「SpawnActor」の対応するピンへ渡す。
-
「Get Actor Forward Vector」の値を50倍にして、「Get Actor Transform」の"Location"と足し合わせる。
-
足した値を「SpawnActor」のLocationへと渡す。
-
最後に、「SpawnActor」からリターンノードへと繋げる。
これでAIController側の攻撃準備は整いました。あとはビヘイビアツリー上で攻撃用のタスクを作成し、そこから今実装したAttack関数を呼び出すだけです。
それでは新規タスクを作成しましょう。名前は「BT_T_Attack」とします。
「Receive Execute」をオーバーライドしてノードを繋げていきます。
- 「Receive Execute」から出力される"Owner Actor"を「BPI_ReactionableCharacter」にキャストする。
- 出力された値から「Attack(インターフェース呼び出し)」を呼び出す。
- 最後にSuccessにチェックを入れた「Finish Execute」を繋げる。
タスクはこれで完了です。
ちなみにインターフェースで呼び出すことで、依存性を低く保つことできます。例えば、今回は「BP_NpcBase_AIController」を使用していますが、これがEnemyのAIControllerとなった場合でも「BPI_ReactionableCharacter」さえ追加していれば、このタスクは再利用することができます。
では、最後にビヘイビアツリーにタスクを追加しましょう。
「BT_AttackNpcBase」を開いてください。
- 元々「BT_T_MoveToTarget」に繋いでいたSelectorを削除し、新たにSequenceを繋ぐ。
- その下に更にSequenceを繋ぐ。
- 二番目のSequenceに「BT_T_Attack」と「Wait」タスクを追加。
これでプレイをプレイをすると…見事にNpcが敵に近寄って玉を撃ち、5秒後に再度玉を撃つ、という挙動になったと思います!
いい感じの位置に移動してから攻撃をする
ここまでで攻撃の実装が完了しました。
最後に、戦闘時のNPCの立ち位置を決めるEQSを作成していきたいと思います。これを追加することで、NPCがプレイヤーと敵との間に移動したり、敵の真後ろに移動してプレイヤーからはNPCの様子を見ることができない、という現象を防ぐことができます。
今回の項目では、NPCが移動する立ち位置を以下の図の緑範囲内として設定します。
(図はPlayerとEnemyが向かい合っていると仮定)
つまり、NPCの立ち位置を以下の条件が当てはまる場所に設定します。
- PlayerとEnemyの間は不可。
- Playerから見て、Enemyの真後ろは不可。
- Enemyから指定した距離より近い場所は不可。
- Enemyから指定した距離より遠い場所は不可。
それでは早速作成していきましょう。
まず、EQS自体を作成してみます。NpcBase > AIフォルダへと移動し、その中で「新規追加」 > AI > 環境クエリ を選択します。名前は、EQSであることがわかるように、接頭辞に「EQS」を付けて「EQS_SearchAttackLocation」とします。
作成したEQSを開いてください。すると、クエリグラフが表示されたと思います。このルートノードから引っ張ると、"Generators"リストが表示されます。これは、どのような形状の範囲にスコア判定用のアイテムを作成していくのかを選択することができます。今回は"Donut"を選びましょう。
Donutは設定した中心位置から指定した値の分距離を空けて、円状にEQS用のアイテムを作成していくGeneratorとなります。名前の通り、ドーナツのように真ん中が空いて、アイテムが円状に作成されます。
選択後、「Donut: Querier 周りにアイテムを作成」と出てくるので、それを選択して詳細プロパティを確認してください。
少しだけ、このDonutに関する解説を加えておきます。
- "Inner Radius"は中心位置から、円の一番内側までの距離を指定できます。
- "Outher Radius"は中心位置から、円の一番外側までの距離を指定できます。
- "Number Of Rings"は"Inner Radius"から"Outher Radius"までの間に、どれくらいの数、アイテムの輪を作成するか指定できます。
- "Points Per Ring"は、一つのアイテムの輪を、何個のアイテムで構成するか、という数を指定できます。
- "Arc Direction"は、二つのコンテキストを指定して"Line From"から"Line To"への指定した角度の分だけアイテムを生成します。
- "Center"では中心位置とするコンテキストを設定できます。
さて、今回は現在ターゲットとしているEnemyを中心のコンテキストとして設定しておきたいです。ただし、現在の状態では、Centerのメニューに"EnvQueryContext_Item"と"EnvQueryContext_Querier"しか表示されていません。なので、Enemyを指すコンテキストを自身で作成する必要があります。
AIフォルダへ戻って、新しいブループリントを作成してください。親クラスは"EnvQueryContext_BlueprintBase"です。
作成したら、EnvQueryContextだと分かるように接頭辞に「EQC」と付けて、「EQC_Enemy」とします。ついでに後程EQSにてプレイヤーを指すコンテキストも必要となるので「EQC_Player」も作成しておきましょう。
では、「EQC_Enemy」を開いてください。
関数オーバーライドに「Provide Single Actor」とあります。これはコンテキストが指すアクターを指定することができる関数です。この関数をオーバーライドしましょう。
実装としては以下のように作成します。
- 「Provide Single Actor」から出力される"Querier Actor"には今回の場合だとNpcBaseが入っているため、ここから「Get AIController」を繋ぐ。
- 出力されたAIControllerから"Blackboard"を取得し、EnemyActorを"Name"として指定して、「Get Value as Object」で出力する。
- 出力した値をActorにキャストし、結果をリターンノードの"Resulting Actor"へと繋ぐ。
「EQC_Player」も同じ要領で作成(もしくはコピペ)し、指定する"Name"だけPlayerActorに変えておいてください。
作成が終わったら、Donutの方に戻りましょう。EQCを作成したことで、"Center"に"EQC_Enemy"と"EQC_Player"が表示されているはずです。ここを、"EQC_Enemy"と設定しておきましょう。
また、他の"Inner Radius"や"Outher Radius"の値をビヘイビアツリー上から設定・変更できるようにしておきたいと思います。
- "Inner Radius"、"Outher Radius"、"Number Of Rings"、"Points Per Ring"、"Arc Direction"の"Data Binding"を"Query Params"に設定しておく。こうすることでビヘイビアツリー上から変更が可能となる。
- "Arc Direction"の"Line From"を"EQC_Player"に、"Line To"を"EQC_Enemy"に設定する。
次に、生成されたアイテムのスコアを決める、テストを追加していきます。テストの結果を用いて、上位のスコアを持つアイテムを選定し、そのアイテムの位置情報を使ってNPCが移動するという流れになります。
では、テストを追加していきましょう。まず、Donutの上で右クリックを押下し、「テストを追加」 > Dot を選択してください。このテストは指定したコンテキストの向いている方向と、指定したコンテキスト又はFromコンテキストからToコンテキストまでのベクトルの向きが一致しているかそうでないかでスコアを付けていくことができます。今回は以下のように設定しました。
- "Test Purpose"を"Score Only"に設定。
- "Line A"の"Mode"を"Rotation"に、"Rotation"を"EQC_Player"に設定。これでまずプレイヤーの向いている方向が指定される。
- "Line B"の"Mode"を"Two Points"に、"Line From"を"EQC_Player"、"Line To"を"EnvQueryContext_Item"に設定。これでプレイヤーの位置からそれぞれのアイテムを見た時の向き情報が設定される。
- "Test Mode"は"Dot 2D"に設定。
- "Score"下の"Scoring Equation"を"Inverse Linear"に設定。これで、プレイヤーの真っすぐ前にあるアイテムはスコアが低くなる。
基本のテストはこれだけでも問題ありませんが、念のため、敵の位置まで到達ができないアイテムは破棄しておきたいところです。新しいテスト「Pathfinding」を追加しましょう。
このテストに関しては、"Pathfinding"下の"Context"を"EQC_Enemy"へと変更するだけです。
これで、以下のようなEQSが作成できたと思います。
次に、EQSで選定した値を格納するために、ブラックボード「BB_NpcBase」上でVector型のキー「TargetLocation」を作成しておいてください。
では、作成したEQSをビヘイビアツリー「BT_AttackNpcBase」に追加して、パラメーターを設定してみましょう。
- 一番上のSequenceに、サービスを追加 > Run EQS を選択してアタッチする。
- 詳細ウィンドウにて、"EQS" > "EQSRequest"下の"Query Template"を先ほど作成したEQS「EQS_SearchAttackLocation」に設定する。
- "Query Config"内にあるエレメントを順に"Inner Radius"=200、"Outher Radius"=300、"Number Of Rings"=3、"Points Per Ring"=15、"Arc Direction"=140に設定する。
- "Run Mode"を"Single Random Item from Best 5%"に設定する。
- 元々ビヘイビアツリーに繋がれていた「BT_T_MoveToTarget」を削除し、新たに「Move To」タスクを作成し、繋げる。
- 「Move To」タスクの"Acceptable Radius"を50に、"BlackboardKey"を"TargetLocation"に設定する。
- 「Move To」タスクにエンジン側で用意されているデコレーター「Blackboard Based Condition」をアタッチする。
- "Notify Observer"を"On Value Change"に設定し、"オブザーバーを中止"に"Self"を設定する。これによって、サービスの「RunEQS」でTargetLocationの値が更新された際に、一旦古い位置情報を目的地としていた移動タスクを中止し、再度新しい位置情報を目的地として移動するようにできる。また、TargetLocationが正常に設定されているかどうかも判断することが可能。
- 「Move To」の横に「Rotate to face BB entry」タスクを作成し、一番上のSequenceから繋ぐ。
- 「Rotate to face BB entry」の"Blackboard Key"を"EnemyActor"に設定する。
ここまで作成できたら、プレイして挙動を確認してみてください。いい感じの位置にNPCが移動しているように見えると思います。
今後の発展
ここまでの基本的なNPCの行動を実装したことで、プレイヤーにとってある程度心強いサポートキャラクターが生まれたと思います。ただし、現状のNPCでは、攻撃をとっさに避けるという反射部分や、敵AIという自分以外の知能を敏感に感じる能力が実装されていません。そのため、自分が危機的状況に陥っても自分自身の生存確率を高めようという動きを行うことはありません。
上記のような挙動を実装し、戦略的な思考を持つ知能を実装することで、より、プレイヤーにゲーム内で「自分以外の知能を感じさせる」ことが可能となります。
また、プレイヤーにゲーム内で「自分以外の知能を感じさせる」ためには、敵AIを作成する場合、プレイヤー自体に危機的状況をもたらすシーンが効果的となります。例えばプレイヤー自体が残りHPわずかの状態で敵と対峙している時、プレイヤーは相手の攻撃をなんとか見切ろうと敵AIを敏感に察知するようになります。この瞬間にプレイヤーにとって敵AIは活き活きとしたキャラクターに見えるようになるのです。
味方AIで「自分以外の知能を感じさせる」ためには、プレイヤーを危機的状況にする以外で、プレイヤーに味方キャラクターを「可愛い」「かっこいい」のように思わせることが重要になっていきます。プレイヤーにそれらの感情を持たせることで、ゲーム内で「自分以外の知性」、つまり味方AIを本当の知性があるかのように感じてもらいやすくなります。
これらを踏まえた上で、三宅さんは以下のような方程式を提示されています。
プレイヤーが敵に感じる知性 = (キャラクターの知能の高さ) * (「自分以外の知能を感じさせる」状況や感情)
また、今回は紹介していなかったNPCが2体以上存在している場合に有効な手法がいくつか存在します。
そのうちの一つが、「メッセージング」です。
メッセージング
AI同士を連携させるために、よく使用される手法です。例えば、AというNPCが敵を見つけた際に、一番近いNPCに対して「待機」ステートから「攻撃」ステートへの遷移をメッセージで送る、といったものです。または、ビヘイビアツリーを使用している場合に、Aが敵と近距離戦を始めた時、Bには遠距離から戦闘のサポートを行わせたいとします。AとBは共通のビヘイビアツリーを持っており、そこには左に「近距離戦」、右に「遠距離戦」のノードがあると仮定します。ここで、「近距離戦」というノードに「ゲート」というデコレーターを設置しておき、Aは自身が戦闘に入った際に他のNPCが「ゲート」を通過できないような処理を行っておけば、Bはゲートを通過できず、「遠距離戦」下のノードを実行していくようになります。
最後に
本記事では、攻撃実装部分をとても簡易的に行っています。今後作りこんでいく場合にはアニメーションブループリント上でアニメーションの任意のタイミングでActor等をSpawnさせたい、というようなことも発生するでしょう。そのような実装を行うに当たって、できるだけ依存性を低くし、再利用可能な状態にすることを目的とした実装手法(ただし自己流)を記事としてまとめられたらなぁと思います。
また、出来るだけnative化をしつつAIを実装していく、というようなことも現在目指しているので、目途がたったらそちらのほうも記事として執筆できたらいいなぁ(願望)と思っています。
それでは、ここまで読んでいただき有難うございました。
参考資料
今回入れられなかった技術メモ
【ProjectPointToNavigation】
— PavilionDV7 (@Dv7Pavilion) 2017年12月6日
引数で渡したPointをNavMeshに投影し投影後の座標を返すナイスなノード。EQSのGeneratorにある「Pathing Grid」と同じことが出来る。
NavMeshが敷かれている場所に位置を補正する感じ。
実際に2枚めの画像右上の球体は本来階段に埋まっているが階段に沿っている。#ue4 pic.twitter.com/tKWEsNblUq