Needle Engine Documentation
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help
Getting Started
Tutorials
How-To Guides
Explanation
Reference
Help

Use Coroutines

Learn how to use coroutines to run code over multiple frames, create delays, and sequence operations. Coroutines in Needle Engine are built on JavaScript generators and run on the main thread, pausing and resuming execution across frames without blocking.

Related Guide

If you're new to Needle Engine components, start with Use Lifecycle Hooks to learn about awake(), start(), and update() methods. Coroutines are a complementary tool for time-based and sequenced operations.

When to Use Coroutines

  • Sequence operations with delays between them
  • Animate values over time
  • Wait for conditions to be met
  • Run periodic tasks without update()
  • Load resources with delays or retries
  • Create smooth transitions and effects

When NOT to Use Coroutines

For code that needs to run every frame (like continuous movement, rotation, or input handling), use the update() lifecycle method instead. See Lifecycle Hooks for more details.


Quick Start

Basic component using a coroutine:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class Spawner extends Behaviour {
    start() {
        // Start a coroutine
        this.startCoroutine(this.spawnRoutine());
    }

    *spawnRoutine() {
        while (true) {
            this.spawnEnemy();
            yield WaitForSeconds(2); // Wait 2 seconds
        }
    }

    private spawnEnemy() {
        console.log("Enemy spawned!");
    }
}

JavaScript Generators

Coroutines use JavaScript generator functions (function*). The yield keyword pauses execution and resumes on the next frame or after a wait. Unlike web workers or threads, coroutines run on the main thread—they don't create true parallelism but allow you to write sequential code that executes over time.


Wait Methods

Needle Engine provides several wait methods for coroutines:

WaitForSeconds

Wait for a specific amount of time in seconds:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class DelayedAction extends Behaviour {
    start() {
        this.startCoroutine(this.delayedMessage());
    }

    *delayedMessage() {
        console.log("Starting...");
        yield WaitForSeconds(3); // Wait 3 seconds
        console.log("3 seconds later!");
    }
}

WaitForFrames

Wait for a specific number of frames:

import { Behaviour, WaitForFrames } from "@needle-tools/engine";

export class FrameDelay extends Behaviour {
    start() {
        this.startCoroutine(this.waitFrames());
    }

    *waitFrames() {
        console.log("Frame:", this.context.time.frameCount);
        yield WaitForFrames(60); // Wait 60 frames
        console.log("60 frames later:", this.context.time.frameCount);
    }
}

yield null

Wait for the next frame:

*myRoutine() {
    console.log("Frame 1");
    yield null; // Wait one frame
    console.log("Frame 2");
    yield null; // Wait another frame
    console.log("Frame 3");
}

Common Patterns

Timed Display Updates

Update UI or text at regular intervals:

import { Behaviour, Text, serializable, WaitForSeconds } from "@needle-tools/engine";

export class DisplayTime extends Behaviour {
    @serializable(Text)
    text?: Text;

    onEnable() {
        this.startCoroutine(this.updateTime());
    }

    private *updateTime() {
        while (true) {
            if (this.text) {
                this.text.text = new Date().toLocaleTimeString();
            }
            yield WaitForSeconds(1); // Update every second
        }
    }
}

Fade Effect

Smoothly animate values over time:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class Fader extends Behaviour {
    fadeIn(duration: number = 1) {
        this.startCoroutine(this.fadeRoutine(0, 1, duration));
    }

    fadeOut(duration: number = 1) {
        this.startCoroutine(this.fadeRoutine(1, 0, duration));
    }

    *fadeRoutine(from: number, to: number, duration: number) {
        const startTime = this.context.time.time;

        while (true) {
            const elapsed = this.context.time.time - startTime;
            const t = Math.min(elapsed / duration, 1);

            const alpha = from + (to - from) * t;
            this.setOpacity(alpha);

            if (t >= 1) break;
            yield null; // Wait for next frame
        }
    }

    private setOpacity(alpha: number) {
        // Set material opacity
        console.log("Opacity:", alpha);
    }
}

Delayed Sequence

Execute multiple actions with delays:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class Sequencer extends Behaviour {
    start() {
        this.startCoroutine(this.playSequence());
    }

    *playSequence() {
        console.log("Action 1");
        yield WaitForSeconds(1);

        console.log("Action 2");
        yield WaitForSeconds(2);

        console.log("Action 3");
        yield WaitForSeconds(1);

        console.log("Sequence complete!");
    }
}

Wait for Condition

Wait until a condition is met:

import { Behaviour, GameObject } from "@needle-tools/engine";

export class Waiter extends Behaviour {
    target?: GameObject;

    start() {
        this.startCoroutine(this.waitForTarget());
    }

    *waitForTarget() {
        console.log("Waiting for target...");

        // Wait until target exists
        while (!this.target) {
            yield null;
        }

        console.log("Target found!", this.target);
    }
}

Periodic Task

Run a task repeatedly with delays:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class PeriodicTask extends Behaviour {
    private _running = false;

    onEnable() {
        this._running = true;
        this.startCoroutine(this.periodicUpdate());
    }

    onDisable() {
        this._running = false;
    }

    *periodicUpdate() {
        while (this._running) {
            this.doTask();
            yield WaitForSeconds(5); // Every 5 seconds
        }
    }

    private doTask() {
        console.log("Task executed at:", new Date().toLocaleTimeString());
    }
}

Stopping Coroutines

You can stop coroutines by storing the generator reference:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class StoppableRoutine extends Behaviour {
    private _routine?: Generator;

    start() {
        this._routine = this.startCoroutine(this.myRoutine());
    }

    stopRoutine() {
        if (this._routine) {
            this.stopCoroutine(this._routine);
            this._routine = undefined;
        }
    }

    *myRoutine() {
        while (true) {
            console.log("Running...");
            yield WaitForSeconds(1);
        }
    }
}

Automatic Cleanup

Coroutines are automatically stopped when a component is disabled or destroyed, so you don't need to manually stop them in most cases.


Coroutines vs update()

When should you use coroutines instead of update()?

Use Coroutines when:

  • You need delays between operations
  • You're sequencing multiple steps
  • You want code to run periodically (not every frame)
  • You're waiting for conditions or events
  • You need clearer, more readable sequential logic

Use update() when:

  • You need to run code every frame
  • You're doing continuous animation or movement
  • You need maximum performance (less overhead)
  • You're implementing physics or game logic that updates constantly
// ❌ Don't use update() for periodic tasks
update() {
    this._timer += this.context.time.deltaTime;
    if (this._timer >= 2) {
        this._timer = 0;
        this.doSomething();
    }
}

// ✅ Use coroutines instead
start() {
    this.startCoroutine(this.periodicTask());
}

*periodicTask() {
    while (true) {
        this.doSomething();
        yield WaitForSeconds(2);
    }
}

Advanced: Nested Coroutines

You can yield other coroutines:

import { Behaviour, WaitForSeconds } from "@needle-tools/engine";

export class NestedRoutines extends Behaviour {
    start() {
        this.startCoroutine(this.mainRoutine());
    }

    *mainRoutine() {
        console.log("Starting main routine");

        // Wait for sub-routine to complete
        yield* this.subRoutine();

        console.log("Main routine complete");
    }

    *subRoutine() {
        console.log("Sub-routine started");
        yield WaitForSeconds(2);
        console.log("Sub-routine finished");
    }
}

Best Practices

Do's

  • Use coroutines for time-based sequences
  • Stop coroutines when they're no longer needed
  • Use meaningful variable names for wait times
  • Check conditions before long operations
  • Use WaitForSeconds for time-based delays

Don'ts

  • Avoid infinite loops without yields (will freeze)
  • Don't use coroutines for every-frame updates
  • Don't forget to yield in loops
  • Avoid too many simultaneous coroutines (performance)

Next Steps

  • Use Lifecycle Hooks - awake, start, update methods
  • Handle User Input - Mouse, touch, keyboard
  • Scripting Examples - More code examples
  • Component API Reference - Complete API

Suggest changes
Last Updated: 1/27/26, 8:30 PM

On this page

Extras

Copy for AI (LLMs)