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:
@@ -17,6 +17,8 @@
|
|||||||
#include "Camera/PlayerCameraManager.h"
|
#include "Camera/PlayerCameraManager.h"
|
||||||
#include "PS_Editor_GameInstanceSubsystem.h"
|
#include "PS_Editor_GameInstanceSubsystem.h"
|
||||||
#include "GameFramework/GameModeBase.h"
|
#include "GameFramework/GameModeBase.h"
|
||||||
|
#include "AIController.h"
|
||||||
|
#include "BrainComponent.h"
|
||||||
#include "GameFramework/GameStateBase.h"
|
#include "GameFramework/GameStateBase.h"
|
||||||
#include "GameFramework/PlayerState.h"
|
#include "GameFramework/PlayerState.h"
|
||||||
|
|
||||||
@@ -32,6 +34,9 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
|
|
||||||
SetInputMode(FInputModeGameAndUI().SetHideCursorDuringCapture(false));
|
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
|
// Create selection manager
|
||||||
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
||||||
SelectionManager->Initialize(this);
|
SelectionManager->Initialize(this);
|
||||||
@@ -201,6 +206,87 @@ void APS_Editor_PlayerController::CloseEditor()
|
|||||||
OnEditorClosed.Broadcast();
|
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)
|
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
||||||
{
|
{
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
void CloseEditor();
|
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.
|
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
||||||
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
||||||
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
|
* @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. */
|
/** Snapshot of spline points before append, for undo. */
|
||||||
TArray<FVector> AppendInitialPoints;
|
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. */
|
/** Currently loaded sublevel instance. */
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ public class PS_Editor : ModuleRules
|
|||||||
"Json",
|
"Json",
|
||||||
"JsonUtilities",
|
"JsonUtilities",
|
||||||
"ProceduralMeshComponent",
|
"ProceduralMeshComponent",
|
||||||
"DesktopPlatform"
|
"DesktopPlatform",
|
||||||
|
"AIModule"
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SceneLoader.h"
|
#include "PS_Editor_SceneLoader.h"
|
||||||
#include "PS_Editor_SplineActor.h"
|
#include "PS_Editor_SplineActor.h"
|
||||||
|
#include "AIController.h"
|
||||||
|
#include "BrainComponent.h"
|
||||||
#include "PS_Editor_SplineEditable.h"
|
#include "PS_Editor_SplineEditable.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.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);
|
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++;
|
SpawnedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "PS_Editor_EditableInterface.h"
|
#include "PS_Editor_EditableInterface.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "AIController.h"
|
||||||
|
#include "BrainComponent.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
|
|
||||||
@@ -166,6 +168,31 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
|||||||
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
|
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());
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Spawned %s at %s"), *Entry.DisplayName, *WorldLocation.ToString());
|
||||||
return SpawnedActor;
|
return SpawnedActor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,6 +585,19 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
SNew(SButton)
|
SNew(SButton)
|
||||||
.OnClicked_Lambda([this]() -> FReply
|
.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())
|
if (SaveNameField.IsValid() && SceneSerializer.IsValid())
|
||||||
{
|
{
|
||||||
FString Name = SaveNameField->GetText().ToString();
|
FString Name = SaveNameField->GetText().ToString();
|
||||||
@@ -780,6 +793,33 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
+ SHorizontalBox::Slot()
|
+ SHorizontalBox::Slot()
|
||||||
.AutoWidth()
|
.AutoWidth()
|
||||||
.Padding(4.0f, 0.0f, 0.0f, 0.0f)
|
.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)
|
SNew(SButton)
|
||||||
.OnClicked_Lambda([this]() -> FReply
|
.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();
|
RefreshPropertiesFromSelection();
|
||||||
RefreshOutliner();
|
RefreshOutliner();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ private:
|
|||||||
TSharedPtr<SWidget> SplineEditButton;
|
TSharedPtr<SWidget> SplineEditButton;
|
||||||
TSharedPtr<SWidget> SplineDeletePointButton;
|
TSharedPtr<SWidget> SplineDeletePointButton;
|
||||||
|
|
||||||
|
// Simulate button
|
||||||
|
TSharedPtr<STextBlock> SimulateButtonText;
|
||||||
|
|
||||||
// Close confirmation
|
// Close confirmation
|
||||||
TSharedPtr<SWidget> CloseConfirmOverlay;
|
TSharedPtr<SWidget> CloseConfirmOverlay;
|
||||||
bool bSceneDirty = false;
|
bool bSceneDirty = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user