From fb95d3b8215c01ee9c5763c975f9736d25da309a Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Tue, 14 Apr 2026 20:29:45 +0200 Subject: [PATCH] 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) --- .../GameMode/PS_Editor_PlayerController.cpp | 86 +++++++++++++++++++ .../GameMode/PS_Editor_PlayerController.h | 20 +++++ .../Source/PS_Editor/PS_Editor.Build.cs | 3 +- .../PS_Editor_SceneSerializer.cpp | 21 +++++ .../Spawn/PS_Editor_SpawnManager.cpp | 27 ++++++ .../UI/Widgets/PS_Editor_MainWidget.cpp | 58 +++++++++++++ .../UI/Widgets/PS_Editor_MainWidget.h | 3 + 7 files changed, 217 insertions(+), 1 deletion(-) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 7beafc3..8c48761 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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(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& 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& Weak : SpawnManager->GetSpawnedActors()) + { + if (APawn* P = Cast(Weak.Get())) + { + if (AAIController* AIC = Cast(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& Weak : SpawnManager->GetSpawnedActors()) + { + if (APawn* P = Cast(Weak.Get())) + { + if (AAIController* AIC = Cast(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 InActorClass) { if (SelectionManager) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index 172ed39..3983f9f 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -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 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, FTransform> SimulationSavedTransforms; + /** Currently loaded sublevel instance. */ UPROPERTY() TObjectPtr CurrentSublevel; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index e3eb1fd..99ecafd 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -30,7 +30,8 @@ public class PS_Editor : ModuleRules "Json", "JsonUtilities", "ProceduralMeshComponent", - "DesktopPlatform" + "DesktopPlatform", + "AIModule" }); PrivateDependencyModuleNames.AddRange(new string[] diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 6550edf..ae90887 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -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(SpawnedActor)) + { + TWeakObjectPtr WeakP = P; + P->GetWorldTimerManager().SetTimerForNextTick([WeakP]() + { + if (APawn* Pawn = WeakP.Get()) + { + if (AAIController* AIC = Cast(Pawn->GetController())) + { + if (UBrainComponent* Brain = AIC->GetBrainComponent()) + { + Brain->StopLogic(TEXT("PS_Editor")); + } + } + } + }); + } + SpawnedCount++; } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 81355e7..1cf660a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -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(PC)) + { + if (!EditorPC->IsSimulating()) + { + if (APawn* SpawnedPawn = Cast(SpawnedActor)) + { + TWeakObjectPtr WeakPawn = SpawnedPawn; + SpawnedPawn->GetWorldTimerManager().SetTimerForNextTick([WeakPawn]() + { + if (APawn* P = WeakPawn.Get()) + { + if (AAIController* AIC = Cast(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; } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index db9b8ba..889e71d 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -585,6 +585,19 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() SNew(SButton) .OnClicked_Lambda([this]() -> FReply { + // Block save during simulation + if (APS_Editor_PlayerController* SimPC = Cast(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 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(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(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(); } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 6ef141d..1a28627 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -105,6 +105,9 @@ private: TSharedPtr SplineEditButton; TSharedPtr SplineDeletePointButton; + // Simulate button + TSharedPtr SimulateButtonText; + // Close confirmation TSharedPtr CloseConfirmOverlay; bool bSceneDirty = false;