Overview
The rollback system exists to record all relevant data at the end of every simulation frame so that it can be recalled again if needed in the immediate future. This system is typically only enabled on the server as it is mainly used for hit validation and to offset the delay that each client is predicting ahead of their own simulation. However in editor and development builds it is also available on the client so that we can draw additional debug data into the Visual Logger.
Enabling Rollback For An Actor
Any actor that requires state to be recorded by the rollback system will require a UGSRollbackComponent. This component manages the storing and recalling of the state each frame and will automatically register itself with the UGSWorldStateSubsystem on spawn. The actor should also inherit from IGSRollbackInterface and implement GetRollbackComponent so that other systems can access the component easily.
If the components RollbackProxyType is set, the rollback component will also spawn it’s own Rollback Proxy. This is a new actor that mimics the primitive setup of the owning actor so that it can be moved independently and only when it is required. If we were to rollback directly on the owning actor we would also be moving many other components irrelevant to the simulation and it would also potentially lose authoritative state if not returned correctly.
Once the Rollback Component has been created, the individual state that needs to be tracked also needs to be registered with the component. It is recommended that all collision-based state is registered via the Rollback Proxy and anything else can be registered directly via the Rollback Component. Examples of these can be found at UGSRollbackComponent::RegisterCollider
and AGSRollbackProxy::SetupSkeletalMeshRenderer
.
Extending Rollback Types
By overriding UGSRollbackComponent::CreateHistoryForType
you can define your own history types to be used as rollback state. This functionality is only available in C++.
UGSTrackedHistory* UGSRollbackComponent::CreateHistoryForType(uint8 Type)
{
if (Type == static_cast<uint8>(CustomHistoryType))
{
return UGSTrackedHistory::CreateNew<UGSCustomHistory, GSCustomHistoryFrame>(FramesToKeepInHistory);
}
return nullptr;
}
First, create a new class that derives from UGSTrackedHistory. This stores all info that is shared between each frame like a reference to the relevant variable or primitive component.
UCLASS()
class UGSCustomHistory : public UGSTrackedHistory
{
GENERATED_BODY()
public:
TWeakObjectPtr<AActor> TrackedActor = nullptr;
};
Then create a class that derives from GSHistoryFrame. This is stores all data about a specific frame which will then later be used to restore to when a rollback is triggered.
class GSCustomHistoryFrame : public GSHistoryFrame
{
public:
// GSHistoryFrame Begin
virtual bool RecordCurrentFrame(const UGSTrackedHistory& History) override;
virtual void Interpolate(GSHistoryFrame* FromFrame, GSHistoryFrame* ToFrame, float PCT) override;
virtual void SetToFrame(const UGSTrackedHistory& History, bool bIsReverting) override;
virtual void SetToDisabledState(const UGSTrackedHistory& History) override;
// GSHistoryFrame End
protected:
float AttributeValue = 0.0f;
};
Finally, implement the functions above. Interpolate
is called when using interpolated network mode and the simulation needs to rollback to a point between two frames. SetToDisabledState
is used to disable the state if the frame data was not available on the requested frame (e.g to avoid a projectile trace).
bool GSCustomHistoryFrame::RecordCurrentFrame(const UGSTrackedHistory& History)
{
const UGSCustomHistory* ColliderHistory = Cast<UGSCustomHistory>(&History);
if (ColliderHistory->TrackedActor.IsValid())
{
// Get the attribute from the tracked actor
AttributeValue = ColliderHistory->TrackedActor->GetValue();
return true;
}
return false;
}
void GSCustomHistoryFrame::SetToFrame(const UGSTrackedHistory& History, bool bIsReverting)
{
const UGSCustomHistory* ColliderHistory = Cast<UGSCustomHistory>(&History);
if (ColliderHistory->TrackedActor.IsValid())
{
// SetValue doesn't exist as a function but this is just used as an example
ColliderHistory->TrackedActor->SetValue(AttributeValue);
}
}
void GSCustomHistoryFrame::Interpolate(GSHistoryFrame* FromFrame, GSHistoryFrame* ToFrame, float PCT)
{
GSHistoryFrame::Interpolate(FromFrame, ToFrame, PCT);
GSCustomHistoryFrame* FromHistoryFrame = static_cast<GSCustomHistoryFrame*>(FromFrame);
GSCustomHistoryFrame* ToHistoryFrame = static_cast<GSCustomHistoryFrame*>(ToFrame);
AttributeValue = FMath::Lerp(FromHistoryFrame->AttributeValue, ToHistoryFrame->AttributeValue, PCT);
}
The last thing to do is to create a function on the Rollback Component that calls CreateHistoryForType
and adds the result to the TrackedHistory array.
void UGSRollbackComponent::RegisterName(AActor* Actor)
{
if (UGSTrackedHistory* NewHistory = CreateHistoryForType(CustomHistoryType))
{
if (UGSCustomHistory* ColliderHistory = Cast<UGSCustomHistory>(NewHistory))
{
ColliderHistory->TrackedActor = Actor;
}
TrackedHistory.Emplace(NewHistory);
}
}
Triggering A Rollback
All rollbacks in Gunsmith are triggered via a request with UGSWorldStateSubsystem::EnqueueRollbackRequest
. By using requests, all requests can be sorted and batched with other requests which reduces the need to continuously move primitives. When simulating all queued requests, the world will rollback to the earliest state, execute any logic and then roll forward to the next request or frame until all requests have been completed or it has caught up to the current frame.
Each request requires a start frame and percentage (if the simulation is running in Interpolated mode due to rendering targets at a point between two frames). A client frame offset is also required which is the servers simulation index minus the clients interpolated index at the time of the request.
UGSPendingRollback can be inherited from to define custom behavior once the rollback has been executed. This is typically when you would resimulate any logic and apply any damage authoritatively.