UnityのNavMeshでNPCを街に歩かせる|経路探索とウェイポイント巡回

これまでのヒューマノイドシリーズ(①移動〜④モーキャプ)で、1体のキャラを動かす技術を積み上げてきました。今回は応 用編として、街を自動で歩き回るNPC(通行人)を、UnityのNavMesh(経路探索システム)で作ります。障害物を 避け、決めたルートを巡回する群衆を、少ないコードで実現します。

NavMeshとは

NavMesh(ナビメッシュ)は、Unityが用意する経路探索(パスファインディング)の仕組みです。歩ける床面 を「ナビメッシュ」として焼き込んでおくと、NPCは目的地までの最短ルートを自動計算し、壁や建物を避けて歩いてくれます。自 分で迷路探索を書く必要はありません。

登場する主役は2つです。

  • NavMesh:歩ける床の地図(シーンに焼き込む)
  • NavMeshAgent:その地図上を移動するNPC本体

ステップ1:NavMeshを焼き込む(Bake)

まず歩ける床をNavMeshとして登録します。

  1. 地面・道路など歩かせたいオブジェクトを選び、インスペクターでStatic(Navigation Static)にする
  2. メニュー Window > AI > Navigation を開く
  3. Bake タブで Agent Radius(NPCの太さ)などを設定し、Bake ボタンを押す
  4. 床が青く塗られればOK。これが歩ける範囲です

※ Unityの新しいバージョンでは、AI Navigationパッケージを導入し、床にNavMeshSurfaceコンポーネントを付け てBakeする方式になります。やることは同じ「歩ける床を焼く」です。

ステップ2:NPCにNavMeshAgentを付けて目的地へ歩かせる

NPCのGameObjectに NavMeshAgent コンポーネントを追加し、SetDestinationで行き先を指定するだけで歩き出します。

using UnityEngine;
  using UnityEngine.AI;

  [RequireComponent(typeof(NavMeshAgent))]
  public class SimpleWalker : MonoBehaviour
  {
      [SerializeField] Transform destination;
      NavMeshAgent _agent;

      void Start()
      {
          _agent = GetComponent<NavMeshAgent>();
          if (destination != null) _agent.SetDestination(destination.position);
      }
  }

これだけで、NPCは障害物を避けて目的地まで歩きます。経路探索・衝突回避はNavMeshAgentが内部でやってくれます。

ステップ3:ウェイポイントを巡回させる(通行人らしく)

1点に向かうだけでは止まってしまいます。街の通行人らしくするには、複数の経由地点(ウェイポイント)を順番に、 あるいはランダムに巡回させます。

using UnityEngine;
  using UnityEngine.AI;

  [RequireComponent(typeof(NavMeshAgent))]
  public class CityNPC : MonoBehaviour
  {
      [SerializeField] Transform[] waypoints;  // 歩道に置いた経由地点
      [SerializeField] float arriveDist = 0.5f;
      NavMeshAgent _agent;
      int _index;

      void Start()
      {
          _agent = GetComponent<NavMeshAgent>();
          if (waypoints.Length > 0)
          {
              _index = Random.Range(0, waypoints.Length); // 開始位置をばらけさせる
              GoNext();
          }
      }

      void Update()
      {
          // 目的地に着いたら次のウェイポイントへ
          if (!_agent.pathPending && _agent.remainingDistance <= arriveDist)
              GoNext();
      }

      void GoNext()
      {
          _index = (_index + 1) % waypoints.Length;          // 順番に巡回
          _agent.SetDestination(waypoints[_index].position);
      }
  }

ランダムに歩かせたい場合は、GoNext_index = Random.Range(0, waypoints.Length);に変えるだ けです。複数のNPCで開始インデックスをばらけさせると、群衆が同じ動きをせず自然に見えます。

ステップ4:歩行アニメと連動させる

NavMeshAgentは「位置」を動かしますが、見た目のアニメは別です。Agentの速度をAnimatorに渡して、歩行アニメを再生しまし ょう。シリーズ①で使ったSpeedパラメータがそのまま使えます。

void Update()
  {
      // 移動速度を正規化してAnimatorへ(①の歩行ブレンドと同じ)
      float speed = _agent.velocity.magnitude / _agent.speed;
      animator.SetFloat("Speed", speed, 0.1f, Time.deltaTime);

      if (!_agent.pathPending && _agent.remainingDistance <= arriveDist)
          GoNext();
  }

歩行アニメの土台づくりはシリーズ①で詳しく解説しています。NavMeshAgentと組み合わせると、計算した移動に自然な足の動き が乗ります。

実践で詰まった落とし穴:地面スナップ

NPCを地面に乗せ直すとき、上空から下方向にレイを飛ばして「最初に当たった面」に乗せると、街路樹・街灯・屋根の 上に乗ってしまうことがあります。これらは天辺が高く、地面より先にヒットするためです。

対策は、Physics.RaycastAllで全ヒットを取り、最も低いY(=本来の地面)を採用すること。 「最初のヒット」を使うと、高所に乗る→救済で落とす、を繰り返す不具合になります。

// 指定座標の「本当の地面」のYを返す
  float GroundYAt(float x, float z)
  {
      Vector3 origin = new Vector3(x, 100f, z);
      var hits = Physics.RaycastAll(origin, Vector3.down, 200f);
      float lowest = 0f;
      bool found = false;
      foreach (var h in hits)
      {
          if (!found || h.point.y < lowest) { lowest = h.point.y; found = true; }
      }
      return lowest;
  }

よくあるハマりどころ

症状原因
NPCが動かないN avMeshが焼かれていない/Agentが青い面の外にいる
床をすり抜ける/浮く地面がNavMeshに含まれてい ない、地面スナップが高所を拾っている
目的地に行けない経路が途切れている(段差・隙間)。Agent Radius/段差設定を見直す
歩いてるのにアニメが止まるAgentの速度をAnimatorに渡していない
全員同じ動きで不自然開始ウェイポイントをばらけさせる/速度に個体差をつける

まとめ

  • 歩ける床をNavMeshにBakeし、NPCにNavMeshAgentを付ける
  • SetDestinationで目的地へ。経路探索と回避は自動
  • 複数ウェイポイントを巡回させ、開始位置をばらけさせると群衆が自然に
  • Agentの速度をAnimatorに渡して歩行アニメと連動
  • 地面スナップはRaycastAllで最も低い面を採用(高所誤判定を防ぐ)

NavMeshを使えば、街中を自然に歩き回る通行人を少ないコードで量産できます。シリーズ①〜④で作った1体の作り込みと組み合 わせれば、賑わいのある街のシーンが完成します。まずは床を1つBakeして、NPCを1体歩かせるところから始めてみてください。