Latent Timer
Latent timer
Why “reinventing the wheel” while we have TimerManager
Well, TimerManager
has many problems when it comes to gameplay:
-
SetTimerForNextTick
actually 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 group
when setting timer -
TimerManager::Tick
itself 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
-
FTimerDta
is 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::SetTimerForThisTick
andRem::Latent::SetTimerForNextTick
for maximum explicitly and flexibility when expressingdelay a tick
-
Latent Action
is processed in the order they get bound to theUObject
-
The tick group of a
Latent Action
could 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_Delay
only has size of 40 bytes to get all the jobs done, whileFTimerDta
has the size of 128, 3.2x bigger! -
A
fire and forget
alternative 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::FTimerHandle
is 32-bit, becauseFLatentActionManager::AddNewAction
only acceptsint32
, while it wasuint64
inTimerManager
-
Infinite loop map happen if
Rem::Latent::SetTimerForThisTick
is called on the same object withinFLatentActionManager::ProcessLatentActions
, useRem::Latent::SetTimerForNextTick
instead in the case -
Rem::Latent::SetTimerForThisTick
will not get called in the relevanttick group
, if the bound object is already ticked this frame, considerRem::Latent::SetTimerForNextTick
instead in the case -
TimeToDelay
,LoopCount
,InitialDelay
are 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);
}
}