Refactor navmesh building to support updating all scenes automatically without specifying one

#3744
This commit is contained in:
Wojtek Figat
2026-01-30 13:11:56 +01:00
parent 2bf9efaf30
commit 9fc9382e58
14 changed files with 145 additions and 135 deletions

View File

@@ -229,7 +229,7 @@ namespace FlaxEditor.Modules
if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = actor.BoxWithChildren;
Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}

View File

@@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors
var options = Editor.Instance.Options.Options.General;
if (options.AutoRebuildNavMesh)
{
Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain
{
if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation))
{
Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(scene);
@@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo
}
// Update navmesh
var scene = Terrain.Scene;
if (_navmeshBoundsModifications != null)
{
foreach (var bounds in _navmeshBoundsModifications)
Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs);
}
Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene);

View File

@@ -303,7 +303,7 @@ namespace FlaxEditor.Actions
if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = node.Actor.BoxWithChildren;
Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -121,12 +121,12 @@ namespace FlaxEditor
// Handle simple case where objects were moved just a little and use one navmesh build request to improve performance
if (data.BeforeBounds.Intersects(ref data.AfterBounds))
{
Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs);
}
else
{
Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs);
Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}

View File

@@ -6,7 +6,7 @@
#if USE_EDITOR
#include "Editor/Editor.h"
#include "Editor/Managed/ManagedEditor.h"
#include "NavMeshBuilder.h"
#include "Navigation.h"
#endif
NavMeshBoundsVolume::NavMeshBoundsVolume(const SpawnParams& params)
@@ -55,9 +55,30 @@ void NavMeshBoundsVolume::OnBoundsChanged(const BoundingBox& prevBounds)
// Auto-rebuild modified navmesh area
if (IsDuringPlay() && IsActiveInHierarchy() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh())
{
BoundingBox dirtyBounds;
BoundingBox::Merge(prevBounds, _box, dirtyBounds);
NavMeshBuilder::Build(GetScene(), dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
if (_box.Intersects(prevBounds))
{
// Bounds were moved a bit so merge into a single request (for performance reasons)
BoundingBox dirtyBounds;
BoundingBox::Merge(prevBounds, _box, dirtyBounds);
Navigation::BuildNavMesh(dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
}
else
{
// Dirty each bounds in separate
Navigation::BuildNavMesh(prevBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
}
}
}
void NavMeshBoundsVolume::OnActiveInTreeChanged()
{
BoxVolume::OnActiveInTreeChanged();
// Auto-rebuild
if (IsDuringPlay() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh())
{
Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs);
}
}

View File

@@ -30,6 +30,7 @@ protected:
void OnDisable() override;
#if USE_EDITOR
void OnBoundsChanged(const BoundingBox& prevBounds) override;
void OnActiveInTreeChanged() override;
Color GetWiresColor() override;
#endif
};

View File

@@ -3,6 +3,7 @@
#if COMPILE_WITH_NAV_MESH_BUILDER
#include "NavMeshBuilder.h"
#include "Navigation.h"
#include "NavMesh.h"
#include "NavigationSettings.h"
#include "NavMeshBoundsVolume.h"
@@ -706,6 +707,7 @@ struct BuildRequest
ScriptingObjectReference<Scene> Scene;
DateTime Time;
BoundingBox DirtyBounds;
bool SpecificScene;
};
CriticalSection NavBuildQueueLocker;
@@ -713,6 +715,7 @@ Array<BuildRequest> NavBuildQueue;
CriticalSection NavBuildTasksLocker;
int32 NavBuildTasksMaxCount = 0;
bool NavBuildCheckMissingNavMeshes = false;
Array<class NavMeshTileBuildTask*> NavBuildTasks;
class NavMeshTileBuildTask : public ThreadPoolTask
@@ -776,13 +779,13 @@ void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime)
NavBuildTasksLocker.Unlock();
}
void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y)
void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y, NavMesh* navMesh)
{
NavBuildTasksLocker.Lock();
for (int32 i = 0; i < NavBuildTasks.Count(); i++)
{
auto task = NavBuildTasks[i];
if (task->Runtime == runtime && task->X == x && task->Y == y)
if (task->Runtime == runtime && task->X == x && task->Y == y && task->NavMesh == navMesh)
{
NavBuildTasksLocker.Unlock();
@@ -838,7 +841,7 @@ void NavMeshBuilder::Init()
Level::SceneUnloading.Bind<OnSceneUnloading>();
}
bool NavMeshBuilder::IsBuildingNavMesh()
bool Navigation::IsBuildingNavMesh()
{
NavBuildTasksLocker.Lock();
const bool hasAnyTask = NavBuildTasks.HasItems();
@@ -847,7 +850,7 @@ bool NavMeshBuilder::IsBuildingNavMesh()
return hasAnyTask;
}
float NavMeshBuilder::GetNavMeshBuildingProgress()
float Navigation::GetNavMeshBuildingProgress()
{
NavBuildTasksLocker.Lock();
float result = 1.0f;
@@ -1023,7 +1026,7 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo
for (const auto& tile : unusedTiles)
{
// Wait for any async tasks that are producing this tile
CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y);
CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y, navMesh);
}
runtime->Locker.Lock();
for (const auto& tile : unusedTiles)
@@ -1106,31 +1109,6 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild
{
BuildDirtyBounds(scene, navMesh, dirtyBounds, rebuild);
}
// Remove unused navmeshes
if (settings->AutoRemoveMissingNavMeshes)
{
for (NavMesh* navMesh : scene->Navigation.Meshes)
{
// Skip used navmeshes
if (navMesh->Data.Tiles.HasItems())
continue;
// Skip navmeshes during async building
int32 usageCount = 0;
NavBuildTasksLocker.Lock();
for (int32 i = 0; i < NavBuildTasks.Count(); i++)
{
if (NavBuildTasks.Get()[i]->NavMesh == navMesh)
usageCount++;
}
NavBuildTasksLocker.Unlock();
if (usageCount != 0)
continue;
navMesh->DeleteObject();
}
}
}
void ClearNavigation(Scene* scene)
@@ -1144,6 +1122,40 @@ void ClearNavigation(Scene* scene)
}
}
void BuildNavigation(BuildRequest& request)
{
// If scene is not specified then build all loaded scenes
if (!request.Scene)
{
for (Scene* scene : Level::Scenes)
{
request.Scene = scene;
BuildNavigation(request);
}
return;
}
// Early out if scene is not using navigation
if (request.Scene->Navigation.Volumes.IsEmpty())
{
ClearNavigation(request.Scene);
return;
}
// Check if similar request is already in a queue
for (auto& e : NavBuildQueue)
{
if (e.Scene == request.Scene && (e.DirtyBounds == request.DirtyBounds || request.DirtyBounds == BoundingBox::Empty))
{
e = request;
return;
}
}
// Enqueue request
NavBuildQueue.Add(request);
}
void NavMeshBuilder::Update()
{
PROFILE_MEM(NavigationBuilding);
@@ -1158,9 +1170,10 @@ void NavMeshBuilder::Update()
if (now - req.Time >= 0)
{
NavBuildQueue.RemoveAt(i--);
const auto scene = req.Scene.Get();
Scene* scene = req.Scene.Get();
if (!scene)
continue;
bool rebuild = req.DirtyBounds == BoundingBox::Empty;
// Early out if scene has no bounds volumes to define nav mesh area
if (scene->Navigation.Volumes.IsEmpty())
@@ -1170,7 +1183,6 @@ void NavMeshBuilder::Update()
}
// Check if build a custom dirty bounds or whole scene
bool rebuild = req.DirtyBounds == BoundingBox::Empty;
if (rebuild)
req.DirtyBounds = scene->Navigation.GetNavigationBounds(); // Compute total navigation area bounds
if (didRebuild)
@@ -1178,26 +1190,37 @@ void NavMeshBuilder::Update()
else
didRebuild = true;
BuildDirtyBounds(scene, req.DirtyBounds, rebuild);
NavBuildCheckMissingNavMeshes = true;
}
}
// Remove unused navmeshes (when all active tasks are done)
// TODO: ignore AutoRemoveMissingNavMeshes in game and make it editor-only?
if (NavBuildCheckMissingNavMeshes && NavBuildTasksMaxCount == 0 && NavigationSettings::Get()->AutoRemoveMissingNavMeshes)
{
NavBuildCheckMissingNavMeshes = false;
NavBuildTasksLocker.Lock();
int32 taskCount = NavBuildTasks.Count();
NavBuildTasksLocker.Unlock();
if (taskCount == 0)
{
for (Scene* scene : Level::Scenes)
{
for (NavMesh* navMesh : scene->Navigation.Meshes)
{
if (!navMesh->Data.Tiles.HasItems())
{
navMesh->DeleteObject();
}
}
}
}
}
}
void NavMeshBuilder::Build(Scene* scene, float timeoutMs)
void Navigation::BuildNavMesh(Scene* scene, float timeoutMs)
{
if (!scene)
{
LOG(Warning, "Could not generate navmesh without scene.");
return;
}
// Early out if scene is not using navigation
if (scene->Navigation.Volumes.IsEmpty())
{
ClearNavigation(scene);
return;
}
PROFILE_CPU_NAMED("NavMeshBuilder");
PROFILE_CPU();
PROFILE_MEM(NavigationBuilding);
ScopeLock lock(NavBuildQueueLocker);
@@ -1205,36 +1228,15 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs)
req.Scene = scene;
req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs);
req.DirtyBounds = BoundingBox::Empty;
for (int32 i = 0; i < NavBuildQueue.Count(); i++)
{
auto& e = NavBuildQueue.Get()[i];
if (e.Scene == scene && e.DirtyBounds == req.DirtyBounds)
{
e = req;
return;
}
}
NavBuildQueue.Add(req);
req.SpecificScene = scene != nullptr;
BuildNavigation(req);
}
void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs)
void Navigation::BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene, float timeoutMs)
{
if (!scene)
{
LOG(Warning, "Could not generate navmesh without scene.");
return;
}
// Early out if scene is not using navigation
if (scene->Navigation.Volumes.IsEmpty())
{
ClearNavigation(scene);
return;
}
PROFILE_CPU_NAMED("NavMeshBuilder");
if (dirtyBounds.GetVolume() <= ZeroTolerance)
return; // Skip updating empty bounds
PROFILE_CPU();
PROFILE_MEM(NavigationBuilding);
ScopeLock lock(NavBuildQueueLocker);
@@ -1242,8 +1244,8 @@ void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float t
req.Scene = scene;
req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs);
req.DirtyBounds = dirtyBounds;
NavBuildQueue.Add(req);
req.SpecificScene = scene != nullptr;
BuildNavigation(req);
}
#endif

View File

@@ -15,11 +15,7 @@ class FLAXENGINE_API NavMeshBuilder
{
public:
static void Init();
static bool IsBuildingNavMesh();
static float GetNavMeshBuildingProgress();
static void Update();
static void Build(Scene* scene, float timeoutMs);
static void Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs);
};
#endif

View File

@@ -2,7 +2,7 @@
#include "NavModifierVolume.h"
#include "NavigationSettings.h"
#include "NavMeshBuilder.h"
#include "Navigation.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Serialization/Serialization.h"
#if USE_EDITOR
@@ -83,7 +83,7 @@ void NavModifierVolume::OnBoundsChanged(const BoundingBox& prevBounds)
#else
const float timeoutMs = 0.0f;
#endif
NavMeshBuilder::Build(GetScene(), dirtyBounds, timeoutMs);
Navigation::BuildNavMesh(dirtyBounds, GetScene(), timeoutMs);
}
#endif
}

View File

@@ -382,30 +382,6 @@ bool Navigation::RayCast(const Vector3& startPosition, const Vector3& endPositio
return NavMeshes.First()->RayCast(startPosition, endPosition, hitInfo);
}
#if COMPILE_WITH_NAV_MESH_BUILDER
bool Navigation::IsBuildingNavMesh()
{
return NavMeshBuilder::IsBuildingNavMesh();
}
float Navigation::GetNavMeshBuildingProgress()
{
return NavMeshBuilder::GetNavMeshBuildingProgress();
}
void Navigation::BuildNavMesh(Scene* scene, float timeoutMs)
{
NavMeshBuilder::Build(scene, timeoutMs);
}
void Navigation::BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs)
{
NavMeshBuilder::Build(scene, dirtyBounds, timeoutMs);
}
#endif
#if COMPILE_WITH_DEBUG_DRAW
void Navigation::DrawNavMesh()

View File

@@ -84,9 +84,7 @@ public:
/// <returns>True if ray hits a matching object, otherwise false.</returns>
API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo);
public:
#if COMPILE_WITH_NAV_MESH_BUILDER
/// <summary>
/// Returns true if navigation system is during navmesh building (any request is valid or async task active).
/// </summary>
@@ -100,32 +98,49 @@ public:
/// <summary>
/// Builds the Nav Mesh for the given scene (discards all its tiles).
/// </summary>
/// <remarks>
/// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
/// </remarks>
/// <param name="scene">The scene.</param>
/// <remarks>Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.</remarks>
/// <param name="scene">The scene. Pass null to build navmesh for all loaded scenes.</param>
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
API_FUNCTION() static void BuildNavMesh(Scene* scene, float timeoutMs = 50);
API_FUNCTION() static void BuildNavMesh(Scene* scene = nullptr, float timeoutMs = 50);
/// <summary>
/// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
/// </summary>
/// <remarks>
/// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.
/// </remarks>
/// <remarks>Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.</remarks>
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
/// <param name="scene">The scene. Pass null to build navmesh for all loaded scenes that intersect with a given bounds.</param>
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene = nullptr, float timeoutMs = 50);
/// <summary>
/// Builds the Nav Mesh for all the loaded scenes (builds only the tiles overlapping the given bounding box).
/// </summary>
/// <remarks>Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.</remarks>
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, float timeoutMs = 50)
{
BuildNavMesh(dirtyBounds, nullptr, timeoutMs);
}
/// <summary>
/// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
/// [Deprecated in v1.12]
/// </summary>
/// <remarks>Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls.</remarks>
/// <param name="scene">The scene.</param>
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
API_FUNCTION() static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50);
API_FUNCTION() DEPRECATED("Use BuildNavMesh with reordered arguments instead") static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50)
{
BuildNavMesh(dirtyBounds, scene, timeoutMs);
}
#endif
#if COMPILE_WITH_DEBUG_DRAW
/// <summary>
/// Draws the navigation for all the scenes (uses DebugDraw interface).
/// </summary>
static void DrawNavMesh();
#endif
};