Compare commits
3 Commits
49cb480e15
...
8f7476d2db
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f7476d2db | |||
| b9ec67f9ff | |||
| 2a53d12e68 |
Binary file not shown.
@@ -13,6 +13,8 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_GroundSnap.h"
|
#include "PS_Editor_GroundSnap.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -254,6 +256,11 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_Focus = NewObject<UInputAction>(this, TEXT("IA_Focus"));
|
IA_Focus = NewObject<UInputAction>(this, TEXT("IA_Focus"));
|
||||||
IA_Focus->ValueType = EInputActionValueType::Boolean;
|
IA_Focus->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_Focus, EKeys::F);
|
EditorMappingContext->MapKey(IA_Focus, EKeys::F);
|
||||||
|
|
||||||
|
// Toggle navmesh visualization (mimics UE editor's P key)
|
||||||
|
IA_ToggleNavMesh = NewObject<UInputAction>(this, TEXT("IA_ToggleNavMesh"));
|
||||||
|
IA_ToggleNavMesh->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_ToggleNavMesh, EKeys::P);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -301,6 +308,7 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
EIC->BindAction(IA_Confirm, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleConfirm);
|
||||||
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
EIC->BindAction(IA_Cancel, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleCancel);
|
||||||
EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus);
|
EIC->BindAction(IA_Focus, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleFocus);
|
||||||
|
EIC->BindAction(IA_ToggleNavMesh, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleToggleNavMesh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -1159,8 +1167,35 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
// Filter out mandatory actors (UPS_Editor_SpawnableComponent::bIsMandatory) — they're
|
||||||
|
// pinned to the scene by design and the delete is silently refused for them. We pre-filter
|
||||||
|
// here so the confirmation dialog count matches what will actually be deleted, and we
|
||||||
|
// skip the dialog entirely if nothing is deletable.
|
||||||
|
UPS_Editor_SpawnManager* SpawnManager = EditorPC->GetSpawnManager();
|
||||||
|
TArray<TWeakObjectPtr<AActor>> DeletableActors;
|
||||||
|
int32 SkippedMandatory = 0;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
|
{
|
||||||
|
AActor* Actor = Weak.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
if (SpawnManager && !SpawnManager->IsActorDeletable(Actor))
|
||||||
|
{
|
||||||
|
++SkippedMandatory;
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: '%s' is a mandatory actor — delete refused."),
|
||||||
|
*Actor->GetName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
DeletableActors.Add(Weak);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DeletableActors.Num() == 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Delete aborted — selection contained only mandatory actor(s) (%d)."), SkippedMandatory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show in-game confirmation dialog (runtime compatible)
|
// Show in-game confirmation dialog (runtime compatible)
|
||||||
const int32 Count = SM->GetSelectedActors().Num();
|
const int32 Count = DeletableActors.Num();
|
||||||
const FText Message = FText::Format(
|
const FText Message = FText::Format(
|
||||||
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
NSLOCTEXT("PS_Editor", "DeleteConfirm", "Delete {0} selected object(s)?"),
|
||||||
FText::AsNumber(Count));
|
FText::AsNumber(Count));
|
||||||
@@ -1173,15 +1208,15 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
|||||||
// Weak refs to avoid dangling pointers in the callback
|
// Weak refs to avoid dangling pointers in the callback
|
||||||
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC = EditorPC;
|
TWeakObjectPtr<APS_Editor_PlayerController> WeakPC = EditorPC;
|
||||||
|
|
||||||
Dialog->Show(Message, FPS_Editor_OnConfirmResult::CreateLambda([WeakPC](bool bConfirmed)
|
Dialog->Show(Message, FPS_Editor_OnConfirmResult::CreateLambda([WeakPC, DeletableActors](bool bConfirmed)
|
||||||
{
|
{
|
||||||
if (!bConfirmed || !WeakPC.IsValid()) return;
|
if (!bConfirmed || !WeakPC.IsValid()) return;
|
||||||
|
|
||||||
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
|
UPS_Editor_SelectionManager* SM = WeakPC->GetSelectionManager();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM) return;
|
||||||
|
|
||||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : DeletableActors)
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
@@ -1460,6 +1495,20 @@ void APS_Editor_Pawn::HandleFocus(const FInputActionValue& Value)
|
|||||||
OrbitDistance = TargetDistance;
|
OrbitDistance = TargetDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleToggleNavMesh(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
// Mimics UE editor's P key — toggles the runtime navmesh visualization. We route
|
||||||
|
// through SpawnManager's session-only bShowNavMesh flag so the Settings popup
|
||||||
|
// checkbox stays in sync.
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||||
|
if (!EditorPC) return;
|
||||||
|
UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
SM->bShowNavMesh = !SM->bShowNavMesh;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
void APS_Editor_Pawn::SetCameraMode(EPS_Editor_CameraMode NewMode)
|
||||||
|
|||||||
@@ -212,6 +212,9 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_Focus;
|
TObjectPtr<UInputAction> IA_Focus;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_ToggleNavMesh;
|
||||||
|
|
||||||
// ---- Spline point / ChildMovable drag state ----
|
// ---- Spline point / ChildMovable drag state ----
|
||||||
TWeakObjectPtr<AActor> DraggedSplineActor;
|
TWeakObjectPtr<AActor> DraggedSplineActor;
|
||||||
TArray<FVector> SplinePointDragInitialState;
|
TArray<FVector> SplinePointDragInitialState;
|
||||||
@@ -260,6 +263,7 @@ private:
|
|||||||
void HandleConfirm(const FInputActionValue& Value);
|
void HandleConfirm(const FInputActionValue& Value);
|
||||||
void HandleCancel(const FInputActionValue& Value);
|
void HandleCancel(const FInputActionValue& Value);
|
||||||
void HandleFocus(const FInputActionValue& Value);
|
void HandleFocus(const FInputActionValue& Value);
|
||||||
|
void HandleToggleNavMesh(const FInputActionValue& Value);
|
||||||
|
|
||||||
// ---- Focus animation state (F key, frame selection) ----
|
// ---- Focus animation state (F key, frame selection) ----
|
||||||
bool bIsFocusing = false;
|
bool bIsFocusing = false;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include "PS_Editor_SplineEditable.h"
|
#include "PS_Editor_SplineEditable.h"
|
||||||
#include "PS_Editor_PointPlaceable.h"
|
#include "PS_Editor_PointPlaceable.h"
|
||||||
#include "PS_Editor_EditableInterface.h"
|
#include "PS_Editor_EditableInterface.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
#include "Engine/LevelStreamingDynamic.h"
|
#include "Engine/LevelStreamingDynamic.h"
|
||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
@@ -29,6 +31,44 @@
|
|||||||
#include "GameFramework/GameStateBase.h"
|
#include "GameFramework/GameStateBase.h"
|
||||||
#include "GameFramework/PlayerState.h"
|
#include "GameFramework/PlayerState.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/** Resolve an editable property path ("Comp.Prop" or "Prop") on Actor to a UObject target +
|
||||||
|
* FProperty pointer. Mirrors PS_Editor_MainWidget::FindPropertyTarget + class lookup so the
|
||||||
|
* PlayerController can snapshot/restore values without depending on the widget. */
|
||||||
|
static bool ResolveEditableProp(AActor* Actor, const FName& Path, UObject*& OutTarget, FProperty*& OutProp)
|
||||||
|
{
|
||||||
|
OutTarget = nullptr;
|
||||||
|
OutProp = nullptr;
|
||||||
|
if (!Actor) return false;
|
||||||
|
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
|
||||||
|
if (CompName.IsEmpty())
|
||||||
|
{
|
||||||
|
OutTarget = Actor;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
Actor->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName)
|
||||||
|
{
|
||||||
|
OutTarget = Comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!OutTarget) return false;
|
||||||
|
OutProp = OutTarget->GetClass()->FindPropertyByName(*PropName);
|
||||||
|
return OutProp != nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
{
|
{
|
||||||
bShowMouseCursor = true;
|
bShowMouseCursor = true;
|
||||||
@@ -275,6 +315,43 @@ void APS_Editor_PlayerController::StartSimulation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot editable property values so we can restore them on Stop. Game logic may mutate
|
||||||
|
// editable properties during simulate (e.g. WalkSpeed driven by gameplay code) — restoring
|
||||||
|
// on Stop matches the convention used for transforms. Skip properties already tracked by the
|
||||||
|
// timeline: the timeline's RestoreBaseline owns those, double-restoring would conflict.
|
||||||
|
SimulationSavedEditableProps.Empty();
|
||||||
|
UPS_Editor_TimelineSubsystem* TimelineSS = GetWorld() ? GetWorld()->GetSubsystem<UPS_Editor_TimelineSubsystem>() : nullptr;
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
||||||
|
{
|
||||||
|
AActor* Actor = Weak.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
|
if (!EditComp || EditComp->EditableProperties.Num() == 0) continue;
|
||||||
|
|
||||||
|
TMap<FName, FString> ActorMap;
|
||||||
|
for (const FPS_Editor_EditablePropertyEntry& Entry : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
if (TimelineSS && TimelineSS->IsPropertyTracked(Actor, Entry.Path))
|
||||||
|
{
|
||||||
|
continue; // timeline owns this one
|
||||||
|
}
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
if (!ResolveEditableProp(Actor, Entry.Path, Target, Prop)) continue;
|
||||||
|
|
||||||
|
FString Exported;
|
||||||
|
Prop->ExportText_InContainer(0, Exported, Target, Target, nullptr, PPF_None);
|
||||||
|
ActorMap.Add(Entry.Path, Exported);
|
||||||
|
}
|
||||||
|
if (ActorMap.Num() > 0)
|
||||||
|
{
|
||||||
|
SimulationSavedEditableProps.Add(Actor, MoveTemp(ActorMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear selection
|
// Clear selection
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
{
|
{
|
||||||
@@ -389,18 +466,23 @@ void APS_Editor_PlayerController::SetSimulationPaused(bool bPaused)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::RequestStopSimulation()
|
void APS_Editor_PlayerController::RequestStopSimulation(bool bImmediate)
|
||||||
{
|
{
|
||||||
if (!bSimulating) return;
|
if (!bSimulating) return;
|
||||||
|
|
||||||
// If a request is already scheduled, do nothing (debounce double-clicks on Stop).
|
// If a request is already scheduled, do nothing (debounce double-clicks on Stop).
|
||||||
|
// Skip the debounce when bImmediate (Restart wants to flush right now).
|
||||||
|
if (!bImmediate)
|
||||||
|
{
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (PendingStopSimulationHandle.IsValid()) return;
|
if (PendingStopSimulationHandle.IsValid()) return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fire OnEditorBeforeSimulateStop immediately on every actor so BP handlers can react
|
// Fire OnEditorBeforeSimulateStop immediately on every actor so BP handlers can react
|
||||||
// while the BT is still live. Then schedule the real cut for ~500ms later.
|
// while the BT is still live. Then schedule the real cut (deferred) or run it now
|
||||||
|
// (immediate, used by timeline Restart).
|
||||||
if (SpawnManager)
|
if (SpawnManager)
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
|
||||||
@@ -413,6 +495,21 @@ void APS_Editor_PlayerController::RequestStopSimulation()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bImmediate)
|
||||||
|
{
|
||||||
|
// Cancel any pending deferred stop so it doesn't fire again later.
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (PendingStopSimulationHandle.IsValid())
|
||||||
|
{
|
||||||
|
World->GetTimerManager().ClearTimer(PendingStopSimulationHandle);
|
||||||
|
PendingStopSimulationHandle.Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StopSimulation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
TWeakObjectPtr<APS_Editor_PlayerController> WeakThis(this);
|
TWeakObjectPtr<APS_Editor_PlayerController> WeakThis(this);
|
||||||
@@ -502,7 +599,49 @@ void APS_Editor_PlayerController::StopSimulation()
|
|||||||
}
|
}
|
||||||
SimulationSavedTransforms.Empty();
|
SimulationSavedTransforms.Empty();
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored"));
|
// Restore editable properties that game logic may have mutated during simulate. The timeline's
|
||||||
|
// own RestoreBaseline already covers the animated ones (we skipped them at snapshot time), so
|
||||||
|
// this loop only touches properties NOT owned by the timeline — no double-write. Mirrors the
|
||||||
|
// canonical UI write path (ImportText + MarkRenderStateDirty + RepNotify + OnEditorPropertyChanged)
|
||||||
|
// so actors reinit consistently regardless of where the value change originated.
|
||||||
|
int32 RestoredCount = 0;
|
||||||
|
for (auto& ActorPair : SimulationSavedEditableProps)
|
||||||
|
{
|
||||||
|
AActor* Actor = ActorPair.Key.Get();
|
||||||
|
if (!Actor) continue;
|
||||||
|
const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass());
|
||||||
|
for (auto& PropPair : ActorPair.Value)
|
||||||
|
{
|
||||||
|
UObject* Target = nullptr;
|
||||||
|
FProperty* Prop = nullptr;
|
||||||
|
if (!ResolveEditableProp(Actor, PropPair.Key, Target, Prop)) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(*PropPair.Value, Target, Target, PPF_None);
|
||||||
|
|
||||||
|
if (UActorComponent* Comp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
Comp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepNotify mirror — ImportText bypasses it, same as the UI write path.
|
||||||
|
if (!Prop->RepNotifyFunc.IsNone())
|
||||||
|
{
|
||||||
|
if (UFunction* RepFunc = Target->FindFunction(Prop->RepNotifyFunc))
|
||||||
|
{
|
||||||
|
Target->ProcessEvent(RepFunc, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bImplementsInterface)
|
||||||
|
{
|
||||||
|
IPS_Editor_EditableInterface::Execute_OnEditorPropertyChanged(Actor, PropPair.Key.ToString());
|
||||||
|
}
|
||||||
|
++RestoredCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SimulationSavedEditableProps.Empty();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored (%d editable properties also restored)"), RestoredCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
||||||
@@ -824,6 +963,16 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-spawn mandatory actors now that the baseLevel is loaded and visible. This covers:
|
||||||
|
// - Initial editor startup (default baseLevel loads at BeginPlay)
|
||||||
|
// - User switching baseLevel via the UI
|
||||||
|
// Idempotent — already-present mandatory instances are skipped. The catalog was rebuilt
|
||||||
|
// just before LoadLevelInstance so its bIsMandatory cache is fresh.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
|
}
|
||||||
|
|
||||||
// Delay 2s then fade in from black over 2 seconds
|
// Delay 2s then fade in from black over 2 seconds
|
||||||
if (bWaitingForSublevelFadeIn)
|
if (bWaitingForSublevelFadeIn)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,9 +76,14 @@ public:
|
|||||||
/** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules
|
/** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules
|
||||||
* the real StopSimulation after a grace period (500ms by default) so BT has time to
|
* the real StopSimulation after a grace period (500ms by default) so BT has time to
|
||||||
* react to whatever the BP handler does in OnEditorBeforeSimulateStop.
|
* react to whatever the BP handler does in OnEditorBeforeSimulateStop.
|
||||||
* Timeline Stop and the Simulate toolbar button both go through this. */
|
* Timeline Stop and the Simulate toolbar button both go through this.
|
||||||
|
*
|
||||||
|
* @param bImmediate If true, fire OnEditorBeforeSimulateStop and run StopSimulation
|
||||||
|
* synchronously in the same call (no grace period). Used by
|
||||||
|
* timeline Restart, where we want transforms restored before the
|
||||||
|
* subsequent Play(). Defaults to false (deferred). */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
void RequestStopSimulation();
|
void RequestStopSimulation(bool bImmediate = false);
|
||||||
|
|
||||||
/** True while simulation is running. */
|
/** True while simulation is running. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS_Editor")
|
||||||
@@ -175,6 +180,12 @@ private:
|
|||||||
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
|
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
|
||||||
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
|
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
|
||||||
|
|
||||||
|
/** Saved exported-text values for editable properties (UPS_Editor_EditableComponent::EditableProperties)
|
||||||
|
* that aren't already managed by the timeline. Game logic may mutate these during simulate;
|
||||||
|
* StopSimulation restores them so the editor returns to the pre-Play state for ALL editable
|
||||||
|
* properties, animated or not. Map: Actor -> { PropertyPath -> ExportedTextValue }. */
|
||||||
|
TMap<TWeakObjectPtr<AActor>, TMap<FName, FString>> SimulationSavedEditableProps;
|
||||||
|
|
||||||
/** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */
|
/** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */
|
||||||
FTimerHandle PendingStopSimulationHandle;
|
FTimerHandle PendingStopSimulationHandle;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class PS_Editor : ModuleRules
|
|||||||
"JsonUtilities",
|
"JsonUtilities",
|
||||||
"ProceduralMeshComponent",
|
"ProceduralMeshComponent",
|
||||||
"DesktopPlatform",
|
"DesktopPlatform",
|
||||||
"AIModule"
|
"AIModule",
|
||||||
|
"NavigationSystem"
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
|||||||
@@ -61,4 +61,11 @@ struct FPS_Editor_SceneData
|
|||||||
/** Global timeline data (animation tracks + duration). Empty on legacy scenes. */
|
/** Global timeline data (animation tracks + duration). Empty on legacy scenes. */
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FPS_Editor_TimelineData Timeline;
|
FPS_Editor_TimelineData Timeline;
|
||||||
|
|
||||||
|
/** If true, the level's nav mesh is switched to Dynamic runtime generation when this
|
||||||
|
* scenario loads — both in the runtime editor (so spawn / move actors update nav live)
|
||||||
|
* and in gameplay (for scenarios that need runtime-built navigation). False keeps the
|
||||||
|
* cooked Static nav, which has zero runtime cost. Default: false. */
|
||||||
|
UPROPERTY()
|
||||||
|
bool bUseDynamicNavigation = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
#include "BrainComponent.h"
|
#include "BrainComponent.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
@@ -130,6 +131,12 @@ bool UPS_Editor_SceneLoader::LoadSceneFromData(UWorld* World, const FPS_Editor_S
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the scenario's dynamic-navigation flag BEFORE spawning actors. If the scenario
|
||||||
|
// needs runtime nav generation, switching the level's RecastNavMesh to Dynamic now
|
||||||
|
// means the actors we're about to spawn will dirty + rebuild tiles around them. Cooked
|
||||||
|
// Static nav is preserved on disk for scenarios that don't need this.
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(World, SceneData.bUseDynamicNavigation);
|
||||||
|
|
||||||
UPS_Editor_TimelineSubsystem* TimelineSS = World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
|
UPS_Editor_TimelineSubsystem* TimelineSS = World->GetSubsystem<UPS_Editor_TimelineSubsystem>();
|
||||||
|
|
||||||
int32 SpawnedCount = 0;
|
int32 SpawnedCount = 0;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
#include "PS_Editor_SplineEditable.h"
|
#include "PS_Editor_SplineEditable.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
@@ -166,6 +167,12 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
SceneData.Timeline = TimelineSS->GetData();
|
SceneData.Timeline = TimelineSS->GetData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-scenario settings — read from the SpawnManager's session state.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SceneData.bUseDynamicNavigation = SpawnManager->bUseDynamicNavigation;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to JSON
|
// Convert to JSON
|
||||||
FString JsonString;
|
FString JsonString;
|
||||||
if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true))
|
if (!FJsonObjectConverter::UStructToJsonObjectString(SceneData, JsonString, 0, 0, 0, nullptr, true))
|
||||||
@@ -438,6 +445,23 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-scenario settings — push into the SpawnManager's session state and apply
|
||||||
|
// the dynamic-navigation switch to the level's RecastNavMesh actors right away.
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->bUseDynamicNavigation = SceneData.bUseDynamicNavigation;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOuter()))
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(EditorPC->GetWorld(), SpawnManager->bUseDynamicNavigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add any mandatory actors missing from the loaded scene. Handles legacy saves
|
||||||
|
// that predate the bIsMandatory flag — the mandatory actor is auto-spawned at the
|
||||||
|
// baseLevel's MandatorySpawnAnchor. If all mandatory actors were already in the JSON,
|
||||||
|
// this is a no-op.
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount);
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scene loaded: %s (%d actors)"), *SceneName, SpawnedCount);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -494,6 +518,14 @@ void UPS_Editor_SceneSerializer::ClearScene(UPS_Editor_SpawnManager* SpawnManage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: Mandatory-actor auto-spawn is intentionally NOT done here. ClearScene is called
|
||||||
|
// from three contexts: (1) "New Scenario" button (we want mandatory spawn — handled in the
|
||||||
|
// widget after ClearScene returns), (2) LoadScene before re-spawning saved actors (we
|
||||||
|
// don't want it — the JSON itself contains the mandatory actor, and there's a final
|
||||||
|
// EnsureMandatoryActorsPresent at the end of LoadScene that handles legacy saves), and
|
||||||
|
// (3) BaseLevel switch (we don't want it — the new baseLevel isn't loaded yet, the
|
||||||
|
// MandatoryAnchor isn't in the world; the post-OnLevelShown hook handles that).
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const
|
FString UPS_Editor_SceneSerializer::GetSceneFilePath(const FString& SceneName) const
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
|
#include "Components/SceneComponent.h"
|
||||||
|
#include "Components/ArrowComponent.h"
|
||||||
|
#include "Components/BillboardComponent.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
|
||||||
|
APS_Editor_MandatoryAnchor::APS_Editor_MandatoryAnchor()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||||
|
RootComponent = Root;
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
SpriteComponent = CreateDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
|
||||||
|
SpriteComponent->SetupAttachment(Root);
|
||||||
|
SpriteComponent->SetHiddenInGame(true);
|
||||||
|
SpriteComponent->bIsScreenSizeScaled = true;
|
||||||
|
SpriteComponent->ScreenSize = 0.0015f;
|
||||||
|
|
||||||
|
// Pivot icon — clearly distinct from APS_Editor_CameraStart's note icon.
|
||||||
|
static ConstructorHelpers::FObjectFinder<UTexture2D> IconFinder(TEXT("/Engine/EditorResources/S_Pivot"));
|
||||||
|
if (IconFinder.Succeeded())
|
||||||
|
{
|
||||||
|
SpriteComponent->SetSprite(IconFinder.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrowComponent = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
|
||||||
|
ArrowComponent->SetupAttachment(Root);
|
||||||
|
ArrowComponent->ArrowColor = FColor(255, 200, 60); // orange so it pops in the viewport
|
||||||
|
ArrowComponent->ArrowSize = 1.5f;
|
||||||
|
ArrowComponent->bIsScreenSizeScaled = true;
|
||||||
|
ArrowComponent->SetHiddenInGame(true);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The anchor is purely a marker — no rendering in PIE / packaged
|
||||||
|
SetActorHiddenInGame(true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone marker actor that defines where mandatory actors
|
||||||
|
* (UPS_Editor_SpawnableComponent::bIsMandatory) auto-spawn when a new scenario is created
|
||||||
|
* or when a legacy save is missing one.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Place ONE instance of this actor in each baseLevel
|
||||||
|
* - Position it freely — independent from APS_Editor_CameraStart
|
||||||
|
* - Save the level
|
||||||
|
*
|
||||||
|
* The actor is hidden in game / PIE; it has a visible orange arrow + sprite in the level
|
||||||
|
* editor for placement. SpawnManager::EnsureMandatoryActorsPresent reads its world transform.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType, Blueprintable, meta = (DisplayName = "PS Editor Mandatory Anchor"))
|
||||||
|
class PS_EDITOR_API APS_Editor_MandatoryAnchor : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
APS_Editor_MandatoryAnchor();
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
protected:
|
||||||
|
/** Editor-only billboard sprite so the actor is easy to pick in the level. */
|
||||||
|
UPROPERTY(VisibleAnywhere, Category = "PS_Editor")
|
||||||
|
TObjectPtr<class UBillboardComponent> SpriteComponent;
|
||||||
|
|
||||||
|
/** Editor-only forward arrow visualizing the anchor's orientation. */
|
||||||
|
UPROPERTY(VisibleAnywhere, Category = "PS_Editor")
|
||||||
|
TObjectPtr<class UArrowComponent> ArrowComponent;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "NavigationSystem.h"
|
||||||
|
#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"
|
||||||
|
#include "ProceduralMeshComponent.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "UObject/ConstructorHelpers.h"
|
||||||
|
#include "UObject/SoftObjectPath.h"
|
||||||
|
|
||||||
|
namespace PS_Editor_NavUtils
|
||||||
|
{
|
||||||
|
// ANavigationData::RuntimeGeneration and bEnableDrawing are both protected. Local
|
||||||
|
// using-trick subclass to re-publish them — the C++ access check uses the static
|
||||||
|
// type, so a static_cast through this exposing class lets us read/write them
|
||||||
|
// without modifying the engine.
|
||||||
|
struct FNavDataAccessor : public ANavigationData
|
||||||
|
{
|
||||||
|
using ANavigationData::RuntimeGeneration;
|
||||||
|
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;
|
||||||
|
|
||||||
|
TArray<AActor*> NavMeshActors;
|
||||||
|
UGameplayStatics::GetAllActorsOfClass(World, ANavigationData::StaticClass(), NavMeshActors);
|
||||||
|
for (AActor* A : NavMeshActors)
|
||||||
|
{
|
||||||
|
if (ANavigationData* NavData = Cast<ANavigationData>(A))
|
||||||
|
{
|
||||||
|
FNavDataAccessor* Accessor = static_cast<FNavDataAccessor*>(NavData);
|
||||||
|
if (Accessor->RuntimeGeneration != Mode)
|
||||||
|
{
|
||||||
|
Accessor->RuntimeGeneration = Mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic)
|
||||||
|
{
|
||||||
|
SetRuntimeGenerationMode(World,
|
||||||
|
bDynamic ? ERuntimeGenerationType::Dynamic : ERuntimeGenerationType::Static);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetNavMeshVisible(UWorld* World, bool bVisible)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (ANavigationData* NavData = Cast<ANavigationData>(A))
|
||||||
|
{
|
||||||
|
FNavDataAccessor* Accessor = static_cast<FNavDataAccessor*>(NavData);
|
||||||
|
Accessor->bEnableDrawing = bVisible;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Lift the cached navmesh geometry slightly so the wireframe + translucent surface don't
|
||||||
|
// z-fight with the floor mesh underneath (the navmesh sits exactly on the walkable surface
|
||||||
|
// per the cell height, which causes shimmering with translucent materials especially).
|
||||||
|
// 2cm is enough to suppress visual artifacts on typical level geometry without making the
|
||||||
|
// overlay look detached from the floor.
|
||||||
|
static constexpr float NavMeshDrawZOffset = 2.0f;
|
||||||
|
|
||||||
|
// ---- Surface visualization (procedural mesh + user-supplied translucent material) ----
|
||||||
|
//
|
||||||
|
// We spawn a single persistent actor with one UProceduralMeshComponent that holds the
|
||||||
|
// triangulated navmesh. Its material is loaded once from a hardcoded soft path:
|
||||||
|
// /PS_Editor/M_PS_Editor_NavMeshSurface
|
||||||
|
// Users create that material in the plugin Content folder with their own settings
|
||||||
|
// (translucent, two-sided, color, etc). If the asset is missing, we log a one-shot warning
|
||||||
|
// and the surface stays hidden — the wireframe still draws so the feature degrades
|
||||||
|
// gracefully rather than crashing.
|
||||||
|
static const TCHAR* GNavMeshSurfaceMaterialPath = TEXT("/PS_Editor/M_PS_Editor_NavMeshSurface.M_PS_Editor_NavMeshSurface");
|
||||||
|
static TWeakObjectPtr<AActor> GSurfaceActor;
|
||||||
|
static TWeakObjectPtr<UProceduralMeshComponent> GSurfaceMesh;
|
||||||
|
static TWeakObjectPtr<UMaterialInterface> GSurfaceMaterial;
|
||||||
|
static bool GSurfaceMaterialMissingWarned = false;
|
||||||
|
|
||||||
|
static UMaterialInterface* LoadSurfaceMaterial()
|
||||||
|
{
|
||||||
|
if (GSurfaceMaterial.IsValid()) return GSurfaceMaterial.Get();
|
||||||
|
|
||||||
|
// Always retry — LoadObject is cheap when the asset is already loaded, and we want
|
||||||
|
// the load to succeed even if the material was created AFTER the first attempt
|
||||||
|
// (Live Coding hot-reload, or user just created the asset). Suppressing retries
|
||||||
|
// breaks "create the material, hit P, see it work" without restarting the editor.
|
||||||
|
UMaterialInterface* Mat = LoadObject<UMaterialInterface>(nullptr, GNavMeshSurfaceMaterialPath);
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
GSurfaceMaterial = Mat;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: NavMesh surface material loaded from %s"), GNavMeshSurfaceMaterialPath);
|
||||||
|
GSurfaceMaterialMissingWarned = false;
|
||||||
|
return Mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GSurfaceMaterialMissingWarned)
|
||||||
|
{
|
||||||
|
GSurfaceMaterialMissingWarned = true;
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: NavMesh surface material not found at %s. ")
|
||||||
|
TEXT("Create a translucent material at that path to enable surface visualization. ")
|
||||||
|
TEXT("Falling back to wireframe-only."), GNavMeshSurfaceMaterialPath);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UProceduralMeshComponent* EnsureSurfaceComponent(UWorld* World)
|
||||||
|
{
|
||||||
|
if (!World) return nullptr;
|
||||||
|
if (GSurfaceMesh.IsValid()) return GSurfaceMesh.Get();
|
||||||
|
|
||||||
|
// Spawn a minimal actor and add a UProceduralMeshComponent at runtime. The actor is
|
||||||
|
// transient — never saved with the scenario, never selectable in the editor.
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.ObjectFlags = RF_Transient;
|
||||||
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
AActor* Actor = World->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity, Params);
|
||||||
|
if (!Actor) return nullptr;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
Actor->SetActorLabel(TEXT("PS_Editor_NavMeshSurfaceViz"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(Actor, TEXT("NavMeshSurface"));
|
||||||
|
Mesh->SetMobility(EComponentMobility::Movable);
|
||||||
|
Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Mesh->bUseAsyncCooking = false;
|
||||||
|
Mesh->SetCastShadow(false);
|
||||||
|
Mesh->SetGenerateOverlapEvents(false);
|
||||||
|
Mesh->RegisterComponent();
|
||||||
|
Actor->SetRootComponent(Mesh);
|
||||||
|
|
||||||
|
GSurfaceActor = Actor;
|
||||||
|
GSurfaceMesh = Mesh;
|
||||||
|
return Mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void DestroySurfaceActor()
|
||||||
|
{
|
||||||
|
if (AActor* Actor = GSurfaceActor.Get())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
}
|
||||||
|
GSurfaceActor.Reset();
|
||||||
|
GSurfaceMesh.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearNavMeshDebugCache()
|
||||||
|
{
|
||||||
|
GCachedNavMeshes.Empty();
|
||||||
|
GTimeSinceRefresh = 9999.f;
|
||||||
|
DestroySurfaceActor();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// As of 5.5 the int32-tile-index overload is deprecated — use the
|
||||||
|
// FNavTileRef overload via GetAllNavMeshTiles.
|
||||||
|
TArray<FNavTileRef> TileRefs;
|
||||||
|
Recast->GetAllNavMeshTiles(TileRefs);
|
||||||
|
for (const FNavTileRef& TileRef : TileRefs)
|
||||||
|
{
|
||||||
|
FRecastDebugGeometry Geo;
|
||||||
|
Recast->GetDebugGeometryForTile(Geo, TileRef);
|
||||||
|
|
||||||
|
const int32 BaseIdx = Cached.Vertices.Num();
|
||||||
|
Cached.Vertices.Reserve(BaseIdx + Geo.MeshVerts.Num());
|
||||||
|
for (const FVector& V : Geo.MeshVerts)
|
||||||
|
{
|
||||||
|
Cached.Vertices.Add(V + FVector(0.f, 0.f, NavMeshDrawZOffset));
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache rebuilt — also refresh the procedural surface mesh so the translucent
|
||||||
|
// overlay tracks live navmesh changes. We merge every navmesh's geometry into a
|
||||||
|
// single section to keep the proc mesh component lightweight (one section, one
|
||||||
|
// material, one draw call).
|
||||||
|
if (UProceduralMeshComponent* Surface = EnsureSurfaceComponent(World))
|
||||||
|
{
|
||||||
|
TArray<FVector> Vertices;
|
||||||
|
TArray<int32> Triangles;
|
||||||
|
for (const FCachedNavMeshGeo& Cached : GCachedNavMeshes)
|
||||||
|
{
|
||||||
|
const int32 BaseIdx = Vertices.Num();
|
||||||
|
Vertices.Append(Cached.Vertices);
|
||||||
|
Triangles.Reserve(Triangles.Num() + Cached.TriIndices.Num());
|
||||||
|
for (int32 Idx : Cached.TriIndices)
|
||||||
|
{
|
||||||
|
Triangles.Add(BaseIdx + Idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty arrays for normals/UVs/colors/tangents — the unlit translucent material
|
||||||
|
// the user provides doesn't need them. ProcMesh treats empty arrays as "skip
|
||||||
|
// this stream", which is fine for an unlit overlay.
|
||||||
|
static const TArray<FVector> EmptyNormals;
|
||||||
|
static const TArray<FVector2D> EmptyUVs;
|
||||||
|
static const TArray<FColor> EmptyColors;
|
||||||
|
static const TArray<FProcMeshTangent> EmptyTangents;
|
||||||
|
|
||||||
|
if (Vertices.Num() > 0 && Triangles.Num() > 0)
|
||||||
|
{
|
||||||
|
Surface->CreateMeshSection(/*SectionIndex=*/ 0, Vertices, Triangles,
|
||||||
|
EmptyNormals, EmptyUVs, EmptyColors, EmptyTangents,
|
||||||
|
/*bCreateCollision=*/ false);
|
||||||
|
|
||||||
|
UMaterialInterface* Mat = LoadSurfaceMaterial();
|
||||||
|
if (Mat)
|
||||||
|
{
|
||||||
|
Surface->SetMaterial(0, Mat);
|
||||||
|
}
|
||||||
|
Surface->SetVisibility(true);
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("PS_Editor: NavMesh surface refreshed — %d verts, %d tris, material=%s"),
|
||||||
|
Vertices.Num(), Triangles.Num() / 3,
|
||||||
|
Mat ? *Mat->GetName() : TEXT("<none, wireframe-only>"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Surface->ClearAllMeshSections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
class UWorld;
|
||||||
|
enum class ERuntimeGenerationType : uint8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralised helpers for switching the level's RecastNavMesh runtime-generation mode and
|
||||||
|
* toggling the runtime navmesh visualization. Used by:
|
||||||
|
* - The runtime editor "Settings" popup (live toggle of dynamic nav + show navmesh)
|
||||||
|
* - SceneLoader (gameplay path: applies the scenario's bUseDynamicNavigation flag)
|
||||||
|
* - Scenario save/load (persists bUseDynamicNavigation in JSON)
|
||||||
|
*/
|
||||||
|
namespace PS_Editor_NavUtils
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Switch every RecastNavMesh in the world to the requested runtime-generation mode.
|
||||||
|
* Static = baked nav only, no runtime updates (fastest gameplay).
|
||||||
|
* DynamicModifiersOnly = only NavModifierVolumes affect nav at runtime.
|
||||||
|
* Dynamic = all collision-affecting actors update the nav mesh on spawn / move.
|
||||||
|
*
|
||||||
|
* Idempotent — calling with the same mode the navmesh already has is a no-op.
|
||||||
|
*/
|
||||||
|
PS_EDITOR_API void SetRuntimeGenerationMode(UWorld* World, ERuntimeGenerationType Mode);
|
||||||
|
|
||||||
|
/** Convenience: switch to Dynamic if bDynamic, Static otherwise. */
|
||||||
|
PS_EDITOR_API void SetDynamicNavigationEnabled(UWorld* World, bool bDynamic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the runtime navmesh visualization (mimics the editor's P key — green nav lines
|
||||||
|
* + tile bounds rendered on top of the world). Affects all RecastNavMesh actors in the
|
||||||
|
* world. Free in editor PIE/Standalone; in shipping packaged builds, debug rendering may
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
@@ -6,8 +6,11 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_TimelineSubsystem.h"
|
#include "PS_Editor_TimelineSubsystem.h"
|
||||||
#include "PS_Editor_GroundSnap.h"
|
#include "PS_Editor_GroundSnap.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
|
#include "PS_Editor_MandatoryAnchor.h"
|
||||||
#include "AIController.h"
|
#include "AIController.h"
|
||||||
#include "BrainComponent.h"
|
#include "BrainComponent.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "GameFramework/CharacterMovementComponent.h"
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
@@ -91,6 +94,7 @@ void UPS_Editor_SpawnManager::AddCatalog(UPS_Editor_SpawnCatalog* InCatalog)
|
|||||||
|
|
||||||
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
Resolved.Category = SpawnComp ? SpawnComp->Category : TEXT("Default");
|
||||||
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
Resolved.Thumbnail = SpawnComp ? SpawnComp->Thumbnail : nullptr;
|
||||||
|
Resolved.bIsMandatory = SpawnComp ? SpawnComp->bIsMandatory : false;
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved '%s' - SpawnComp=%s, Thumbnail=%s"),
|
||||||
*Resolved.DisplayName,
|
*Resolved.DisplayName,
|
||||||
@@ -469,6 +473,112 @@ void UPS_Editor_SpawnManager::NotifyEditorModeChanged(bool bIsEditing)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SpawnManager::IsActorDeletable(const AActor* Actor) const
|
||||||
|
{
|
||||||
|
if (!Actor) return true;
|
||||||
|
const UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||||
|
return !(SpawnComp && SpawnComp->bIsMandatory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_SpawnManager::EnsureMandatoryActorsPresent()
|
||||||
|
{
|
||||||
|
APlayerController* PC = OwnerPC.Get();
|
||||||
|
UWorld* World = PC ? PC->GetWorld() : nullptr;
|
||||||
|
if (!World) return;
|
||||||
|
|
||||||
|
// Diagnostic: count how many catalog entries are flagged mandatory. If zero, the user
|
||||||
|
// likely hasn't restarted PIE since marking the BP — Live Coding doesn't re-resolve the
|
||||||
|
// catalog, so cached bIsMandatory values stay stale. Surface this clearly in the log.
|
||||||
|
int32 MandatoryCount = 0;
|
||||||
|
for (const FPS_Editor_ResolvedEntry& E : ResolvedEntries)
|
||||||
|
{
|
||||||
|
if (E.bIsMandatory) ++MandatoryCount;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: EnsureMandatoryActorsPresent — scanning %d catalog entries (%d marked mandatory)"),
|
||||||
|
ResolvedEntries.Num(), MandatoryCount);
|
||||||
|
if (MandatoryCount == 0)
|
||||||
|
{
|
||||||
|
// Nothing to do. Don't bother locating an anchor.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Locate the standalone APS_Editor_MandatoryAnchor in the loaded baseLevel. We take
|
||||||
|
// the first one found. If absent, fall back to world origin (with a warning) so
|
||||||
|
// mandatory actors still land somewhere reachable.
|
||||||
|
FTransform AnchorXf = FTransform::Identity;
|
||||||
|
bool bAnchorFound = false;
|
||||||
|
for (TActorIterator<APS_Editor_MandatoryAnchor> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
if (APS_Editor_MandatoryAnchor* Anchor = *It)
|
||||||
|
{
|
||||||
|
AnchorXf = Anchor->GetActorTransform();
|
||||||
|
bAnchorFound = true;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: MandatoryAnchor found at %s"), *AnchorXf.GetLocation().ToString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bAnchorFound)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EnsureMandatoryActorsPresent — no APS_Editor_MandatoryAnchor placed in the baseLevel. Mandatory actors will spawn at world origin."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. For each mandatory class in the catalog: skip if any tracked actor of that class
|
||||||
|
// already exists, otherwise spawn one at the anchor transform. SpawnActorDirect routes
|
||||||
|
// through the standard tracking path so the new actor lands in SpawnedActors and gets
|
||||||
|
// a stable timeline ActorId.
|
||||||
|
int32 SpawnedNow = 0;
|
||||||
|
int32 SkippedExisting = 0;
|
||||||
|
for (const FPS_Editor_ResolvedEntry& Entry : ResolvedEntries)
|
||||||
|
{
|
||||||
|
if (!Entry.bIsMandatory || !Entry.ActorClass) continue;
|
||||||
|
|
||||||
|
// Already present?
|
||||||
|
bool bAlreadyPresent = false;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
|
{
|
||||||
|
AActor* Existing = Weak.Get();
|
||||||
|
if (Existing && Existing->IsA(Entry.ActorClass))
|
||||||
|
{
|
||||||
|
bAlreadyPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bAlreadyPresent)
|
||||||
|
{
|
||||||
|
++SkippedExisting;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Mandatory '%s' already present, skipping."), *Entry.DisplayName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* NewActor = SpawnActorDirect(Entry.ActorClass, AnchorXf.GetLocation(),
|
||||||
|
AnchorXf.Rotator(), AnchorXf.GetScale3D());
|
||||||
|
if (NewActor)
|
||||||
|
{
|
||||||
|
++SpawnedNow;
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Mandatory actor auto-spawned: %s at %s"),
|
||||||
|
*Entry.DisplayName, *AnchorXf.GetLocation().ToString());
|
||||||
|
|
||||||
|
// Fire OnPropertiesLoaded so the actor's BP can run its init logic. For loaded
|
||||||
|
// scenes, the SceneLoader/Serializer already fires this after applying JSON
|
||||||
|
// properties; mandatory auto-spawn has no properties to apply, but the actor
|
||||||
|
// still needs the same init hook (e.g. setting up visuals based on defaults).
|
||||||
|
// Without this call, BP code that relies on OnPropertiesLoaded silently doesn't
|
||||||
|
// run for auto-spawned actors → the actor stays in its raw post-BeginPlay state.
|
||||||
|
if (NewActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
IPS_Editor_EditableInterface::Execute_OnPropertiesLoaded(NewActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Failed to spawn mandatory actor %s"), *Entry.DisplayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: EnsureMandatoryActorsPresent — done. Spawned=%d, AlreadyPresent=%d"),
|
||||||
|
SpawnedNow, SkippedExisting);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
||||||
{
|
{
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
@@ -480,6 +590,24 @@ void UPS_Editor_SpawnManager::TickEditorMode(float DeltaTime)
|
|||||||
IPS_Editor_EditableInterface::Execute_OnEditorTick(Actor, DeltaTime);
|
IPS_Editor_EditableInterface::Execute_OnEditorTick(Actor, DeltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom navmesh visualization (P key / Settings checkbox). Bypasses UE's nav debug
|
||||||
|
// renderer (which is gated behind UE_ALLOW_NAVMESH_DEBUG_DRAWING_IN_GAME and unusable
|
||||||
|
// on binary engine builds at runtime). Triangulates each ARecastNavMesh and emits
|
||||||
|
// DrawDebugLine calls per edge — works in PIE / Standalone / packaged. The proc-mesh
|
||||||
|
// surface overlay is also (re)built inside DrawNavMeshDebug; on the on→off transition
|
||||||
|
// we explicitly clear the cache so the surface actor is destroyed (DrawDebugLine entries
|
||||||
|
// auto-expire after 1 frame, but the proc-mesh actor needs an explicit teardown).
|
||||||
|
if (bShowNavMesh)
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::DrawNavMeshDebug(GetWorld(), DeltaTime);
|
||||||
|
bWasShowingNavMesh = true;
|
||||||
|
}
|
||||||
|
else if (bWasShowingNavMesh)
|
||||||
|
{
|
||||||
|
PS_Editor_NavUtils::ClearNavMeshDebugCache();
|
||||||
|
bWasShowingNavMesh = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ struct FPS_Editor_ResolvedEntry
|
|||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UTexture2D> Thumbnail;
|
TObjectPtr<UTexture2D> Thumbnail;
|
||||||
|
|
||||||
|
/** Cached from UPS_Editor_SpawnableComponent::bIsMandatory at catalog resolution time.
|
||||||
|
* Mandatory entries are auto-spawned on new-scene/load and hidden from the catalog UI. */
|
||||||
|
bool bIsMandatory = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,6 +79,40 @@ public:
|
|||||||
/** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */
|
/** Notify all spawned actors that implement IPS_Editor_EditableInterface of editor mode change. */
|
||||||
void NotifyEditorModeChanged(bool bIsEditing);
|
void NotifyEditorModeChanged(bool bIsEditing);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the catalog, and for every class flagged bIsMandatory that has no instance currently
|
||||||
|
* tracked in SpawnedActors, spawn one at the world's APS_Editor_CameraStart::MandatorySpawnAnchor
|
||||||
|
* (or world origin if none found). Idempotent — calling repeatedly is safe; missing instances
|
||||||
|
* are added, existing ones are left untouched.
|
||||||
|
*
|
||||||
|
* Called from SceneSerializer::ClearScene (new scene) and SceneLoader (after JSON load) so
|
||||||
|
* mandatory actors are guaranteed present whether the user starts fresh or opens a legacy
|
||||||
|
* save that predates the mandatory flag.
|
||||||
|
*/
|
||||||
|
void EnsureMandatoryActorsPresent();
|
||||||
|
|
||||||
|
/** Returns false if Actor has a UPS_Editor_SpawnableComponent with bIsMandatory==true.
|
||||||
|
* Used by the delete code path (Pawn::HandleDelete) to skip mandatory actors silently. */
|
||||||
|
bool IsActorDeletable(const AActor* Actor) const;
|
||||||
|
|
||||||
|
// ---- Per-scenario settings (persisted in JSON) ----
|
||||||
|
|
||||||
|
/** If true, switch the level's RecastNavMesh to Dynamic runtime generation when this
|
||||||
|
* scenario is loaded — both in editor and gameplay. False = keep cooked Static nav.
|
||||||
|
* Persisted in the scenario JSON; default false. */
|
||||||
|
bool bUseDynamicNavigation = false;
|
||||||
|
|
||||||
|
// ---- Session-only UI preferences (NOT persisted) ----
|
||||||
|
|
||||||
|
/** If true, the navmesh is rendered at runtime (mimics the editor's P key). Personal
|
||||||
|
* visualization preference, never saved with the scenario. */
|
||||||
|
bool bShowNavMesh = false;
|
||||||
|
|
||||||
|
/** Tracks the previous tick's bShowNavMesh state so TickEditorMode can detect the
|
||||||
|
* on→off transition and tear down the proc-mesh surface actor that DrawNavMeshDebug
|
||||||
|
* spawned. Without this, the surface would stay visible after the user toggled it off. */
|
||||||
|
bool bWasShowingNavMesh = false;
|
||||||
|
|
||||||
/** Tick all spawned actors that implement IPS_Editor_EditableInterface. */
|
/** Tick all spawned actors that implement IPS_Editor_EditableInterface. */
|
||||||
void TickEditorMode(float DeltaTime);
|
void TickEditorMode(float DeltaTime);
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||||
bool bPreload = false;
|
bool bPreload = false;
|
||||||
|
|
||||||
|
/** If true, this actor class is mandatory for any scenario:
|
||||||
|
* - Auto-spawned on "New Scene" (one instance, at the MandatorySpawnAnchor of the
|
||||||
|
* baseLevel's APS_Editor_CameraStart, falling back to world origin)
|
||||||
|
* - On scene load, if missing, auto-added (handles legacy saves cleanly)
|
||||||
|
* - Cannot be deleted by the user (delete is silently refused with a warning log)
|
||||||
|
* - Hidden from the catalog (you can't add a 2nd instance) */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spawnable")
|
||||||
|
bool bIsMandatory = false;
|
||||||
|
|
||||||
// ---- Runtime-only: saved movement/rotation settings before editor override ----
|
// ---- Runtime-only: saved movement/rotation settings before editor override ----
|
||||||
// Set when the editor disables AI/rotation overrides, restored on StartSimulation.
|
// Set when the editor disables AI/rotation overrides, restored on StartSimulation.
|
||||||
bool bSavedMovementState = false;
|
bool bSavedMovementState = false;
|
||||||
|
|||||||
@@ -194,20 +194,16 @@ void UPS_Editor_TimelineSubsystem::CaptureBaselinesForAllMissingTracks()
|
|||||||
|
|
||||||
void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
||||||
{
|
{
|
||||||
if (PrePlayBaseline.Num() == 0) return;
|
|
||||||
|
|
||||||
// Server-only write path (same guard as ApplyValuesAtTime).
|
// Server-only write path (same guard as ApplyValuesAtTime).
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (World->GetNetMode() == NM_Client) return;
|
if (World->GetNetMode() == NM_Client) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const float ZeroEpsilon = 0.0001f;
|
||||||
|
|
||||||
for (const FPS_Editor_TimelineTrack& Track : Data.Tracks)
|
for (const FPS_Editor_TimelineTrack& Track : Data.Tracks)
|
||||||
{
|
{
|
||||||
const TPair<FGuid, FName> Key(Track.ActorId, Track.PropertyPath);
|
|
||||||
const FString* BaselineValue = PrePlayBaseline.Find(Key);
|
|
||||||
if (!BaselineValue) continue;
|
|
||||||
|
|
||||||
AActor* Actor = FindActorById(Track.ActorId);
|
AActor* Actor = FindActorById(Track.ActorId);
|
||||||
if (!Actor) continue;
|
if (!Actor) continue;
|
||||||
UObject* Target = nullptr;
|
UObject* Target = nullptr;
|
||||||
@@ -215,7 +211,39 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
|||||||
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
ResolveTarget(Actor, Track.PropertyPath, Target, Prop);
|
||||||
if (!Target || !Prop) continue;
|
if (!Target || !Prop) continue;
|
||||||
|
|
||||||
Prop->ImportText_InContainer(**BaselineValue, Target, Target, PPF_None);
|
// Pick the value to restore:
|
||||||
|
// - If the user placed a key at (or before) t=0 → use that key's value. This
|
||||||
|
// matches the natural mental model where a key at t=0 sets the timeline's
|
||||||
|
// starting state — and lets the user adjust it just by re-keying at t=0
|
||||||
|
// (otherwise Stop would keep snapping back to the captured pre-animation
|
||||||
|
// baseline forever, ignoring their edits).
|
||||||
|
// - Otherwise fall back to the captured pre-animation baseline so Stop still
|
||||||
|
// undoes whatever the timeline pushed (relevant for tracks whose first key
|
||||||
|
// is past t=0, e.g. a property that only changes mid-timeline).
|
||||||
|
const FString* RestoreValue = nullptr;
|
||||||
|
FString KeyZeroValue;
|
||||||
|
for (const FPS_Editor_TimelineKey& K : Track.Keys)
|
||||||
|
{
|
||||||
|
if (K.Time <= ZeroEpsilon)
|
||||||
|
{
|
||||||
|
KeyZeroValue = K.Value;
|
||||||
|
RestoreValue = &KeyZeroValue;
|
||||||
|
// Continue scanning in case there are several keys at t≈0 — the LAST one wins
|
||||||
|
// (matches sort order: later same-time entries override earlier ones).
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break; // keys are sorted by time; no need to look past t>0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!RestoreValue)
|
||||||
|
{
|
||||||
|
const TPair<FGuid, FName> BaselineKey(Track.ActorId, Track.PropertyPath);
|
||||||
|
RestoreValue = PrePlayBaseline.Find(BaselineKey);
|
||||||
|
}
|
||||||
|
if (!RestoreValue) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(**RestoreValue, Target, Target, PPF_None);
|
||||||
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
||||||
{
|
{
|
||||||
AsComp->MarkRenderStateDirty();
|
AsComp->MarkRenderStateDirty();
|
||||||
@@ -234,15 +262,40 @@ void UPS_Editor_TimelineSubsystem::RestoreBaseline()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't clear the map — a subsequent Stop (or Restart) must still restore the same baseline.
|
// Don't clear PrePlayBaseline — subsequent Stops without a t=0 key still need it.
|
||||||
// SetData/Clear are the only resetters.
|
// SetData/Clear are the only resetters.
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_TimelineSubsystem::Restart()
|
void UPS_Editor_TimelineSubsystem::Restart()
|
||||||
{
|
{
|
||||||
// Rewind + replay from the start. Done in two steps so simulation transforms are restored
|
// Rewind + replay from the start.
|
||||||
// by Stop() before Play() re-enables AI, giving a clean "from the beginning" rerun.
|
//
|
||||||
Stop();
|
// We can't just call Stop() then Play() because Stop() goes through
|
||||||
|
// PlayerController::RequestStopSimulation which DEFERS the actual StopSimulation by
|
||||||
|
// 500ms (BT grace period). The Play() call right after cancels that pending timer
|
||||||
|
// AND re-enters StartSimulation early (bSimulating still true), so transforms are
|
||||||
|
// never restored — characters stay at their current position instead of rewinding.
|
||||||
|
//
|
||||||
|
// Fix: do the full reset synchronously here. Fire OnEditorBeforeSimulateStop (via
|
||||||
|
// RequestStopSimulation(bImmediate=true) so BP behaviour-reset hooks still run),
|
||||||
|
// then immediately StopSimulation (which restores transforms + fires
|
||||||
|
// OnEditorSimulateStop), then Play() to capture the rewound transforms and restart
|
||||||
|
// AI from the clean state.
|
||||||
|
bIsPlaying = false;
|
||||||
|
CurrentTime = 0.f;
|
||||||
|
LastAppliedValues.Empty();
|
||||||
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
|
|
||||||
|
RestoreBaseline();
|
||||||
|
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld()))
|
||||||
|
{
|
||||||
|
if (EditorPC->IsSimulating())
|
||||||
|
{
|
||||||
|
EditorPC->RequestStopSimulation(/*bImmediate=*/ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Play();
|
Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +374,14 @@ static int32 FindTrack(const FPS_Editor_TimelineData& Data, const FGuid& ActorId
|
|||||||
return INDEX_NONE;
|
return INDEX_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_TimelineSubsystem::IsPropertyTracked(AActor* Actor, const FName& PropertyPath) const
|
||||||
|
{
|
||||||
|
if (!Actor) return false;
|
||||||
|
const FGuid ActorId = FindActorId(Actor);
|
||||||
|
if (!ActorId.IsValid()) return false;
|
||||||
|
return FindTrack(Data, ActorId, PropertyPath) != INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, const FName& PropertyPath)
|
bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, const FName& PropertyPath)
|
||||||
{
|
{
|
||||||
if (!Actor) return false;
|
if (!Actor) return false;
|
||||||
@@ -364,6 +425,14 @@ bool UPS_Editor_TimelineSubsystem::AddOrUpdateKeyAtCurrentTime(AActor* Actor, co
|
|||||||
if (FMath::IsNearlyEqual(Track.Keys[InsertAt].Time, CurrentTime, Epsilon))
|
if (FMath::IsNearlyEqual(Track.Keys[InsertAt].Time, CurrentTime, Epsilon))
|
||||||
{
|
{
|
||||||
Track.Keys[InsertAt].Value = CurrentValue;
|
Track.Keys[InsertAt].Value = CurrentValue;
|
||||||
|
// Invalidate the per-track applied-value cache so the next ApplyTrackAtTime
|
||||||
|
// re-writes the (possibly different) value instead of skipping based on the
|
||||||
|
// stale cache entry. Otherwise, updating a key with a value the cache happened
|
||||||
|
// to hold from a previous apply would silently revert the property to it.
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -404,6 +473,12 @@ void UPS_Editor_TimelineSubsystem::RemoveKey(int32 TrackIndex, int32 KeyIndex)
|
|||||||
Data.Tracks.RemoveAt(TrackIndex);
|
Data.Tracks.RemoveAt(TrackIndex);
|
||||||
LastAppliedValues.SetNum(Data.Tracks.Num());
|
LastAppliedValues.SetNum(Data.Tracks.Num());
|
||||||
}
|
}
|
||||||
|
else if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
// Track still has keys but its surrounding keys for any time may have changed —
|
||||||
|
// invalidate the cache so the next apply re-writes the correct value.
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +488,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyInterpolate(int32 TrackIndex, int32 Key
|
|||||||
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
FPS_Editor_TimelineTrack& Track = Data.Tracks[TrackIndex];
|
||||||
if (!Track.Keys.IsValidIndex(KeyIndex)) return;
|
if (!Track.Keys.IsValidIndex(KeyIndex)) return;
|
||||||
Track.Keys[KeyIndex].bInterpolate = bInterpolate;
|
Track.Keys[KeyIndex].bInterpolate = bInterpolate;
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +507,10 @@ void UPS_Editor_TimelineSubsystem::SetKeyTime(int32 TrackIndex, int32 KeyIndex,
|
|||||||
{
|
{
|
||||||
return A.Time < B.Time;
|
return A.Time < B.Time;
|
||||||
});
|
});
|
||||||
|
if (LastAppliedValues.IsValidIndex(TrackIndex))
|
||||||
|
{
|
||||||
|
LastAppliedValues[TrackIndex].Empty();
|
||||||
|
}
|
||||||
OnTimelineChanged.Broadcast();
|
OnTimelineChanged.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ public:
|
|||||||
|
|
||||||
const TArray<FPS_Editor_TimelineTrack>& GetTracks() const { return Data.Tracks; }
|
const TArray<FPS_Editor_TimelineTrack>& GetTracks() const { return Data.Tracks; }
|
||||||
|
|
||||||
|
/** Returns true if the timeline holds a track for (Actor, PropertyPath). Used by the
|
||||||
|
* PlayerController's editable-property snapshot/restore so we don't double-restore
|
||||||
|
* properties already handled by the timeline's RestoreBaseline. */
|
||||||
|
bool IsPropertyTracked(AActor* Actor, const FName& PropertyPath) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a key at CurrentTime for (Actor, PropertyPath), reading the actor's current value.
|
* Add a key at CurrentTime for (Actor, PropertyPath), reading the actor's current value.
|
||||||
* Creates the track if it doesn't exist. If a key already exists at this time, updates its value.
|
* Creates the track if it doesn't exist. If a key already exists at this time, updates its value.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_MainWidget.h"
|
#include "PS_Editor_MainWidget.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -500,6 +501,110 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings popup (per-scenario nav option + session navmesh visu)
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Center)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SettingsPopup, SBorder)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.BorderImage(BrushGlassStrong.Get())
|
||||||
|
.Padding(FMargin(28.0f, 20.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnUseDynamicNavigationToggled(State); })
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Use Dynamic Navigation")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario).")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State) { OnShowNavMeshToggled(State); })
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Show NavMesh (P)")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ButtonStyle(StyleFlatButton.Get())
|
||||||
|
.OnClicked_Lambda([this]() -> FReply { CloseSettingsPopup(); return FReply::Handled(); })
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Close")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
// ============= COMMAND PALETTE (modal, Ctrl+K) =============
|
// ============= COMMAND PALETTE (modal, Ctrl+K) =============
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
@@ -976,6 +1081,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||||
|
// Re-spawn mandatory actors immediately after the wipe — this is
|
||||||
|
// the "New Scenario" path, where the user expects a fresh-but-not-empty
|
||||||
|
// scene (mandatory class instances at the MandatoryAnchor).
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
LastKnownSpawnedCount = 0;
|
LastKnownSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
@@ -987,6 +1096,26 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings button
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.AutoWidth()
|
||||||
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ButtonStyle(StyleFlatButton.Get())
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu).")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
OpenSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
]
|
||||||
+ SHorizontalBox::Slot()
|
+ SHorizontalBox::Slot()
|
||||||
.AutoWidth()
|
.AutoWidth()
|
||||||
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
@@ -1548,6 +1677,10 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
{
|
{
|
||||||
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
||||||
|
|
||||||
|
// Mandatory classes are auto-spawned and singletons — hide them from the catalog so
|
||||||
|
// the user can't add a 2nd instance.
|
||||||
|
if (Entry.bIsMandatory) continue;
|
||||||
|
|
||||||
// Category header
|
// Category header
|
||||||
if (Entry.Category != CurrentCategory)
|
if (Entry.Category != CurrentCategory)
|
||||||
{
|
{
|
||||||
@@ -3207,6 +3340,43 @@ void UPS_Editor_MainWidget::CancelBaseLevelPopup()
|
|||||||
PendingBaseLevelSelection.Empty();
|
PendingBaseLevelSelection.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OpenSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::CloseSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnUseDynamicNavigationToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bEnabled = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bUseDynamicNavigation = bEnabled;
|
||||||
|
bSceneDirty = true;
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnShowNavMeshToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bVisible = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bShowNavMesh = bVisible;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::OnCloseClicked()
|
void UPS_Editor_MainWidget::OnCloseClicked()
|
||||||
{
|
{
|
||||||
if (CloseConfirmOverlay.IsValid())
|
if (CloseConfirmOverlay.IsValid())
|
||||||
|
|||||||
@@ -134,6 +134,13 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu).
|
||||||
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
|
void OpenSettingsPopup();
|
||||||
|
void CloseSettingsPopup();
|
||||||
|
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
||||||
|
void OnShowNavMeshToggled(ECheckBoxState State);
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "PS_Editor_MainWidget_Legacy.h"
|
#include "PS_Editor_MainWidget_Legacy.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_NavUtils.h"
|
||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
@@ -448,6 +449,124 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
// Settings popup (per-scenario nav option + session navmesh visu)
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Center)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(SettingsPopup, SBorder)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f))
|
||||||
|
.Padding(FMargin(28.0f, 20.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
// Header
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
// Use Dynamic Navigation (saved per-scenario)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bUseDynamicNavigation ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
|
||||||
|
{
|
||||||
|
OnUseDynamicNavigationToggled(State);
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Use Dynamic Navigation")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Rebuild navmesh at runtime when actors are spawned/moved (saved with scenario).")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Show NavMesh (session-only)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(0.0f, 8.0f, 0.0f, 4.0f)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SCheckBox)
|
||||||
|
.IsChecked_Lambda([this]() -> ECheckBoxState
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = SpawnManager.Get())
|
||||||
|
{
|
||||||
|
return SM->bShowNavMesh ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
|
||||||
|
}
|
||||||
|
return ECheckBoxState::Unchecked;
|
||||||
|
})
|
||||||
|
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
|
||||||
|
{
|
||||||
|
OnShowNavMeshToggled(State);
|
||||||
|
})
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Show NavMesh (P)")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Render the navmesh in green (mimics editor's P key). Personal preference, never saved.")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.AutoWrapText(true)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Close button
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 16.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
CloseSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Close")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,6 +924,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||||
|
// Re-spawn mandatory actors immediately after the wipe — this is
|
||||||
|
// the "New Scenario" path, where the user expects a fresh-but-not-empty
|
||||||
|
// scene (mandatory class instances at the MandatoryAnchor).
|
||||||
|
SpawnManager->EnsureMandatoryActorsPresent();
|
||||||
bSceneDirty = false;
|
bSceneDirty = false;
|
||||||
LastKnownSpawnedCount = 0;
|
LastKnownSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
@@ -816,6 +939,25 @@ TSharedRef<SWidget> UPS_Editor_MainWidget_Legacy::BuildSceneButtons()
|
|||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Settings button — opens the global Settings popup
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.AutoWidth()
|
||||||
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ToolTipText(FText::FromString(TEXT("Open scenario / session settings (dynamic nav, navmesh visu).")))
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
OpenSettingsPopup();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Settings")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
]
|
||||||
+ SHorizontalBox::Slot()
|
+ SHorizontalBox::Slot()
|
||||||
.AutoWidth()
|
.AutoWidth()
|
||||||
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
||||||
@@ -1369,6 +1511,10 @@ void UPS_Editor_MainWidget_Legacy::PopulateSpawnCatalog()
|
|||||||
{
|
{
|
||||||
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
const FPS_Editor_ResolvedEntry& Entry = Entries[i];
|
||||||
|
|
||||||
|
// Mandatory classes are auto-spawned and singletons — hide them from the catalog so
|
||||||
|
// the user can't add a 2nd instance.
|
||||||
|
if (Entry.bIsMandatory) continue;
|
||||||
|
|
||||||
// Category header
|
// Category header
|
||||||
if (Entry.Category != CurrentCategory)
|
if (Entry.Category != CurrentCategory)
|
||||||
{
|
{
|
||||||
@@ -2868,6 +3014,55 @@ void UPS_Editor_MainWidget_Legacy::CancelBaseLevelPopup()
|
|||||||
PendingBaseLevelSelection.Empty();
|
PendingBaseLevelSelection.Empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OpenSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::CloseSettingsPopup()
|
||||||
|
{
|
||||||
|
if (SettingsPopup.IsValid())
|
||||||
|
{
|
||||||
|
SettingsPopup->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OnUseDynamicNavigationToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bEnabled = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bUseDynamicNavigation = bEnabled;
|
||||||
|
bSceneDirty = true; // user changed a per-scenario setting
|
||||||
|
|
||||||
|
// Apply live to the level's RecastNavMesh actors.
|
||||||
|
PS_Editor_NavUtils::SetDynamicNavigationEnabled(GetWorld(), bEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::OnShowNavMeshToggled(ECheckBoxState State)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
const bool bVisible = (State == ECheckBoxState::Checked);
|
||||||
|
SM->bShowNavMesh = bVisible;
|
||||||
|
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), bVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget_Legacy::ToggleShowNavMesh()
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM) return;
|
||||||
|
|
||||||
|
SM->bShowNavMesh = !SM->bShowNavMesh;
|
||||||
|
PS_Editor_NavUtils::SetNavMeshVisible(GetWorld(), SM->bShowNavMesh);
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget_Legacy::OnCloseClicked()
|
void UPS_Editor_MainWidget_Legacy::OnCloseClicked()
|
||||||
{
|
{
|
||||||
if (CloseConfirmOverlay.IsValid())
|
if (CloseConfirmOverlay.IsValid())
|
||||||
|
|||||||
@@ -107,6 +107,15 @@ private:
|
|||||||
void ConfirmBaseLevelSelection();
|
void ConfirmBaseLevelSelection();
|
||||||
void CancelBaseLevelPopup();
|
void CancelBaseLevelPopup();
|
||||||
|
|
||||||
|
// Global Settings popup (per-scenario nav option + session-only navmesh visu).
|
||||||
|
TSharedPtr<SWidget> SettingsPopup;
|
||||||
|
void OpenSettingsPopup();
|
||||||
|
void CloseSettingsPopup();
|
||||||
|
void OnUseDynamicNavigationToggled(ECheckBoxState State);
|
||||||
|
void OnShowNavMeshToggled(ECheckBoxState State);
|
||||||
|
/** Toggle the navmesh visualization (P key handler). */
|
||||||
|
void ToggleShowNavMesh();
|
||||||
|
|
||||||
// Transform fields
|
// Transform fields
|
||||||
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
|||||||
Reference in New Issue
Block a user