【Unity】NavMesh(AI Navigation)完全理解
はじめに
こんにちは、ambr, Inc.でクライアントエンジニアをしているサックーです。
この記事はambr, Inc. Advent Calendar 2024 25日目の記事です。
今回はUnity公式パッケージの1つであるAI Navigationについて、そこそこ詳細に解説していきます。
またこの記事の内容はIwakenLab Tech Conference 2024で発表した内容を基に、大幅な加筆修正を行ったものになります。
この記事で分かることは
- AI Navigationパッケージの基本の使い方
- 各コンポーネントの効果と使い方
- 実際に使う際のTipsなど
となります。
検証環境は
- Unity 2022.3.22f1
- AI Navigation 1.1.5
となっています。
Ai Navigationパッケージの対応バージョンは、1.1.xでUnity2022.2以降、最新の2.0.xでUnity6以降となっています。
また、Unity2022.2より前のバージョンにおけるNavigationSystemには基本的に触れません。歴史的経緯として、旧来のNavigationSystemにNavMeshComponentsという別で開発されていたパッケージが統合されたものがAI Navigationなので、その組み合わせで使う場合はおおむね同等のことができます。
AI Navigationとは
AI Navigationパッケージとは、Unity公式から提供されている主にキャラクターの移動の制御をするAIシステムとなっています。
AIとはいっても昨今の生成AIとは関係がなく、A*という経路探索アルゴリズムが使われています。
もっとも一般的な用途はゲームにおけるNPCの移動制御だと思われます。
「敵キャラクターがプレイヤーに向かって移動してくる」などの挙動を簡単に実現できます。
キャラクターをプレイヤーに向けて動かしている様子
基本の使い方
基本的には事前に移動できる範囲を設定し、ランタイムでキャラクターに目的地を設定して動かすという流れになります。
この章では、最低限キャラクターにプレイヤーを追いかけさせるまでを実際にやってみます。
パッケージのインストール
まず最初にパッケージをインストールしましょう。
PackageManagerのUnity RegistryでAI Navigationと検索すればヒットします。
移動できる範囲の設定(NavMeshのベイク)
移動できる範囲のことをNavMeshと言い、それを設定することをベイクすると言います。
準備として適当な地面を準備しましょう。Cubeなどで良いです。
その際対象のメッシュが1つの空のGameObject配下になるようにしてください。
そしてそのGameObjectにNavMeshSurface
コンポーネントを付けます。
CollectObjects
はCurrentObjectHierarchy
にしてください。
これによりこのGameObject配下のObjectがこのコンポーネントの作用対象となります。
次に右下にあるBakeボタンを押してください。
しばらく待つとNavMeshがベイクされてシーンビューで確認できると思います。
見えない場合はGizmo表示が有効になっているか確認してください。
水色の部分が移動可能なエリアで、元のメッシュの表面に張り付くように新たなメッシュが生成されていることが分かると思います。
キャラクターにプレイヤーを追いかけさせる
今度は追いかける側の仕組みを実装していきます。
適当なキャラクターのRootにNavMeshAgent
というコンポーネントを付けます。
階層については、このコンポーネント配下が動くことを意識して貰えればよいです。
ちなみにキャラクターと言っていますが実体は何でもよく、ただのCubeでもいいですし空オブジェクトでも支障ありません。
AgentTypeのHumanoidもRig構造のHumanoidとは一切関係ありません。
後はスクリプトで目的地を設定してあげるわけですが、以下の関数を使うだけで非常に簡単です。
仮にターゲットのTransformを追いかけさせるには、以下のように書けばよいです。
using UnityEngine;
using UnityEngine.AI;
public class NavMeshAgentController : MonoBehaviour
{
[SerializeField]
private NavMeshAgent agent;
[SerializeField]
private Transform target;
private void Update()
{
agent.SetDestination(target.position);
}
}
これだけで図のようなことが実現できました。
概念やコンポーネント詳細解説
ここからはより実戦的な要件に対応するために、いくつかの概念とそれぞれのコンポーネントについて解説していきます。
AreaとCost
経路算出の際に距離が使われるわけですが、その距離に係数をかけることができ、その値がCostで値ごとに名前を付けたものがAreaです。
例えばCostが2のArea部分の経路は、実際より2倍の長さとして計上されるようになります。
Window > AI > NavigationウィンドウのAreasタブから設定できます。
上3つは既定のものになっていますが、NotWalkable以外はCostを変更可能です。
0のWalkableが普通の歩ける場所です。
1のNotWalkableは移動不可エリアであり、常に最優先されます。
2のJumpは後述の自動生成されるLinkに割り当てられます。
それ以降はユーザー側が任意のAreaとCostを設定可能です。
例としては画像にあるように「水の中だからコストを上げよう=できるだけ水を避けて動くようにさせよう」のようなことができます。
水を避けて移動する様子
実際にAreaを適用するやり方は後述します。
Agent
AI Navigationで動かす対象のことをAgentと言い、AgentTypeという種類ごとに様々なパラメータを設定可能です。
さらに個体ごとのパラメーターも存在していますが、種類ごとのパラメータはWindow > AI > NavigationウィンドウのAgentsタブから設定できます。
Humanoidの設定
Enemyの設定
上の例ではHumanoidとEnemyという2つのAgentType設定しています。
簡単に言うと身長や体幅、どれだけの段差を越えられるかなどの値を与えることができ、これによって移動できる範囲が変わります。
下段のGeneratedLinksの項目は後述のNavMeshSurfaceで生成するLinkの最大距離などにかかわります。
背の高いロボット(Humanoid)は屋根の下に入れない様子
幅の広いドラゴン(Enemy)は狭い通路を通れない様子
NavMeshAgent
今度はコンポーネントの方のAgentです。
これは実際に動かすキャラクターのモデルごとにアタッチする必要があり、AgentTypeを選んだうえで個体ごとのパラメータを設定できます。
BaseOffsetはキャラクターとするモデルのRootとNavMeshAgentがついているGameObjectのRoot位置がずれていた場合の補正値だそうです。
https://docs.unity3d.com/Packages/com.unity.ai.navigation@1.1/manual/AboutAgents.html
Steeringでは移動周りのパラメータを設定します。
移動速度、回転角速度、加速度の他、目的地から何m手前で止まるか、自動ブレーキをかけるかを設定できます。
ObstacleAvoidanceでは動的な障害物を避ける関係のパラメータを設定します。
動的な障害物とは、他のAgentや後述のNavMeshObstacleのうちCurveが無効の物です。
RadiusとHeightについてはNavigationウィンドウで設定したAgentごとの値とは別の値を設定することもできます。
Priorityについては衝突時の優先度です。値が小さい方が優先度が高く、優先度の低い対象を無視して進みます。
大きなRadiusを設定した高優先度のロボットがドラゴンを押しのけて進む様子
NavMeshSurface
NavMeshを設定していくうえで最も重要なコンポーネントです。
AgentTypeはこのコンポーネントで生成するNavMeshを使う対象で、その身長などのパラメータによって生成される結果が変わります。
複数のAgentTypeを運用する場合は、少なくともその種類分だけNavMeshSurfaceを用意する必要があります。
具体的な運用についてはTipsの章で紹介します。
DefaultAreaはこのコンポーネントで生成したNavMeshに適用されるAreaです。後述のNavMeshModifierコンポーネントで上書きができます。
GenerateLinksは孤立したNavMeshとの間にLinkを生成するかのフラグです。Trueの場合、対象のAgentTypeのDropHeight, JumpDistanceの値に応じて不連続のメッシュ間にLink(テレポート)が生成されます。
Jump Distance未満の距離にあるメッシュへのLinkが貼られている様子
Use GeometryはNavMeshをベイクする際に参照するジオメトリの種類です。通常のメッシュかコライダーのどちらかを選べます。
CollectObjectsはこのコンポーネントが対象とするオブジェクトの選び方です。
- All Game Objects
- Volume
- Current Object Hierarchy
- NavMeshModifier Component Only
の4つから選べます。
All Game Objectsは、シーンでアクティブになっている全てのGameObjectを対象とします。
Volumeは、指定した矩形の中が対象となります。オブジェクト単位では無くて純粋に範囲指定のようです。
Current Object Hierarchyは、このコンポーネントがついているGameObjectの全ての子を対象とします。
NavMeshModifier Component Onlyは、後述のNavMeshModifierコンポーネントが付いているGameObjectを対象とします。
IncludeLayersは上記のCollectObjectsとANDでかかる条件です。一部のレイヤーのオブジェクトを対象から外したりできます。
OverrideVoxelSizeはジオメトリをどの程度正確に処理するかの値の制御です。広い場所では値を大きくし、狭い場所では小さくするとよいようですが、ベイク速度と正確性はトレードオフです。
OverrideTileSizeはTileSizeの制御です。TileSizeが小さい方がメモリ効率が上がる分、NavMeshが断片化されてしまうようです。
MinimumRegionAreaは、存在を許されるNavMeshの最小面積(m^2)です。この値以下の面積の孤立したNavMeshはベイク時に取り除かれます。
BuildHeightMeshは、NavMeshとは別にキャラクターを立たせるための実際のジオメトリに沿ったメッシュを生成するかの設定です。
階段などでNavMeshが斜めになっている場所でも、実際のメッシュに沿って立たせることができるようになります。
左がBuildHeightMeshが有効な場合、右が無効な場合
NavMeshModifire
主にNavMeshSurfaceと組み合わせて使い、基本的にその設定を上書きするために使います。
影響を受けるSurfaceは後述のAffectedAgentsによって決まります。
ModeはAdd or Modify ObjectとRemove Objectがあります。
前者はNavMeshSurfaceでは対象外のオブジェクトを対象に含めたり、その設定を上書きするために使います。
後者は逆に対象となっているオブジェクトを対象外にできます。
AffectedAgentsは影響を受けるAgentTypeの指定です。
ApplyToChildrenは自身の子オブジェクトも対象にするかどうかです。NavMeshSurfaceのCollectObjects
でNavMeshModifier Component Only
を選んだ場合、このオプションはそれにも反映されます。
Add or Modify Object
モードの場合、下2つのオプションが使用できます。
OverrideAreaではNavMeshSurfaceで設定したDefaultArea
でないAreaに変更できます。
「基本的には1つのNavMeshSurfaceで一括で設定しつつ、ここだけ別のAreaにする」といったことができます。
OverrideGenerateLinksはLinkを生成するかの設定を上書きできます。
NavMeshObstacle
このコンポーネントを使うと動的な障害物を作ったり、動的にNavMeshに穴をあけたりできます。
Shapeは障害物の形状で、BoxかCapsuleが選べます。
CarveはNavMeshに穴をあけるかどうかのフラグです。
Trueの時、このコンポーネントと接しているNavMeshにShapeに沿って穴が開きます。
Falseの時、NavMeshへの影響は与えず動的な障害物(Agentと同等)としてふるまいます。Agentを押しのけることも可能です。
MoveThresholdは、これが動いたと判定する距離の閾値(m)で、これを超えるとNavMeshの再計算が必要な状態となります。
TimeToStationaryは、止まったと判定される時間の閾値(s)で、この値以上の時間静止していた場合このオブジェクトが止まったと判定されます。
CarveOnlyStationaryは、静止時にのみNavMeshに穴をあけるかどうかのフラグです。
Trueの場合、MoveThreshold
を越えて動いた時に一旦NavMeshから穴が消えます。そしてTimeToStationary
を越えて静止した場合にNavMeshの再計算が行われ、NavMeshに穴が開きます。なお、動いている間は動的な障害物としてふるまいます。
Falseの場合、常にNavMeshの再計算が行われNavMeshの穴が更新され続けます。ただしその位置はObstacleの1フレーム前の物が適用されるようです。
CarveOnlyStationary=trueのObstacleを動かしている様子
NavMeshLink
任意の2点間をつなぐテレポートを作ることができます。
AgentTypeはこのテレポートを利用可能なAgentTypeです。
StartPoint及びEndPointはこのテレポートがつなぐ2地点です。自身のからの相対位置になります。
後述のBidirectional
がfalseの場合はStart→Endの一方通行になります。
ちなみにAI Navigation v2以降では、この地点はTransformを参照するように改修が入っています。
Widthは、このテレポートの幅です。橋のようなイメージになります。
CostModifierは、後述のAreaTypeで設定したAreaのCostに係数をかけることができます。
自然数の場合にのみ適用されます。ただ、Areaを選べるのでそちらで調整すればよい気はします…。
Auto Update Positionは、Start及びEndPoint、GameObjectが移動したときにリンクの終端を自動的に移動する機能らしいですが、動作を確認できませんでした。
Bidirectionalは、移動方向の制御です。Trueの場合は双方向に行き来が可能ですが、Falseの場合はStart→Endの一方通行になります。
AreaTypeは、このテレポートを使用した経路を算出するときに適用されるAreaです。
Bidirectional=trueでWidth=0.5のNavMeshLink
Tips集
ここでは実例に基づいた関連するTipsについて書いていきます。
複数のAgentTypeを運用する場合のNavMeshの作り方
NavMeshSurfaceの紹介時に、複数のAgentTypeを運用する場合はその数だけNavMeshSurfaceが必要と書きましたが、共通部分や独自部分をうまく構築するためにどのようなHierarchy構成にするのが良さそうか、僕なりの例を紹介します。
例示していたシーンでは、RootにHumanoidを選択したSurface(図のNavMeshSurface_Humanoid)、その子にEnemyを選択したSurface(NavMeshSurface_Common)を配置しました。
そして共通部分のメッシュをCommon配下に置き、Humanoid専用のメッシュをHumanoid直下に配置しました。
CollectObjects
はCurrent Object Hierarchy
を選択しています。
このようにTypeが2種類のみで片方にしか専用部分がない場合はSurfaceの入れ子によって実現ができます。
ただし、3種類以上になったり、それぞれに専用部分がある場合はこれでは不十分です。
そこでNavMeshModifireを用いて制御することにします。
AgentTypeの数だけNavMeshSurfaceを用意するところまでは一緒ですが、階層関係はなくフラットで、CollectObjects
はNavMeshModifier Component Only
を選択します。
そして今度はAgentTypeの組み合わせ分のNavMeshModifire
を作成します。
共通部分であればAffectedAgents
はAllを選択し、特定のTypeのみが行ける場所ではそのTypeを選択します。複数選択も可能です。
さらにApplyToChildren
にチェックを入れることでそれの子にも設定を適用し、子としてメッシュを配置します。
必要に応じてMode
をRemove Object
にしたModifireを子要素にアタッチすることで、それだけ除外するなどの制御も可能ですが管理しきれなくなる気はします。
このようにすることで複数種類のAgentTypeを運用する際も効率的にNavMeshを生成できるのではないでしょうか。
グレーが全種類、白がHumanoidのみ、茶がEnemyのみ、緑がEnemy&Animal
移動に合わせてキャラクターをアニメーションさせたい
おそらくキャラクターを動かすにあたって、その動作に応じたアニメーションをさせたいというのはほぼ必ず発生すると思います。
その際役立つのがNavMeshAgent.velocity
です。この値をfloatにしてAnimatorのパラメータに流し込んであげれば、簡単に待機状態と移動状態でアニメーション切り替えを実現できると思います。
リアルタイムでNavMeshを生成したい
NavMeshは基本的に事前に焼き付けておくと書きましたが、リアルタイムに生成・更新していくことも可能です。
特にARの文脈だと、環境メッシュに対してNavMeshを更新していきたい需要があると思います。
その場合NavMeshSurface.BuildNavMesh()
とNavMeshSurface.UpdateNavMesh()
を使うことで実現できます。
1回BuildNavMesh()
を呼んだ後に、NavMeshSurface.navMeshData
を渡してUpdateNavMesh()
を呼び続けてあげればよいです。
using Unity.AI.Navigation;
using UnityEngine;
public class NavMeshUpdater : MonoBehaviour
{
[SerializeField]
private NavMeshSurface navMeshSurface = null;
void Start()
{
navMeshSurface.BuildNavMesh();
}
void Update()
{
navMeshSurface.UpdateNavMesh(navMeshSurface.navMeshData);
}
}
NavMeshが存在しない状態から実行し、ランタイムでメッシュの移動や複製をした様子
下記例はAR Foundation
で生成した環境メッシュを対象としたNavMeshSurfaceを用意し、それに対して指定間隔でNavMeshを更新させています。
毎フレーム呼んで大丈夫なのかなど負荷の計測は行っていないです。
NavMeshをきれいにBakeするには
例示したものはどれもCubeなどのプリミティブな図形のみで構成した地形を扱っていましたが、実際のシーンではもっと複雑なメッシュが用いられることがほとんどです。
その際、見せる用のメッシュをそのままNavMeshBakeに流用すると、思わぬアーティファクトが発生することが多いです。
それにより経路探索に時間がかかったり、思わぬ経路ができてしまうことが起こりえます。
そこでシンプルな形状のモデルや、プリミティブなモデルで構成したBake専用のメッシュを用意し、それに対してBakeを行うことで綺麗にBakeができます。
簡略化したBake専用メッシュ
見せる用のメッシュをそのまま使った例
専用のメッシュを使った場合
どうしても旧来のNavigationSystemをUnity2022.2以降で使いたい
古いプロジェクトをアップデートした際などに、古い方をそのまま使いたいということがあると思います。
結論から言うとまだ利用可能です。
NavMeshのベイクをしたい際は、 Window > AI > Navigation (Obsolete) ウィンドウを開くことでできます。
大きな問題として、StaticからNavigation Static
を選択不可になっている点があります。
ただし、Everything
にチェックを入れることでNavigation Static
にもチェックを入れることができるので、必要に応じて他を外す運用で回避は可能です。とは言え手間なので何かしらエディタスクリプトを用意する方が良さそうです。
NavigationSystemからAI Navigationに移行したい
実は公式で移行ツールが提供されています。
Window > AI > NavMesh Updaterから開くことができます。
NavMesh Scene Converterにチェックを入れ、Initialize Convertersボタンを押すと、対象となるシーンが選択されます。
その状態でConvert Assetsボタンを押すと処理が走ります。
成功するとNavigation Static
がついていたオブジェクトに対して、Staticフラグの変更とNavMeshModifireコンポーネント追加が行われます。
そこでNavMeshSurfaceをNavMeshModifier Component Only
モードでBakeすれば、これまでのものと同様の結果が得られるはずです。
まとめ
それなりの分量になってしまいましたが、新しいAI Navigationになってからの記事が少なかったのでしっかり目に書きました。
このパッケージを使うことで、経路探索してキャラクターを動かすといったことが比較的簡単に実現できることをお伝え出来たかと思います。
もし機会があればぜひ使ってみてください!
また、例示した動画で出てきたプロジェクトは下記リンク先でVRChatワールドとして公開しているので、併せて訪れていただければ幸いです。
Discussion