【Unity2021.3.2f1】NavMeshでユニットを動かしたい話

# 前回
https://qiita.com/Amatsuki/items/4fbfb5b8f7a346515e9f

# NavAgentで動かす
動かす仕組みとしては、タッチ位置からRayを打って、ヒット位置をNavAgentのdestinationにセットするだけ、というシンプルなもの。
タッチイベントの検知は、もちろんUpdateでInputを監視する形でも問題ないですが、今回はUniRXを使用しています。
若干学習コストはかかりますが、Updateがキレイになったり、可読性が上がるのでオススメです。

https://assetstore.unity.com/packages/tools/integration/unirx-reactive-extensions-for-unity-17276?locale=ja-JP

<details>
  <summary>移動制御クラス</summary>

```
using System;
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class PlayUnit : MonoBehaviour
{
    protected NavMeshAgent agent;

    protected void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    public void SetTarget(Vector3 target)
    {
        agent.destination = target;
    }
}
```
</details>
<details>
  <summary>タップ制御クラス</summary>

```
using System;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.EventSystems;

public class PlayManager : MonoBehaviour
{
    [SerializeField] private UIBehaviour touchArea = null;
    [SerializeField] private PlayerUnit playerUnit = null;

    public void Start()
    {
        touchArea.OnPointerUpAsObservable().Subscribe(OnTouchUp).AddTo(touchArea);
    }

    private void OnTouchUp(PointerEventData data)
    {
        RaySystem(data, hit =>
        {
            playerUnit.SetTarget(hit.point);
        });
    }

    private void RaySystem(PointerEventData data, Action<RaycastHit> onHit)
    {
        Ray ray = Camera.main.ScreenPointToRay(data.position);

        if (Physics.Raycast(ray, out RaycastHit hit, 100))
        {
            onHit?.Invoke(hit);
            debugHitPos = hit.point;
        }
    }

    private Vector3? debugHitPos = null;

    private void OnDrawGizmos()
    {
        if (debugHitPos != null)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawSphere(debugHitPos.Value, 0.2f);
        }
    }
}
```
</details>

![NavMeshV1_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/fff04a06-8575-8fb7-a39a-eb40c4b7639c.gif)
たった数行のスクリプトでキャラを動かせるのは、ちょっとした感動がありますね。
もう少し速度があった方がいいのでNevAgentのSpeedを上げてみます。
![NavMeshV1_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/9c37ac7a-5f9f-7edb-0e35-bbd4b51918c2.gif)
**<font color="Red">どうした！？</font>**
これはこれでレースゲームやら氷の床みたいなところで有用かも知れないですが、今回は綺麗に目的地にたどり着いて欲しいので、解決策を探します。

## 解決案
- 旋回速度（AngularSpeed）を上げる。
    - 多少、マシになる様子だけれども、多少の域はでなそう。
- Speedを上げない。
    - 縮尺やゲームスピードを変えて、現在の速度でも違和感ないようにする手がある気がします。
- キャラの移動をNavMeshAgentに任せない。
    - 今回はこれで実装を行いました。
- カーブの際にSpeedを落とすような実装をする。
    - Qiitaでそういう記事を見つけたけれど、調整はちょっと難しそう。

https://qiita.com/MoriArty3/items/52f15e67a27d2584b23a

# DOTweenで動かす
DOTweenはUnityでメジャーなオブジェクトの移動・変形をするアセットで、位置情報の配列を順々に移動するDOPath、指定した方向を向くDOLookAtという機能があるので今回はそれを使用した。

https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676?locale=ja-JP

<details>
  <summary>移動制御クラスVer.2</summary>

```
using System;
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class PlayUnit : MonoBehaviour
{
    protected NavMeshAgent agent;
    public float moveSpeed = 1;
    public float rotateTime = 1;

    protected Tweener moveTweener = null;

    // Start is called before the first frame update
    protected void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    public void SetTarget(Vector3 target)
    {
        moveTweener?.Kill();
        moveTweener = MakeMoveTweener(target);
    }

    /// <summary>
    /// 任意の場所に移動するTweenerを生成する
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    protected Tweener MakeMoveTweener(Vector3 target)
    {
        // 経路取得用のインスタンス作成
        var path = new NavMeshPath();
        // 明示的な経路計算実行
        agent.CalculatePath(target, path);
        // コーナーでユニットを次のコーナーの向きに回転させる
        Action<int> rotateUnit = (index) =>
        {
            // NOTE:この関数は終点でもコールされる。
            //      終点でDOLockAtが呼ばれると向きがデフォルトになってしまうのでindexのチェックは注意
            if (index >= path.corners.Length - 1) { return; }
            transform.DOLookAt(path.corners[index], rotateTime);
        };

        var time = CalculateDistance(path.corners) * moveSpeed;
        return transform.DOPath(path.corners, time)
            .OnWaypointChange(index => rotateUnit(index));
    }

    /// <summary>
    /// 経路の距離を計算
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    protected float CalculateDistance(Vector3[] path)
    {
        var result = 0f;
        var lastPos = transform.position;
        for (int i = 0; i < path.Length; i++)
        {
            var tmp = lastPos - path[i];
            result += tmp.magnitude;
            lastPos = path[i];
        }
        return result;
    }
}
```
</details>

![NavMeshV2_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/9aacc810-6fa8-a758-d273-f7b536c4ee52.gif)
なかなかよいのでは？

これで少し触っていたら、問題が発生。
![NavMesh_Path.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/d46d9626-09d5-d2e9-c946-02a5acacc61b.gif)
指定した場所がNavMeshの領域から外れていた場合、パスが生成されない模様。
よく見ると、NavMeshの領域が外れていても、少しなら反応する様子。

Navigationの『Agents』タブで半径を広くしてみたところ、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/1e19ff89-f640-b615-a0da-7a6d0da79387.png)
無事、最も近い場所に移動するようになりました。
![NavMesh_PathV2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99896/c34c0e12-cb00-ab2b-5948-bbd5d7a5b004.gif)
UnitにアタッチしていたNavMeshAgentの方にも半径を指定する項目があったが、どうやらそちらは障害物との衝突判定でパス検索には関係ない模様。


ただ、『タップ位置して移動』というようなプレイを考えると、プレイヤーの思った場所と違うところに移動することになりそうなので、あんまりな気もします。