Unity - A* Pathfinding Projectでペナルティエリアを作って移動制限をかけてみる

この記事は「Unity アセット真夏のアドベントカレンダー」の8/7の記事です。
前回は[えなこいんさん](https://program.enakko.net)の[[Unity Asset] Flat Minimalist GUI / UI pack を買ってみた](https://program.enakko.net/flat-minimalist-gui-ui-pack/)でした。
次回は[汗人柱さん](https://www.asset-sale.net)の揺れものアセット 未開拓編です。

今回はUnityにおける第二の経路探索手法（だと自分は思っています:relaxed:）である、A* Pathfinding Projectで経路に制限がつけられるペナルティエリアを作成する手順を紹介します。
AIがステージ上の特定の場所に侵入しづらい（侵入しないわけでは無い）エリアを作れたり、完全に侵入できなくなるエリアを作れたりします。

ちなみに、Unity標準のNavMeshの拡張機能「[NavMeshComponents](https://github.com/Unity-Technologies/NavMeshComponents)」を導入すると大体同じものが作れる模様ですが、若干挙動が違う箇所があります。
これに比較してA* Pathfinding Projectを採用するメリットは、こんな感じになりそうです。

- 数種類のグラフタイプから経路探索タイプを選べる
  - 負荷的に軽い経路探索処理が行える（動的に経路を更新する際にも有利）
- 自分の足元に急に立ち入り禁止エリアが出た時の挙動が自然（個人的にはこれが大きいです）

:::note
環境
Unity 2021.1.12f1
A* Pathfinding Project Free版 4.2.15
:::

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今日は、ちょっと気が早いですがUnityアセットストアの夏アドベントカレンダー用記事を書いていました。<br><br>A* Pathfinding Projectでペナルティエリアを作ってみる記事です。<br>動画はペナルティエリア派生の立ち入り禁止エリアのデモです<br><br>自分の担当は8/7なので、もう少し手直しして寝かせておきます😃 <a href="https://t.co/Uu1uf9h6jE">pic.twitter.com/Uu1uf9h6jE</a></p>&mdash; asiram / unityゲーム開発 (@asiramasiram) <a href="https://twitter.com/asiramasiram/status/1418908984782639104?ref_src=twsrc%5Etfw">July 24, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
こんな感じのことができるようになります。

# 本編の前に
アセットストア(Pro版)：[A* Pathfinding Project Pro](https://assetstore.unity.com/packages/tools/ai/a-pathfinding-project-pro-87744?utm_source=twitter&utm_medium=social&utm_campaign=jp-advent-calendar-summer)
Free版：https://arongranberg.com/astar/download

A* Pathfinding Projectの紹介や、導入方法は以前の記事にまとめてありますので、こちらからどうぞ。
[Unity - 経路探索第二の選択肢、A* Pathfinding Projectを導入してみる](https://qiita.com/asiram/items/93ced6f042e56540e03f)

また、今回の記事はA* Pathfinding ProjectのFree版だけで完結する機能なので、有料(Pro)版は導入していなくても動作します。
（ペナルティエリアを使用かつ、衝突回避したくなった場合はPro版が必要です）

# ペナルティエリアを作ってみる
さて、本編です。
ここで言うペナルティエリアとは、以下のようなものです。

- 経路上のある範囲をペナルティエリアとして指定する
- AI毎にペナルティエリアの重み(嫌がる度合いと言うとわかりやすいかも)が設定されている
- AIは重みによって、ペナルティエリアをなるべく避けた経路にしようとする
- 目的地がペナルティエリア内に設定された場合は、ペナルティエリアに侵入する

このセクションではペナルティエリアを作成しますが、後半では完全に立ち入りを制限するエリアを作成するので、そちらも合わせて見てみてくださいませ。

ちなみに、Free版の「ExampleScenes」内にある「Example9_Penalties」サンプルでここで説明する機能が実装されています。
解説みるより実際に動いているやつを見たい！と言う方はそちらもどうぞ〜

## 基本的なシーンの準備
[前回の記事](https://qiita.com/asiram/items/93ced6f042e56540e03f)の、「追跡！」までの設定が終わっているプロジェクトを使って先に進めていきます。
ざっと設定を説明すると、以下のオブジェクトが配置・設定されています。

- Pathfinderコンポーネント(経路探索コアコンポーネント)は、`Grid`タイプのグラフを使用
- 地面として、Colliderと地面を表すタグ(Ground)が設定されたPlane
- 障害物として、Colliderが設定され障害物を示すタグ(Obstacles)が設定されたCube
- 移動するAIとして、`AIPath`と`Seeker`、`AIDestinationSetter`コンポーネントがアタッチされたCapsule
- AIの移動目的地を示すSphere
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/469c4ce7-34c9-9ea4-01c7-2c86aa80aebd.png)

## ペナルティエリアの設置
準備が整ったところで、いよいよペナルティエリアを配置していきましょう。

ペナルティエリアの設定には`GraphUpdateScene`コンポーネントを使用します。
`GraphUpdateScene`を使用してペナルティエリアの範囲を示す方法は二種類あります。

1. コライダーが持つboundsを使用する方法
2. 手動で座標を指定する方法

今回はより簡単な`1`の方法で実現してみようと思います。
`1`の方法は、コライダーで設定した範囲がペナルティエリアとしての範囲になります。
`2`の方法は、より複雑な範囲指定の際に役立つと思います。

:::note
2の方法は、Pointsに座標を複数登録することで範囲を設定することができます。
:::

今回はこんな感じでCubeを潰してペナルティエリアとして配置してみました。
(コライダーで範囲指定できればいいので、メッシュはいらないのですが、わかりやすさを優先しています)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/67029f50-7002-609c-3251-54f2ee3ac08e.png)

- scaleを(4, 0.1, 8)に設定。（ペナルティエリアに高さはいらないので、少しだけ厚みを持たせる程度で良い）
- 地面から浮いているとうまくペナルティエリア扱いしてくれない場合があるので、地面に少しかぶるくらいの高さに配置すると良いです。（今回はY座標を-0.01にしてあります）

:::note warn
ペナルティエリアはある程度の厚みが無いと上手くいかないようです。
手動で座標を指定するときにも注意してください。
:::

また、Cubeに`GraphUpdateScene`コンポーネントをアタッチして、`Modify Tag`にチェック、`Tag Value`を`1`に設定しています。
この設定によって、この範囲をタグ付けすることができ、これが実質ペナルティエリアの種類になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/b02f46e0-03d5-1c1f-c41d-1c9c26c66b2b.png)

## AIの設定
ペナルティエリア設定の仕上げに、AI側の設定をいじります。
AI(今回はCapsuleオブジェクト)にアタッチされた`Seeker`コンポーネントの`Tags`を、以下のようにTagが`1`の`Penalty`を`3000`に設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/746ea215-4d1b-2309-d64c-f2ca6ff507e6.png)
この設定をすることによって、`Tag`が`1`(という名前)のエリアに重み(ペナルティ値)を3000つけることができます。

先ほど`GraphUpdateScene`の`Tag Value`を`1`に設定しましたが、ここで繋がってくるわけです。
この設定は、AI(Seeker)毎に設定できるので、同じ`1`エリアでもAI毎に重みづけを変えることができるので色々使えそうですね〜。

:::note
Tagの名前は任意のものに変更ができます。
Pathfinderコンポーネントの「Settings」→「Tags Name」から変更可能です。
:::

## 動作の確認
ここまでできたらペナルティエリアは完成しています！
実行してみましょう。
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/36201168-1bb6-c8b3-fb11-78ed66fec7dd.gif)
こんな感じで、ペナルティエリアを避けつつ目的地にAIが移動すればOKです。
GIFの最後では、ペナルティエリア内に目的地を置いていますが、問題なく目的地に到達できています。

## 設定されたペナルティエリアをグラフ上で確認する
ペナルティエリアの最後に、実際は経路探索グラフ上でどのように設定されているか確認して見ましょう。
`Pathfinder`コンポーネントの`Settings`→`Debug`→`Graph Coloring`の設定を`Tags`に設定します。
この状態でシーンビューに表示されている「Scan」ボタンを押してみましょう(Show Graphsのチェックも忘れずに)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/06a9d312-cb87-fbcb-ea17-4d899ce66aae.png)
そうすると、こんな風にTagが設定されている範囲が可視化されます。
この部分がペナルティエリアとして設定されているわけですね。

:::note info
実際に設定した範囲から若干ずれているように見えますね。
これは、今回使用しているGridグラフのマス目に準拠した範囲しか取れないためにずれています。
Gridの1マスの一定範囲にかぶっていないと、GraphUpdateSceneのTag設定が反映されないので、注意が必要です。
より厳密に範囲指定したい場合は、Gridのマスの大きさを小さくするか、GraphタイプをMeshなどにすると良さそうです。（いずれも処理負荷とのトレードオフになりそうなので要考慮です(特に経路の再計算時)）
:::

# 立ち入り禁止エリアの作成
ペナルティエリアが設定できたので、次は立ち入り禁止エリアを作ってみましょう。
立ち入り禁止エリアの挙動はこんな感じです。

- 経路上のある範囲を立ち入り禁止エリアとして指定する
- AI毎に立ち入り禁止エリアが設定されている
- AIは立ち入り禁止エリアを避けて経路を探索する
- 目的地が立ち入り禁止エリア内に設定された場合は、立ち入り禁止エリアの手前で止まる（目的地に到達できない）

## AIの設定
ここまで設定した項目を流用して設定していきます。

先ほど作成した`GraphUpdateScene`の設定はそのまま使用します。（つまり、Tagが`1`のエリアを立ち入り禁止エリアにしてみます）

AIの`Seeker`コンポーネントの`Tag`の`1`の`Traversable`のチェックを外します。
（Penalty値は使用されないのでいじらなくてもOKです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/e81acb1a-5cce-57cb-057a-4ce80962645b.png)
これだけで、Tagが`1`のエリアはこのAIにとって立ち入り禁止エリアとなります。

## 動作確認
実行してみましょう。
![ezgif.com-video-to-gif-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/b25de9e8-855c-baa2-d75c-4eccf9efd9b2.gif)
ちゃんと立ち入り禁止エリアを避けて移動していますね。
GIFの最後はちょっとわかりにくいですが、立ち入り禁止エリア内に目的地を置いており、AIは立ち入り禁止エリアには入らずその手前で止まっています。

# 動的に立ち入り禁止エリアを作ってみる
最後のバリエーションとして、動的に立ち入り禁止エリアを作ってみて、AIの動作がどうなるのか見てみましょう。
個人的に、この挙動が気に入っています(というかこれがやりたかった故に導入しました)。

経路探索の準備動作として、A* Pathfinding Projectの挙動はこんな感じになっています。

- `Pathfinder`コンポーネントが`Awake`タイミングで経路計算(スキャン)を行う
  - `GraphUpdateScene`オブジェクトがすでに配置してあるので、このタイミングで立ち入り禁止エリア(ペナルティエリア)が設定される

:::note
最初の経路計算タイミングは、Scan on Awakeオプションで変更可能です
:::

今回立入禁止エリアを動的に作成する際、`GraphUpdateScene`をシーン上に動的に配置するだけでは経路に影響が及ばないので、配置したタイミングで経路の再計算が必要になります。

## スクリプトを用意する
なるべく簡単に設定したいので、今回は次のような方針でいきます。

- 実行時に、シーンにあらかじめ配置してある`GraphUpdatescene`をシーンビューで移動
- スクリプトで経路の再計算を行う

本来であれば、`GraphUpdateScene`がアタッチされたオブジェクトをプレハブなどから読んで来て、シーンに動的配置するのが良いと思います。

以下のスクリプトを適当なオブジェクトにアタッチします。
今回は`Pathfinder`がアタッチされているオブジェクトにアタッチしました。

```C#:PathRescanController.cs
using UnityEngine;
using Pathfinding;

public class PathRescanController : MonoBehaviour
{
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.S))
            AstarData.active.Scan(AstarData.active.graphs[0]);
    }
}
```

正直若干無理やり実装していますが、`S`キーを押した時に経路の再計算が実行されるようなコードが書いてあります。
`AstarData.active`というのは、現在シーン上でアクティブな`Pathfinder`オブジェクトを表します。
`AstarData.active.graphs`は、↑のオブジェクトに紐づけられているグラフオブジェクトを表し、今回はGridグラフ一つしか使っていないので、最初の要素を指定しているといった感じです。

## 動作の確認
実行してみましょう。
GIFでは、以下のような操作をしています。

- ゲーム実行後、シーンビューで立ち入り禁止エリアオブジェクトを、AIの足元に移動させる
- ゲームビューに移って`S`キーを押す
- →AIが立ち入り禁止エリアから出ようとする
![ezgif.com-video-to-gif-3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62924/03f97201-5927-17ff-9697-b80e4771b66e.gif)

`S`キーを押した瞬間に、シーンビューの範囲指定が動いていますね。
これで経路の再計算によって、立入禁止エリアが動的に変更されていることがわかります。
そして、めっちゃわかりにくいですが、自分の足元が立ち入り禁止エリアになったAIは、その範囲から脱出しています。
この脱出動作は、現在の目的地に一番近い立ち入り禁止エリア外に移動するような動作になっています。

# まとめ
A* Pathfinding Projectを使用して、ペナルティエリアと立入禁止エリアを作成してみました。
そこそこ込み入ったゲームを作ったら、結構な頻度で使うことになりそうな機能ですよね。

冒頭でも触れたUnity標準NavMeshComponentsを使うことももちろんできるのですが、A* Pathfinding Projectを使用する大きなメリットは、使うグラフにもよりますが軽量なところかなと思います。
特に経路の再計算でアドバンテージが大きそうです。
（あと地味に、AIの足元に急に立入禁止エリアが出現した時の挙動が自然なのが良いです。NavMeshComponentsだとワープする感じになっちゃってました）

次のアドベントカレンダーの記事もお楽しみに〜