Runtime nav settings + custom debug viz (P key) + editable restore on stop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 11:46:27 +02:00
parent 2a53d12e68
commit b9ec67f9ff
2 changed files with 156 additions and 1 deletions

View File

@@ -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<AActor*> NavMeshActors;
UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors);
for (AActor* A : NavMeshActors)
@@ -54,7 +82,116 @@ namespace PS_Editor_NavUtils
{
FNavDataAccessor* Accessor = static_cast<FNavDataAccessor*>(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<ARecastNavMesh>(NavData))
{
FRecastAccessor* RA = static_cast<FRecastAccessor*>(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<FVector> Vertices;
TArray<int32> TriIndices; // groups of 3 — A B C
};
static TArray<FCachedNavMeshGeo> 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<AActor*> NavMeshActors;
UGameplayStatics::GetAllActorsOfClass(World, ARecastNavMesh::StaticClass(), NavMeshActors);
for (AActor* A : NavMeshActors)
{
if (ARecastNavMesh* Recast = Cast<ARecastNavMesh>(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<int32>& 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);
}
}
}

View File

@@ -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();
}