Project Type: CRPG
Role: Time Rewind System
Team Size: 8 people
Duration: 4 Weeks
Engine: Unreal
Language: C++, Python
Time to Breach is a CRPG with a player-only rewind mechanic. The player can scrub back along their own timeline—position, rotation, velocity, and selected player variables—while the world and enemies continue forward in normal time. This is a true state rewind of the player, not a cinematic effect.
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.
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.
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.
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.
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;
}
Core snapshot recording into a fixed-size ring buffer; optional animation state captured for correct rewind playback.
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.
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.