【UE】動的NavMeshを扱うときの注意点

今回はUnreal Engineで動的NavMeshを扱うときの注意点と対処法について紹介します。

Information

今回の記事は、Unreal Engine 4 時点で得た知見となります。
そのため、情報がやや古いかもしれませんが、現在のバージョンでも役に立つ部分があるかもしれないと思い、記事にまとめました

動的NavMeshとは

通常、NavMeshは決まった地形(メッシュ)に合わせて事前に作っておきますが、扉が開いてルートが増えたり、オブジェクトが壊れて道を塞ぐなどルートが変化することがあります。

そういった場合に、ランタイム(ゲーム実行中)に NavMesh を作り直したい場合、動的NavMeshを使用します。

動的NavMeshの注意点

NavMeshのルートを作り変えることを「NavMeshの構築」と呼びます。

この再構築はとても負荷の高い処理であることを開発者は理解する必要があります。

NavMeshは内部的にスレッドセーフ (複数スレッドからDirty Areaの更新があっても破綻しない) を強く意識した設計となっており、再構築中は NavMesh データへの同時アクセスが制限されます。

その結果、NavMeshの構築中に別の再構築リクエストが実行されたり、NavMeshの走査が行われると CPUストールが発生し、大幅なロスが生まれます。

NavMesh構築はスレッドで動作していますが、デフォルトの稼働スレッドは "1tickあたりコアx2" しか処理しないようになっています。(UE4.x系時点)

Information

UE5以降では、内部実装が一部変更されておりもう少し増えている可能性があります。

ただそれでもスレッドセーフの仕様が変わらない限り、NavMesh再構築中に構築リクエストが発生すると、致命的な速度低下が発生する可能性が高いことは変わらないと思われます。

更新タイミングの設定

NavMeshの更新タイミングは「プロジェクト設定 > エンジン > ナビゲーションシステム」から「Navigation System > ダーティ領域の更新頻度」で設定できます。

Information

UE5.5.4で確認したところ、更新頻度の項目は廃止されて変更不可となったようです。

どれくらいの「領域」が更新されるのか

NavMeshは空間ごとに「タイル」と呼ばれる単位で生成されていきます。そして「タイル」→「セル」→「各メッシュポリゴン」といった管理方式を持っています。

タイルサイズは「プロジェクト設定」、レベルごとに作成したNavMeshのインスタンス (RecastNavMesh) にて設定可能で、このタイルサイズに基づき、NavMeshの再構築リクエストが実行されると、そのリクエスト領域に合致するタイルがすべて再構築されます。(合致しないタイルは再構築されません)

Information

UE5ではタイルごとに「解像度パラメータ (プリセット)」を定義できるようになりました。(UE5.5.4で確認)

細かくは確認できていませんが、NavModifierVolumeごとに「Low・Default・Hight」の解像度のプリセットを指定できるようになったようです。

再構築リクエストはどの「タイミング」で実行されるのか

NavMeshの再構築リクエストは、"NavMesh影響" を及ぼす設定を行っているインスタンスのTransformが変動した際などに行われます。"NavMesh影響" を及ぼすかどうかの設定は、 UActorComponentにて定義されています。(例えば StaticMeshComponent、SkeletalMeshComponent、PrimitiveComponentなどです)

そして再構築に影響を及ぼすかどうかは「デフォルトがON」になっているため、以下の方針で適切に ON/OFF を設定すると良いです。

ケース設定具体例
シーン中は動かない再構築が起きないのでON壁、床など
シーン中に動く。しかし変動タイミングは限定的「更新頻度:低い=処理負荷への影響:低い」のでON破壊オブジェクトなど
シーン中に "常に" 動く "小さな" オブジェクト「更新頻度:高い=処理負荷への影響:高い」のでOFF物理挙動する小物など
シーン中に "常に" 動く "大きな" オブジェクト処理負荷と要相談だが、基本的にOFF移動床など

"NavMesh影響" を及ぼすかどうかの設定箇所は、以下のように各BPやインスタンスのコンポーネントの詳細で設定、またはC++・BPで CanEverAffectNavigation関数を呼び出して設定します。(UE 4.x時点)

Information

UE5.4.4でも "Can Ever Affect Navigation" の設定項目は存在しており、ここでの設定は可能のようです。

ただし、BPからの呼び出しができなくなっており、C++であれば以下のコードで変更が可能でした。

USceneComponent* RootComp = GetRootComponent();
RootComp->SetCanEverAffectNavigation(false); // ナビゲーションに影響を与えないように設定

対策①:再構築によるCPUストールを防ぐ方法

構築中の再構築やNavMeshの走査によるCPUストールを防ぐには、主に以下の方針にすると良いです。

  • NavMesh構築時間を監視するシステムを作る: これは直接の解決ではありませんが、ゲームが許容する構築時間を監視し警告を出力するシステムを作ります
  • 同時に複数のNavMesh再構築が起きないようにする:多重のNavMesh再構築がCPUストールの主な原因です。これを防ぐのが最も効果的です
  • NavMeshが構築中に再構築リクエストを送らないように待ちを入れる:暗転中以外のゲームプレイ中に再構築リクエストが重複する場合は、演出を入れるなどで待ち処理を誤魔化す仕組みがあると良いです

前述の「再構築リクエストのタイミング」の項で説明した通り、ゲームでの目的や扱いによって “NavMesh影響” を適切に ON/OFF します。

NavMeshが構築中かどうかの判定処理は BPには提供されていません。

そのため以下のようなC++コードで実装する必要があります。

bool UCPP_SystemFunctions::IsNavigationBuildInProgress( UWorld* World )
{
  if ( !World )	return false;
  UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World);
  if ( !NavSys )	return false;

  bool isProgress = NavSys->IsNavigationBuildInProgress( );
  if ( isProgress )	return true;	// ビルド中.

  // ----------------------------------------------------
  // 以下はビルド終了後の処理.
  return false;
}
Information

上記コードは “ゲームスレッド” で実行されることが前提となります。

また厳密には非同期タスク全体の完了チェックではなく「NavMesh関連タスクが残っているかどうか」の判定となります。

例えば、ゲーム開始前のロード画面での暗転中に、自動生成された地形に NavMesh を構築し、その後 “NavMesh影響” を持ったオブジェクトを Spawn したいときがあると思います。しかしこれが行われると、オブジェクトの Spawn ごとに NavMeshの再構築が繰り返し実行され、CPUストールが発生します。

これを防ぐには NavMesh のビルド (構築) を一時的に停止すると良いです。

これもBPには用意されておらず、C++で自作する必要があります。

// ビルドロック
void UCPP_SystemFunctions::NavimeshBuildLock(UWorld* World)
{
  if (UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
  {
    uint8 id = 1; // フラグ番号.
    NavSys->AddNavigationBuildLock( id );
  }
}

// ビルドアンロック
void UCPP_SystemFunctions::NavimeshBuildUnlock(UWorld* World)
{
  if (UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
  {
    uint8 id = 1; // フラグ番号.
    NavSys->RemoveNavigationBuildLock( id );
  }
}
Information

このフラグは Lock ID でありシステム内でユニークである必要があります。そのため他のロックと衝突すると解除できなくなる可能性があることに注意します。

そのため、ロック、アンロックで渡すフラグは UENUMクラスを定義しておくと良いと思います。

対策②:NavMesh構築のスレッド数を増やす

NavMesh構築のスレッドはデフォルトで1 tickあたり「コア x 2」なので増やしておきます。

これもC++での実装が必要で、以下は64コアに増やした例です。

void UCPP_SystemFunctions::NavimeshBuildPriority(UWorld* World, int Prio)
{
  // FRecastNavMeshGenerator::ProcessTileTasksAsyncのPendingDirtyTilesが
  // 4000タスクとかあるのに1tickにコア*2しか処理しないのでもっと処理する量を上げる
  int32 mult = (Prio == 0) ? 2 : 64;
  if (UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
  {
    for (ANavigationData* NavData : NavSys->NavDataSet)
    {
      if (!NavData) continue;
      if (auto* Generator = NavData->GetGenerator())
      {
        const int32 NumberOfWorkerThreads = FTaskGraphInterface::Get().GetNumWorkerThreads();
        auto maxTasks = FMath::Max(NumberOfWorkerThreads * mult, 1);
        ((FRecastNavMeshGenerator*)Generator)->SetMaxTileGeneratorTasks(maxTasks);
      }
    }
  }
}
Information

FRecastNavMeshGenerator*でキャストしている部分がありますが、これはエンジンバージョンに依存する可能性があります。

確実に動作するかどうか念入りに確認し、Shippingでの動作確認をおすすめします。

おしまい

以上、動的NavMeshの注意点でした。

UE4時点で得た知見なので、やや古い情報もありますが参考になれば嬉しいです。

\ 最新情報をチェック /