β±οΈ Lesson 5.3: Timers & Latent Actions
"Do X after 3 seconds." "Fire every 0.2 seconds while held." "Damage the player each second they stand in lava." All of these are timers β and using them instead of counting in Tick is both cleaner and faster. Let's schedule work the right way.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Set a one-shot delay and a looping timer with the timer manager
- Store and use an
FTimerHandleto clear or query a timer - Bind a timer to a member function, a lambda, or with parameters
- Explain why timers beat manual countdowns in
Tick - Clean up timers safely to avoid callbacks into destroyed actors
Estimated Time: 45 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
Why Not Just Tick?
You could accumulate DeltaTime in Tick and act when it crosses a threshold. But that means the actor ticks every frame just to check a clock that matters occasionally β wasteful, and it clutters your code with counters.
manual bookkeeping"] Q -->|"Timer"| Ti["Engine calls you once,
at the right time"]
β The win
Timers are managed by the world's timer manager. You register "call this function in N seconds" and the engine handles the rest β no per-frame cost on your actor, no counter variables, no Tick just for timing. Cleaner code and better performance.
One-Shot Delays
Access the timer manager via GetWorldTimerManager(). A one-shot delay runs a function once after a delay:
// Header: store a handle so you can cancel/query the timer later.
FTimerHandle RespawnTimerHandle;
void AEnemy::Die()
{
// Schedule Respawn() to run once, 5 seconds from now.
GetWorldTimerManager().SetTimer(
RespawnTimerHandle, // handle (out)
this, // object to call on
&AEnemy::Respawn, // member function
5.0f, // delay in seconds
false); // bLoop = false β one shot
}
void AEnemy::Respawn()
{
UE_LOG(LogTemp, Log, TEXT("Respawning..."));
}
π The FTimerHandle is your receipt
The FTimerHandle identifies this specific timer. Keep it as a member if you'll ever need to cancel it, check whether it's active, or see the remaining time. If you truly fire-and-forget, you can pass a temporary handle β but most gameplay timers benefit from being cancelable.
Looping Timers
Pass bLoop = true to repeat at a fixed interval β perfect for the "damage each second in lava" case from Lesson 5.1's exercise:
void ADamageVolume::OnBeginOverlap(/* ...params... */)
{
if (UHealthComponent* Health = OtherActor->FindComponentByClass<UHealthComponent>())
{
VictimHealth = Health; // remember who to hurt (UPROPERTY/weak ptr)
// Every 1 second, call ApplyTick, starting after 1 second, forever.
GetWorldTimerManager().SetTimer(
DamageTimerHandle, this, &ADamageVolume::ApplyTick, 1.0f, true, 1.0f);
}
}
void ADamageVolume::OnEndOverlap(/* ...params... */)
{
// Stop the repeating damage when they leave.
GetWorldTimerManager().ClearTimer(DamageTimerHandle);
}
void ADamageVolume::ApplyTick()
{
if (VictimHealth.IsValid()) // TWeakObjectPtr from Lesson 2.5
{
VictimHealth->ApplyDamage(DamagePerTick);
}
}
β The SetTimer signature's last arg
The final parameter is the first-delay β how long before the first call. Here 1.0f means "wait a second, then start ticking every second." Pass 0.0f to fire immediately then repeat. This lets you separate "how often" from "how soon it starts."
Lambdas & Parameters
Sometimes you want a quick inline action, or to pass arguments the plain member-function form can't. Two tools:
A lambda for short inline work
FTimerHandle Handle;
GetWorldTimerManager().SetTimer(Handle, [this]()
{
// Runs after the delay. Capture 'this' to reach members.
UE_LOG(LogTemp, Log, TEXT("Delayed hello from %s"), *GetName());
}, 2.0f, false);
A delegate to pass parameters
// Bind a function that takes an argument via a timer delegate:
FTimerDelegate Del = FTimerDelegate::CreateUObject(
this, &AEnemy::TakeDamageOverTime, 5.0f /* the DamageAmount arg */);
GetWorldTimerManager().SetTimer(DotHandle, Del, 1.0f, true);
β οΈ Lambda capture & lifetime
A lambda capturing this will call into the actor when it fires β but if the actor was destroyed in the meantime, that's a crash. Timers on an actor are automatically cleared when the actor is destroyed, which covers the common case; but if you capture other objects, make sure they outlive the timer, or capture weak pointers and check validity. Prefer CreateUObject delegates for UObject targets β they integrate with lifetime better than raw captures.
Managing & Clearing
The timer manager offers more than set/clear:
FTimerManager& TM = GetWorldTimerManager();
TM.ClearTimer(Handle); // cancel it
bool bActive = TM.IsTimerActive(Handle); // is it running?
float Remaining = TM.GetTimerRemaining(Handle); // seconds left
float Elapsed = TM.GetTimerElapsed(Handle); // seconds so far
TM.PauseTimer(Handle); // pause / UnPauseTimer
β οΈ Clear timers you own on cleanup
Symmetric to Lesson 2.6's EndPlay discipline: if a component or actor starts a timer, it should clear it when it goes away or when the condition ends (like the lava's OnEndOverlap above). Actor-bound timers auto-clear on Destroy, but explicit cleanup makes intent clear and prevents a timer firing into a half-torn-down state.
π‘ SetTimerForNextTick: when you need something to run next frame (e.g. defer work until after the current frame settles),GetWorldTimerManager().SetTimerForNextTick(this, &AMyActor::DoLater)is the clean idiom β noTick, no zero-delay hack.
Hands-on Exercise & Quiz
ποΈ Exercise: A weapon fire rate
Objective: Use a looping timer for automatic fire.
- On "fire pressed" (Started), fire once immediately, then start a looping timer at your fire interval that calls
Fire(). - On "fire released" (Completed), clear the timer.
- Store the
FTimerHandleas a member so you can clear it. - Why is this better than checking elapsed time in
Tick?
β Sketch & reasoning
void AWeapon::StartFiring()
{
Fire(); // immediate first shot
GetWorldTimerManager().SetTimer(
FireTimerHandle, this, &AWeapon::Fire, FireInterval, true);
}
void AWeapon::StopFiring()
{
GetWorldTimerManager().ClearTimer(FireTimerHandle);
}
Better than Tick because the weapon doesn't run any per-frame code while idle, the interval is exact, and there's no counter to maintain β the engine schedules the shots.
π― Quick Quiz
Question 1: How do you run a function once after a delay?
Question 2: What is the FTimerHandle for?
Question 3: A looping damage timer should be cleared whenβ¦
Summary
π Key Takeaways
- Use the timer manager (
GetWorldTimerManager()) instead of counting inTickβ cleaner and cheaper. SetTimer(Handle, this, &Fn, Time, bLoop, FirstDelay)βbLoopmakes it repeat; the last arg is the first-delay.- Keep the
FTimerHandletoClearTimer, checkIsTimerActive, or read remaining time. - Bind a member function, a lambda (capture carefully), or a
FTimerDelegateto pass parameters. - Clear timers when their condition ends; actor timers auto-clear on
Destroy, but explicit cleanup is safest.
π Additional Resources
π What's Next?
You've been binding to engine events (collision, input) with AddDynamic. Now you'll create your own events: delegates β letting a UHealthComponent announce OnDeath to anyone listening, the loosely-coupled pattern promised in Module 3.5.
π Lesson complete!
You command time. Now let's build your own events.