Simulate mode: AI start/stop, save lock, position restore

- Simulate button (orange) toggles AI on/off in editor
- StartSimulation: saves all actor transforms, RestartLogic on all BrainComponents
- StopSimulation: StopLogic on all BrainComponents, restores saved transforms
- Save blocked during simulation with help bar message
- AI auto-stopped per-pawn on spawn in editor (deferred next tick for AIController)
- SceneSerializer also stops AI on loaded pawns
- AIModule added to Build.cs dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 20:29:45 +02:00
parent 16fdcb55da
commit fb95d3b821
7 changed files with 217 additions and 1 deletions

View File

@@ -17,6 +17,8 @@
#include "Camera/PlayerCameraManager.h"
#include "PS_Editor_GameInstanceSubsystem.h"
#include "GameFramework/GameModeBase.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "GameFramework/GameStateBase.h"
#include "GameFramework/PlayerState.h"
@@ -32,6 +34,9 @@ void APS_Editor_PlayerController::BeginPlay()
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
// AI logic will be stopped per-pawn when spawned (see SpawnManager)
// Global StopLogic not available in UE 5.5 — handled per BrainComponent instead
// Create selection manager
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
SelectionManager->Initialize(this);
@@ -201,6 +206,87 @@ void APS_Editor_PlayerController::CloseEditor()
OnEditorClosed.Broadcast();
}
void APS_Editor_PlayerController::StartSimulation()
{
if (bSimulating) return;
bSimulating = true;
// Save all spawned actor transforms
SimulationSavedTransforms.Empty();
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (AActor* Actor = Weak.Get())
{
SimulationSavedTransforms.Add(Actor, Actor->GetActorTransform());
}
}
}
// Clear selection
if (SelectionManager)
{
SelectionManager->ClearSelection();
}
// Enable AI logic on all spawned pawns
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (APawn* P = Cast<APawn>(Weak.Get()))
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->RestartLogic();
}
}
}
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation started (%d actors saved)"), SimulationSavedTransforms.Num());
}
void APS_Editor_PlayerController::StopSimulation()
{
if (!bSimulating) return;
bSimulating = false;
// Disable AI logic on all spawned pawns
if (SpawnManager)
{
for (const TWeakObjectPtr<AActor>& Weak : SpawnManager->GetSpawnedActors())
{
if (APawn* P = Cast<APawn>(Weak.Get()))
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
}
}
// Restore all saved transforms
for (auto& Pair : SimulationSavedTransforms)
{
if (AActor* Actor = Pair.Key.Get())
{
Actor->SetActorTransform(Pair.Value);
}
}
SimulationSavedTransforms.Empty();
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation stopped, transforms restored"));
}
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
{
if (SelectionManager)

View File

@@ -60,6 +60,20 @@ public:
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void CloseEditor();
// ---- Simulate Mode ----
/** Start simulation: enable AI, store actor positions, disable saving. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void StartSimulation();
/** Stop simulation: disable AI, restore actor positions, re-enable saving. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
void StopSimulation();
/** True while simulation is running. */
UFUNCTION(BlueprintCallable, Category = "PS Editor")
bool IsSimulating() const { return bSimulating; }
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
@@ -133,6 +147,12 @@ private:
/** Snapshot of spline points before append, for undo. */
TArray<FVector> AppendInitialPoints;
/** True while simulate mode is active (AI running, save disabled). */
bool bSimulating = false;
/** Saved transforms for all spawned actors before simulation, to restore on stop. */
TMap<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
/** Currently loaded sublevel instance. */
UPROPERTY()
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;

View File

@@ -30,7 +30,8 @@ public class PS_Editor : ModuleRules
"Json",
"JsonUtilities",
"ProceduralMeshComponent",
"DesktopPlatform"
"DesktopPlatform",
"AIModule"
});
PrivateDependencyModuleNames.AddRange(new string[]

View File

@@ -6,6 +6,8 @@
#include "PS_Editor_PlayerController.h"
#include "PS_Editor_SceneLoader.h"
#include "PS_Editor_SplineActor.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "PS_Editor_SplineEditable.h"
#include "JsonObjectConverter.h"
#include "Misc/FileHelper.h"
@@ -227,6 +229,25 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
}
// Stop AI on spawned pawns (editor mode — AI only runs during Simulate)
if (APawn* P = Cast<APawn>(SpawnedActor))
{
TWeakObjectPtr<APawn> WeakP = P;
P->GetWorldTimerManager().SetTimerForNextTick([WeakP]()
{
if (APawn* Pawn = WeakP.Get())
{
if (AAIController* AIC = Cast<AAIController>(Pawn->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
});
}
SpawnedCount++;
}
}

View File

@@ -4,6 +4,8 @@
#include "PS_Editor_EditableInterface.h"
#include "PS_Editor_UndoManager.h"
#include "PS_Editor_PlayerController.h"
#include "AIController.h"
#include "BrainComponent.h"
#include "GameFramework/PlayerController.h"
#include "Engine/World.h"
@@ -166,6 +168,31 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
}
// Stop AI logic on spawned pawns in editor mode (deferred — AIController may not be assigned yet)
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
{
if (!EditorPC->IsSimulating())
{
if (APawn* SpawnedPawn = Cast<APawn>(SpawnedActor))
{
TWeakObjectPtr<APawn> WeakPawn = SpawnedPawn;
SpawnedPawn->GetWorldTimerManager().SetTimerForNextTick([WeakPawn]()
{
if (APawn* P = WeakPawn.Get())
{
if (AAIController* AIC = Cast<AAIController>(P->GetController()))
{
if (UBrainComponent* Brain = AIC->GetBrainComponent())
{
Brain->StopLogic(TEXT("PS_Editor"));
}
}
}
});
}
}
}
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString());
return SpawnedActor;
}

View File

@@ -585,6 +585,19 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
// Block save during simulation
if (APS_Editor_PlayerController* SimPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (SimPC->IsSimulating())
{
if (HelpBarText.IsValid())
{
HelpBarText->SetText(FText::FromString(TEXT("Cannot save during simulation. Stop simulation first.")));
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f));
}
return FReply::Handled();
}
}
if (SaveNameField.IsValid() && SceneSerializer.IsValid())
{
FString Name = SaveNameField->GetText().ToString();
@@ -780,6 +793,33 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
if (!EditorPC) return FReply::Handled();
if (EditorPC->IsSimulating())
{
EditorPC->StopSimulation();
}
else
{
EditorPC->StartSimulation();
}
return FReply::Handled();
})
[
SAssignNew(SimulateButtonText, STextBlock)
.Text(FText::FromString(TEXT("Simulate")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
.ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f))
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
@@ -1313,6 +1353,24 @@ void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDelt
}
}
// Update simulate button text
if (SimulateButtonText.IsValid())
{
if (APS_Editor_PlayerController* SimPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (SimPC->IsSimulating())
{
SimulateButtonText->SetText(FText::FromString(TEXT("Stop")));
SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f));
}
else
{
SimulateButtonText->SetText(FText::FromString(TEXT("Simulate")));
SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f));
}
}
}
RefreshPropertiesFromSelection();
RefreshOutliner();
}

View File

@@ -105,6 +105,9 @@ private:
TSharedPtr<SWidget> SplineEditButton;
TSharedPtr<SWidget> SplineDeletePointButton;
// Simulate button
TSharedPtr<STextBlock> SimulateButtonText;
// Close confirmation
TSharedPtr<SWidget> CloseConfirmOverlay;
bool bSceneDirty = false;