Guides Behaviours Coroutines and Timers

Behaviours 3 min read Updated Apr 2026

Coroutines and Timers

`WaitForSeconds` is the cleanest way to express short timed sequences in Lenga when the code should read as a sequence instead of a state machine.

It uses gameplay time, which means it respects engine pause automatically.

Use it for work like:

  • countdowns before play begins
  • delaying a respawn
  • flashing a hit effect for half a second
  • firing enemies in bursts
  • showing a tutorial prompt, then hiding it

If the logic reads as "do this, wait, do the next thing", a coroutine is usually the right fit.

The Core Pattern

There are two pieces:

  1. start the routine with startCoroutine(...)
  2. return a \Generator and yield new WaitForSeconds(...) inside it

Minimal example:

use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\WaitForSeconds;

final class GameIntro extends Behaviour
{
    public function start(): void
    {
        $this->startCoroutine($this->runIntro());
    }

    private function runIntro(): \Generator
    {
        $this->showBanner('Ready');
        yield new WaitForSeconds(1.0);

        $this->showBanner('Go');
        yield new WaitForSeconds(0.5);

        $this->hideBanner();
    }

    private function showBanner(string $text): void
    {
    }

    private function hideBanner(): void
    {
    }
}

That is much easier to read than tracking several float timers in update().

A Real Gameplay Example: Delayed Respawn

This pattern is common in action or arcade games.

use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\GameObject;
use Lenga\Engine\Core\Vector3;
use Lenga\Engine\Core\WaitForSeconds;

final class EnemyRespawn extends Behaviour
{
    public ?GameObject $enemy = null;
    public float $respawnDelay = 2.5;

    private ?Vector3 $spawnPosition = null;

    public function awake(): void
    {
        $this->spawnPosition = $this->enemy?->transform->position->clone();
    }

    public function handleEnemyDefeated(): void
    {
        $this->startCoroutine($this->respawnEnemy());
    }

    private function respawnEnemy(): \Generator
    {
        $this->enemy?->setActive(false);
        yield new WaitForSeconds($this->respawnDelay);

        if ($this->enemy !== null && $this->spawnPosition !== null) {
            $this->enemy->transform->position = $this->spawnPosition->clone();
            $this->enemy->setActive(true);
        }
    }
}

This keeps the timing rules close to the gameplay action that needs them.

A UI Example: Show, Wait, Hide

Coroutines are also a great fit for HUD prompts and temporary messages.

use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\WaitForSeconds;
use Lenga\Engine\SceneManagement\Scene;
use Lenga\Engine\UI\Text;

final class ToastMessage extends Behaviour
{
    private ?Text $message = null;

    public function start(): void
    {
        $canvas = Scene::getActive()?->findCanvas('HUD');
        $this->message = $canvas?->findTextByName('Toast');
    }

    public function showPickupMessage(string $text): void
    {
        $this->startCoroutine($this->showMessageForSeconds($text, 1.5));
    }

    private function showMessageForSeconds(string $text, float $seconds): \Generator
    {
        if (!$this->message instanceof Text) {
            return;
        }

        $this->message->text = $text;
        $this->message->visible = true;
        yield new WaitForSeconds($seconds);
        $this->message->visible = false;
    }
}

A Burst-Fire Example

Not every timer needs its own component.

use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\WaitForSeconds;

final class TurretController extends Behaviour
{
    public float $burstDelay = 2.0;
    public float $shotSpacing = 0.12;

    public function start(): void
    {
        $this->startCoroutine($this->fireLoop());
    }

    private function fireLoop(): \Generator
    {
        while (true) {
            yield new WaitForSeconds($this->burstDelay);

            $this->fireShot();
            yield new WaitForSeconds($this->shotSpacing);

            $this->fireShot();
            yield new WaitForSeconds($this->shotSpacing);

            $this->fireShot();
        }
    }

    private function fireShot(): void
    {
    }
}

This is the kind of code where coroutine syntax really pays off.

When to Prefer update() Instead

Do not force everything into coroutines.

update() is still better when:

  • the value changes every frame from input
  • you are steering or blending continuously
  • the logic is truly stateful and ongoing
  • you need immediate per-frame reaction to changing conditions

Good rule of thumb:

  • use update() for continuous behaviour
  • use coroutines for authored sequences with waits between steps

Common Mistakes

Forgetting the \Generator return type

This will not behave like a coroutine:

private function flashDamage(): void
{
    yield new WaitForSeconds(0.2);
}

Correct version:

private function flashDamage(): \Generator
{
    yield new WaitForSeconds(0.2);
}

Calling the routine directly

This is also wrong:

$this->flashDamage();

Start it with:

$this->startCoroutine($this->flashDamage());

How Pause Affects Coroutines

WaitForSeconds counts down against gameplay time.

That means:

  • the countdown advances normally while the game is running
  • the countdown freezes while the engine is paused
  • the coroutine resumes once gameplay resumes

Small example:

use Lenga\Engine\Core\Application;
use Lenga\Engine\Core\Behaviour;
use Lenga\Engine\Core\WaitForSeconds;

final class RespawnBanner extends Behaviour
{
    public function start(): void
    {
        $this->startCoroutine($this->showBanner());
    }

    private function showBanner(): \Generator
    {
        $this->setBannerVisible(true);
        yield new WaitForSeconds(2.0);
        $this->setBannerVisible(false);
    }

    public function pauseGame(): void
    {
        Application::pause();
    }

    private function setBannerVisible(bool $visible): void
    {
    }
}

If the game pauses halfway through that two-second wait, the remaining half-second will still be there when the game resumes.

WaitForSecondsRealtime is different: it uses unscaled time, so it keeps counting down while the game is paused.

WaitUntil and WaitWhile are evaluated during regular update(). Since update() still runs while paused, those predicates can continue to unblock coroutines during pause if their condition becomes true.

WaitForFixedUpdate resumes on the next fixed simulation step, so it naturally stays blocked while the engine is paused because fixedUpdate() and physics stepping are frozen.

That is usually the correct behavior for gameplay countdowns, intros, and respawn delays.

If you need pause menus or overlays to keep animating while gameplay is frozen, use Time::unscaledDeltaTime() for that UI work instead of relying on a coroutine wait.

Using coroutines for input polling

If the code should read input every frame, keep that in update() and only hand off the delayed parts to a coroutine.

Stopping a Coroutine

If you need to keep a handle, startCoroutine(...) returns one.

use Lenga\Engine\Core\Coroutine;

private ?Coroutine $blinkRoutine = null;

public function onEnable(): void
{
    $this->blinkRoutine = $this->startCoroutine($this->blink());
}

public function onDisable(): void
{
    if ($this->blinkRoutine !== null) {
        $this->stopCoroutine($this->blinkRoutine);
        $this->blinkRoutine = null;
    }
}

private function blink(): \Generator
{
    while (true) {
        $this->toggleVisible();
        yield new WaitForSeconds(0.15);
    }
}

If you are resetting a whole behaviour, stopAllCoroutines() is often the simplest option.

Coroutine Tools In This Workflow

Lenga supports:

  • startCoroutine(...)
  • stopCoroutine(...)
  • stopAllCoroutines()
  • WaitForSeconds
  • WaitForSecondsRealtime
  • WaitUntil
  • WaitWhile
  • WaitForFixedUpdate
  • TweenHandle::wait() for tween sequencing

WaitForSeconds is still the simplest building block for countdowns and short scripted delays, but you can also wait on real time, a predicate, the next fixed step, or a tween when that fits the gameplay flow better.

For motion sequences, read Sequence Tweens with Coroutines.