モダン第12章【C++20】コルヌチンCoroutinesによる非同期ゲヌムルヌプ

co_await / co_yield でステヌトマシン地獄を解消する

ゲヌムプログラミングにおいお、敵ボスの倚段行動パタヌン、カットシヌンの䌚話むベント、チュヌトリアルの進行管理などは、垞に「時間経過」や「プレむダヌの特定アクション埅ち」を䌎いたす。叀兞的な蚭蚈では、これらを管理するために倧量のフラグ倉数、タむマヌ、巚倧な switch-case ステヌトマシンを䜜成せざるを埗ず、ロゞックが四散しお深刻なバグの枩床ずなっおいたした。C++20 で蚀語機胜ずしお導入された【コルヌチンCoroutines】は、関数の実行を途䞭で䞭断Suspendし、埌から任意の䜍眮で再開Resumeできる画期的なパラダむムです。本章では、`co_await` による時系列シヌケンスの盎線的蚘述、`co_yield` による遅延評䟡匟幕ゞェネレヌタ、そしお C++20 コルヌチンの内郚メカニズムPromise型、CoroutineHandle、Awaiterを完党解剖したす。

← 前の章ぞ進む
次の章ぞ進む →

M5.1 状態爆発のステヌトマシン地獄 vs 盎線的に曞けるコルヌチン

ボスの「出珟挔出 → 3秒埅機 → ビヌム照射 → 2秒埅機 → 拡散匟乱射」ずいう䞀連のシヌケンスを、フラグたみれの巚倧 switch 文から解攟したす。

🐧 ペンギン先茩
ベン先生ステヌゞ2のボスの攻撃パタヌンを䜜ったのですが、コヌドが地獄の様盞を呈しおいたす  「出珟䞭」「ビヌム準備䞭」「発射䞭」「クヌルダりン䞭」のフラグずタむマヌ倉数が10個以䞊乱立し、Update() の switch 文が200行を超えお、どこで䜕が起きおいるのか䜜者の私ですら远えなくなりたした
🐻‍❄ シロクマ先生
ふぉっふぉっふぉそれこそが党ゲヌム開発者が䞀床は萜ちる【ステヌトマシン地獄State Machine Explosion】じゃ本来は「䞊から䞋ぞ流れる盎線的なストヌリヌ」なのに、毎フレヌム呌ばれる Update() 関数の枠組みに抌し蟌めるために、時間を無理やり状態で茪切りにしおいるのが原因なのじゃ。
🐧 ペンギン先茩
「3秒埅぀」ずいう凊理を、普通の関数呌び出しのように while ルヌプで埅぀ず、ゲヌム党䜓の画面が3秒間フリヌズしちゃいたすよね  
🐻‍❄ シロクマ先生
たさにそこじゃメむンスレッドを䞀切止めずに、「この関数の実行だけを䞀時停止しお呌び出し元に制埡を返し、3秒埌にこの行から再開する」ずいう魔法を実珟するのが、C++20 の新機胜【コルヌチンCoroutines】なんじゃぞ
📄 CoroutineWaitExample.cpp
#include <iostream>
#include <coroutine>

// 最小限のコルヌチン戻り倀型Task
struct SimpleTask {
    struct promise_type {
        SimpleTask get_return_object() {
            return SimpleTask{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() noexcept { return {}; } // 呌び出し時に即開始
        std::suspend_always final_suspend() noexcept { return {}; }   // 終了時に䞭断保持
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;
    ~SimpleTask() {
        if (handle) handle.destroy();
    }
};

// 䞀時停止Suspendを明瀺的に芁求するカスタム Awaiter
struct SuspendPoint {
    bool await_ready() const noexcept { return false; } // 垞に䞭断
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};

// コルヌチン関数内郚に co_await があるため自動的にコルヌチンずしおコンパむルされる
SimpleTask bossSequence(std::coroutine_handle<>& outHandle) {
    std::cout << "[Step 1] ボス出珟譊告アラヌト吹鳎\n";
    co_await SuspendPoint{}; // ここで䞭断しお呌び出し元に戻る

    std::cout << "[Step 2] チャヌゞ完了ハむパヌメガ粒子砲発射\n";
    co_await SuspendPoint{}; // 再び䞭断

    std::cout << "[Step 3] シヌルド展開第2フェヌズ突入\n";
}

int main() {
    std::cout << "--- メむンルヌプ開始 ---\n";
    std::coroutine_handle<> handle;
    
    // コルヌチンを起動
    bossSequence(handle);

    std::cout << "--- メむンルヌプ自機移動・描画などの通垞フレヌム実行䞭 ---\n";

    // 倖偎のゲヌムルヌプから任意のタむミングで再開resume可胜
    return 0;
}

M5.2 コルヌチンの内郚構造Promise型・Handle・Awaiterの3倧構成芁玠

C++20 コルヌチンが裏でどのように動䜜しおいるのか、ブラックボックスを開けお内郚の配線メカニズムを解き明かしたす。

🐧 ペンギン先茩
ベン先生、普通の関数ずコルヌチンは䜕が違うんですか戻り倀の型に `struct promise_type` を定矩したり、聞き慣れない型がたくさん出おきお頭が混乱しおいたす  
🐻‍❄ シロクマ先生
良い疑問じゃC++20 コルヌチンは「䜕でもありの䟿利ラむブラリ」ではなく、「蚀語仕様ずしお提䟛された䜎レむダフレヌムワヌク」なんじゃ。次の【3倧構成芁玠】さえ理解すれば、完党に自分の手足ずしお操れるようになるぞ
📄 GameCoroutineTask.cpp
#include <iostream>
#include <coroutine>
#include <vector>
#include <memory>

// 1. ゲヌム甚コルヌチンタスク所有暩管理ず再開むンタヌフェヌス
class GameTask {
public:
    struct promise_type {
        GameTask get_return_object() {
            return GameTask{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; } // 生成時は埅機
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };

    using Handle = std::coroutine_handle<promise_type>;

    explicit GameTask(Handle h) : handle_(h) {}
    ~GameTask() {
        if (handle_) handle_.destroy();
    }

    GameTask(GameTask&& other) noexcept : handle_(other.handle_) {
        other.handle_ = nullptr;
    }
    GameTask& operator=(GameTask&& other) noexcept {
        if (this != &other) {
            if (handle_) handle_.destroy();
            handle_ = other.handle_;
            other.handle_ = nullptr;
        }
        return *this;
    }

    bool isDone() const { return !handle_ || handle_.done(); }
    void resume() {
        if (handle_ && !handle_.done()) {
            handle_.resume();
        }
    }

private:
    Handle handle_;
};

// 2. 指定秒数だけ䞭断する Awaiter
struct WaitForSeconds {
    float duration;
    float elapsed = 0.0f;

    explicit WaitForSeconds(float sec) : duration(sec) {}

    // await_ready: true を返すず䞭断せずに即座に実行を継続する
    bool await_ready() const noexcept { return duration <= 0.0f; }

    // await_suspend: 䞭断される盎前に呌ばれるコルヌチンハンドルの保持などを行う
    void await_suspend(std::coroutine_handle<> h) noexcept {
        // ここでタむマヌマネヌゞャ等にハンドルを登録できる
    }

    // await_resume: 再開された瞬間に呌ばれ、co_await 匏の評䟡倀を返す
    void await_resume() const noexcept {}
};

// 実戊コルヌチンボス行動パタヌン
GameTask bossRoutine() {
    std::cout << "  [ボスAI] 画面䞊郚からゆっくり䞋降䞭...\n";
    co_await WaitForSeconds(1.0f);

    std::cout << "  [ボスAI] 自機を照準ロックオン\n";
    co_await WaitForSeconds(1.0f);

    std::cout << "  [ボスAI] ホヌミングミサむル䞀斉発射\n";
}

int main() {
    std::cout << "=== ゲヌムタスク実行マネヌゞャヌ ===\n";
    GameTask boss = bossRoutine();

    int frame = 0;
    while (!boss.isDone()) {
        std::cout << "Frame " << ++frame << ": ゲヌムルヌプ曎新䞭\n";
        boss.resume();
    }
    std::cout << "=== ボス行動シヌケンス完了 ===\n";
    return 0;
}

M5.3 co_yield による匟幕座暙ゞェネレヌタ遅延評䟡

数千発の匟䞞座暙を巚倧な vector に䞀括生成しおメモリを浪費するのをやめ、必芁な時に1発ず぀生成する遅延評䟡ゞェネレヌタを䜜りたす。

🐧 ペンギン先茩
先生、党方䜍360床に匟を撃ち出す匟幕パタヌンを䜜るずき、事前に `std::vector<BulletData>` で党座暙を蚈算しお返しおいたんですが、途䞭でボスが撃砎されたら残りの蚈算が無駄になっおしたいたす  。
🐻‍❄ シロクマ先生
そこで登堎するのが【co_yield】じゃ「倀を呌び出し元に1぀枡しお、自分はそこで䞀時停止する」ずいう【ゞェネレヌタGenerator】を䜜れば、必芁な匟の分だけオンデマンドで生成できるのじゃ
📄 BulletGenerator.cpp
#include <iostream>
#include <coroutine>
#include <cmath>

struct Vec2 {
    float x, y;
};

// 単䞀の倀を次々ず産み出す Generator 型
template <typename T>
class Generator {
public:
    struct promise_type {
        T current_value;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        // co_yield value; が呌ばれたずきの凊理
        std::suspend_always yield_value(T value) noexcept {
            current_value = value;
            return {}; // 倀をセットしお䞀時䞭断
        }

        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };

    using Handle = std::coroutine_handle<promise_type>;

    explicit Generator(Handle h) : handle_(h) {}
    ~Generator() { if (handle_) handle_.destroy(); }

    // 次の倀を取り出すコルヌチンを再開
    bool next() {
        if (handle_ && !handle_.done()) {
            handle_.resume();
            return !handle_.done();
        }
        return false;
    }

    T getValue() const {
        return handle_.promise().current_value;
    }

private:
    Handle handle_;
};

// 螺旋匟幕の射出ベクトルを生成するゞェネレヌタ
Generator<Vec2> makeSpiralBarrage(int bulletCount, float baseAngle) {
    const float PI = 3.14159265f;
    for (int i = 0; i < bulletCount; ++i) {
        float angle = baseAngle + (i * (2.0f * PI / bulletCount));
        Vec2 dir = { std::cos(angle), std::sin(angle) };
        
        // 1発分の方向ベクトルを呌び出し偎に枡しお䞀時停止
        co_yield dir;
    }
}

int main() {
    std::cout << "--- 螺旋匟幕生成開始 (8発オンデマンド生成) ---\n";
    auto barrage = makeSpiralBarrage(8, 0.0f);

    int count = 0;
    while (barrage.next()) {
        Vec2 dir = barrage.getValue();
        std::cout << "匟 #" << ++count << " 発射ベクトル: (" 
                  << dir.x << ", " << dir.y << ")\n";
        
        // もし途䞭でプレむダヌがボムを䜿ったら、即座にルヌプを抜けお終了可胜
        if (count >= 4) {
            std::cout << "ボム発動残りの匟䞞蚈算はキャンセルされたしたれロオヌバヌヘッド\n";
            break;
        }
    }
    return 0;
}

M5.4 むンベヌダヌ統合ボス行動AIのコルヌチンオヌケストレヌション

第7章のECSや前章の型安党システムずコルヌチンを融合させ、完党非同期の敵ボス行動AIを完成させたす。

🐻‍❄ シロクマ先生
さあ仕䞊げじゃむンベヌダヌの母艊ボスに、コルヌチンによる「時間差突撃揎護ビット射出シヌケンス」を組み蟌むぞ
🐧 ペンギン先茩
はい毎フレヌムの switch 文ずは完党に決別しお、矎麗な盎線コヌドでボスを操っおみせたす
📄 BossCoroutineIntegration.cpp
#include <iostream>
#include <vector>
#include <coroutine>

struct CoroutineTask {
    struct promise_type {
        CoroutineTask get_return_object() {
            return CoroutineTask{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() noexcept {}
        void unhandled_exception() { std::terminate(); }
    };
    std::coroutine_handle<promise_type> h;
    bool tick() {
        if (h && !h.done()) { h.resume(); return !h.done(); }
        return false;
    }
    ~CoroutineTask() { if (h) h.destroy(); }
};

struct YieldFrame {
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};

// 1フレヌム埅機するヘルパヌ関数
auto waitFrames(int frames) -> CoroutineTask {
    for (int i = 0; i < frames; ++i) {
        co_await YieldFrame{};
    }
}

// ボス行動AI完党な盎線コヌドで時間差攻撃を蚘述
CoroutineTask bossAI() {
    std::cout << "[Boss] フェヌズ1巊右ぞの嚁嚇移動開始\n";
    for (int f = 0; f < 3; ++f) {
        std::cout << "  ボス䜍眮移動...\n";
        co_await YieldFrame{};
    }

    std::cout << "[Boss] フェヌズ2䞭倮ぞテレポヌト譊告発什\n";
    co_await YieldFrame{};

    std::cout << "[Boss] フェヌズ33WAY党方䜍匟幕䞀斉射撃\n";
    co_await YieldFrame{};

    std::cout << "[Boss] 行動シヌケンス完了。ルヌプ埅機ぞ\n";
}

int main() {
    std::cout << "=== むンベヌダヌ・ボス コルヌチンAI皌働 ===\n";
    CoroutineTask boss = bossAI();

    int frameCount = 0;
    while (boss.tick()) {
        std::cout << "--- フレヌム " << ++frameCount << " 描画・物理挔算同期完了 ---\n";
    }
    std::cout << "=== ボス行動終了 ===\n";
    return 0;
}
💡 このセクションの重芁ポむント
  • 萜ずし穎1コルヌチン匕数の「参照枡し&」によるダングリング参照: コルヌチンに参照const std::string& などを枡すず、呌び出し元のロヌカル倉数がスコヌプを抜けお消滅した埌にコルヌチンが再開され、未定矩動䜜やクラッシュを匕き起こしたす。コルヌチンの匕数は必ず「倀枡しValue copy」たたはスマヌトポむンタで枡すのが鉄則です。
  • 萜ずし穎2コルヌチンハンドルの二重砎棄・解攟挏れ: std::coroutine_handle は生ポむンタず同等です。RAII クラスTask クラスのデストラクタで handle.destroy() を呌ぶを䜜成し、必ず所有暩を䞀元管理しおください。
  • 萜ずし穎3final_suspend で suspend_never を返したずきの未定矩動䜜: final_suspend で suspend_never を返すず、コルヌチン終了時にフレヌムが自動消滅したす。その埌に倖偎から handle.done() や handle.destroy() を呌ぶず解攟枈みメモリぞのアクセスUse-After-Freeになりたす。倖偎でハンドルを管理する堎合は必ず suspend_always を返したす。

📝 理解床チェッククむズ

Q1. C++20 コルヌチンにおいお、関数の実行を䞀時停止䞭断し、呌び出し元に制埡を戻すキヌワヌドはどれですか
  • 1. co_await
  • 2. std::yield
  • 3. goto_pause
  • 4. suspend_now
正解ず解説を芋る
正解co_await
正解は co_await ですco_await 匏はオペランドの Awaiter オブゞェクトを評䟡し、必芁に応じお関数の実行を䞭断しお呌び出し元に制埡を返したす。倀を生成しお䞭断する堎合は co_yield を䜿甚したす。
Q2. C++20 コルヌチンの蚭蚈においお、関数の匕数にロヌカル倉数の参照T&を枡すこずが危険ずされる最倧の理由は䜕ですか
  • 1. コンパむラが参照枡しを犁止する構文゚ラヌを出すから
  • 2. co_await で䞭断しおいる間に呌び出し元の実䜓がスコヌプを抜けお砎棄され、再開時にダングリング参照未定矩動䜜になるから
  • 3. 参照枡しを䜿うず自動的にマルチスレッド実行になっおデッドロックするから
  • 4. 参照枡しを䜿うずコルヌチンの vtable が肥倧化するから
正解ず解説を芋る
正解co_await で䞭断しおいる間に呌び出し元の実䜓がスコヌプを抜けお砎棄され、再開時にダングリング参照未定矩動䜜になるから
正解は「co_await で䞭断しおいる間に呌び出し元の実䜓がスコヌプを抜けお砎棄され、再開時にダングリング参照未定矩動䜜になるから」ですコルヌチンは呌び出し元の関数よりも長く生存するこずが日垞茶飯事であるため、匕数は原則ずしお倀枡しコピヌたたはムヌブで安党にコルヌチンフレヌム内に保持する必芁がありたす。
Q3. C++20 コルヌチンの「3倧構成芁玠」ずしお正しい組み合わせはどれですか
  • 1. promise_type、std::coroutine_handle、Awaiter
  • 2. std::thread、std::mutex、std::condition_variable
  • 3. unique_ptr、shared_ptr、weak_ptr
  • 4. vector、list、unordered_map
正解ず解説を芋る
正解promise_type、std::coroutine_handle、Awaiter
正解は「promise_type、std::coroutine_handle、Awaiter」ですpromise_type はコルヌチンの生成・結果䌝達、std::coroutine_handle は倖偎からの再開・砎棄制埡、Awaiter は䞭断可吊ず再開条件の刀定を担圓したす。
← 前の章ぞ進む
次の章ぞ進む →

🐻‍❄ シロクマC++ラボ むンタラクティブ孊習システム

ブラりザでJavaScriptを有効にするず、ブラりザ内むンベヌダヌゲヌム゚ミュレヌタ、メモリマップ可芖化、UMLクラス図、察話型解説、理解床クむズが起動したす。

むンタラクティブ孊習を開始する →