MovieScene EntityID Overflow Crash from Paused Widget Animations

`FWidgetAnimationState::Pause()` sets `PlaybackStatus` to `EMovieScenePlayerStatus::Stopped` (acknowledged as legacy behavior in engine source comments). This causes `UUserWidget::ClearStoppedAnimationStates()` to tear down paused animation states, destroying their MovieScene entities. When the animation is played again, a new entity with a new EntityID is allocated. Since MovieScene EntityIDs use a 16-bit sequential counter that is never recycled, this destroy-recreate cycle eventually exhausts the 65,535 limit, causing the assertion crash above.

// Engine/Source/Runtime/UMG/Private/UserWidget.cpp
void UUserWidget::ClearStoppedAnimationStates()
{
    for (auto It = ActiveAnimations.CreateIterator(); It; ++It)
    {
        FWidgetAnimationState& State(*It);
        if (!State.IsValid())
        {
            It.RemoveCurrent();
        }
        else if ((State.GetPlaybackStatus() == EMovieScenePlayerStatus::Stopped)
                 && !State.IsStopping())
        {
            State.TearDown();     // ← Also tears down PAUSED states!
            It.RemoveCurrent();
        }
    }
}

// Since `Pause()` sets status to `Stopped`, this function cannot distinguish between truly stopped and paused animations. All paused states get destroyed.


// Added a `bIsPaused` flag to `FWidgetAnimationState` to distinguish Paused from Stopped.

// WidgetAnimationState.h


    bool IsPaused() const { return bIsPaused; }
    // ...
    bool bIsPaused : 1;

// WidgetAnimationState.cpp

// Constructor
bIsPaused = false;

// Play()
bIsPaused = false;

// Stop()
bIsPaused = false;

// Pause()
PlaybackManager.SetPlaybackStatus(EMovieScenePlayerStatus::Stopped);
bIsPaused = true;

// UserWidget.cpp — ClearStoppedAnimationStates

else if ((State.GetPlaybackStatus() == EMovieScenePlayerStatus::Stopped)
         && !State.IsStopping())
{
    // Pause() sets PlaybackStatus to Stopped, but the animation is not truly stopped.
    // Tearing it down would discard the EntityID, leading to EntityID overflow on long sessions.
    if (State.IsPaused())
    {
        continue;
    }

    State.TearDown();
    It.RemoveCurrent();
}

1. **Is `bIsPaused` the correct approach?** Or would Epic prefer changing `Pause()` to set `EMovieScenePlayerStatus::Paused` instead of `Stopped`?

2. **Why was the `Pause() → Stopped` legacy behavior preserved?** Are there other systems that depend on paused animations having `Stopped` status?

3. **Are MovieScene EntityIDs intentionally non-recyclable?** If recycling were implemented, the overflow would not occur regardless of the Pause/Stop issue.

4. **Is there a plan to address this in 5.7+?** We see CL-2 does not fix this. Is a separate fix planned?

Steps to Reproduce
To reproduce the crash quickly without waiting hours:

1. Create a `UserWidget` blueprint with two animations:

- **Anim_Gauge**: Any 1-second animation (e.g., opacity 0→1)

- **Anim_Trigger**: A very short animation (**0.2 seconds**, e.g., scale pulse)

2. In the widget’s **Event Tick**:

```

PlayAnimation(Anim_Gauge, StartAtTime=0.5)

PauseAnimation(Anim_Gauge)

```

3. Add a **looping Timer** (period = 0.2s):

```

PlayAnimation(Anim_Trigger)

// Anim_Trigger completes in 0.2s → OnStopped → ClearStoppedAnimationStates

// This tears down Anim_Gauge (Paused but Status=Stopped) every 0.2s

```

4. Create a test level that spawns **100~200 instances** of this widget (e.g., using a Uniform Grid Panel or spawning into a Vertical Box in a loop).

Hey there,

Just wanted to let you know I’m checking with the team now on the best course of action here. We haven’t adjusted this code for a bit so this is still an issue.

Dustin