Skip to main content

⏱️ 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 FTimerHandle to 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.

graph TD A["Need: do X after a delay"] --> Q{"How?"} Q -->|"Tick + counter"| T["Runs every frame,
manual bookkeeping"] Q -->|"Timer"| Ti["Engine calls you once,
at the right time"]
Figure 1: A timer lets the engine call you exactly when needed, so the actor doesn't have to tick every frame just to watch a clock.

βœ… 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 β€” no Tick, no zero-delay hack.

Hands-on Exercise & Quiz

πŸ‹οΈ Exercise: A weapon fire rate

Objective: Use a looping timer for automatic fire.

  1. On "fire pressed" (Started), fire once immediately, then start a looping timer at your fire interval that calls Fire().
  2. On "fire released" (Completed), clear the timer.
  3. Store the FTimerHandle as a member so you can clear it.
  4. 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 in Tick β€” cleaner and cheaper.
  • SetTimer(Handle, this, &Fn, Time, bLoop, FirstDelay) β€” bLoop makes it repeat; the last arg is the first-delay.
  • Keep the FTimerHandle to ClearTimer, check IsTimerActive, or read remaining time.
  • Bind a member function, a lambda (capture carefully), or a FTimerDelegate to 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.