diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp index 90203af..2fc2a10 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.cpp @@ -3,7 +3,10 @@ #include "NavigationData.h" #include "NavMesh/RecastNavMesh.h" #include "Engine/World.h" +#include "Engine/GameViewportClient.h" +#include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" +#include "DrawDebugHelpers.h" namespace PS_Editor_NavUtils { @@ -17,6 +20,15 @@ namespace PS_Editor_NavUtils using ANavigationData::bEnableDrawing; }; + // Recast-specific draw flags exposer. + struct FRecastAccessor : public ARecastNavMesh + { + using ARecastNavMesh::bDrawTriangleEdges; + using ARecastNavMesh::bDrawPolyEdges; + using ARecastNavMesh::bDrawNavMeshEdges; + using ARecastNavMesh::bDrawTileBounds; + }; + void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode) { if (!World) return; @@ -46,6 +58,22 @@ namespace PS_Editor_NavUtils { if (!World) return; + // PIE/runtime nav visualization in UE has multiple gates that all need to align: + // + // 1. ANavigationData::bEnableDrawing — controls whether the rendering + // component creates a scene proxy at all. + // 2. ARecastNavMesh::bDrawNavMeshEdges (+ bDrawPolyEdges, bDrawTileBounds, etc.) + // — control what the proxy actually paints. With bEnableDrawing=true but every + // bDraw* flag false, the proxy is created but draws nothing. + // 3. GameViewportClient::EngineShowFlags.Navigation — the show-flag the + // `show navigation` console command toggles. The proxy is only rendered + // when this flag is on. + // 4. ReregisterAllComponents — if bEnableDrawing was false at construction + // time, the rendering component never created its proxy. Toggling + // bEnableDrawing alone won't recreate it; we have to re-register the + // components so the proxy is rebuilt with the new flag value. + // + // All four are required for runtime visualization to actually appear. TArray NavMeshActors; UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors); for (AActor* A : NavMeshActors) @@ -54,7 +82,116 @@ namespace PS_Editor_NavUtils { FNavDataAccessor* Accessor = static_cast(NavData); Accessor->bEnableDrawing = bVisible; - NavData->MarkComponentsRenderStateDirty(); + + // Recast-specific: enable enough draw flags that the proxy actually paints + // something visible. Edges only — tile bounds are noisy. + if (ARecastNavMesh* Recast = Cast(NavData)) + { + FRecastAccessor* RA = static_cast(Recast); + RA->bDrawNavMeshEdges = bVisible; + RA->bDrawPolyEdges = bVisible; + RA->bDrawTriangleEdges = bVisible; + } + + // Force proxy recreation. MarkComponentsRenderStateDirty alone is not + // enough when the proxy was never created (bEnableDrawing=false at init). + NavData->ReregisterAllComponents(); + } + } + + if (UGameViewportClient* GVC = World->GetGameViewport()) + { + GVC->EngineShowFlags.SetNavigation(bVisible); + } + } + + // ---- Custom navmesh visualization (works without engine modifications) ---- + // + // Cache the geometry between frames to avoid the expensive GetDebugGeometry call every + // tick. We refresh it on a small interval (default 0.5s) — enough for visual feedback + // during dynamic nav rebuilds without burning CPU on static maps. + struct FCachedNavMeshGeo + { + TArray Vertices; + TArray TriIndices; // groups of 3 — A B C + }; + static TArray GCachedNavMeshes; + static float GTimeSinceRefresh = 9999.f; // force initial refresh + + void ClearNavMeshDebugCache() + { + GCachedNavMeshes.Empty(); + GTimeSinceRefresh = 9999.f; + } + + void DrawNavMeshDebug(UWorld* World, float DeltaTime, float RefreshIntervalSeconds) + { + if (!World) return; + + GTimeSinceRefresh += DeltaTime; + if (GTimeSinceRefresh >= RefreshIntervalSeconds || GCachedNavMeshes.Num() == 0) + { + GTimeSinceRefresh = 0.f; + GCachedNavMeshes.Empty(); + + TArray NavMeshActors; + UGameplayStatics::GetAllActorsOfClass(World, ARecastNavMesh::StaticClass(), NavMeshActors); + for (AActor* A : NavMeshActors) + { + if (ARecastNavMesh* Recast = Cast(A)) + { + Recast->BeginBatchQuery(); + + FCachedNavMeshGeo Cached; + + // Iterate every tile and pull its debug geometry. UE 5.5 dropped the + // single-call GetDebugGeometry; only the per-tile variant remains. + const int32 NumTiles = Recast->GetNavMeshTilesCount(); + for (int32 TileIdx = 0; TileIdx < NumTiles; ++TileIdx) + { + FRecastDebugGeometry Geo; + Recast->GetDebugGeometryForTile(Geo, TileIdx); + + const int32 BaseIdx = Cached.Vertices.Num(); + Cached.Vertices.Append(Geo.MeshVerts); + for (int32 AreaIdx = 0; AreaIdx < RECAST_MAX_AREAS; ++AreaIdx) + { + const TArray& AreaIndices = Geo.AreaIndices[AreaIdx]; + Cached.TriIndices.Reserve(Cached.TriIndices.Num() + AreaIndices.Num()); + for (int32 Idx : AreaIndices) + { + Cached.TriIndices.Add(BaseIdx + Idx); + } + } + } + + Recast->FinishBatchQuery(); + GCachedNavMeshes.Add(MoveTemp(Cached)); + } + } + } + + // Per-frame draw of cached geometry. Lifetime=0 + bPersistent=false means each call + // only paints for one frame, so we get clean redraw every tick. + const FColor EdgeColor(40, 220, 90); // green + for (const FCachedNavMeshGeo& Cached : GCachedNavMeshes) + { + const int32 NumTris = Cached.TriIndices.Num() / 3; + for (int32 t = 0; t < NumTris; ++t) + { + const int32 I0 = Cached.TriIndices[t * 3 + 0]; + const int32 I1 = Cached.TriIndices[t * 3 + 1]; + const int32 I2 = Cached.TriIndices[t * 3 + 2]; + if (!Cached.Vertices.IsValidIndex(I0) || !Cached.Vertices.IsValidIndex(I1) || !Cached.Vertices.IsValidIndex(I2)) + { + continue; + } + const FVector& V0 = Cached.Vertices[I0]; + const FVector& V1 = Cached.Vertices[I1]; + const FVector& V2 = Cached.Vertices[I2]; + DrawDebugLine(World, V0, V1, EdgeColor, /*bPersistent=*/ false, /*LifeTime=*/ 0.f, /*DepthPriority=*/ 0, /*Thickness=*/ 1.0f); + DrawDebugLine(World, V1, V2, EdgeColor, false, 0.f, 0, 1.0f); + DrawDebugLine(World, V2, V0, EdgeColor, false, 0.f, 0, 1.0f); } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h index e88b7d8..5c20c73 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_NavUtils.h @@ -34,4 +34,22 @@ namespace PS_Editor_NavUtils * be stripped depending on build flags. */ PS_EDITOR_API void SetNavMeshVisible(UWorld* World, bool bVisible); + + /** + * Draw the navmesh as green debug lines for the current frame. Bypasses UE's own nav + * debug renderer (which is gated behind UE_ALLOW_NAVMESH_DEBUG_DRAWING_IN_GAME and + * doesn't work on binary engine installs at runtime). Pulls the geometry from each + * ARecastNavMesh and emits DrawDebugLine calls — works in PIE / Standalone / packaged + * shipping alike. + * + * Internally caches the FRecastDebugGeometry between calls and only refreshes it when + * RefreshIntervalSeconds elapses, since GetDebugGeometry is expensive on big navmeshes. + * + * Call from a per-tick code path while the user wants the visualization shown; stop + * calling when they hide it (DrawDebugLine entries auto-expire after 1 frame). + */ + PS_EDITOR_API void DrawNavMeshDebug(UWorld* World, float DeltaTime, float RefreshIntervalSeconds = 0.5f); + + /** Drop the cached debug geometry (call when leaving editor mode / unloading scene). */ + PS_EDITOR_API void ClearNavMeshDebugCache(); }