System Breakdown
The rewind system stores two different kinds of data: continuous motion state and discrete gameplay events.
Separating these keeps memory stable and avoids unnecessary data recording.
1) Continuous State (Ring Buffer)
The component samples the owner’s state at a fixed interval and stores each snapshot in a constant-size ring buffer. This keeps memory usage stable and prevents unbounded growth over long sessions.
Each snapshot contains:
- World transform
- Linear velocity
- Angular velocity
- Movement mode + movement input velocity (used to drive correct animation state during rewind)
When rewind is active:
- Physics simulation is disabled on the actor
- Snapshots are read backwards through the ring buffer
- The actor is positioned using interpolation to avoid visual snapping or jitter
This recreates the player's physical motion in reverse, smoothly.
2) Event Timeline (Health Only in This Project)
Discrete gameplay changes are not stored every frame.
Instead, the system records health change events with timestamps.
During rewind:
- The timeline is stepped backwards
- Any health change that occurred during the rewind window is undone
The system is structured so additional reversible events (resources, status flags, etc.) could be added the same way, but only health was implemented in this project due to scope.
Continuous motion is reversed mathematically.
Health changes are reversed logically.
Editor Workflow Support (Python)
A small Unreal Python tool was written to automatically generate reversed animation assets (AnimSequences and Montages) for testing rewind animation playback. This allowed animation variations to be batch-processed instead of duplicated by hand.
Code
void URewindComponent::RecordSnapshot(float DeltaTime)
{
TimeSinceSnapshotsChanged += DeltaTime;
LocalTimeFromStart += DeltaTime;
// sample at fixed frequency
if (TimeSinceSnapshotsChanged < SnapshotFrequencySeconds &&
TransformAndVelocityRecentHistory.Num() != 0)
return;
// ring buffer: drop oldest when full
if (TransformAndVelocityRecentHistory.Num() == MaxSnapshots)
TransformAndVelocityRecentHistory.PopFront();
const FTransform T = GetOwner()->GetActorTransform();
const FVector Lin = OwnerRootComponent
? OwnerRootComponent->GetPhysicsLinearVelocity() : FVector::ZeroVector;
const FVector Ang = OwnerRootComponent
? OwnerRootComponent->GetPhysicsAngularVelocityInRadians() : FVector::ZeroVector;
LatestSnapshotIndex =
TransformAndVelocityRecentHistory.Emplace(LocalTimeFromStart,
TimeSinceSnapshotsChanged,
T, Lin, Ang);
// optional animation sampling (if enabled)
if (bSnapshotAnimationVariables && OwnerMovementComponent)
{
if (AnimationRecentHistory.Num() == MaxSnapshots)
AnimationRecentHistory.PopFront();
AnimationRecentHistory.Emplace(LocalTimeFromStart,
TimeSinceSnapshotsChanged,
OwnerMovementComponent->Velocity,
OwnerMovementComponent->MovementMode);
}
TimeSinceSnapshotsChanged = 0.f;
}
Results
- The player rewind mechanic worked reliably and remained visually smooth due to interpolation during playback.
- The ring buffer kept memory usage fixed and allowed rewind duration to scale predictably from game settings.
- The component could be added to any actor without additional setup, enabling reuse beyond the original design.
- The health event rewind restored player state consistently, avoiding desync between animation/motion reversal and gameplay effects.
- World state and enemy behavior continued in forward time while the player rewound, preserving clarity and spatial awareness in combat.
If I Had More Time
- Extend Event Timeline beyond health to include ammunition, cooldowns, and status flags using the same reversible event pattern.
- Generalize Animation Rewind for montage-based actions (e.g., firing, rolling) so the animation layer matches the reversed motion more precisely.