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 30b7250..ab16572 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 @@ -9,6 +9,7 @@ #include "PS_Editor_SplineActor.h" #include "PS_Editor_SplineEditable.h" #include "PS_Editor_PointPlaceable.h" +#include "PS_Editor_EditableInterface.h" #include "Engine/LevelStreamingDynamic.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" @@ -237,6 +238,16 @@ void APS_Editor_PlayerController::CloseEditor() void APS_Editor_PlayerController::StartSimulation() { + // If a RequestStopSimulation is in flight, cancel it — user wants to resume. + if (UWorld* World = GetWorld()) + { + if (PendingStopSimulationHandle.IsValid()) + { + World->GetTimerManager().ClearTimer(PendingStopSimulationHandle); + PendingStopSimulationHandle.Invalidate(); + } + } + if (bSimulating) return; bSimulating = true; @@ -286,7 +297,18 @@ void APS_Editor_PlayerController::StartSimulation() } } - if (AAIController* AIC = Cast(P->GetController())) + AController* RawCtrl = P->GetController(); + + // Safety net: if auto-possession never fired (e.g. AutoPossessAI=Disabled, or + // pawn got its tick frozen before possession completed), spawn the default + // AIController now so the BT can run during simulation. + if (!RawCtrl && P->AIControllerClass) + { + P->SpawnDefaultController(); + RawCtrl = P->GetController(); + } + + if (AAIController* AIC = Cast(RawCtrl)) { AIC->SetActorTickEnabled(true); if (UBrainComponent* Brain = AIC->GetBrainComponent()) @@ -298,11 +320,35 @@ void APS_Editor_PlayerController::StartSimulation() } } + // Broadcast OnEditorSimulateStart to every spawned actor that implements the editable + // interface — fired AFTER AI has been enabled so BP handlers can query the AI state. + if (SpawnManager) + { + for (const TWeakObjectPtr& Weak : SpawnManager->GetSpawnedActors()) + { + AActor* Actor = Weak.Get(); + if (Actor && Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorSimulateStart(Actor); + } + } + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Simulation started (%d actors saved)"), SimulationSavedTransforms.Num()); } void APS_Editor_PlayerController::SetSimulationPaused(bool bPaused) { + // Resuming? Cancel any in-flight deferred stop (user chose to keep going). + if (!bPaused && PendingStopSimulationHandle.IsValid()) + { + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().ClearTimer(PendingStopSimulationHandle); + PendingStopSimulationHandle.Invalidate(); + } + } + // Toggle AI tick + BT on every spawned pawn without touching bSimulating or the saved // transforms — the timeline's Pause uses this so characters freeze instead of snapping // back to their pre-play positions. @@ -332,9 +378,73 @@ void APS_Editor_PlayerController::SetSimulationPaused(bool bPaused) } } +void APS_Editor_PlayerController::RequestStopSimulation() +{ + if (!bSimulating) return; + + // If a request is already scheduled, do nothing (debounce double-clicks on Stop). + if (UWorld* World = GetWorld()) + { + if (PendingStopSimulationHandle.IsValid()) return; + } + + // 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. + if (SpawnManager) + { + for (const TWeakObjectPtr& Weak : SpawnManager->GetSpawnedActors()) + { + AActor* Actor = Weak.Get(); + if (Actor && Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorBeforeSimulateStop(Actor); + } + } + } + + if (UWorld* World = GetWorld()) + { + TWeakObjectPtr WeakThis(this); + World->GetTimerManager().SetTimer(PendingStopSimulationHandle, + [WeakThis]() + { + if (APS_Editor_PlayerController* Self = WeakThis.Get()) + { + if (Self->IsSimulating()) + { + Self->StopSimulation(); + } + Self->PendingStopSimulationHandle.Invalidate(); + } + }, + 0.5f, false); + } + else + { + // No world timer available (shouldn't happen in practice) — fall back to sync stop. + StopSimulation(); + } +} + void APS_Editor_PlayerController::StopSimulation() { if (!bSimulating) return; + + // Broadcast OnEditorSimulateStop to every spawned actor BEFORE we actually cut AI or + // restore transforms. At this point bSimulating is still true and the BT is still + // live — BP handlers can read world state, stop audio, log telemetry, etc. + if (SpawnManager) + { + for (const TWeakObjectPtr& Weak : SpawnManager->GetSpawnedActors()) + { + AActor* Actor = Weak.Get(); + if (Actor && Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + IPS_Editor_EditableInterface::Execute_OnEditorSimulateStop(Actor); + } + } + } + bSimulating = false; // Disable AI logic and re-apply rotation/movement overrides on all spawned pawns 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 b329376..0c0d943 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 @@ -62,14 +62,24 @@ public: // ---- Simulate Mode ---- - /** Start simulation: enable AI, store actor positions, disable saving. */ + /** Start simulation: enable AI, store actor positions, disable saving. + * Cancels any in-flight deferred RequestStopSimulation. */ UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor") void StartSimulation(); - /** Stop simulation: disable AI, restore actor positions, re-enable saving. */ + /** Stop simulation: disable AI, restore actor positions, re-enable saving. + * Synchronous — fires OnEditorSimulateStop and cuts AI immediately. Callers that want + * a BT grace period (with OnEditorBeforeSimulateStop) should use RequestStopSimulation. */ UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor") void StopSimulation(); + /** Request a deferred stop: fires OnEditorBeforeSimulateStop immediately, then schedules + * the real StopSimulation after a grace period (500ms by default) so BT has time to + * react to whatever the BP handler does in OnEditorBeforeSimulateStop. + * Timeline Stop and the Simulate toolbar button both go through this. */ + UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor") + void RequestStopSimulation(); + /** True while simulation is running. */ UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor") bool IsSimulating() const { return bSimulating; } @@ -165,6 +175,9 @@ private: /** Saved transforms for all spawned actors before simulation, to restore on stop. */ TMap, FTransform> SimulationSavedTransforms; + /** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */ + FTimerHandle PendingStopSimulationHandle; + /** Currently loaded sublevel instance. */ UPROPERTY() TObjectPtr CurrentSublevel; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h index 529f4d4..bdcb0c3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableInterface.h @@ -84,6 +84,55 @@ public: UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor") void OnEditorModeChanged(bool bIsEditing); + /** + * Fired on every spawned actor once when the simulation starts — either via the + * Simulate button or the timeline Play button (which calls StartSimulation under + * the hood). Use this to kick off behaviour that should only happen while AI is live + * (spawn FX, reset state counters, unlock input, etc.). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor") + void OnEditorSimulateStart(); + + /** + * Fired on every spawned actor once when the simulation is about to stop — either via + * the Simulate button, the Stop button, or after a natural end. Called BEFORE AI tick + * is actually cut and BEFORE transforms are restored, so the BT is still live and you + * can still read / write state. Use this for cleanup (stop audio, save telemetry, etc.). + * + * Timing: fires at the "AI cut" moment, AFTER any grace period the timeline applied + * (typically 500ms after Stop was clicked). BT is still live but about to be disabled. + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor") + void OnEditorSimulateStop(); + + /** + * Fired synchronously on the Stop sequence, AFTER the timeline has restored the + * pre-animation baseline onto every track (so properties are back to their initial + * values and OnEditorPropertyChanged has already fired for each of them) but BEFORE + * the grace period that leads up to the actual AI cut. + * + * Use this hook to trigger transition animations, resume BT branches, stop in-flight + * actions — whatever reacts to the baseline being restored. You have ~500ms before + * OnEditorSimulateStop fires and the AI tick is disabled, so anything you start here + * has time to play out on-screen. + * + * Timing sequence: + * t=0 Stop() starts + * RestoreBaseline → ImportText + OnEditorPropertyChanged per track + * >>> OnEditorBeforeSimulateStop fires here <<< + * Grace period scheduled (500ms) + * t=500 OnEditorSimulateStop fires → AI tick cut → transforms restored + * + * Fires for ALL stop paths that go through RequestStopSimulation: + * - Timeline Stop / Restart (baseline restored first, then this event) + * - Simulate toolbar button when at least one timeline track exists + * - Simulate toolbar button without any timeline track (no baseline restore, but + * event still fires + grace period still granted) + * Does NOT fire for direct StopSimulation calls (rare — e.g. forced editor close). + */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "ASTERION|PS_Editor") + void OnEditorBeforeSimulateStop(); + /** * Called every frame while in editor mode. * Use this to update visuals dynamically based on property changes, animate helpers, etc. diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp index dc6333b..8e6fad3 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.cpp @@ -3,6 +3,7 @@ #include "PS_Editor_EditableComponent.h" #include "PS_Editor_EditableInterface.h" #include "PS_Editor_PlayerController.h" +#include "PS_Editor_SpawnManager.h" #include "Engine/World.h" #include "Components/ActorComponent.h" #include "GameFramework/Actor.h" @@ -87,23 +88,16 @@ bool UPS_Editor_TimelineSubsystem::IsTickable() const void UPS_Editor_TimelineSubsystem::Play() { - if (Data.Tracks.Num() == 0) return; + // NOTE: do NOT early-return when Data.Tracks.Num() == 0 — the timeline Play button is + // the only user-facing way to start simulation now (the old toolbar Simulate button was + // removed), so it must drive StartSimulation unconditionally. An empty timeline still + // runs the sim lifecycle (Start/BeforeStop/Stop events fire), it just doesn't animate + // any property. if (CurrentTime >= Data.Duration) { CurrentTime = 0.f; } - // Cancel any pending deferred StopSimulation (user hit Play again during the BT grace - // period after a previous Stop — don't cut the AI now). - if (UWorld* World = GetWorld()) - { - if (PendingStopSimulationHandle.IsValid()) - { - World->GetTimerManager().ClearTimer(PendingStopSimulationHandle); - PendingStopSimulationHandle.Invalidate(); - } - } - // Make sure every track has a captured baseline. New tracks typically get their baseline // at creation time (in AddOrUpdateKeyAtCurrentTime), tracks loaded from a scenario get // theirs at SetData — this call is a safety net for anything that slipped through. @@ -152,37 +146,19 @@ void UPS_Editor_TimelineSubsystem::Stop() LastAppliedValues.Empty(); LastAppliedValues.SetNum(Data.Tracks.Num()); - // Restore pre-play baseline values onto the actors BEFORE stopping simulation, so the - // BT sees the restored values (via OnEditorPropertyChanged / OnRep) and has a chance - // to transition to a neutral state during the grace period below. + // Restore pre-play baseline values onto the actors BEFORE asking the PC to stop sim. + // This fires OnEditorPropertyChanged per track so the BT sees the restored values + // while it's still live (during the grace period that RequestStopSimulation sets up). RestoreBaseline(); - // Defer the actual AI cut by a few frames so the BT can react to the restored values. - // If Play() is called again during this window, the timer is cancelled there. + // Delegate to the PlayerController: it fires OnEditorBeforeSimulateStop, waits for its + // grace period, then runs the actual StopSimulation (which in turn fires the final + // OnEditorSimulateStop and cuts AI). Same sequence the Simulate toolbar button uses. if (APS_Editor_PlayerController* EditorPC = FindEditorPC(GetWorld())) { if (EditorPC->IsSimulating()) { - if (UWorld* World = GetWorld()) - { - TWeakObjectPtr WeakPC(EditorPC); - World->GetTimerManager().SetTimer(PendingStopSimulationHandle, - [WeakPC]() - { - if (APS_Editor_PlayerController* PC = WeakPC.Get()) - { - if (PC->IsSimulating()) - { - PC->StopSimulation(); - } - } - }, - 0.1f, false); - } - else - { - EditorPC->StopSimulation(); - } + EditorPC->RequestStopSimulation(); } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.h index bb4e2d9..8e7798e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Timeline/PS_Editor_TimelineSubsystem.h @@ -156,10 +156,6 @@ private: * and Clear reset it. */ TMap, FString> PrePlayBaseline; - /** Deferred StopSimulation timer — set by Stop() so the BT gets a few frames to react - * to the restored baseline before AI tick is cut. Cancelled if Play() is called again. */ - FTimerHandle PendingStopSimulationHandle; - /** Map of stable IDs for every actor that participates in the timeline / scene registry. */ TMap, FGuid> ActorIds; 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 8a1fe66..09e05bb 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 @@ -1079,74 +1079,6 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) - [ - SNew(SButton) - .ButtonStyle(StyleFlatButton.Get()) - .ToolTipText(FText::FromString(TEXT("Start/stop simulation. If a timeline exists, also plays/pauses it in sync."))) - .OnClicked_Lambda([this]() -> FReply - { - APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); - if (!EditorPC) return FReply::Handled(); - - UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); - - // If there's a timeline with at least one track, route through the timeline - // so simulate + animation are always in lockstep (Simulate is effectively - // the Play/Pause button of the timeline). - if (TS && TS->GetNumTracks() > 0) - { - if (TS->IsPlaying()) - { - TS->Pause(); - } - else - { - TS->Play(); - } - } - else - { - // No animation to sync with — just toggle simulation directly. - 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) - .ButtonStyle(StyleFlatButton.Get()) - .ToolTipText(FText::FromString(TEXT("Show/hide the timeline panel"))) - .OnClicked_Lambda([this]() -> FReply - { - ToggleTimelineVisible(); - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Timeline"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) - ] - ] - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .ButtonStyle(StyleFlatButton.Get()) @@ -1816,30 +1748,7 @@ void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDelt } } - // Update simulate button text / color. Keep the label "Simulate" / "Stop" regardless of - // the timeline state so it doesn't clash with the dedicated Play button in the timeline - // panel — a second "Play" on the toolbar was confusing. The button still drives the - // timeline under the hood (see BuildToolbar lambda). - if (SimulateButtonText.IsValid()) - { - if (APS_Editor_PlayerController* SimPC = Cast(GetOwningPlayer())) - { - UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); - const bool bTimelinePlaying = TS && TS->GetNumTracks() > 0 && TS->IsPlaying(); - const bool bActive = SimPC->IsSimulating() || bTimelinePlaying; - - if (bActive) - { - 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)); - } - } - } + // (Simulate toolbar button removed — Play/Pause/Restart/Stop live in the timeline panel.) RefreshPropertiesFromSelection(); RefreshOutliner(); @@ -3570,7 +3479,7 @@ TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() const FSlateFontInfo Bold = FPS_Editor_UIStyle::BodyFontBold(S); return SAssignNew(TimelinePanel, SBorder) - .Visibility(EVisibility::Collapsed) + .Visibility(EVisibility::Visible) .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(12.0f, 8.0f)) [ @@ -4266,7 +4175,9 @@ TSharedRef UPS_Editor_MainWidget::BuildTopbar() { if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) { - if (PC->IsSimulating()) PC->StopSimulation(); + // Route through RequestStopSimulation so OnEditorBeforeSimulateStop + // fires + BT gets its grace period before AI is cut. + if (PC->IsSimulating()) PC->RequestStopSimulation(); else PC->StartSimulation(); } return FReply::Handled(); @@ -4307,7 +4218,9 @@ TSharedRef UPS_Editor_MainWidget::BuildTopbar() { if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) { - if (PC->IsSimulating()) PC->StopSimulation(); + // Route through RequestStopSimulation so OnEditorBeforeSimulateStop + // fires + BT gets its grace period before AI is cut. + if (PC->IsSimulating()) PC->RequestStopSimulation(); else PC->StartSimulation(); } return FReply::Handled(); @@ -4390,18 +4303,6 @@ TSharedRef UPS_Editor_MainWidget::BuildTopbar() FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Sidebar, 14.0f) ] ] - // Timeline toggle - + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) - [ - SNew(SButton) - .ButtonStyle(StyleFlatButton.Get()) - .ContentPadding(FMargin(6.0f, 3.0f)) - .ToolTipText(FText::FromString(TEXT("Toggle timeline"))) - .OnClicked_Lambda([this]() -> FReply { ToggleTimelineVisible(); return FReply::Handled(); }) - [ - FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::PanelBottom, 14.0f) - ] - ] // Inspector (panel right) toggle + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) [ @@ -4953,7 +4854,8 @@ void UPS_Editor_MainWidget::ExecuteCommand(const FString& CommandId) { if (PC) { - if (PC->IsSimulating()) PC->StopSimulation(); else PC->StartSimulation(); + // Route through RequestStopSimulation so OnEditorBeforeSimulateStop fires + BT grace. + if (PC->IsSimulating()) PC->RequestStopSimulation(); else PC->StartSimulation(); } } else if (CommandId == TEXT("timeline")) { ToggleTimelineVisible(); } 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 752d16b..f9c255a 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 @@ -155,9 +155,6 @@ private: TSharedPtr SplineDeletePointButton; TSharedPtr ChildEditButton; - // Simulate button - TSharedPtr SimulateButtonText; - // Timeline panel TSharedPtr TimelinePanel; TSharedPtr TimelineSlider; @@ -165,7 +162,7 @@ private: TSharedPtr TimelinePlayButtonText; TSharedPtr TimelineDurationBox; TSharedPtr TimelineTracksContainer; - bool bTimelineVisible = false; + bool bTimelineVisible = true; FDelegateHandle TimelineChangedHandle; /** Currently selected timeline key (updated on click). -1 means no selection. */ int32 SelectedTimelineTrackIdx = -1; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp index 93e4a91..cc99e94 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp @@ -179,7 +179,7 @@ TSharedRef UPS_Editor_MainWidget_Legacy::RebuildWidget() BuildOutliner() ] ] - // Timeline panel (hidden by default; toggled via toolbar button) + // Timeline panel (always visible — toolbar toggle removed) + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Bottom) @@ -907,72 +907,6 @@ TSharedRef UPS_Editor_MainWidget_Legacy::BuildSceneButtons() + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) - [ - SNew(SButton) - .ToolTipText(FText::FromString(TEXT("Start/stop simulation. If a timeline exists, also plays/pauses it in sync."))) - .OnClicked_Lambda([this]() -> FReply - { - APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); - if (!EditorPC) return FReply::Handled(); - - UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); - - // If there's a timeline with at least one track, route through the timeline - // so simulate + animation are always in lockstep (Simulate is effectively - // the Play/Pause button of the timeline). - if (TS && TS->GetNumTracks() > 0) - { - if (TS->IsPlaying()) - { - TS->Pause(); - } - else - { - TS->Play(); - } - } - else - { - // No animation to sync with — just toggle simulation directly. - 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) - .ToolTipText(FText::FromString(TEXT("Show/hide the timeline panel"))) - .OnClicked_Lambda([this]() -> FReply - { - ToggleTimelineVisible(); - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Timeline"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) - ] - ] - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .OnClicked_Lambda([this]() -> FReply @@ -1566,30 +1500,7 @@ void UPS_Editor_MainWidget_Legacy::NativeTick(const FGeometry& MyGeometry, float } } - // Update simulate button text / color. Keep the label "Simulate" / "Stop" regardless of - // the timeline state so it doesn't clash with the dedicated Play button in the timeline - // panel — a second "Play" on the toolbar was confusing. The button still drives the - // timeline under the hood (see BuildToolbar lambda). - if (SimulateButtonText.IsValid()) - { - if (APS_Editor_PlayerController* SimPC = Cast(GetOwningPlayer())) - { - UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); - const bool bTimelinePlaying = TS && TS->GetNumTracks() > 0 && TS->IsPlaying(); - const bool bActive = SimPC->IsSimulating() || bTimelinePlaying; - - if (bActive) - { - 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)); - } - } - } + // (Simulate toolbar button removed — Play/Pause/Restart/Stop live in the timeline panel.) RefreshPropertiesFromSelection(); RefreshOutliner(); @@ -3231,7 +3142,7 @@ TSharedRef UPS_Editor_MainWidget_Legacy::BuildTimelinePanel() const FSlateFontInfo Bold = FCoreStyle::GetDefaultFontStyle("Bold", 10); return SAssignNew(TimelinePanel, SBorder) - .Visibility(EVisibility::Collapsed) + .Visibility(EVisibility::Visible) .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.95f)) .Padding(FMargin(12.0f, 8.0f)) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h index 1c1644e..417164c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h @@ -128,9 +128,6 @@ private: TSharedPtr SplineDeletePointButton; TSharedPtr ChildEditButton; - // Simulate button - TSharedPtr SimulateButtonText; - // Timeline panel TSharedPtr TimelinePanel; TSharedPtr TimelineSlider; @@ -138,7 +135,7 @@ private: TSharedPtr TimelinePlayButtonText; TSharedPtr TimelineDurationBox; TSharedPtr TimelineTracksContainer; - bool bTimelineVisible = false; + bool bTimelineVisible = true; FDelegateHandle TimelineChangedHandle; /** Currently selected timeline key (updated on click). -1 means no selection. */ int32 SelectedTimelineTrackIdx = -1; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp index 1a5b010..d8148b0 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp @@ -99,7 +99,8 @@ void UPS_Editor_MainWidget_UMG::StopSimulation() { if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) { - PC->StopSimulation(); + // Route through RequestStopSimulation so OnEditorBeforeSimulateStop fires + BT grace. + PC->RequestStopSimulation(); OnSimulateChanged.Broadcast(); } }