Files
Coroutines/docs/coroutines/example.md
T
2026-09-14 09:42:06 -03:00

5.4 KiB

Coroutines — Examples

Based on test/coroutines/main.cpp.

#include <acrux/coroutines/Coroutine.h>
#include <acrux/coroutines/CoroutineRunner.h>
#include <acrux/coroutines/standard/WaitConditions.h>

using namespace acrux::coroutines;
using namespace acrux::coroutines::standard;

struct Character
{
    int hp = 100;

    Coroutine attack(Character& target)
    {
        print("Attacking...\n");
        co_yield waitForSeconds(1.0f);        // waits one real-time second
        target.hp -= 20;
        co_yield nullptr;                     // waits exactly one frame
    }

    Coroutine battleSequence(Character& enemy)
    {
        co_yield attack(enemy);               // starts a child Coroutine and waits
                                               // for it to finish

        co_yield waitUntil([&]{ return enemy.hp <= 0; }); // waits on a predicate
    }
};

int main()
{
    CoroutineRunner runner;                   // becomes the active runner

    Character hero, enemy;
    CoroutineHandle battle = startCoroutine(hero.battleSequence(enemy));

    while (battle.isRunning())
    {
        runner.update();                      // called once per frame/tick
    }
}

Yield expressions

co_yield nullptr;                              // resume next frame
co_yield waitForSeconds(2.5f);                 // resume after a real-time delay
co_yield waitUntil([this]{ return ready; });   // resume once the predicate is true
co_yield waitWhile([this]{ return loading; }); // resume once the predicate is false
co_yield someOtherCoroutine();                 // start a coroutine and wait for it to finish
co_yield waitFor(handleStartedEarlier);        // wait for a coroutine already in progress

waitForSeconds/waitUntil/waitWhile/waitFor are declared in standard/WaitConditions.h, in the acrux::coroutines::standard namespace. They are not part of the core module — see architecture.md for what that means in practice, and how to write an equivalent helper of your own.

Starting a coroutine without waiting on it immediately

CoroutineHandle bg = startCoroutine(controller.fadeOut(delta)); // starts immediately
// ... other work, possibly across several yields ...
co_yield waitFor(bg); // waits on it later, if needed

Stopping a coroutine

CoroutineHandle h = startCoroutine(enemy.patrol());
...
stopCoroutine(h); // h becomes INVALID_COROUTINE_HANDLE; safe to call again
// equivalently:
h.stop();

Attaching extra data to a coroutine

CoroutineRunner can be derived from to associate extra data with each coroutine it starts — for example, which entity a coroutine belongs to in a game engine. CoroutineHandle is hashable, so it can be used directly as a key. Children started by a coroutine — whether through co_yield childTask() or a direct startCoroutine() call from inside its body — can inherit the same data automatically, using the parent argument to onCoroutineStarted().

#include <unordered_map>

using EntityId = int;

class GameCoroutineRunner : public CoroutineRunner
{
public:
    // Without this, declaring startCoroutine(task, owner) below would hide
    // the base class's startCoroutine(task) entirely.
    using CoroutineRunner::startCoroutine;

    CoroutineHandle startCoroutine(Coroutine task, EntityId owner)
    {
        CoroutineHandle handle = CoroutineRunner::startCoroutine(std::move(task));
        if (handle) m_owners[handle] = owner;
        return handle;
    }

    EntityId ownerOf(CoroutineHandle handle) const
    {
        auto it = m_owners.find(handle);
        return it != m_owners.end() ? it->second : -1;
    }

protected:
    // If `parent` is a coroutine we're already tracking an owner for,
    // inherit it -- this is what makes a coroutine's children automatically
    // belong to the same entity, without either call site passing an
    // owner explicitly. The explicit-owner startCoroutine() overload above
    // still runs its own assignment afterward, so an explicit owner always
    // takes priority over an inherited one.
    void onCoroutineStarted(CoroutineHandle handle, CoroutineHandle parent) override
    {
        auto it = m_owners.find(parent);
        if (it != m_owners.end())
        {
            m_owners[handle] = it->second;
        }
    }

    // Called whenever a tracked coroutine finishes, however it finished --
    // keeps m_owners from accumulating stale entries.
    void onCoroutineFinished(CoroutineHandle handle) override
    {
        m_owners.erase(handle);
    }

private:
    std::unordered_map<CoroutineHandle, EntityId> m_owners;
};
GameCoroutineRunner runner;
CoroutineHandle h = runner.startCoroutine(enemy.patrol(), enemy.id());
// ...
EntityId owner = runner.ownerOf(h);

With this in place, a coroutine spawning its own sub-coroutines needs no special handling at all — they simply belong to the same entity:

Coroutine Enemy::patrol()
{
    // co_yield of a child: inherits this coroutine's owner automatically.
    co_yield lookAround();

    // a fire-and-forget child, started directly: also inherits it.
    startCoroutine(playFootstepSounds());

    co_yield waitForSeconds(1.0f);
}

This pattern is not limited to a single piece of data. A derived class can store anything it needs — a full struct, a pointer back to a subsystem, several parallel maps — since CoroutineRunner places no constraints on what "extra data" means, or on how onCoroutineStarted() chooses to propagate it.