Unity - 経路探索第二の選択肢、A* Pathfinding Projectを導入してみる

Unityでは標準で経路探索を行うための強力な機能が、NavMeshで提供されています。
それなりのケースではNavMeshで十分なのですが、ちょっと凝ったことをしようとするとできない場合があります。

自分の場合は、「味方キャラは侵入をできないが、自キャラはなんの問題もなく通過できるエリアを動的に作りたい」というやりたいことに対して、NavMeshだと実現するのが難しそうだったので、A* Pathfinding Projectというアセットを導入しました。

今回の記事では、A* Pathfinding Project（フリー版と一部Pro版）の導入について書きたいと思います。
前述の自分がやりたかった機能まで説明すると込み入るので、NavMeshと同等の部分を実現するところまで説明したいと思います！
（経路探索がよくわからない人は、最初にUnity標準のNavMeshを触ることをおすすめします！）

環境
Unity 2021.1.1.2f1
A* Pathfinding Project Free版・Pro版 4.2.15

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">A* Pathfinding Project検証中です。<br><br>色がついているエリアがペナルティエリア。<br>そのエリアをまたぐと最短ですが、ペナルティが効いて回り道します。<br>ペナルティエリア内に目的地を置くとしぶしぶ(？)、ペナルティエリアを通って目的地に行ってくれます。<br><br>これが動的にできてばいい感じ...!<a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://t.co/3GY26aOfst">pic.twitter.com/3GY26aOfst</a></p>&mdash; asiram@unityゲーム開発 (@asiramasiram) <a href="https://twitter.com/asiramasiram/status/1391256281545994245?ref_src=twsrc%5Etfw">May 9, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

↑こんなことがA* Pathfinding Projectでできます。（が、今回の記事はこれの前段までの紹介にとどめています）


# A* Pathfinding Projectとは
アセットストア：https://assetstore.unity.com/packages/tools/ai/a-pathfinding-project-pro-87744
Free版：https://arongranberg.com/astar/download

- 経路探索が行えるアセット
- 経路の定義に使用できるグラフの種類が豊富(Unity標準のNavMeshでは、メッシュを使用するもの一択)
  - Unity標準と同等のNavMeshGraph
  - Gridで定義するGridGraph
  - 点で経路を決定するPointGraph などなど
- Free版も存在するが、AI同士の衝突回避が使用できない（これに目をつむればFree版でも結構色々できる）
  - [ここ](https://arongranberg.com/astar/freevspro)からFree版とPro版、NavMeshの機能比較が見られます
- [ドキュメント](https://arongranberg.com/astar/docs/)もそれなりに豊富に用意されています（全部英語ですが）
- 軽量に動作し、mobileでも問題なく動きます（もちろんフィールドが広すぎないことは前提ですが）
  - ランタイムでの経路再スキャンもある程度軽量なので、mobileでもいけそうです

# 導入してみる
では早速導入してみましょう。
Free版を使用して、対象を追いかけるAIを作ってみようと思います。

## Free版のインポート
まずは適当にUnityプロジェクトを作成、Free版が配布されている[サイトからDL](https://arongranberg.com/astar/download)します。
unitypackageがダウンロードされるので（パッケージマネージャではないです）、ファイルをクリックしてインポートしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/ca4655df-2bce-29cb-dee5-d10eb801fed9.png)
余裕がある人は、「ExampleScenes」に入っているサンプルを見てみると参考になります。

## 適当にフィールドを作成
経路探索させるには当然ながら歩けるフィールドがないとダメですよね。
今回は、Unity標準のPlaneやCubeなどを使ってフィールドを雑に作ります。

ここで、重要なのがコライダーがアタッチされているオブジェクトを使用することです。
(Unity標準のPlaneやCubeなどは最初からコライダーが設定されています)
これから作成するGridGraphはコライダーの存在で経路を判別するようなので、コライダーが存在しないとそこは通れる経路になってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/45ba533c-9182-d861-61cb-e9c57d7f350d.png)
こんな感じにしてみました。...センスとかはないので期待しないようにしてください。
中央にあるCubeを長くしたものは障害物の扱いのつもりで置きました。

ここで、ついでに各オブジェクトにレイヤーの設定をしておきましょう。
地面に`Ground`、障害物に`Obstacles`と設定しました。
経路探索のコンポーネントに、レイヤーを指定するときに使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/36c03b86-5af8-3f45-aab2-f0f0f9de04d7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/66ba10ae-67e5-3826-a749-214c9556c563.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/43d31746-ea19-3336-0211-40adcc0f7c33.png)



## Pathfinderコンポーネントの設定
次に、経路探索を行うためのコアのコンポーネントを設定していきます。
適当に空のオブジェクトを作成、`Pathfinder`コンポーネントを追加します。
(AddComponentするときはPathfinderと出ますが、本当の名前は`AStarPath`です)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/76fc5d8e-f8d4-2086-a94e-b5bfb529cd47.png)
インスペクターに各種項目が表示されるのでGraphsを展開、Add New Graphの`Grid Graph`を選択します。（今回はGrid Graphを使用します）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/bc597212-de8f-69a6-afe1-cf9921cc2046.png)
ボタンを押すと、ずらずらと設定項目が表示されます。（表示されない時は、さらにメニューを展開してください）

今回はこんな感じで設定しました↓
初期設定から変えたのは、`Width(nodes)`、`Depth(nodes)`、`Diameter`、`Obstacle Layer Mask`、`Mask(Height Testingの設定)`です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/1047b5e3-29ef-595e-b7d3-faab101904db.png)
この状態で、シーンタブもしくはインスペクターの`Pathfinder`コンポーネント下部の`Scan`ボタンを押すと以下のように経路が設定されます。
青い部分が歩ける経路、赤い点が歩けない場所(経路と障害物の境界)です。
（色がつかない人は、`Show Graphs`にチェックを入れるもしくは、SettingsのDebug - Graph Coloringを`Solid Color`にすると出るかもしれません）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/fb610f66-c116-f8e2-b881-14e171f25ec0.png)

主要な設定項目について簡単に解説します。

**Shape**
ノードの形状。大体はGridで良さそうですが、用途により`Hexagonal`とかにしても良いかもです。
`Grid`だと四角に区切られて経路が決定するので、斜めに配置してある障害物に対して若干ムラが出てしまう時に使うと...いいのかも...です（あんまり良い使い方がわかってないです）

**Width(nodes), Depth(nodes)**
ノードのそれぞれの方向の数です。
経路を作りたいフィールドの広さに合わせて、増減させましょう。
当然ですが、ノードが増えれば増えるほど経路のスキャンが重くなりますのでご注意を。

**Node Size**
グリッドを作るノード一つの大きさです。
大きくすれば計算負荷は小さくなりますが、経路と障害物の境界が大まかになったり、移動経路がバラけづらくなります。

**Center, Rotation**
ノードが配置される中央の点と、回転です。

**Collision Testing**
歩ける範囲の検知を行うかどうかです。
経路探索を行う時は基本オンで良さそうです。

**Diameter, Height/Length**
歩くオブジェクトの大きさと高さの定義です。

**Obstacle Layer Mask**
障害物としてみなすレイヤー設定。
今回は、先ほど追加した`Obstacles`レイヤーを指定しています。
必要に応じて複数選択できます。

**Height Testing**
高い位置に登ることができるかどうかの判定を行うかどうかの設定です。
オフにすると、高い場所もそのまま経路として判断されるようになります。
これも通常はオンのままで使うのが良さそうです。

**Mask(Height Testingの設定)**
Height Testingに使用するレイヤーマスクです。
今回は先ほど追加した`Ground`レイヤーを指定しています。

## 経路探索を行うAIの設定
経路が設定できたので、次は経路を歩くAIの設定を行いましょう。
Unity標準のNavMeshの`NavMeshAgent`にあたるものを設定していきます。

これまた適当に3Dオブジェクトを用意します。
そのオブジェクトに`AIPath`をアタッチします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/60f25a8c-7f77-1c2f-acf6-1d6e3fbac421.png)
AIPathをアタッチすると、自動的に`Seeker`コンポーネントもアタッチされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/67f51956-0f16-cf4c-e202-a29507b2ea89.png)
↑設定はこんな感じです。今回は`Max Speed`、`Gravity`ぐらいしか変えていません。

よく使いそうな項目を簡単に解説します。

**Radius, Height**
AIの半径と高さです。
オブジェクトの大きさに合わせて設定しましょう。

**Can Search, Can Move**
経路探索を行えるか、移動できるかのフラグです。
Can Moveをオフにすれば（もちろんスクリプトからも可能）、移動を強制的に停止することができます。

**Max Speed, Rotation Speed**
最大移動速度と、回転速度です。

**Slow Down Distance**
目的地の手前どれくらいの距離で、減速するかの設定です。
減速がいらない場合は0にしましょう。

**End Reached Distance**
目的地の手前どれくらいで移動を終えるかの設定です。

**Constrain Inside Graph**
グラフ内に移動を制限する設定です。
基本的にAIは、設定された経路しか歩かないのですが、衝突回避のために押し出されて経路外に出てしまうことがあるようです。
この時に、このオプションが有効化されていると、経路内から出ないようにしてくれるようです。
また、自前で移動を行う際（AIPathのMove()メソッドによる移動）に、このオプションを設定しておくと経路外に出てしまうことがなくなります。

**Gravity**
重力を使用するかどうかの設定です。
基本的には使わなくて良さそうなので、自分はオフにしています。

## 目標を追跡するための設定
AIの経路探索設定までできたので、そのAIが目標を追いかけるようにする設定を追加しましょう。
スクリプトで追いかける目標を設定する場合は、`AIPath.destination`に目標の座標を設定すればOKです。

今回は、最初から用意されている`AIDestinationSetter`を使用して、インスペクター上から目標を設定できるようにします。
先ほどAIPathを設定したオブジェクトに、`AIDestinationSetter`をアタッチします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/7496ede6-3dca-568b-4202-ca1358865c67.png)
そして、追跡目標として適当なオブジェクトを作成して（今回はSphereを使いました）、`AIDestinationSetter`のTargetに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/e6d181ad-14fe-f417-7519-2f3ab5d34db1.png)
## 追跡！
これで目標を追跡するための設定は終わったので、実行してみましょう。
シーンビューで、追跡目標にしたオブジェクトを動かすと、AIも移動しますね。
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/07f9fa49-f2b4-d425-6412-19001f05a9b6.gif)

# 衝突回避の設定
さて、目標の追跡まではできました。
一見いい感じに動いていますが、AIが二体以上いる場合に同じ目標に対して追跡させると、見事に重なり合います。
これは、衝突回避の機能が有効になっていないからです。
![ezgif.com-video-to-gif-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/5d887d3b-c3b5-77ef-b829-883ad2843bc5.gif)
↑こんな感じに、重なって移動してしまいます（緑色AIと青色AIが重なっています。わかりづらくてすみません...）。

ということで、衝突回避の機能をつけてあげようと思いますが、**ここからは有料版が必要**です。
以降の内容はアセットストアの[A* Pathfinding Project Pro](https://assetstore.unity.com/packages/tools/ai/a-pathfinding-project-pro-87744)がすでに導入済み前提で進めます。
(衝突回避を有料版onlyにする...商売上手ですね！)

## 衝突回避コアコンポーネントの追加
まずはPro版の導入ですが、アセットの購入後にパッケージマネージャからインポートするだけでOKです。
自分はFree版から上書きインストールしましたが、特に問題はなさそうです。

では、衝突回避に必須なコアコンポーネント`RVOSimulator`を追加していきます。
空のオブジェクトを作成、`RVOSimulator`をアタッチします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/8a9f989a-463e-cccf-346d-412ed392e3ac.png)
こちらのコンポーネントは、デフォルト設定のままでも十分使用できるので、今回は設定は特にいじりません。

## AIに衝突回避コンポーネント追加
最後に、AIに`RVOController`をアタッチしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/6db1cb70-18c5-60a1-b58d-afa2bfde898e.png)
すでに`AIPath`がアタッチされているので、`Radius`、`Height`、`Center`はAIPathの値が使用されると書いてありますね。

この状態で実行してみましょう！
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/3617c85e-5c3f-d645-a6b8-6b15583861e4.gif)
無事二つのAIが重なり合うことなく、目標を追いかけていますね。

gifの最後に、目標到達後に青いAIが目標地点をぐるぐる回り始めますが、これは緑のAIが目標地点にいていつまで経っても目標地点に到達できないためです。
これを防ぐには、AIPathの`End Reached Distance`をもう少し大きく設定して、目標地点の手前で移動を終えるようにするのが一つの手段だと思います。

# まとめ
結構長くなってしまいましたが、これでA* Pathfinding Projectによる最低限の経路探索ができるようになりました。
正直、ここまでのことならUnity標準のNavMeshでも問題なくできるのですが、ここからA* Pathfinding Project独自の設定を入れていくところがある種本番ですね...!

また余裕のある時に、ページ冒頭のtweetのようなペナルティエリア設定などの紹介ができればいいなと思います。