From 27ed23c1b9e8019bf000845f57712f5e09348b0a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 Jan 2021 14:28:46 +0100 Subject: [PATCH] Add support for multiple navmeshes on a scene --- Source/Engine/Level/Scene/Scene.cpp | 88 +++-- Source/Engine/Level/Scene/Scene.h | 37 +- .../{NavigationScene.cpp => NavMesh.cpp} | 133 ++++--- Source/Engine/Navigation/NavMesh.h | 76 ++++ .../Engine/Navigation/NavMeshBoundsVolume.cpp | 7 +- Source/Engine/Navigation/NavMeshBuilder.cpp | 274 +++++++------ Source/Engine/Navigation/NavMeshBuilder.h | 7 +- Source/Engine/Navigation/NavMeshRuntime.cpp | 276 ++++++++++++- Source/Engine/Navigation/NavMeshRuntime.h | 86 ++++- Source/Engine/Navigation/Navigation.cpp | 364 +++++++----------- Source/Engine/Navigation/Navigation.h | 4 +- Source/Engine/Navigation/NavigationScene.h | 94 ----- .../Engine/Navigation/NavigationSettings.cs | 66 ++++ Source/Engine/Navigation/NavigationSettings.h | 77 ++-- Source/Engine/Navigation/NavigationTypes.h | 49 ++- 15 files changed, 1012 insertions(+), 626 deletions(-) rename Source/Engine/Navigation/{NavigationScene.cpp => NavMesh.cpp} (52%) create mode 100644 Source/Engine/Navigation/NavMesh.h delete mode 100644 Source/Engine/Navigation/NavigationScene.h diff --git a/Source/Engine/Level/Scene/Scene.cpp b/Source/Engine/Level/Scene/Scene.cpp index 2d48b9f14..b86610ab9 100644 --- a/Source/Engine/Level/Scene/Scene.cpp +++ b/Source/Engine/Level/Scene/Scene.cpp @@ -8,7 +8,10 @@ #include "Engine/Physics/Colliders/MeshCollider.h" #include "Engine/Level/Actors/StaticModel.h" #include "Engine/Level/ActorsCache.h" -#include "Engine/Navigation/NavigationScene.h" +#include "Engine/Navigation/NavigationSettings.h" +#include "Engine/Navigation/NavMeshBoundsVolume.h" +#include "Engine/Navigation/NavMesh.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/Serialization.h" REGISTER_JSON_ASSET(SceneAsset, "FlaxEngine.SceneAsset"); @@ -27,7 +30,6 @@ Scene::Scene(const SpawnParams& params) , Ticking(this) , LightmapsData(this) , CSGData(this) - , Navigation(::New(this)) { // Default name _name = TEXT("Scene"); @@ -42,7 +44,6 @@ Scene::Scene(const SpawnParams& params) Scene::~Scene() { - Delete(Navigation); } LightmapSettings Scene::GetLightmapSettings() const @@ -55,6 +56,31 @@ void Scene::SetLightmapSettings(const LightmapSettings& value) Info.LightmapSettings = value; } +BoundingBox Scene::GetNavigationBounds() +{ + if (NavigationVolumes.IsEmpty()) + return BoundingBox::Empty; + PROFILE_CPU_NAMED("GetNavigationBounds"); + auto box = NavigationVolumes[0]->GetBox(); + for (int32 i = 1; i < NavigationVolumes.Count(); i++) + BoundingBox::Merge(box, NavigationVolumes[i]->GetBox(), box); + return box; +} + +NavMeshBoundsVolume* Scene::FindNavigationBoundsOverlap(const BoundingBox& bounds) +{ + NavMeshBoundsVolume* result = nullptr; + for (int32 i = 0; i < NavigationVolumes.Count(); i++) + { + if (NavigationVolumes[i]->GetBox().Intersects(bounds)) + { + result = NavigationVolumes[i]; + break; + } + } + return result; +} + void Scene::ClearLightmaps() { LightmapsData.ClearLightmaps(); @@ -216,12 +242,6 @@ void Scene::Serialize(SerializeStream& stream, const void* otherObj) // Update scene info object SaveTime = DateTime::NowUTC(); -#if USE_EDITOR - // Save navmesh tiles to asset (if modified) - if (Navigation->IsDataDirty) - Navigation->SaveNavMesh(); -#endif - LightmapsData.SaveLightmaps(Info.Lightmaps); Info.Serialize(stream, other ? &other->Info : nullptr); @@ -230,8 +250,6 @@ void Scene::Serialize(SerializeStream& stream, const void* otherObj) stream.JKEY("CSG"); stream.Object(&CSGData, other ? &other->CSGData : nullptr); } - - SERIALIZE_MEMBER(NavMesh, Navigation->DataAsset); } void Scene::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -243,7 +261,37 @@ void Scene::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) LightmapsData.LoadLightmaps(Info.Lightmaps); CSGData.DeserializeIfExists(stream, "CSG", modifier); - DESERIALIZE_MEMBER(NavMesh, Navigation->DataAsset); + // [Deprecated on 13.01.2021, expires on 13.01.2023] + if (modifier->EngineBuild <= 6215 && NavigationMeshes.IsEmpty()) + { + const auto e = SERIALIZE_FIND_MEMBER(stream, "NavMesh"); + if (e != stream.MemberEnd()) + { + // Upgrade from old single hidden navmesh data into NavMesh actors on a scene + AssetReference dataAsset; + Serialization::Deserialize(e->value, dataAsset, modifier); + const auto settings = NavigationSettings::Get(); + if (dataAsset && settings->NavMeshes.HasItems()) + { + auto navMesh = New(); + navMesh->SetStaticFlags(StaticFlags::FullyStatic); + navMesh->SetName(TEXT("NavMesh.") + settings->NavMeshes[0].Name); + navMesh->DataAsset = dataAsset; + navMesh->Properties = settings->NavMeshes[0]; + if (IsDuringPlay()) + { + navMesh->SetParent(this, false); + } + else + { + navMesh->_parent = this; + navMesh->_scene = this; + Children.Add(navMesh); + navMesh->CreateManaged(); + } + } + } + } } void Scene::OnDeleteObject() @@ -309,22 +357,6 @@ void Scene::EndPlay() Actor::EndPlay(); } -void Scene::OnEnable() -{ - // Base - Actor::OnEnable(); - - Navigation->OnEnable(); -} - -void Scene::OnDisable() -{ - Navigation->OnDisable(); - - // Base - Actor::OnDisable(); -} - void Scene::OnTransformChanged() { // Base diff --git a/Source/Engine/Level/Scene/Scene.h b/Source/Engine/Level/Scene/Scene.h index 628726e4b..d3c1502c6 100644 --- a/Source/Engine/Level/Scene/Scene.h +++ b/Source/Engine/Level/Scene/Scene.h @@ -12,8 +12,9 @@ class MeshCollider; class Level; -class NavigationScene; class ReloadScriptsAction; +class NavMeshBoundsVolume; +class NavMesh; /// /// The scene root object that contains a hierarchy of actors. @@ -69,11 +70,6 @@ public: /// CSG::SceneCSGData CSGData; - /// - /// The navigation scene (always valid). - /// - NavigationScene* Navigation; - /// /// Gets the lightmap settings (per scene). /// @@ -85,6 +81,31 @@ public: /// API_PROPERTY() void SetLightmapSettings(const LightmapSettings& value); +public: + + /// + /// The list of registered navigation bounds volumes (in the scene). + /// + Array NavigationVolumes; + + /// + /// The list of registered navigation meshes (in the scene). + /// + Array NavigationMeshes; + + /// + /// Gets the total navigation volumes bounds. + /// + /// The navmesh bounds. + BoundingBox GetNavigationBounds(); + + /// + /// Finds the navigation volume bounds that have intersection with the given world-space bounding box. + /// + /// The bounds. + /// The intersecting volume or null if none found. + NavMeshBoundsVolume* FindNavigationBoundsOverlap(const BoundingBox& bounds); + public: /// @@ -148,12 +169,10 @@ public: protected: - // [Scene] + // [Actor] void PostLoad() override; void PostSpawn() override; void BeginPlay(SceneBeginData* data) override; - void OnEnable() override; - void OnDisable() override; void OnTransformChanged() override; }; diff --git a/Source/Engine/Navigation/NavigationScene.cpp b/Source/Engine/Navigation/NavMesh.cpp similarity index 52% rename from Source/Engine/Navigation/NavigationScene.cpp rename to Source/Engine/Navigation/NavMesh.cpp index 2138db009..ef90e2989 100644 --- a/Source/Engine/Navigation/NavigationScene.cpp +++ b/Source/Engine/Navigation/NavMesh.cpp @@ -1,61 +1,33 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. -#include "NavigationScene.h" -#include "Navigation.h" +#include "NavMesh.h" #include "NavMeshRuntime.h" -#include "NavMeshBoundsVolume.h" -#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Level/Scene/Scene.h" -#include "Engine/Content/Assets/RawDataAsset.h" -#include "Engine/Core/Log.h" -#if USE_EDITOR -#include "Editor/Editor.h" -#endif +#include "Engine/Serialization/Serialization.h" #if COMPILE_WITH_ASSETS_IMPORTER #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/Serialization/MemoryWriteStream.h" +#if USE_EDITOR +#include "Editor/Editor.h" +#endif #endif -NavigationScene::NavigationScene(::Scene* scene) - : Scene(scene) +NavMesh::NavMesh(const SpawnParams& params) + : Actor(params) , IsDataDirty(false) { - DataAsset.Loaded.Bind(this); + DataAsset.Loaded.Bind(this); } -BoundingBox NavigationScene::GetNavigationBounds() -{ - if (Volumes.IsEmpty()) - return BoundingBox::Empty; - - PROFILE_CPU_NAMED("GetNavigationBounds"); - - auto box = Volumes[0]->GetBox(); - for (int32 i = 1; i < Volumes.Count(); i++) - BoundingBox::Merge(box, Volumes[i]->GetBox(), box); - return box; -} - -NavMeshBoundsVolume* NavigationScene::FindNavigationBoundsOverlap(const BoundingBox& bounds) -{ - NavMeshBoundsVolume* result = nullptr; - - for (int32 i = 0; i < Volumes.Count(); i++) - { - if (Volumes[i]->GetBox().Intersects(bounds)) - { - result = Volumes[i]; - break; - } - } - - return result; -} - -void NavigationScene::SaveNavMesh() +void NavMesh::SaveNavMesh() { #if COMPILE_WITH_ASSETS_IMPORTER + // Skip if scene is missing + const auto scene = GetScene(); + if (!scene) + return; + #if USE_EDITOR // Skip if game is running in editor (eg. game scripts update dynamic navmesh) if (Editor::IsPlayMode) @@ -77,7 +49,7 @@ void NavigationScene::SaveNavMesh() Guid assetId = DataAsset.GetID(); if (!assetId.IsValid()) assetId = Guid::New(); - const String assetPath = Scene->GetDataFolderPath() / TEXT("NavMesh") + ASSET_FILES_EXTENSION_WITH_DOT; + const String assetPath = scene->GetDataFolderPath() / TEXT("NavMesh") + Properties.Name + ASSET_FILES_EXTENSION_WITH_DOT; // Generate navmesh tiles data const int32 streamInitialCapacity = Math::RoundUpToPowerOf2((Data.Tiles.Count() + 1) * 1024); @@ -99,32 +71,46 @@ void NavigationScene::SaveNavMesh() #endif } -void NavigationScene::OnEnable() +void NavMesh::ClearData() { - auto navMesh = NavMeshRuntime::Get(); - CHECK(navMesh); + if (Data.Tiles.HasItems()) + { + IsDataDirty = true; + Data.TileSize = 0.0f; + Data.Tiles.Resize(0); + } +} + +NavMeshRuntime* NavMesh::GetRuntime(bool createIfMissing) const +{ + return NavMeshRuntime::Get(Properties, createIfMissing); +} + +void NavMesh::AddTiles() +{ + auto navMesh = NavMeshRuntime::Get(Properties, true); navMesh->AddTiles(this); } -void NavigationScene::OnDisable() +void NavMesh::RemoveTiles() { - auto navMesh = NavMeshRuntime::Get(); + auto navMesh = NavMeshRuntime::Get(Properties, false); if (navMesh) navMesh->RemoveTiles(this); } -void NavigationScene::OnDataAssetLoaded() +void NavMesh::OnDataAssetLoaded() { // Skip if already has data (prevent reloading navmesh on saving) if (Data.Tiles.HasItems()) return; - const bool isEnabled = Scene->IsDuringPlay() && Scene->IsActiveInHierarchy(); + const bool isEnabled = IsDuringPlay() && IsActiveInHierarchy(); // Remove added tiles if (isEnabled) { - OnDisable(); + RemoveTiles(); } // Load navmesh tiles @@ -136,6 +122,49 @@ void NavigationScene::OnDataAssetLoaded() // Add loaded tiles if (isEnabled) { - OnEnable(); + AddTiles(); } } + +void NavMesh::Serialize(SerializeStream& stream, const void* otherObj) +{ + // Base + Actor::Serialize(stream, otherObj); + +#if USE_EDITOR + // Save navmesh tiles to asset (if modified) + if (IsDataDirty) + SaveNavMesh(); +#endif + + SERIALIZE_GET_OTHER_OBJ(NavMesh); + SERIALIZE(DataAsset); + SERIALIZE(Properties); +} + +void NavMesh::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + // Base + Actor::Deserialize(stream, modifier); + + DESERIALIZE(DataAsset); + DESERIALIZE(Properties); +} + +void NavMesh::OnEnable() +{ + // Base + Actor::OnEnable(); + + GetScene()->NavigationMeshes.Add(this); + AddTiles(); +} + +void NavMesh::OnDisable() +{ + RemoveTiles(); + GetScene()->NavigationMeshes.Remove(this); + + // Base + Actor::OnDisable(); +} diff --git a/Source/Engine/Navigation/NavMesh.h b/Source/Engine/Navigation/NavMesh.h new file mode 100644 index 000000000..bd0eab703 --- /dev/null +++ b/Source/Engine/Navigation/NavMesh.h @@ -0,0 +1,76 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "NavMeshData.h" +#include "NavigationTypes.h" +#include "Engine/Content/AssetReference.h" +#include "Engine/Content/Assets/RawDataAsset.h" +#include "Engine/Level/Actor.h" + +class NavMeshBoundsVolume; +class NavMeshRuntime; + +/// +/// The navigation mesh actor that holds a navigation data for a scene. +/// +API_CLASS() class FLAXENGINE_API NavMesh : public Actor +{ +DECLARE_SCENE_OBJECT(NavMesh); +public: + + /// + /// The flag used to mark that navigation data has been modified since load. Used to save runtime data to the file on scene serialization. + /// + bool IsDataDirty; + + /// + /// The navmesh tiles data. + /// + NavMeshData Data; + + /// + /// The cached navmesh data asset. + /// + AssetReference DataAsset; + + /// + /// The navigation mesh properties. + /// + API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"Nav Mesh\")") NavMeshProperties Properties; + +public: + + /// + /// Saves the nav mesh tiles data to the asset. Supported only in builds with assets saving enabled (eg. editor) and not during gameplay (eg. design time). + /// + void SaveNavMesh(); + + /// + /// Clears the data. + /// + void ClearData(); + + /// + /// Gets the navmesh runtime object that matches with properties. + /// + NavMeshRuntime* GetRuntime(bool createIfMissing = true) const; + +private: + + void AddTiles(); + void RemoveTiles(); + void OnDataAssetLoaded(); + +public: + + // [Actor] + void Serialize(SerializeStream& stream, const void* otherObj) override; + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; + +protected: + + // [Actor] + void OnEnable() override; + void OnDisable() override; +}; diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp index 34a881ede..a3f02249a 100644 --- a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp +++ b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp @@ -2,7 +2,6 @@ #include "NavMeshBoundsVolume.h" #include "Engine/Level/Scene/Scene.h" -#include "NavigationScene.h" #if USE_EDITOR #include "Editor/Editor.h" #include "Editor/Managed/ManagedEditor.h" @@ -16,15 +15,15 @@ NavMeshBoundsVolume::NavMeshBoundsVolume(const SpawnParams& params) void NavMeshBoundsVolume::OnEnable() { - GetScene()->Navigation->Volumes.Add(this); - // Base Actor::OnEnable(); + + GetScene()->NavigationVolumes.Add(this); } void NavMeshBoundsVolume::OnDisable() { - GetScene()->Navigation->Volumes.Remove(this); + GetScene()->NavigationVolumes.Remove(this); // Base Actor::OnDisable(); diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index caa2513d2..dca9a445c 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -3,6 +3,11 @@ #if COMPILE_WITH_NAV_MESH_BUILDER #include "NavMeshBuilder.h" +#include "NavMesh.h" +#include "NavigationSettings.h" +#include "NavMeshBoundsVolume.h" +#include "NavLink.h" +#include "NavMeshRuntime.h" #include "Engine/Core/Math/BoundingBox.h" #include "Engine/Core/Math/VectorInt.h" #include "Engine/Physics/Colliders/BoxCollider.h" @@ -15,12 +20,6 @@ #include "Engine/Level/Level.h" #include "Engine/Level/SceneQuery.h" #include "Engine/Core/Log.h" -#include "NavigationScene.h" -#include "NavigationSettings.h" -#include "NavMeshBoundsVolume.h" -#include "NavLink.h" -#include "Navigation.h" -#include "NavMeshRuntime.h" #include #include #include @@ -113,11 +112,9 @@ struct NavigationSceneRasterization { PROFILE_CPU_NAMED("BoxCollider"); - OrientedBoundingBox box = boxCollider->GetOrientedBox(); - + const OrientedBoundingBox box = boxCollider->GetOrientedBox(); vb.Resize(8); box.GetCorners(vb.Get()); - ib.Add(BoxTrianglesIndicesCache, 36); e.RasterizeTriangles(); @@ -127,7 +124,7 @@ struct NavigationSceneRasterization PROFILE_CPU_NAMED("MeshCollider"); auto collisionData = meshCollider->CollisionData.Get(); - if (!collisionData || collisionData->WaitForLoaded(1000.0f)) + if (!collisionData || collisionData->WaitForLoaded()) return true; collisionData->ExtractGeometry(vb, ib); @@ -181,7 +178,7 @@ void RasterizeGeometry(const BoundingBox& tileBounds, rcContext* context, rcConf // Builds navmesh tile bounds and check if there are any valid navmesh volumes at that tile location // Returns true if tile is intersecting with any navmesh bounds volume actor - which means tile is in use -bool GetNavMeshTileBounds(NavigationScene* scene, int32 x, int32 y, float tileSize, BoundingBox& tileBounds) +bool GetNavMeshTileBounds(Scene* scene, int32 x, int32 y, float tileSize, BoundingBox& tileBounds) { // Build initial tile bounds (with infinite extent) tileBounds.Minimum.X = (float)x * tileSize; @@ -194,9 +191,9 @@ bool GetNavMeshTileBounds(NavigationScene* scene, int32 x, int32 y, float tileSi // Check if any navmesh volume intersects with the tile bool foundAnyVolume = false; Vector2 rangeY; - for (int32 i = 0; i < scene->Volumes.Count(); i++) + for (int32 i = 0; i < scene->NavigationVolumes.Count(); i++) { - const auto volume = scene->Volumes[i]; + const auto volume = scene->NavigationVolumes[i]; const auto& volumeBounds = volume->GetBox(); if (volumeBounds.Intersects(tileBounds)) { @@ -224,27 +221,27 @@ bool GetNavMeshTileBounds(NavigationScene* scene, int32 x, int32 y, float tileSi return foundAnyVolume; } -void RemoveTile(NavMeshRuntime* navMesh, NavigationScene* scene, int32 x, int32 y, int32 layer) +void RemoveTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, int32 layer) { - ScopeLock lock(navMesh->Locker); + ScopeLock lock(runtime->Locker); // Find tile data and remove it - for (int32 i = 0; i < scene->Data.Tiles.Count(); i++) + for (int32 i = 0; i < navMesh->Data.Tiles.Count(); i++) { - auto& tile = scene->Data.Tiles[i]; + auto& tile = navMesh->Data.Tiles[i]; if (tile.PosX == x && tile.PosY == y && tile.Layer == layer) { - scene->Data.Tiles.RemoveAt(i); - scene->IsDataDirty = true; + navMesh->Data.Tiles.RemoveAt(i); + navMesh->IsDataDirty = true; break; } } // Remove tile from navmesh - navMesh->RemoveTile(x, y, layer); + runtime->RemoveTile(x, y, layer); } -bool GenerateTile(NavigationScene* scene, int32 x, int32 y, BoundingBox& tileBounds, float tileSize, rcConfig& config) +bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, BoundingBox& tileBounds, float tileSize, rcConfig& config) { rcContext context; int32 layer = 0; @@ -355,7 +352,7 @@ bool GenerateTile(NavigationScene* scene, int32 x, int32 y, BoundingBox& tileBou if (polyMesh->nverts == 0) { // Empty tile - RemoveTile(NavMeshRuntime::Get(), scene, x, y, layer); + RemoveTile(navMesh, runtime, x, y, layer); return false; } @@ -438,11 +435,11 @@ bool GenerateTile(NavigationScene* scene, int32 x, int32 y, BoundingBox& tileBou { PROFILE_CPU_NAMED("Navigation.CreateTile"); - ScopeLock lock(NavMeshRuntime::Get()->Locker); + ScopeLock lock(runtime->Locker); // Add tile data - scene->IsDataDirty = true; - auto& tile = scene->Data.Tiles.AddOne(); + navMesh->IsDataDirty = true; + auto& tile = navMesh->Data.Tiles.AddOne(); tile.PosX = x; tile.PosY = y; tile.Layer = layer; @@ -451,7 +448,7 @@ bool GenerateTile(NavigationScene* scene, int32 x, int32 y, BoundingBox& tileBou tile.Data.Copy(navData, navDataSize); // Add tile to navmesh - NavMeshRuntime::Get()->AddTile(scene, tile); + runtime->AddTile(navMesh, tile); } dtFree(navData); @@ -465,16 +462,17 @@ float GetTileSize() return settings.CellSize * settings.TileSize; } -void InitConfig(rcConfig& config) +void InitConfig(rcConfig& config, NavMesh* navMesh) { auto& settings = *NavigationSettings::Get(); + auto& navMeshProperties = navMesh->Properties; config.cs = settings.CellSize; config.ch = settings.CellHeight; - config.walkableSlopeAngle = settings.WalkableMaxSlopeAngle; - config.walkableHeight = (int)(settings.WalkableHeight / config.ch + 0.99f); - config.walkableClimb = (int)(settings.WalkableMaxClimb / config.ch); - config.walkableRadius = (int)(settings.WalkableRadius / config.cs + 0.99f); + config.walkableSlopeAngle = navMeshProperties.Agent.MaxSlopeAngle; + config.walkableHeight = (int)(navMeshProperties.Agent.Height / config.ch + 0.99f); + config.walkableClimb = (int)(navMeshProperties.Agent.StepHeight / config.ch); + config.walkableRadius = (int)(navMeshProperties.Agent.Radius / config.cs + 0.99f); config.maxEdgeLen = (int)(settings.MaxEdgeLen / config.cs); config.maxSimplificationError = settings.MaxEdgeError; config.minRegionArea = rcSqr(settings.MinRegionArea); @@ -506,7 +504,9 @@ class NavMeshTileBuildTask : public ThreadPoolTask { public: - NavigationScene* Scene; + Scene* Scene; + NavMesh* NavMesh; + NavMeshRuntime* Runtime; BoundingBox TileBounds; int32 X; int32 Y; @@ -520,7 +520,7 @@ public: { PROFILE_CPU_NAMED("BuildNavMeshTile"); - if (GenerateTile(Scene, X, Y, TileBounds, TileSize, Config)) + if (GenerateTile(NavMesh, Runtime, X, Y, TileBounds, TileSize, Config)) { LOG(Warning, "Failed to generate navmesh tile at {0}x{1}.", X, Y); } @@ -557,7 +557,7 @@ void OnSceneUnloading(Scene* scene, const Guid& sceneId) for (int32 i = 0; i < NavBuildTasks.Count(); i++) { auto task = NavBuildTasks[i]; - if (task->Scene == scene->Navigation) + if (task->Scene == scene) { NavBuildTasksLocker.Unlock(); @@ -600,15 +600,16 @@ float NavMeshBuilder::GetNavMeshBuildingProgress() return result; } -void BuildTileAsync(NavigationScene* scene, int32 x, int32 y, rcConfig& config, const BoundingBox& tileBounds, float tileSize) +void BuildTileAsync(NavMesh* navMesh, int32 x, int32 y, rcConfig& config, const BoundingBox& tileBounds, float tileSize) { + NavMeshRuntime* runtime = navMesh->GetRuntime(); NavBuildTasksLocker.Lock(); // Skip if this tile is already during cooking for (int32 i = 0; i < NavBuildTasks.Count(); i++) { const auto task = NavBuildTasks[i]; - if (task->X == x && task->Y == y) + if (task->X == x && task->Y == y && task->Runtime == runtime) { NavBuildTasksLocker.Unlock(); return; @@ -617,7 +618,9 @@ void BuildTileAsync(NavigationScene* scene, int32 x, int32 y, rcConfig& config, // Create task auto task = New(); - task->Scene = scene; + task->Scene = navMesh->GetScene(); + task->NavMesh = navMesh; + task->Runtime = runtime; task->X = x; task->Y = y; task->TileBounds = tileBounds; @@ -632,70 +635,10 @@ void BuildTileAsync(NavigationScene* scene, int32 x, int32 y, rcConfig& config, task->Start(); } -void BuildWholeScene(NavigationScene* scene) +void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBounds) { const float tileSize = GetTileSize(); - const auto navMesh = NavMeshRuntime::Get(); - - // Compute total navigation area bounds - const BoundingBox worldBounds = scene->GetNavigationBounds(); - - // Align total bounds to tile size - BoundingBox worldBoundsAligned; - worldBoundsAligned.Minimum = Vector3::Floor(worldBounds.Minimum / tileSize) * tileSize; - worldBoundsAligned.Maximum = Vector3::Ceil(worldBounds.Maximum / tileSize) * tileSize; - - // Calculate tiles range for the given navigation world bounds (aligned to tiles size) - const Int3 tilesMin = Int3(worldBoundsAligned.Minimum / tileSize); - const Int3 tilesMax = Int3(worldBoundsAligned.Maximum / tileSize); - const int32 tilesX = tilesMax.X - tilesMin.X; - const int32 tilesY = tilesMax.Z - tilesMin.Z; - - { - PROFILE_CPU_NAMED("Prepare"); - - // Prepare navmesh - navMesh->RemoveTiles(scene); - navMesh->SetTileSize(tileSize); - navMesh->EnsureCapacity(tilesX * tilesY); - - // Prepare scene data - scene->Data.TileSize = tileSize; - scene->Data.Tiles.Clear(); - scene->Data.Tiles.EnsureCapacity(tilesX * tilesX); - scene->IsDataDirty = true; - } - - // Initialize nav mesh configuration - rcConfig config; - InitConfig(config); - - // Generate all tiles that intersect with the navigation volume bounds - { - PROFILE_CPU_NAMED("StartBuildingTiles"); - - for (int32 y = tilesMin.Z; y < tilesMax.Z; y++) - { - for (int32 x = tilesMin.X; x < tilesMax.X; x++) - { - BoundingBox tileBounds; - if (GetNavMeshTileBounds(scene, x, y, tileSize, tileBounds)) - { - BuildTileAsync(scene, x, y, config, tileBounds, tileSize); - } - else - { - RemoveTile(navMesh, scene, x, y, 0); - } - } - } - } -} - -void BuildDirtyBounds(NavigationScene* scene, const BoundingBox& dirtyBounds) -{ - const float tileSize = GetTileSize(); - const auto navMesh = NavMeshRuntime::Get(); + NavMeshRuntime* runtime = navMesh->GetRuntime(); // Align dirty bounds to tile size BoundingBox dirtyBoundsAligned; @@ -703,8 +646,8 @@ void BuildDirtyBounds(NavigationScene* scene, const BoundingBox& dirtyBounds) dirtyBoundsAligned.Maximum = Vector3::Ceil(dirtyBounds.Maximum / tileSize) * tileSize; // Calculate tiles range for the given navigation dirty bounds (aligned to tiles size) - const Int3 tilesMin = Int3(dirtyBoundsAligned.Minimum / tileSize); - const Int3 tilesMax = Int3(dirtyBoundsAligned.Maximum / tileSize); + const Int3 tilesMin(dirtyBoundsAligned.Minimum / tileSize); + const Int3 tilesMax(dirtyBoundsAligned.Maximum / tileSize); const int32 tilesX = tilesMax.X - tilesMin.X; const int32 tilesY = tilesMax.Z - tilesMin.Z; @@ -712,28 +655,28 @@ void BuildDirtyBounds(NavigationScene* scene, const BoundingBox& dirtyBounds) PROFILE_CPU_NAMED("Prepare"); // Prepare scene data and navmesh - if (Math::NotNearEqual(scene->Data.TileSize, tileSize)) + if (Math::NotNearEqual(navMesh->Data.TileSize, tileSize)) { - navMesh->RemoveTiles(scene); - navMesh->SetTileSize(tileSize); - navMesh->EnsureCapacity(tilesX * tilesY); + runtime->RemoveTiles(navMesh); + runtime->SetTileSize(tileSize); + runtime->EnsureCapacity(tilesX * tilesY); - scene->Data.TileSize = tileSize; - scene->Data.Tiles.Clear(); - scene->Data.Tiles.EnsureCapacity(tilesX * tilesX); - scene->IsDataDirty = true; + navMesh->Data.TileSize = tileSize; + navMesh->Data.Tiles.Clear(); + navMesh->Data.Tiles.EnsureCapacity(tilesX * tilesX); + navMesh->IsDataDirty = true; } else { // Prepare navmesh - navMesh->SetTileSize(tileSize); - navMesh->EnsureCapacity(tilesX * tilesY); + runtime->SetTileSize(tileSize); + runtime->EnsureCapacity(tilesX * tilesY); } } // Initialize nav mesh configuration rcConfig config; - InitConfig(config); + InitConfig(config, navMesh); // Generate all tiles that intersect with the navigation volume bounds { @@ -746,17 +689,103 @@ void BuildDirtyBounds(NavigationScene* scene, const BoundingBox& dirtyBounds) BoundingBox tileBounds; if (GetNavMeshTileBounds(scene, x, y, tileSize, tileBounds)) { - BuildTileAsync(scene, x, y, config, tileBounds, tileSize); + BuildTileAsync(navMesh, x, y, config, tileBounds, tileSize); } else { - RemoveTile(navMesh, scene, x, y, 0); + RemoveTile(navMesh, runtime, x, y, 0); } } } } } +void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds) +{ + auto settings = NavigationSettings::Get(); + + // Sync navmeshes + for (auto& navMeshProperties : settings->NavMeshes) + { + NavMesh* navMesh = nullptr; + for (auto e : scene->NavigationMeshes) + { + if (e->Properties.Name == navMeshProperties.Name) + { + navMesh = e; + break; + } + } + if (navMesh) + { + // Sync settings + auto runtime = navMesh->GetRuntime(false); + navMesh->Properties = navMeshProperties; + if (runtime) + runtime->Properties = navMeshProperties; + } + else if (settings->AutoAddMissingNavMeshes) + { + // Spawn missing navmesh + navMesh = New(); + navMesh->SetStaticFlags(StaticFlags::FullyStatic); + navMesh->SetName(TEXT("NavMesh.") + navMeshProperties.Name); + navMesh->Properties = navMeshProperties; + navMesh->SetParent(scene, false); + } + } + + // Build all navmeshes on the scene + for (NavMesh* navMesh : scene->NavigationMeshes) + { + BuildDirtyBounds(scene, navMesh, dirtyBounds); + } + + // Remove unused navmeshes + if (settings->AutoRemoveMissingNavMeshes) + { + for (NavMesh* navMesh : scene->NavigationMeshes) + { + // 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[i]->NavMesh == navMesh) + usageCount++; + } + NavBuildTasksLocker.Unlock(); + if (usageCount != 0) + continue; + + navMesh->DeleteObject(); + } + } +} + +void BuildWholeScene(Scene* scene) +{ + // Compute total navigation area bounds + const BoundingBox worldBounds = scene->GetNavigationBounds(); + + BuildDirtyBounds(scene, worldBounds); +} + +void ClearNavigation(Scene* scene) +{ + const bool autoRemoveMissingNavMeshes = NavigationSettings::Get()->AutoRemoveMissingNavMeshes; + for (NavMesh* navMesh : scene->NavigationMeshes) + { + navMesh->ClearData(); + if (autoRemoveMissingNavMeshes) + navMesh->DeleteObject(); + } +} + void NavMeshBuilder::Update() { ScopeLock lock(NavBuildQueueLocker); @@ -769,15 +798,12 @@ void NavMeshBuilder::Update() if (now - req.Time >= 0) { NavBuildQueue.RemoveAt(i--); - auto scene = req.Scene->Navigation; + const auto scene = req.Scene.Get(); // Early out if scene has no bounds volumes to define nav mesh area - if (scene->Volumes.IsEmpty()) + if (scene->NavigationVolumes.IsEmpty()) { - // Cleanup if no navigation to use - scene->Data.TileSize = 0; - scene->Data.Tiles.Resize(0); - scene->IsDataDirty = true; + ClearNavigation(scene); continue; } @@ -797,9 +823,9 @@ void NavMeshBuilder::Update() void NavMeshBuilder::Build(Scene* scene, float timeoutMs) { // Early out if scene is not using navigation - if (scene->Navigation->Volumes.IsEmpty()) + if (scene->NavigationVolumes.IsEmpty()) { - scene->Navigation->ClearData(); + ClearNavigation(scene); return; } @@ -828,9 +854,9 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) { // Early out if scene is not using navigation - if (scene->Navigation->Volumes.IsEmpty()) + if (scene->NavigationVolumes.IsEmpty()) { - scene->Navigation->ClearData(); + ClearNavigation(scene); return; } diff --git a/Source/Engine/Navigation/NavMeshBuilder.h b/Source/Engine/Navigation/NavMeshBuilder.h index 4e59027ce..27dab4273 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.h +++ b/Source/Engine/Navigation/NavMeshBuilder.h @@ -4,9 +4,14 @@ #if COMPILE_WITH_NAV_MESH_BUILDER +#include "Engine/Core/Compiler.h" + class Scene; -class NavMeshBuilder +/// +/// The navigation mesh building utility. +/// +class FLAXENGINE_API NavMeshBuilder { public: diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp index e160a11a6..4ca3b7041 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.cpp +++ b/Source/Engine/Navigation/NavMeshRuntime.cpp @@ -1,18 +1,23 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. #include "NavMeshRuntime.h" -#include "NavigationScene.h" +#include "NavMesh.h" #include "Engine/Core/Log.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/Threading.h" #include #include +#include #define MAX_NODES 2048 #define USE_DATA_LINK 0 #define USE_NAV_MESH_ALLOC 1 -NavMeshRuntime::NavMeshRuntime() +#define DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL 50.0f +#define DEFAULT_NAV_QUERY_EXTENT_VERTICAL 250.0f + +NavMeshRuntime::NavMeshRuntime(const NavMeshProperties& properties) + : Properties(properties) { _navMesh = nullptr; _navMeshQuery = dtAllocNavMeshQuery(); @@ -30,6 +35,150 @@ int32 NavMeshRuntime::GetTilesCapacity() const return _navMesh ? _navMesh->getMaxTiles() : 0; } +bool NavMeshRuntime::FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance) const +{ + ScopeLock lock(Locker); + + const auto query = GetNavMeshQuery(); + if (!query) + { + return false; + } + + dtQueryFilter filter; + Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); + + dtPolyRef startPoly = 0; + query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); + if (!startPoly) + { + return false; + } + + return dtStatusSucceed(query->findDistanceToWall(startPoly, &startPosition.X, maxDistance, &filter, &hitInfo.Distance, &hitInfo.Position.X, &hitInfo.Normal.X)); +} + +bool NavMeshRuntime::FindPath(const Vector3& startPosition, const Vector3& endPosition, Array& resultPath) const +{ + resultPath.Clear(); + ScopeLock lock(Locker); + + const auto query = GetNavMeshQuery(); + if (!query) + { + return false; + } + + dtQueryFilter filter; + Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); + + dtPolyRef startPoly = 0; + query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); + if (!startPoly) + { + return false; + } + dtPolyRef endPoly = 0; + query->findNearestPoly(&endPosition.X, &extent.X, &filter, &endPoly, nullptr); + if (!endPoly) + { + return false; + } + + dtPolyRef path[NAV_MESH_PATH_MAX_SIZE]; + int32 pathSize; + const auto findPathStatus = query->findPath(startPoly, endPoly, &startPosition.X, &endPosition.X, &filter, path, &pathSize, NAV_MESH_PATH_MAX_SIZE); + if (dtStatusFailed(findPathStatus)) + { + return false; + } + + // Check for special case, where path has not been found, and starting polygon was the one closest to the target + if (pathSize == 1 && dtStatusDetail(findPathStatus, DT_PARTIAL_RESULT)) + { + // In this case we find a point on starting polygon, that's closest to destination and store it as path end + resultPath.Resize(2); + resultPath[0] = startPosition; + resultPath[1] = startPosition; + query->closestPointOnPolyBoundary(startPoly, &endPosition.X, &resultPath[1].X); + } + else + { + int straightPathCount = 0; + resultPath.EnsureCapacity(NAV_MESH_PATH_MAX_SIZE); + const auto findStraightPathStatus = query->findStraightPath(&startPosition.X, &endPosition.X, path, pathSize, (float*)resultPath.Get(), nullptr, nullptr, &straightPathCount, resultPath.Capacity(), DT_STRAIGHTPATH_AREA_CROSSINGS); + if (dtStatusFailed(findStraightPathStatus)) + { + return false; + } + resultPath.Resize(straightPathCount); + } + + return true; +} + +bool NavMeshRuntime::ProjectPoint(const Vector3& point, Vector3& result) const +{ + ScopeLock lock(Locker); + + const auto query = GetNavMeshQuery(); + if (!query) + { + return false; + } + + dtQueryFilter filter; + Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); + + dtPolyRef startPoly = 0; + query->findNearestPoly(&point.X, &extent.X, &filter, &startPoly, &result.X); + if (!startPoly) + { + return false; + } + + return true; +} + +bool NavMeshRuntime::RayCast(const Vector3& startPosition, const Vector3& endPosition, NavMeshHit& hitInfo) const +{ + ScopeLock lock(Locker); + + const auto query = GetNavMeshQuery(); + if (!query) + { + return false; + } + + dtQueryFilter filter; + Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); + + dtPolyRef startPoly = 0; + query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); + if (!startPoly) + { + return false; + } + + dtRaycastHit hit; + hit.path = nullptr; + hit.maxPath = 0; + const bool result = dtStatusSucceed(query->raycast(startPoly, &startPosition.X, &endPosition.X, &filter, 0, &hit)); + if (hit.t >= MAX_float) + { + hitInfo.Position = endPosition; + hitInfo.Distance = 0; + } + else + { + hitInfo.Position = startPosition + (endPosition - startPosition) * hit.t; + hitInfo.Distance = hit.t; + } + hitInfo.Normal = *(Vector3*)&hit.hitNormal; + + return result; +} + void NavMeshRuntime::SetTileSize(float tileSize) { ScopeLock lock(Locker); @@ -132,13 +281,13 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount) } } -void NavMeshRuntime::AddTiles(NavigationScene* scene) +void NavMeshRuntime::AddTiles(NavMesh* navMesh) { // Skip if no data - ASSERT(scene); - if (scene->Data.Tiles.IsEmpty()) + ASSERT(navMesh); + if (navMesh->Data.Tiles.IsEmpty()) return; - auto& data = scene->Data; + auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTiles"); @@ -164,14 +313,14 @@ void NavMeshRuntime::AddTiles(NavigationScene* scene) // Add new tiles for (auto& tileData : data.Tiles) { - AddTileInternal(scene, tileData); + AddTileInternal(navMesh, tileData); } } -void NavMeshRuntime::AddTile(NavigationScene* scene, NavMeshTileData& tileData) +void NavMeshRuntime::AddTile(NavMesh* navMesh, NavMeshTileData& tileData) { - ASSERT(scene); - auto& data = scene->Data; + ASSERT(navMesh); + auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTile"); @@ -195,17 +344,17 @@ void NavMeshRuntime::AddTile(NavigationScene* scene, NavMeshTileData& tileData) EnsureCapacity(1); // Add new tile - AddTileInternal(scene, tileData); + AddTileInternal(navMesh, tileData); } bool IsTileFromScene(const NavMeshRuntime* navMesh, const NavMeshTile& tile, void* customData) { - return tile.Scene == (NavigationScene*)customData; + return tile.NavMesh == (NavMesh*)customData; } -void NavMeshRuntime::RemoveTiles(NavigationScene* scene) +void NavMeshRuntime::RemoveTiles(NavMesh* navMesh) { - RemoveTiles(IsTileFromScene, scene); + RemoveTiles(IsTileFromScene, navMesh); } void NavMeshRuntime::RemoveTile(int32 x, int32 y, int32 layer) @@ -274,6 +423,101 @@ void NavMeshRuntime::RemoveTiles(bool (* prediction)(const NavMeshRuntime* navMe } } +#if COMPILE_WITH_DEBUG_DRAW + +#include "Engine/Debug/DebugDraw.h" + +void DrawPoly(NavMeshRuntime* navMesh, const dtMeshTile& tile, const dtPoly& poly) +{ + const unsigned int ip = (unsigned int)(&poly - tile.polys); + const dtPolyDetail& pd = tile.detailMeshes[ip]; + const Color color = navMesh->Properties.Color; + const float drawOffsetY = 10.0f + ((float)GetHash(color) / (float)MAX_uint32) * 10.0f; // Apply some offset to prevent Z-fighting for different navmeshes + const Color fillColor = color * 0.5f; + const Color edgesColor = Color::FromHSV(color.ToHSV() + Vector3(20.0f, 0, -0.1f), color.A); + + for (int i = 0; i < pd.triCount; i++) + { + Vector3 v[3]; + const unsigned char* t = &tile.detailTris[(pd.triBase + i) * 4]; + + for (int k = 0; k < 3; k++) + { + if (t[k] < poly.vertCount) + { + v[k] = *(Vector3*)&tile.verts[poly.verts[t[k]] * 3]; + } + else + { + v[k] = *(Vector3*)&tile.detailVerts[(pd.vertBase + t[k] - poly.vertCount) * 3]; + } + } + + v[0].Y += drawOffsetY; + v[1].Y += drawOffsetY; + v[2].Y += drawOffsetY; + + DEBUG_DRAW_TRIANGLE(v[0], v[1], v[2], fillColor, 0, true); + } + + for (int k = 0; k < pd.triCount; k++) + { + const unsigned char* t = &tile.detailTris[(pd.triBase + k) * 4]; + Vector3 v[3]; + + for (int m = 0; m < 3; m++) + { + if (t[m] < poly.vertCount) + v[m] = *(Vector3*)&tile.verts[poly.verts[t[m]] * 3]; + else + v[m] = *(Vector3*)&tile.detailVerts[(pd.vertBase + (t[m] - poly.vertCount)) * 3]; + } + + v[0].Y += drawOffsetY; + v[1].Y += drawOffsetY; + v[2].Y += drawOffsetY; + + for (int m = 0, n = 2; m < 3; n = m++) + { + // Skip inner detail edges + if (((t[3] >> (n * 2)) & 0x3) == 0) + continue; + + DEBUG_DRAW_LINE(v[n], v[m], edgesColor, 0, true); + } + } +} + +void NavMeshRuntime::DebugDraw() +{ + ScopeLock lock(Locker); + + const dtNavMesh* dtNavMesh = GetNavMesh(); + const int tilesCount = dtNavMesh ? dtNavMesh->getMaxTiles() : 0; + if (tilesCount == 0) + return; + + for (int tileIndex = 0; tileIndex < tilesCount; tileIndex++) + { + const dtMeshTile* tile = dtNavMesh->getTile(tileIndex); + if (!tile->header) + continue; + + //DebugDraw::DrawWireBox(*(BoundingBox*)&tile->header->bmin[0], Color::CadetBlue); + + for (int i = 0; i < tile->header->polyCount; i++) + { + const dtPoly* poly = &tile->polys[i]; + if (poly->getType() != DT_POLYTYPE_GROUND) + continue; + + DrawPoly(this, *tile, *poly); + } + } +} + +#endif + void NavMeshRuntime::Dispose() { if (_navMesh) @@ -284,7 +528,7 @@ void NavMeshRuntime::Dispose() _tiles.Resize(0); } -void NavMeshRuntime::AddTileInternal(NavigationScene* scene, NavMeshTileData& tileData) +void NavMeshRuntime::AddTileInternal(NavMesh* navMesh, NavMeshTileData& tileData) { // Check if that tile has been added to navmesh NavMeshTile* tile = nullptr; @@ -313,7 +557,7 @@ void NavMeshRuntime::AddTileInternal(NavigationScene* scene, NavMeshTileData& ti ASSERT(tile); // Copy tile properties - tile->Scene = scene; + tile->NavMesh = navMesh; tile->X = tileData.PosX; tile->Y = tileData.PosY; tile->Layer = tileData.Layer; diff --git a/Source/Engine/Navigation/NavMeshRuntime.h b/Source/Engine/Navigation/NavMeshRuntime.h index 6c538ae79..5d1b8ca63 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.h +++ b/Source/Engine/Navigation/NavMeshRuntime.h @@ -5,27 +5,41 @@ #include "Engine/Core/Types/BaseTypes.h" #include "Engine/Platform/CriticalSection.h" #include "NavMeshData.h" +#include "NavigationTypes.h" -class NavigationScene; class dtNavMesh; class dtNavMeshQuery; +class NavMesh; -class NavMeshTile +/// +/// The navigation mesh tile data. +/// +class FLAXENGINE_API NavMeshTile { public: int32 X; int32 Y; int32 Layer; - NavigationScene* Scene; + NavMesh* NavMesh; BytesContainer Data; }; -class NavMeshRuntime +/// +/// The navigation mesh runtime object that builds the navmesh from all loaded scenes. +/// +class FLAXENGINE_API NavMeshRuntime { public: - static NavMeshRuntime* Get(); + // Gets the navigation mesh runtime for a given navmesh name. Return null if missing. + static NavMeshRuntime* Get(const StringView& navMeshName); + + // Gets the navigation mesh runtime for a given agent properties trying to pick the best matching navmesh. + static NavMeshRuntime* Get(const NavAgentProperties& agentProperties); + + // Gets the navigation mesh runtime for a given navmesh properties. + static NavMeshRuntime* Get(const NavMeshProperties& navMeshProperties, bool createIfMissing = false); private: @@ -36,7 +50,7 @@ private: public: - NavMeshRuntime(); + NavMeshRuntime(const NavMeshProperties& properties); ~NavMeshRuntime(); public: @@ -46,6 +60,11 @@ public: /// CriticalSection Locker; + /// + /// The navigation mesh properties. + /// + NavMeshProperties Properties; + /// /// Gets the size of the tile (in world-units). Returns zero if not initialized yet. /// @@ -66,6 +85,43 @@ public: int32 GetTilesCapacity() const; +public: + + /// + /// Finds the distance from the specified start position to the nearest polygon wall. + /// + /// The start position. + /// The result hit information. Valid only when query succeed. + /// The maximum distance to search for wall (search radius). + /// True if ray hits an matching object, otherwise false. + bool FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance = MAX_float) const; + + /// + /// Finds the path between the two positions presented as a list of waypoints stored in the corners array. + /// + /// The start position. + /// The end position. + /// The result path. + /// True if found valid path between given two points (it may be partial), otherwise false if failed. + bool FindPath(const Vector3& startPosition, const Vector3& endPosition, Array& resultPath) const; + + /// + /// Projects the point to nav mesh surface (finds the nearest polygon). + /// + /// The source point. + /// The result position on the navmesh (valid only if method returns true). + /// True if found valid location on the navmesh, otherwise false. + bool ProjectPoint(const Vector3& point, Vector3& result) const; + + /// + /// Casts a 'walkability' ray along the surface of the navigation mesh from the start position toward the end position. + /// + /// The start position. + /// The end position. + /// The result hit information. Valid only when query succeed. + /// True if ray hits an matching object, otherwise false. + bool RayCast(const Vector3& startPosition, const Vector3& endPosition, NavMeshHit& hitInfo) const; + public: /// @@ -83,21 +139,21 @@ public: /// /// Adds the tiles from the given scene to the runtime navmesh. /// - /// The navigation scene. - void AddTiles(NavigationScene* scene); + /// The navigation mesh. + void AddTiles(NavMesh* navMesh); /// /// Adds the tile from the given scene to the runtime navmesh. /// - /// The navigation scene. + /// The navigation mesh. /// The tile data. - void AddTile(NavigationScene* scene, NavMeshTileData& tileData); + void AddTile(NavMesh* navMesh, NavMeshTileData& tileData); /// /// Removes all the tiles from the navmesh that has been added from the given navigation scene. /// - /// The scene. - void RemoveTiles(NavigationScene* scene); + /// The navigation mesh. + void RemoveTiles(NavMesh* navMesh); /// /// Removes the tile from the navmesh. @@ -114,6 +170,10 @@ public: /// The user data passed to the callback method. void RemoveTiles(bool (*prediction)(const NavMeshRuntime* navMesh, const NavMeshTile& tile, void* customData), void* userData); +#if COMPILE_WITH_DEBUG_DRAW + void DebugDraw(); +#endif + /// /// Releases the navmesh. /// @@ -121,5 +181,5 @@ public: private: - void AddTileInternal(NavigationScene* scene, NavMeshTileData& tileData); + void AddTileInternal(NavMesh* navMesh, NavMeshTileData& tileData); }; diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 6decb4be7..d146e9c1e 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -11,21 +11,90 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Engine/EngineService.h" #include "Engine/Profiler/ProfilerCPU.h" -#include +#include "Engine/Serialization/Serialization.h" #include -#include - -#define DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL 50.0f -#define DEFAULT_NAV_QUERY_EXTENT_VERTICAL 250.0f +#include namespace { - NavMeshRuntime* _navMesh; + Array> NavMeshes; } -NavMeshRuntime* NavMeshRuntime::Get() +NavMeshRuntime* NavMeshRuntime::Get(const StringView& navMeshName) { - return ::_navMesh; + NavMeshRuntime* result = nullptr; + for (auto navMesh : NavMeshes) + { + if (navMesh->Properties.Name == navMeshName) + { + result = navMesh; + break; + } + } + return result; +} + +NavMeshRuntime* NavMeshRuntime::Get(const NavAgentProperties& agentProperties) +{ + NavMeshRuntime* result = nullptr; + // TODO: maybe build lookup table for agentProperties -> navMesh to improve perf on frequent calls? + float bestAgentRadiusDiff = -MAX_float; + float bestAgentHeightDiff = -MAX_float; + bool bestIsValid = false; + for (auto navMesh : NavMeshes) + { + const auto& navMeshProperties = navMesh->Properties; + const float agentRadiusDiff = navMeshProperties.Agent.Radius - agentProperties.Radius; + const float agentHeightDiff = navMeshProperties.Agent.Height - agentProperties.Height; + const bool isValid = agentRadiusDiff >= 0.0f && agentHeightDiff >= 0.0f; + + // NavMesh must be valid for an agent and be first valid or have better properties than the best matching result so far + if (isValid + && + ( + !bestIsValid + || + (agentRadiusDiff + agentHeightDiff < bestAgentRadiusDiff + bestAgentHeightDiff) + ) + ) + { + result = navMesh; + bestIsValid = true; + bestAgentRadiusDiff = agentRadiusDiff; + bestAgentHeightDiff = agentHeightDiff; + } + } + return result; +} + +NavMeshRuntime* NavMeshRuntime::Get(const NavMeshProperties& navMeshProperties, bool createIfMissing) +{ + NavMeshRuntime* result = nullptr; + for (auto navMesh : NavMeshes) + { + if (navMesh->Properties == navMeshProperties) + { + result = navMesh; + break; + } + } + if (!result && createIfMissing) + { + // Create a new navmesh + result = New(navMeshProperties); + NavMeshes.Add(result); + } + return result; +} + +bool NavAgentProperties::operator==(const NavAgentProperties& other) const +{ + return Math::NearEqual(Radius, other.Radius) && Math::NearEqual(Height, other.Height) && Math::NearEqual(StepHeight, other.StepHeight) && Math::NearEqual(MaxSlopeAngle, other.MaxSlopeAngle); +} + +bool NavMeshProperties::operator==(const NavMeshProperties& other) const +{ + return Name == other.Name && Quaternion::NearEqual(Rotation, other.Rotation, 0.001f) && Agent == other.Agent; } class NavigationService : public EngineService @@ -59,17 +128,59 @@ void* rcAllocDefault(size_t size, rcAllocHint) return Allocator::Allocate(size); } +NavigationSettings::NavigationSettings() +{ + NavMeshes.Resize(1); + auto& navMesh = NavMeshes[0]; + navMesh.Name = TEXT("Default"); +} + IMPLEMENT_SETTINGS_GETTER(NavigationSettings, Navigation); +void NavigationSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) +{ + DESERIALIZE(AutoAddMissingNavMeshes); + DESERIALIZE(AutoRemoveMissingNavMeshes); + DESERIALIZE(CellHeight); + DESERIALIZE(CellSize); + DESERIALIZE(TileSize); + DESERIALIZE(MinRegionArea); + DESERIALIZE(MergeRegionArea); + DESERIALIZE(MaxEdgeLen); + DESERIALIZE(MaxEdgeError); + DESERIALIZE(DetailSamplingDist); + DESERIALIZE(MaxDetailSamplingError); + if (modifier->EngineBuild >= 6215) + { + DESERIALIZE(NavMeshes); + } + else + { + // [Deprecated on 12.01.2021, expires on 12.01.2022] + float WalkableRadius = 34.0f; + float WalkableHeight = 144.0f; + float WalkableMaxClimb = 35.0f; + float WalkableMaxSlopeAngle = 60.0f; + DESERIALIZE(WalkableRadius); + DESERIALIZE(WalkableHeight); + DESERIALIZE(WalkableMaxClimb); + DESERIALIZE(WalkableMaxSlopeAngle); + NavMeshes.Resize(1); + auto& navMesh = NavMeshes[0]; + navMesh.Name = TEXT("Default"); + navMesh.Agent.Radius = WalkableRadius; + navMesh.Agent.Height = WalkableHeight; + navMesh.Agent.StepHeight = WalkableMaxClimb; + navMesh.Agent.MaxSlopeAngle = WalkableMaxSlopeAngle; + } +} + bool NavigationService::Init() { // Link memory allocation calls to use engine default allocator dtAllocSetCustom(dtAllocDefault, Allocator::Free); rcAllocSetCustom(rcAllocDefault, Allocator::Free); - // Create global nav mesh - _navMesh = New(); - return false; } @@ -84,156 +195,42 @@ void NavigationService::Update() void NavigationService::Dispose() { - if (_navMesh) + // Release nav meshes + for (auto navMesh : NavMeshes) { - _navMesh->Dispose(); - Delete(_navMesh); - _navMesh = nullptr; + navMesh->Dispose(); + Delete(navMesh); } + NavMeshes.Clear(); + NavMeshes.ClearDelete(); } bool Navigation::FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance) { - ScopeLock lock(_navMesh->Locker); - - const auto query = _navMesh->GetNavMeshQuery(); - if (!query) - { + if (NavMeshes.IsEmpty()) return false; - } - - dtQueryFilter filter; - Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); - - dtPolyRef startPoly = 0; - query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); - if (!startPoly) - { - return false; - } - - return dtStatusSucceed(_navMesh->GetNavMeshQuery()->findDistanceToWall(startPoly, &startPosition.X, maxDistance, &filter, &hitInfo.Distance, &hitInfo.Position.X, &hitInfo.Normal.X)); + return NavMeshes.First()->FindDistanceToWall(startPosition, hitInfo, maxDistance); } bool Navigation::FindPath(const Vector3& startPosition, const Vector3& endPosition, Array& resultPath) { - resultPath.Clear(); - ScopeLock lock(_navMesh->Locker); - - const auto query = _navMesh->GetNavMeshQuery(); - if (!query) - { + if (NavMeshes.IsEmpty()) return false; - } - - dtQueryFilter filter; - Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); - - dtPolyRef startPoly = 0; - query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); - if (!startPoly) - { - return false; - } - dtPolyRef endPoly = 0; - query->findNearestPoly(&endPosition.X, &extent.X, &filter, &endPoly, nullptr); - if (!endPoly) - { - return false; - } - - dtPolyRef path[NAV_MESH_PATH_MAX_SIZE]; - int32 pathSize; - const auto findPathStatus = _navMesh->GetNavMeshQuery()->findPath(startPoly, endPoly, &startPosition.X, &endPosition.X, &filter, path, &pathSize, NAV_MESH_PATH_MAX_SIZE); - if (dtStatusFailed(findPathStatus)) - { - return false; - } - - // Check for special case, where path has not been found, and starting polygon was the one closest to the target - if (pathSize == 1 && dtStatusDetail(findPathStatus, DT_PARTIAL_RESULT)) - { - // In this case we find a point on starting polygon, that's closest to destination and store it as path end - resultPath.Resize(2); - resultPath[0] = startPosition; - resultPath[1] = startPosition; - _navMesh->GetNavMeshQuery()->closestPointOnPolyBoundary(startPoly, &endPosition.X, &resultPath[1].X); - } - else - { - int straightPathCount = 0; - resultPath.EnsureCapacity(NAV_MESH_PATH_MAX_SIZE); - const auto findStraightPathStatus = _navMesh->GetNavMeshQuery()->findStraightPath(&startPosition.X, &endPosition.X, path, pathSize, (float*)resultPath.Get(), nullptr, nullptr, &straightPathCount, resultPath.Capacity(), DT_STRAIGHTPATH_AREA_CROSSINGS); - if (dtStatusFailed(findStraightPathStatus)) - { - return false; - } - resultPath.Resize(straightPathCount); - } - - return true; + return NavMeshes.First()->FindPath(startPosition, endPosition, resultPath); } bool Navigation::ProjectPoint(const Vector3& point, Vector3& result) { - ScopeLock lock(_navMesh->Locker); - - const auto query = _navMesh->GetNavMeshQuery(); - if (!query) - { + if (NavMeshes.IsEmpty()) return false; - } - - dtQueryFilter filter; - Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); - - dtPolyRef startPoly = 0; - query->findNearestPoly(&point.X, &extent.X, &filter, &startPoly, &result.X); - if (!startPoly) - { - return false; - } - - return true; + return NavMeshes.First()->ProjectPoint(point, result); } bool Navigation::RayCast(const Vector3& startPosition, const Vector3& endPosition, NavMeshHit& hitInfo) { - ScopeLock lock(_navMesh->Locker); - - const auto query = _navMesh->GetNavMeshQuery(); - if (!query) - { + if (NavMeshes.IsEmpty()) return false; - } - - dtQueryFilter filter; - Vector3 extent(DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL, DEFAULT_NAV_QUERY_EXTENT_VERTICAL, DEFAULT_NAV_QUERY_EXTENT_HORIZONTAL); - - dtPolyRef startPoly = 0; - query->findNearestPoly(&startPosition.X, &extent.X, &filter, &startPoly, nullptr); - if (!startPoly) - { - return false; - } - - dtRaycastHit hit; - hit.path = nullptr; - hit.maxPath = 0; - const bool result = dtStatusSucceed(_navMesh->GetNavMeshQuery()->raycast(startPoly, &startPosition.X, &endPosition.X, &filter, 0, &hit)); - if (hit.t >= MAX_float) - { - hitInfo.Position = endPosition; - hitInfo.Distance = 0; - } - else - { - hitInfo.Position = startPosition + (endPosition - startPosition) * hit.t; - hitInfo.Distance = hit.t; - } - hitInfo.Normal = *(Vector3*)&hit.hitNormal; - - return result; + return NavMeshes.First()->RayCast(startPosition, endPosition, hitInfo); } #if COMPILE_WITH_NAV_MESH_BUILDER @@ -260,96 +257,13 @@ void Navigation::BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, floa #endif -#if USE_EDITOR - -#include "Engine/Debug/DebugDraw.h" - -void DrawPoly(const dtMeshTile& tile, const dtPoly& poly) -{ - const unsigned int ip = (unsigned int)(&poly - tile.polys); - const dtPolyDetail& pd = tile.detailMeshes[ip]; - const float DrawOffsetY = 20.0f; - - for (int i = 0; i < pd.triCount; i++) - { - Vector3 v[3]; - const unsigned char* t = &tile.detailTris[(pd.triBase + i) * 4]; - - for (int k = 0; k < 3; k++) - { - if (t[k] < poly.vertCount) - { - v[k] = *(Vector3*)&tile.verts[poly.verts[t[k]] * 3]; - } - else - { - v[k] = *(Vector3*)&tile.detailVerts[(pd.vertBase + t[k] - poly.vertCount) * 3]; - } - } - - v[0].Y += DrawOffsetY; - v[1].Y += DrawOffsetY; - v[2].Y += DrawOffsetY; - - DEBUG_DRAW_TRIANGLE(v[0], v[1], v[2], Color::Green * 0.5f, 0, true); - } - - for (int k = 0; k < pd.triCount; k++) - { - const unsigned char* t = &tile.detailTris[(pd.triBase + k) * 4]; - Vector3 v[3]; - - for (int m = 0; m < 3; m++) - { - if (t[m] < poly.vertCount) - v[m] = *(Vector3*)&tile.verts[poly.verts[t[m]] * 3]; - else - v[m] = *(Vector3*)&tile.detailVerts[(pd.vertBase + (t[m] - poly.vertCount)) * 3]; - } - - v[0].Y += DrawOffsetY; - v[1].Y += DrawOffsetY; - v[2].Y += DrawOffsetY; - - for (int m = 0, n = 2; m < 3; n = m++) - { - // Skip inner detail edges - if (((t[3] >> (n * 2)) & 0x3) == 0) - continue; - - DEBUG_DRAW_LINE(v[n], v[m], Color::YellowGreen, 0, true); - } - } -} +#if COMPILE_WITH_DEBUG_DRAW void Navigation::DrawNavMesh() { - if (!_navMesh) - return; - - ScopeLock lock(_navMesh->Locker); - - const dtNavMesh* dtNavMesh = _navMesh->GetNavMesh(); - const int tilesCount = dtNavMesh ? dtNavMesh->getMaxTiles() : 0; - if (tilesCount == 0) - return; - - for (int tileIndex = 0; tileIndex < tilesCount; tileIndex++) + for (auto navMesh : NavMeshes) { - const dtMeshTile* tile = dtNavMesh->getTile(tileIndex); - if (!tile->header) - continue; - - //DebugDraw::DrawWireBox(*(BoundingBox*)&tile->header->bmin[0], Color::CadetBlue); - - for (int i = 0; i < tile->header->polyCount; i++) - { - const dtPoly* poly = &tile->polys[i]; - if (poly->getType() != DT_POLYTYPE_GROUND) - continue; - - DrawPoly(*tile, *poly); - } + navMesh->DebugDraw(); } } diff --git a/Source/Engine/Navigation/Navigation.h b/Source/Engine/Navigation/Navigation.h index ba15a9ff7..2557e31e8 100644 --- a/Source/Engine/Navigation/Navigation.h +++ b/Source/Engine/Navigation/Navigation.h @@ -6,8 +6,6 @@ class Scene; -#define NAV_MESH_PATH_MAX_SIZE 200 - /// /// The navigation service used for path finding and agents navigation system. /// @@ -88,7 +86,7 @@ public: #endif -#if USE_EDITOR +#if COMPILE_WITH_DEBUG_DRAW /// /// Draws the navigation for all the scenes (uses DebugDraw interface). diff --git a/Source/Engine/Navigation/NavigationScene.h b/Source/Engine/Navigation/NavigationScene.h deleted file mode 100644 index ecdca0744..000000000 --- a/Source/Engine/Navigation/NavigationScene.h +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. - -#pragma once - -#include "NavMeshData.h" -#include "Engine/Content/AssetReference.h" -#include "Engine/Content/Assets/RawDataAsset.h" - -class Scene; -class NavMeshBoundsVolume; - -/// -/// Scene object navigation data. -/// -class NavigationScene -{ - friend Scene; - -public: - - /// - /// Initializes a new instance of the class. - /// - /// The scene. - NavigationScene(Scene* scene); - -public: - - /// - /// The parent scene. - /// - Scene* Scene; - - /// - /// The flag used to mark that navigation data has been modified since load. Used to save runtime data to the file on scene serialization. - /// - bool IsDataDirty; - - /// - /// The navmesh tiles data. - /// - NavMeshData Data; - - /// - /// The cached navmesh data asset. - /// - AssetReference DataAsset; - -public: - - /// - /// The list of registered navigation bounds volumes (in the scene). - /// - Array Volumes; - - /// - /// Gets the total navigation volumes bounds. - /// - /// The navmesh bounds. - BoundingBox GetNavigationBounds(); - - /// - /// Finds the navigation volume bounds that have intersection with the given world-space bounding box. - /// - /// The bounds. - /// The intersecting volume or null if none found. - NavMeshBoundsVolume* FindNavigationBoundsOverlap(const BoundingBox& bounds); - -public: - - /// - /// Saves the nav mesh tiles data to the asset. Supported only in builds with assets saving enabled (eg. editor) and not during gameplay (eg. design time). - /// - void SaveNavMesh(); - - /// - /// Clears the data. - /// - void ClearData() - { - if (Data.Tiles.HasItems()) - { - IsDataDirty = true; - Data.TileSize = 0.0f; - Data.Tiles.Resize(0); - } - } - -protected: - - void OnEnable(); - void OnDisable(); - void OnDataAssetLoaded(); -}; diff --git a/Source/Engine/Navigation/NavigationSettings.cs b/Source/Engine/Navigation/NavigationSettings.cs index ff6f2aa4a..da300b3f1 100644 --- a/Source/Engine/Navigation/NavigationSettings.cs +++ b/Source/Engine/Navigation/NavigationSettings.cs @@ -1,8 +1,74 @@ // Copyright (c) 2012-2020 Wojciech Figat. All rights reserved. +using System; +using FlaxEngine; + namespace FlaxEditor.Content.Settings { partial class NavigationSettings { + // [Deprecated on 12.01.2021, expires on 12.01.2022] + private void UpgradeToNavMeshes() + { + if (NavMeshes != null && NavMeshes.Length == 1) + return; + NavMeshes = new NavMeshProperties[1]; + ref var navMesh = ref NavMeshes[0]; + navMesh.Name = "Default"; + navMesh.Color = Color.Green; + navMesh.Rotation = Quaternion.Identity; + navMesh.Agent.Radius = 34.0f; + navMesh.Agent.Height = 144.0f; + navMesh.Agent.StepHeight = 35.0f; + navMesh.Agent.MaxSlopeAngle = 60.0f; + } + + // [Deprecated on 12.01.2021, expires on 12.01.2022] + [Serialize, Obsolete] + private float WalkableRadius + { + get => throw new Exception(); + set + { + UpgradeToNavMeshes(); + NavMeshes[0].Agent.Radius = value; + } + } + + // [Deprecated on 12.01.2021, expires on 12.01.2022] + [Serialize, Obsolete] + private float WalkableHeight + { + get => throw new Exception(); + set + { + UpgradeToNavMeshes(); + NavMeshes[0].Agent.Height = value; + } + } + + // [Deprecated on 12.01.2021, expires on 12.01.2022] + [Serialize, Obsolete] + private float WalkableMaxClimb + { + get => throw new Exception(); + set + { + UpgradeToNavMeshes(); + NavMeshes[0].Agent.StepHeight = value; + } + } + + // [Deprecated on 12.01.2021, expires on 12.01.2022] + [Serialize, Obsolete] + private float WalkableMaxSlopeAngle + { + get => throw new Exception(); + set + { + UpgradeToNavMeshes(); + NavMeshes[0].Agent.MaxSlopeAngle = value; + } + } } } diff --git a/Source/Engine/Navigation/NavigationSettings.h b/Source/Engine/Navigation/NavigationSettings.h index fe4189955..e225a2f07 100644 --- a/Source/Engine/Navigation/NavigationSettings.h +++ b/Source/Engine/Navigation/NavigationSettings.h @@ -4,7 +4,7 @@ #include "NavigationTypes.h" #include "Engine/Core/Config/Settings.h" -#include "Engine/Serialization/Serialization.h" +#include "Engine/Core/Collections/Array.h" /// /// The navigation system settings container. @@ -12,108 +12,93 @@ API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API NavigationSettings : public SettingsBase { DECLARE_SCRIPTING_TYPE_MINIMAL(NavigationSettings); +public: + + /// + /// If checked, enables automatic navmesh actors spawning on a scenes that are using it during navigation building. + /// + API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Navigation\")") + bool AutoAddMissingNavMeshes = true; + + /// + /// If checked, enables automatic navmesh actors removing from a scenes that are not using it during navigation building. + /// + API_FIELD(Attributes="EditorOrder(110), EditorDisplay(\"Navigation\")") + bool AutoRemoveMissingNavMeshes = true; + public: /// /// The height of a grid cell in the navigation mesh building steps using heightfields. A lower number means higher precision on the vertical axis but longer build times. /// - API_FIELD(Attributes="DefaultValue(10.0f), Limit(1, 400), EditorOrder(10), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(1, 400), EditorOrder(210), EditorDisplay(\"Nav Mesh Options\")") float CellHeight = 10.0f; /// /// The width/height of a grid cell in the navigation mesh building steps using heightfields. A lower number means higher precision on the horizontal axes but longer build times. /// - API_FIELD(Attributes="DefaultValue(30.0f), Limit(1, 400), EditorOrder(20), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(1, 400), EditorOrder(220), EditorDisplay(\"Nav Mesh Options\")") float CellSize = 30.0f; /// /// Tile size used for Navigation mesh tiles, the final size of a tile is CellSize*TileSize. /// - API_FIELD(Attributes="DefaultValue(64), Limit(8, 4096), EditorOrder(30), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(8, 4096), EditorOrder(230), EditorDisplay(\"Nav Mesh Options\")") int32 TileSize = 64; /// /// The minimum number of cells allowed to form isolated island areas. /// - API_FIELD(Attributes="DefaultValue(0), Limit(0, 100), EditorOrder(40), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(0, 100), EditorOrder(240), EditorDisplay(\"Nav Mesh Options\")") int32 MinRegionArea = 0; /// /// Any regions with a span count smaller than this value will, if possible, be merged with larger regions. /// - API_FIELD(Attributes="DefaultValue(20), Limit(0, 100), EditorOrder(50), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(0, 100), EditorOrder(250), EditorDisplay(\"Nav Mesh Options\")") int32 MergeRegionArea = 20; /// /// The maximum allowed length for contour edges along the border of the mesh. /// - API_FIELD(Attributes="DefaultValue(1200.0f), Limit(100), EditorOrder(60), EditorDisplay(\"Nav Mesh Options\", \"Max Edge Length\")") + API_FIELD(Attributes="Limit(100), EditorOrder(260), EditorDisplay(\"Nav Mesh Options\", \"Max Edge Length\")") float MaxEdgeLen = 1200.0f; /// /// The maximum distance a simplified contour's border edges should deviate from the original raw contour. /// - API_FIELD(Attributes="DefaultValue(1.3f), Limit(0.1f, 4), EditorOrder(70), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(0.1f, 4), EditorOrder(270), EditorDisplay(\"Nav Mesh Options\")") float MaxEdgeError = 1.3f; /// /// The sampling distance to use when generating the detail mesh. /// - API_FIELD(Attributes="DefaultValue(600.0f), Limit(1), EditorOrder(80), EditorDisplay(\"Nav Mesh Options\", \"Detail Sampling Distance\")") + API_FIELD(Attributes="Limit(1), EditorOrder(280), EditorDisplay(\"Nav Mesh Options\", \"Detail Sampling Distance\")") float DetailSamplingDist = 600.0f; /// /// The maximum distance the detail mesh surface should deviate from heightfield data. /// - API_FIELD(Attributes="DefaultValue(1.0f), Limit(0, 3), EditorOrder(90), EditorDisplay(\"Nav Mesh Options\")") + API_FIELD(Attributes="Limit(0, 3), EditorOrder(290), EditorDisplay(\"Nav Mesh Options\")") float MaxDetailSamplingError = 1.0f; - /// - /// The radius of the smallest objects to traverse this nav mesh. Objects can't pass through gaps of less than twice the radius. - /// - API_FIELD(Attributes="DefaultValue(34.0f), Limit(0), EditorOrder(1000), EditorDisplay(\"Agent Options\")") - float WalkableRadius = 34.0f; +public: /// - /// The height of the smallest objects to traverse this nav mesh. Objects can't enter areas with ceilings lower than this value. + /// The configuration for navmeshes. /// - API_FIELD(Attributes="DefaultValue(144.0f), Limit(0), EditorOrder(1010), EditorDisplay(\"Agent Options\")") - float WalkableHeight = 144.0f; - - /// - /// The maximum ledge height that is considered to still be traversable. - /// - API_FIELD(Attributes="DefaultValue(35.0f), Limit(0), EditorOrder(1020), EditorDisplay(\"Agent Options\")") - float WalkableMaxClimb = 35.0f; - - /// - /// The maximum slope that is considered walkable (in degrees). Objects can't go up or down slopes higher than this value. - /// - API_FIELD(Attributes="DefaultValue(60.0f), Limit(0, 89.0f), EditorOrder(1030), EditorDisplay(\"Agent Options\")") - float WalkableMaxSlopeAngle = 60.0f; + API_FIELD(Attributes="EditorOrder(1000), EditorDisplay(\"Agents\")") + Array NavMeshes; public: + NavigationSettings(); + /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static NavigationSettings* Get(); // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - DESERIALIZE(CellHeight); - DESERIALIZE(CellSize); - DESERIALIZE(TileSize); - DESERIALIZE(MinRegionArea); - DESERIALIZE(MergeRegionArea); - DESERIALIZE(MaxEdgeLen); - DESERIALIZE(MaxEdgeError); - DESERIALIZE(DetailSamplingDist); - DESERIALIZE(MaxDetailSamplingError); - DESERIALIZE(WalkableRadius); - DESERIALIZE(WalkableHeight); - DESERIALIZE(WalkableMaxClimb); - DESERIALIZE(WalkableMaxSlopeAngle); - } + void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override; }; diff --git a/Source/Engine/Navigation/NavigationTypes.h b/Source/Engine/Navigation/NavigationTypes.h index a5672f342..f00c1683e 100644 --- a/Source/Engine/Navigation/NavigationTypes.h +++ b/Source/Engine/Navigation/NavigationTypes.h @@ -7,61 +7,88 @@ #include "Engine/Core/Math/Color.h" #include "Engine/Core/Math/Vector3.h" #include "Engine/Core/Math/Quaternion.h" +#include "Engine/Core/ISerializable.h" + +#define NAV_MESH_PATH_MAX_SIZE 200 /// /// The navigation system agent properties container for navmesh building and querying. /// -API_STRUCT() struct FLAXENGINE_API NavAgentProperties +API_STRUCT() struct FLAXENGINE_API NavAgentProperties : ISerializable { +API_AUTO_SERIALIZATION(); DECLARE_SCRIPTING_TYPE_MINIMAL(NavAgentProperties); /// /// The radius of the agent used for navigation. Agents can't pass through gaps of less than twice the radius. /// - API_FIELD() float Radius = 34.0f; + API_FIELD(Attributes="EditorOrder(0)") + float Radius = 34.0f; /// /// The height of the agent used for navigation. Agents can't enter areas with ceilings lower than this value. /// - API_FIELD() float Height = 144.0f; + API_FIELD(Attributes="EditorOrder(10)") + float Height = 144.0f; /// /// The step height used for navigation. Defines the maximum ledge height that is considered to still be traversable by the agent. /// - API_FIELD() float StepHeight = 35.0f; + API_FIELD(Attributes="EditorOrder(20)") + float StepHeight = 35.0f; /// /// The maximum slope (in degrees) that is considered walkable for navigation. Agents can't go up or down slopes higher than this value. /// - API_FIELD() float MaxSlopeAngle = 60.0f; + API_FIELD(Attributes="EditorOrder(30)") + float MaxSlopeAngle = 60.0f; + + bool operator==(const NavAgentProperties& other) const; + + bool operator!=(const NavAgentProperties& other) const + { + return !operator==(other); + } }; /// /// The navigation mesh properties container for navmesh building. /// -API_STRUCT() struct FLAXENGINE_API NavMeshProperties +API_STRUCT() struct FLAXENGINE_API NavMeshProperties : ISerializable { +API_AUTO_SERIALIZATION(); DECLARE_SCRIPTING_TYPE_MINIMAL(NavMeshProperties); /// - /// The navmesh type name (for debugging). + /// The navmesh type name. Identifies different types of the navmeshes, used to sync navmesh properties with settings asset. /// - API_FIELD() String Name; + API_FIELD(Attributes="EditorOrder(0)") + String Name; /// /// The navmesh type color (for debugging). /// - API_FIELD() Color Color; + API_FIELD(Attributes="EditorOrder(10)") + Color Color = Color::Green; /// /// The navmesh rotation applied to navigation surface. Used during building to the rotate scene geometry and to revert back result during path finding queries. Can be used to generate navmesh on walls. /// - API_FIELD() Quaternion Rotation; + API_FIELD(Attributes="EditorOrder(20)") + Quaternion Rotation = Quaternion::Identity; /// /// The properties of the agent used to generate walkable navigation surface. /// - API_FIELD() NavAgentProperties Agent; + API_FIELD(Attributes="EditorOrder(30)") + NavAgentProperties Agent; + + bool operator==(const NavMeshProperties& other) const; + + bool operator!=(const NavMeshProperties& other) const + { + return !operator==(other); + } }; ///