Unity - boltでの敵AIセオリーまとめ 〜 bolt kit Third Personからわかること

あけおめです。
かなり久しぶりに記事を書きます、asiramでございます。
ちょっと思うこともあり、これからちょいちょい投稿できればなーと思っております。

さて本題ですが、2020年に無償化・Unityファミリー入りしたビジュアルスクリプティングツール「bolt」について、敵AIセオリーがサンプルプロジェクトからある程度読み取れたので、まとめてみようかなと思います。

# その前にちょっとだけboltについて
**boltの何がいいの？**

* 視覚的に挙動を組むことができる
* 実行中に処理の流れをGUI上で確認できて、エラー個所もわかりやすくデバッグしやすい
* プログラムを書かないので、プログラミング知識が乏しくても組みやすい（かもしれない

**勉強するには？**
unity公式のサンプルがとてもわかりやすいです。
定番のボールを転がすやつです。インストールから一連の機能を触れるのでオススメですよ。
https://learn.unity.com/project/bolt-roll-a-ball-tutorial

# 本題。bolt kit Third Personから読み取れる敵AIセオリー
今回はフリーアセット「[Bolt Kit: Third Person for Unity](https://assetstore.unity.com/packages/templates/tutorials/bolt-kit-third-person-for-unity-167662)」から読み取れる敵AIのセオリーになりそうなフローグラフを見ていきましょう。
ちなみに、このアセットはUnity Technologies公式から提供されており、他にもFPS版や2Dゲーム版もあるので興味がある方は見てみてくださいねー(bolt kitでasset storeにて検索すれば出てきます)。

今回の環境
Unity 2020.2.1f1
bolt 1.4.13

## bolt kit Third Personの敵AIの挙動
敵はこんな見た目
![スクリーンショット 2021-01-01 14.36.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/2853ff3a-c6f5-4050-f2b8-9d49429a4b5b.png)
主な挙動は以下の通りです。

* パトロール - 指定された範囲をランダムで巡回
* 追跡 - プレイヤーを追跡
* 攻撃 - プレイヤーに向かって弾を撃って攻撃

この他にも、頭上にあるHP的なものの制御やダメージを受けた時の処理などもフローグラフで表現されていますが、今回は上記３つの挙動についてみていきます。

敵オブジェクトにはState Machineが設定されており、EnemyAiというマクロが設定されています。
上記３つの動作はこのマクロで定義されています。
当然(?)ですが、各種Variables(変数)も定義されてます。
![スクリーンショット 2021-01-01 14.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/df513ff0-8c16-8b10-a749-ef108487a254.png)


## 状態遷移図(ステートグラフ)
![スクリーンショット 2021-01-01 14.51.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/a754d602-6c78-69ed-4a99-0e0b9ac39846.png)
こちらが状態遷移設定です。
「パトロール(Patrol)」「追跡(Chase)」「攻撃(Attack)」が状態遷移条件によってそれぞれ結ばれています。
シンプルなそれぞれの状態の中には、挙動が定義されています。

### 状態遷移条件
よくある「敵の視界にプレイヤーが入ったら/出たら」というものと「プレイヤーが攻撃範囲に入ったら/出たら」が状態遷移の条件として定義されています。
![スクリーンショット 2021-01-01 14.58.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/3253e92d-6f45-4120-9bf8-94eef84ecf65.png)
これは、「敵の視界にプレイヤーが入ったら次の状態に遷移する」処理です。
「パトロール」→「追跡」に遷移する時の条件ですね。
`Update`イベントで「IsWithinRange」のbool戻り値で判定しています。
ちなみに、`Player`変数はプレイヤーGameObject、`ChaseRange`はfloat値です。

基本的に他の状態遷移条件の処理も同じように、「IsWithinRange」で距離を測って判定しています。

#### 再利用可能なSuperUnit
状態遷移条件処理で出てきた「IsWithinRange」は、SuperUnitといって、関数のように扱えるフローグラフです。
![スクリーンショット 2021-01-01 15.06.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/d0d995af-7b2a-7318-c629-b53124f5f961.png)
使いまわせる関数として、ファイルそのものを別で用意しておいて、他のフローグラフ内でinputを受け取ってoutputを返す処理として使えます。
boltを使う場合、頻出する処理はもちろん出てくるでしょうから、このようにSuperUnitで切り出すのは必須でやったほうが効率が良さそうですね。グラフもスッキリします。

今回の「IsWithinRange」はこんな定義になっています。
![スクリーンショット 2021-01-01 15.10.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/e45b0970-534e-8ab8-b63c-a77a01f4f331.png)
自分の`position`と入力のオブジェクトの`position`のDistanceが指定float値より小さければtrueが返る感じですね。

## パトロールステート
次はそれぞれの状態に書かれている処理を見ていきます。
まずは指定された範囲をランダム巡回するパトロールステートです。
二つに分けて見ていきます。

### 巡回する目的地の設定
![スクリーンショット 2021-01-01 15.32.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/5f3a6366-8344-0dfe-59ba-7fbf9731f87a.png)
一言で言うとこんな感じでしょうか。
「指定された範囲内のランダム値秒おきに、ナビメッシュのdestination(宛先)を決定」
destinationに設定する値(緑色の矢印)は、この後触れます。

ナビメッシュは、簡単に言うとあらかじめキャラクターが通れる場所を定義しておいて、宛先を指定すると最適な経路を通って移動してくれる便利な機能です。（詳しくは調べてくださいませ！

while loopで無限ループにしてるのはちょっと意外でしたが、よく考えるとこの場合はupdateイベントで呼ぶより確かに良さそうです。

* Waitで待ち時間を設定しているので、必要以上に処理が呼ばれない（updateにすると、都度呼ばれてしまう

### 目的地の算出
![スクリーンショット 2021-01-01 15.53.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/26485567-db39-a743-7e49-4f2314a2e5d0.png)
後半部分(ランダムで目的地を設定する挙動のうちの、目的地を算出する部分)です。

こちらも一言で言うと
「指定されたパトロールエリア(BoxCollider)の端から端までのxyz座標をランダムで算出してVector3を返す」
と言う感じでしょうか。

colliderからboundsにアクセスして算出しちゃうなんてダイナミックですね。
unityの機能をカバーできちゃうboltだからこその組み方ですねー。

## 追跡ステート
追跡ステートはとてもシンプルです
![スクリーンショット 2021-01-01 16.02.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/ea3710ce-04f0-7319-4f20-4beb1fed39f0.png)
updateで毎フレームプレイヤー位置を目的地として、ナビメッシュのdestinationに設定しているだけですね。

## 攻撃ステート
攻撃ステートも二つに分けてみてみます。

### プレイヤーの方向に体(頭)を向ける
![スクリーンショット 2021-01-01 16.05.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/ae574a78-ce3d-18c1-9750-6de5ddf4d70a.png)
やっていることはシンプルで、基本的にはプレイヤーの座標に向けて`LookAt`を呼んでいるだけですね。
ちょっとした注意点としては、y座標だけは自分の座標を使って、体を傾かせないように工夫しているところですね。

### プレイヤーに向けて弾を撃つ
![スクリーンショット 2021-01-01 16.08.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/a37a6156-921b-f425-c9e7-16d49c0cbc7c.png)
こちらの処理は、先ほどのLookAtの処理の次に実行されるように設定されています(白い矢印)。

ここでもSuperUnitが使用されています。
実は、プレイヤーも弾を撃てるんですが、そこで再利用したいのもありSuperUnitになっているんだと思われます。

必要な変数を持ってきて、Fireに渡していますね。
![スクリーンショット 2021-01-01 16.12.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/f13807a0-2dd7-9429-60a7-4613175a0f38.png)
これがFireの中身です。

一言だと
「指定オブジェクトの向きに回転させてから、Instantiateを行う（クールタイムあり）」
ですかね。

このままだとInstantiateして終わりになってしまう...と思いきや、ちゃんとその先があります。
弾オブジェクトに設定されているフローグラフでまっすぐにvelocityを加えてやる記述があり、それで弾は飛んでいくといった具合です。

# まとめ
bolt kit Third Personから読み取れる敵AIセオリーを紹介しました。
パトロール - 追跡 - 攻撃の遷移の流れは、いろんなゲームにそのまま使えそうですね。
パトロール、追跡は特に一般的なゲームに見られるものにも近い気がします。（とはいえ改造は必要そう

本当は、敵AIがもう少し複雑になっている例を見てみたかったんですが、シンプルな例でわかりやすく良いサンプルでした。
個人的には敵AIは複雑になりがちなので、boltだけでやろうとするとかなり複雑になりそうだなあと思っており、ビヘイビアツリーを使う方法(こっちだとboltでは未対応)と合わせて模索中です。
知見が出来てきたらそこらへんも投稿できればなあと思っています。

# OMAKE
おまけですよ。
若干愚痴入りですが...

bolt kit Third Personを導入に少し手間取ったんです。
2020.2.1f1という最先端でやったせいかもしれませんが...
同じ境遇の人もいるかもなのでちょっとだけ書きます

asset storeから落としてきて導入。
そのままだとboltが入っていないのでboltも一緒に導入。(Package Managerで管理されてないので一緒に入ってこない)
サンプルシーンを開くと...、bolt周りの設定が全て消えてる...(フローグラフの割り当てとか変数の設定が消えてる
なぜか元プレハブを開いて編集モードにすると設定が蘇るので、そこでComponentのコピーをして変数を復元したり、フローグラフの割り当てをしました。

また、「Use Deterministic Compilation」をオフにしてねというエラーが出ているので、Player settingsからチェックを外すのも行いました。

あとは、パトロールステートの目的地を設定するフローグラフは、そもそもエラーが出ていて、つなぎ直してあげると直る箇所がありました。

boltのプロジェクトを外部から持ってくるときは要注意かもしれない...