Latent Timer
Latent timer
Why “reinventing the wheel” while we have TimerManager
Well, TimerManager has many problems when it comes to gameplay:
-
SetTimerForNextTickactually called this tick rather than the next tick -
Callback order is not guaranteed to be the same as the timer set order
-
Can’t specify
tick groupwhen setting timer -
TimerManager::Tickitself is hardcoded in some late time of a frame -
Only support delay in time seconds, do you want to delay in frames?
-
Don’t support loop count
-
FTimerDtais still quite bloated, with size optimization done onFTimerUnifiedDelegate
Could we solve all the problems?
Well, well, solving 100% of them is hard, but 90% is piece of cake with our great savior — FLatentActionManager
FLatentActionManager is a simple but powerful tool to tick any instance of FPendingLatentAction every frame.
Every LatentAction is bound to a UObject, it ticks in the tick group of the bound object ! Or “by the end of frame” if tick disabled for the bound object where is just near and ahead of TimerManager::Tick.
FPendingLatentAction could be derived to do anything you want. Eg: FDelayUntilNextTickAction, FDelayAction they are the heroes behind the beloved delay node in blueprint.
The way I solve it with Rem::Latent::FTimerLatentAction_Delay
-
Providing
Rem::Latent::SetTimerForThisTickandRem::Latent::SetTimerForNextTickfor maximum explicitly and flexibility when expressingdelay a tick -
Latent Actionis processed in the order they get bound to theUObject -
The tick group of a
Latent Actioncould be controlled by specifying a ticking object within the target tick group. And it support specifying tick dependency with no efforts! -
Support delay in frames! Which doesn’t likely to exist in
TimerManager. Two helper struct:FTimerParameterHelper_Time,FTimerParameterHelper_Frame, one API:Rem::Latent::SetTimer -
Support specific loop count :
FTimerParameterHelper_Time::LoopCount,FTimerParameterHelper_Frame::LoopCount -
Support counting from next frame: see
FTimerParameterHelper_Time::bSkipCountingThisFrame -
My
FTimerLatentAction_Delayonly has size of 40 bytes to get all the jobs done, whileFTimerDtahas the size of 128, 3.2x bigger! -
A
fire and forgetalternative for pausing timer for one frame pause:Rem::Latent::SetTimerPausedOneFrame -
Familiar APIs:
Rem::Latent::PauseTimer,Rem::Latent::UnpauseTimer,Rem::Latent::SetTimerPaused(did we met before?),Rem::Latent::StopTimer,Rem::Latent::FindTimerAction -
Re-triggerable is natively supported:
Rem::Latent::ResetTimerDelay, support both delay in time and in frame -
Call count compensation and opting out it with
FTimerParameterHelper_Time::bMaxOncePerFrame(Same as what’s inTimerManager) -
27 bits wasted for now, they are the hope for the future!
Limitations
-
Rem::Latent::FTimerHandleis 32-bit, becauseFLatentActionManager::AddNewActiononly acceptsint32, while it wasuint64inTimerManager -
Infinite loop map happen if
Rem::Latent::SetTimerForThisTickis called on the same object withinFLatentActionManager::ProcessLatentActions, useRem::Latent::SetTimerForNextTickinstead in the case -
Rem::Latent::SetTimerForThisTickwill not get called in the relevanttick group, if the bound object is already ticked this frame, considerRem::Latent::SetTimerForNextTickinstead in the case -
TimeToDelay,LoopCount,InitialDelayare all 4 bytes only for simplicity, might consider extended to those 27 spared bits in the future -
Requires tick enabled on the bound object to “set tick group” for our timer latent action
Sample code
1
2
3
4
5
void UYourObject::DoJob()
{
auto TimerHandle = Rem::Latent::SetTimerForThisTick(*this,
FTimerDelegate::CreateUObject(this, &ThisClass::Callback));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UYourObject::TryDoJobUntilSucceed()
{
bool bWantToRetry{true};
ON_SCOPE_EXIT
{
if (bWantToRetry)
{
Rem::Latent::SetTimerForNextTick(*this, FTimerDelegate::CreateWeakLambda(this,
[this]
{
TryDoJobUntilSucceed();
}));
}
};
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void UYourObject::RetriggerableJob()
{
// ...
if (!TimerHandle.IsValid())
{
TimerHandle = Rem::Latent::SetTimer(*this, FTimerDelegate::CreateWeakLambda(this, [this]
{
// ...
Rem::Latent::StopTimer(*this, TimerHandle);
TimerHandle = {};
}), {.TimeToDelay = 1.0f, .LoopCount = 0/*loop infinite*/});
}
else
{
Rem::Latent::ResetTimerDelay(*this, TimerHandle);
}
}