【Unity(C#)】NavMeshで"追いかけると逃げるオブジェクト"の実装

# はじめに

現在、unity1weekというゲームジャムに参加し、簡単な一人称視点ゲームの作成を行っています。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">なかなかに野蛮なものを作ってしまった気がしますｗ<a href="https://twitter.com/hashtag/unity1week?src=hash&amp;ref_src=twsrc%5Etfw">#unity1week</a> <a href="https://t.co/vasPCevICZ">pic.twitter.com/vasPCevICZ</a></p>&mdash; KENTO⚽️XRエンジニア😎Shader100記事マラソン挑戦中39/100 (@okprogramming) <a href="https://twitter.com/okprogramming/status/1439159318780149763?ref_src=twsrc%5Etfw">September 18, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

その中で、追いかけると逃げ惑う人を実装しようと考え、
いろいろ調べる中で[NavMesh](https://docs.unity3d.com/ja/2019.4/Manual/nav-NavigationSystem.html)を使えば簡単にできそうだという結論に至り、実装しました。

簡単に言うとNavMeshは、Meshに沿って移動させたいオブジェクトが移動可能なエリアを設定できます。
面倒な処理を自動で構築してくれる最強機能でした。忘れないうちにメモします。

# バージョン情報

| 諸々名前 | バージョン |
|:-:|:-:|
|  Unity | 2020.3.4f1  |
|  UniRx | 7.1.0  |
|  UniTask | 2.2.5  |

# NavMesh導入
まず専用のウィンドウをUnityEditorで開きます。

**Window -> AI -> Navigation**の順に選択します。

Navigationウィンドウの`Object`を開いた状態でHierarchyのオブジェクトを選択すると
下記画像のように選択したオブジェクトのMeshが参照されます。
![NavMesh1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/de7f4451-9877-674d-ab64-51385f3cc426.png)

その状態でNavigation Staticにチェック入れます。
次に、Bakeを開き、右下のBakeボタンを押下します。
![NavMesh2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/2579d844-0916-0b39-fd08-974e2cf01989.png)


そうすると選択したオブジェクトの平面が移動可能なMeshとして処理されるようになります。
Navigationウィンドウを開いた状態なら、
Scene上でどのエリアがNavigation MeshとしてBakeされたか確認できます。
青くなっているのが該当のエリアです。

![Chu5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/089b4deb-0471-7d9f-679c-a0469a0891c8.gif)

# Nav Mesh Obstacle

次に動的にNavigation Meshエリアを変更したい場合の手法です。
事前に障害物として設定することで、可能となります。
下記GIFのように車を障害物として設定しておけばNavigation Meshエリアが動的に計算、変更されます。
![Chu6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/ce0f728d-5b46-d919-d1cb-769f03ca744f.gif)

設定は任意のMeshを持つオブジェクトに`NavMeshObstacleコンポーネント`をアタッチするだけです。
![NavMesh3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/afb95fc5-a847-be81-df6f-5b97bbdfe491.png)

Carveにチェックを入れることで、迂回するルートを探索することが可能になります。
【参考リンク】：[Nav Mesh Obstacle](https://docs.unity3d.com/2020.3/Documentation/Manual/class-NavMeshObstacle.html)

# Agent
Navigation Meshに沿って移動するオブジェクトのことを`Agent`と言います。
NavMeshAgentコンポーネントを移動させたいオブジェクトにアタッチすることでAgentとして扱われ、
Navigation Meshで移動可能な反映を移動できるようになります。移動速度や障害の回避に関する設定が可能です。
![NavMesh4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/edf6dc5d-07eb-c760-25c2-ad754600f255.png)

# コード
ここまでの設定でAgentの移動準備ができました。
あとはコードで追いかけると逃げる部分の実装を行います。
下記を移動させたいオブジェクトにアタッチすればOKです。

```cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
using UnityEngine.AI;
using Random = UnityEngine.Random;


/// <summary>
/// 敵のNavMesh上での挙動
/// </summary>
public class EnemyNavigation : MonoBehaviour
{
    [SerializeField,Range(5,30)] private float _runAwayDistance = 10f;
    
    /// <summary>
    /// 自身にアタッチされたNavMeshAgent
    /// </summary>
    private NavMeshAgent _myAgent;

    /// <summary>
    /// プレイヤーが近くにいるかどうかのReactiveProperty
    /// </summary>
    private readonly ReactiveProperty<bool> _isNearPlayerReactiveProperty = new ReactiveProperty<bool>();

    void Start()
    {
        _myAgent = GetComponent<NavMeshAgent>();

        //プレイヤーが近くにいるか監視
        _isNearPlayerReactiveProperty
            .Where(isNear => !isNear)
            .Subscribe(_ => SetRandomDestinationAsync(this.GetCancellationTokenOnDestroy()).Forget())
            .AddTo(this);
    }

    void Update()
    {
        //カメラからプレイヤーの位置と自身までの距離を計算
        var playerTransform = Camera.main.transform;
        var distance = Vector3.Distance(playerTransform.position, transform.position);
        //NavMeshの目的地を計算
        var direction =  (transform.position - playerTransform.position).normalized;
        direction.y = 0;
        var destination = transform.position + direction * _runAwayDistance;

        //プレイヤーとの距離を判定
        if (distance >= _runAwayDistance)
        {
            _isNearPlayerReactiveProperty.Value = false;
        }
        else
        {
            _myAgent.SetDestination(destination);
            _isNearPlayerReactiveProperty.Value = true;
        }
    }

    /// <summary>
    /// ランダムな目的地を設定
    /// </summary>
    /// <param name="ct">キャンセルトークン</param>
    private async UniTask SetRandomDestinationAsync(CancellationToken ct)
    {
        while (!_isNearPlayerReactiveProperty.Value)
        {
            var randomValue = Random.Range(-100, 100);
            if (_myAgent == null) return;
            _myAgent.SetDestination(new Vector3(randomValue,0,randomValue));
            await UniTask.Delay(TimeSpan.FromSeconds(10f), cancellationToken: ct);
        }
    }
}
```

`NavMeshAgent.SetDestination`に移動したい目的地の座標を渡すとそこに向かって動き始めます。

離れていく実装はメインカメラとAgent自身の位置を用いて計算しています。
メインカメラとは反対方向のベクトルを計算し、目的として設定することで実現可能です。

また、ある程度距離が離れたあと、ずっと同じ方向に動き続けるのも止まるのも変なので、
適当に動き回るように実装しています。

# デモ
逃げ惑う人が作れました。
![Chu7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/bbbe5b9a-58c6-6c34-59ff-3cf4e3e2ea0b.gif)


# おわりに
抱えている問題として、Agentが急旋回するときの挙動の不自然さや、
まれにひっかかりって動けなくなるAgentが出現することが挙げられます。

より厳密な挙動を追求するにはもう少し頑張る必要がありそうです。

# 参考リンク
[Unityの経路探索: NavMeshとAgentとObstacle](https://www.sambaiz.net/article/115/)
[NavMeshAgentでよい感じにキャラクターを歩かせる](https://am1tanaka.hatenablog.com/entry/2018/08/20/133619)
[[Unity][C# Script] 敵キャラをNavMeshでかしこくかっこよく動かしてみよう。](https://zenn.dev/k1togami/articles/71519622146168)