Unify simulate/timeline controls + BeforeStop event + AI auto-possess safety net
- Remove the Simulate toolbar button (both Legacy + UMG widgets). Play / Pause / Restart / Stop now live exclusively on the Timeline panel, and the panel is visible by default (no more toggle button on the toolbar). Keeps a global Play button on the Legacy toolbar that opens the level outside the editor. - Move the grace-period / deferred-stop logic out of the TimelineSubsystem and into APS_Editor_PlayerController::RequestStopSimulation. This lets the timeline Stop button AND the UMG topbar simulate buttons share the same event sequence: OnEditorBeforeSimulateStop fires immediately (after RestoreBaseline), then a 500ms BT grace period, then StopSimulation fires OnEditorSimulateStop and cuts AI. New event OnEditorBeforeSimulateStop added to the editable interface with full timing docs. - Drop the early-return in TimelineSubsystem::Play when Tracks.Num()==0 — the timeline Play button is now the only way to start the sim, so it must drive StartSimulation unconditionally. - StartSimulation now force-calls SpawnDefaultController() on any pawn that enters sim without a Controller but has an AIControllerClass set, so AutoPossessAI=Disabled or race-lost possessions don't leave the BT unreachable. - Route the UMG topbar Play / Simulate buttons and the "simulate" command through RequestStopSimulation too, so every path emits the full event sequence consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<AAIController>(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<AAIController>(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<AActor>& 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<AActor>& 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<APS_Editor_PlayerController> 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<AActor>& 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
|
||||
|
||||
@@ -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<TWeakObjectPtr<AActor>, FTransform> SimulationSavedTransforms;
|
||||
|
||||
/** Timer handle for the deferred StopSimulation used by RequestStopSimulation. */
|
||||
FTimerHandle PendingStopSimulationHandle;
|
||||
|
||||
/** Currently loaded sublevel instance. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<APS_Editor_PlayerController> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,10 +156,6 @@ private:
|
||||
* and Clear reset it. */
|
||||
TMap<TPair<FGuid, FName>, 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<TWeakObjectPtr<AActor>, FGuid> ActorIds;
|
||||
|
||||
|
||||
@@ -1079,74 +1079,6 @@ TSharedRef<SWidget> 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<APS_Editor_PlayerController>(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<APS_Editor_PlayerController>(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<SWidget> 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<SWidget> UPS_Editor_MainWidget::BuildTopbar()
|
||||
{
|
||||
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(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<SWidget> UPS_Editor_MainWidget::BuildTopbar()
|
||||
{
|
||||
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(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<SWidget> 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(); }
|
||||
|
||||
@@ -155,9 +155,6 @@ private:
|
||||
TSharedPtr<SWidget> SplineDeletePointButton;
|
||||
TSharedPtr<SWidget> ChildEditButton;
|
||||
|
||||
// Simulate button
|
||||
TSharedPtr<STextBlock> SimulateButtonText;
|
||||
|
||||
// Timeline panel
|
||||
TSharedPtr<SWidget> TimelinePanel;
|
||||
TSharedPtr<class SSlider> TimelineSlider;
|
||||
@@ -165,7 +162,7 @@ private:
|
||||
TSharedPtr<STextBlock> TimelinePlayButtonText;
|
||||
TSharedPtr<SEditableTextBox> TimelineDurationBox;
|
||||
TSharedPtr<SVerticalBox> TimelineTracksContainer;
|
||||
bool bTimelineVisible = false;
|
||||
bool bTimelineVisible = true;
|
||||
FDelegateHandle TimelineChangedHandle;
|
||||
/** Currently selected timeline key (updated on click). -1 means no selection. */
|
||||
int32 SelectedTimelineTrackIdx = -1;
|
||||
|
||||
@@ -179,7 +179,7 @@ TSharedRef<SWidget> 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<SWidget> 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<APS_Editor_PlayerController>(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<APS_Editor_PlayerController>(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<SWidget> 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))
|
||||
|
||||
@@ -128,9 +128,6 @@ private:
|
||||
TSharedPtr<SWidget> SplineDeletePointButton;
|
||||
TSharedPtr<SWidget> ChildEditButton;
|
||||
|
||||
// Simulate button
|
||||
TSharedPtr<STextBlock> SimulateButtonText;
|
||||
|
||||
// Timeline panel
|
||||
TSharedPtr<SWidget> TimelinePanel;
|
||||
TSharedPtr<class SSlider> TimelineSlider;
|
||||
@@ -138,7 +135,7 @@ private:
|
||||
TSharedPtr<STextBlock> TimelinePlayButtonText;
|
||||
TSharedPtr<SEditableTextBox> TimelineDurationBox;
|
||||
TSharedPtr<SVerticalBox> TimelineTracksContainer;
|
||||
bool bTimelineVisible = false;
|
||||
bool bTimelineVisible = true;
|
||||
FDelegateHandle TimelineChangedHandle;
|
||||
/** Currently selected timeline key (updated on click). -1 means no selection. */
|
||||
int32 SelectedTimelineTrackIdx = -1;
|
||||
|
||||
@@ -99,7 +99,8 @@ void UPS_Editor_MainWidget_UMG::StopSimulation()
|
||||
{
|
||||
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||
{
|
||||
PC->StopSimulation();
|
||||
// Route through RequestStopSimulation so OnEditorBeforeSimulateStop fires + BT grace.
|
||||
PC->RequestStopSimulation();
|
||||
OnSimulateChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user