【Unity】NavMeshAgentで制御するNPCに動く床の影響を与える

AvatarPosted by

この記事は GRIPHONE Advent Calendar 2020 17日目の記事です

こんにちは、Unityエンジニアの黒板です
今回はNavMeshAgentで制御しているキャラクターの動きが自然に見えるように干渉する方法を提案します

やりたいこと

レースゲームのようにスタート地点とゴール地点が存在する状況で、NavMeshAgent制御のNPCとプレイヤーが競争するような状況を想定します
経路の途中に動く床が存在している場合、そこを通る間プレイヤーの操作キャラクターとNPCには動いている床の影響を与えたいと思います

準備

まずはPlaneやCylinderを使って簡単なステージを作成します

ステージ例

できました
手前の立っているシリンダーがNPCと操作キャラになります

次に、NPCを動かすためにNavMeshAgentコンポーネントをアタッチし、必要に応じて設定したあとBakeします
このあたりはリファレンスや他の詳しい記事を参考にしてみてください

Bake後のSceneビュー

これで準備が整いました

実装をみてみる

NPC、操作キャラ、回転シリンダーの上空にそれぞれColliderを設定し、処理用のスクリプトを用意します

回転シリンダーのスクリプトでは、回転の制御と上にいるキャラに対しての影響を実装します

public class RotationCylinderGimmick : MonoBehaviour
{
    public float Speed = 0.2f;
    public Vector3 velocity;
    public GameObject Cylinder;
    
    private void Update()
    {
        Cylinder.transform.Rotate(Vector3.forward * Speed, Space.World);
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.gameObject.CompareTag("Player") && other.attachedRigidbody)
        {
            var player = other.attachedRigidbody.GetComponent<PlayerCharacterBase>();
            player.MoveInfluence(velocity);
        }
    }
}

操作用のキャラはこんな感じです

public class PlayerCharacter : PlayerCharacterBase
{
    public Rigidbody Rigid;

    public override void MoveInfluence(Vector3 direction)
    {
        Rigid.MovePosition(transform.position + direction);
    }
}

最後に一番問題のNPCです

[RequireComponent(typeof(NavMeshAgent))]
public class NonPlayerCharacter : PlayerCharacterBase
{
    private NavMeshAgent _navMeshAgent;
    private GameObject _goalGameObject;
    private NavMeshAgent NavMeshAgent => _navMeshAgent ? _navMeshAgent : _navMeshAgent = GetComponent<NavMeshAgent>();
    private GameObject GoalObject => _goalGameObject ? _goalGameObject : _goalGameObject = GameObject.Find("goal");

    private const int InfluenceCountMin = 5;
    private const int InfluenceCountMax = 20;
    private int _influenceCount;
    private int _influenceCurrentCount;
    
    protected override void Initialization()
    {
        _influenceCount = Random.Range(InfluenceCountMin, InfluenceCountMax);
        NavMeshAgent.SetDestination(GoalObject.transform.position);
    }

    public override void MoveInfluence(Vector3 direction)
    {
        _influenceCurrentCount++;
        if (_influenceCount < _influenceCurrentCount)
        {
            NavMeshAgent.velocity += direction * 12;
            StartCoroutine(Goback(direction * 12));
            _influenceCurrentCount = 0;
            _influenceCount = Random.Range(InfluenceCountMin, InfluenceCountMax);
        }
    }

    private IEnumerator Goback(Vector3 direction)
    {
        yield return new WaitForSeconds(1.2f);
        NavMeshAgent.velocity -= direction;
    }
}

振り返り

正直今回の実装が正解だとは思っていません
ただ、試行錯誤した結果かなりマシだなと思えました

他にNPCのMoveInfluenceでは次のようなことを試しました

  • NavMeshAgent.enabledをfalseにして横に移動させたあとtrueに戻す
    →speedとdestinationがリセットされてしまうのでかなり減速する
  • Speedパラメータを遅くして苦戦しているように見せる
    →経路がまっすぐのままなのでロボット感が強く不自然

物理演算を使ってaddforceでキャラを飛ばしたいときなどはNavMeshAgentを無効にしてから移動可能になったタイミングで戻す方法でいいと思います

今回はスムーズに移動し続ける必要があったのですが、NavMeshAgentと物理演算の相性があまり良くないために苦戦しました

回転するフロアの上で気絶するなどの仕様がある場合は追加でその場合の挙動を追加しなければいけません

まとめ

NavMeshAgentは経路探索をして機械的な動きをさせるためには便利です
しかし、物理法則と合わせて使ったりする場合は注意が必要です

また、NPCに自然な動きをさせたい場合はロジックでゴリ押す必要もあると思うので、こだわる場合はある程度労力を要します

しっくりくる方法があったらまた提案したいと思います