
2026/03/22 8:19
Unity を見てみたおかげで、C++ のコルーチンが何を目的としているのか理解できました。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
(すべての重要ポイントを統合し、曖昧な表現を明確化したもの)
要約
C++23では
<generator> と co_yield キーワードが導入され、開発者は Unity の C# yield return null パターンに非常に近いコルーチンスタイルのコードを書ける標準的な手段を得ました。Unity ではコルーチンは主に数フレームにわたってオブジェクトをフェードアウトさせるなどの一時的な挙動(例:StartCoroutine(Fade()))に使用されます。Unity のシステムは C# の await より前に構築されたため、ボイラープレートを低く抑える生成器スタイルのワークアラウンドに頼っています。
対照的に、C++ で同様のロジックを書きたい場合、生成器が無いと完全な状態機械(手動 enum 状態、カウンタ、しばしば冗長な演算子オーバーロード)が必要になります。新しい
<generator> 機能により、開発者は Fibonacci などの単純な生成器を最小限のコードで書けますが、co_await はまだ問題があります。言語自体に具体的な実行フレームワークがないため、スケジューリングや実行キュー、既存の並列モデルとの統合についての疑問は未解決です。
この記事では、C++ で最小限の Unity スタイルのエグゼキュータを提示しています。これは
std::generator<std::monostate> オブジェクトを保持するマネージャーで、各フレームごとにそれらを進め、完了したものは「remove‑if」パターンで削除します。また、このエグゼキュータを拡張して描画可能オブジェクトを返すカスタム Draw 構造体を yield する方法も示し、データ駆動型のレンダリングパイプラインを実現しています。効果更新の並列実行は TBB の parallel_for を使用しており、多数のコルーチンが同時に走る際に性能向上が確認できます。
C++26 では最終的に適切な
co_await を可能にする実行フレームワークが追加される予定です。しかし、既にプロジェクトは Boost.Asio や Intel TBB のようなカスタムスケジューラやライブラリを使用しているため、新しいフレームワークの統合は挑戦的になる可能性があります。それまでは、生成器ベースのエグゼキュータを採用して冗長な状態機械を置き換えることで、保守性と性能を向上させつつ、サードパーティの非同期ライブラリに対する標準化された代替手段を提供できます。本文
2026年3月20日 – C++ とゲーム開発
私はコルーチンに関する多くの講演を見てきましたが、Unity が C# でそれらをどのように使っているかを見るまで、本当に理解できたことはありませんでした。
C++ におけるコルーチンはすでに6年間存在しますが、それでも実際のプロダクションコードで触れたことはありません。これは、低レベル機能であり、プロジェクトへ組み込むには多くのカスタム plumbing が必要だからだと考えられます。さらに重要なのは、具体的な例が不足している点です。
Unity のコルーチン例
void Update() { if (Input.GetKeyDown("f")) StartCoroutine(Fade()); } IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { c.a = alpha; renderer.material.color = c; yield return null; // “次のフレーム” } }
C# の純粋主義者は
yield を使ったこの書き方に異議を唱えるかもしれません。意味論が誤っています:何も返さずに yield していますが、実際には await NextFrame() に似た挙動を期待しているのです。Unity のトリックは初期の C# が await をサポートしなかったことから生まれました。それが今日まで使われ続けています。
なぜコルーチンなのか?
上記の例は単純です。もう少し複雑なエフェクトを試してみましょう。
IEnumerator TimeWarp() { // 左へジャンプ transform.position.x -= 1.f; yield return null; // 右へステップ for (int i = 0; i < 4; ++i) { transform.position.x += 0.2f; yield return null; } // … 手を腰に当てる … // 再度タイムワープ for (int i = 0; i < 4; ++i) { transform.Rotate(0.f, 90.f * i, 0.f); yield return null; } }
これを C++ の通常の関数オブジェクトやラムダで書くと、汚い状態機械が出来上がります。
class TimeWarp { public: enum class State { Jump, StepRight, HandsOnHips, DoAgain }; State _state = State::Jump; int _i = 0; Transform* _transform; TimeWarp(Transform& transform) : _transform(&transform) {} bool operator()() { switch (_state) { case State::Jump: _transform->position.x -= 1.f; _state = State::StepRight; break; case State::StepRight: _transform->position.x += 0.2f; if (++_i == 4) { _state = State::HandsOnHips; _i = 0; } break; // … case State::DoAgain: _transform->Rotate(0.f, 90.f * i, 0.f); if (++_i == 4) return true; // 完了 break; } return false; } };
見た目が汚く、読みづらいです。エフェクトを個別の動作に分割したり継続をキューに入れたりすることは可能ですが、面倒です。
C++23 実装
<generator> を使えば、各フレームで制御を yield するコルーチンを書けます。
std::generator<std::monostate> TimeWarp(GameObject& obj) { // 左へジャンプ obj.transform.position.x -= 1.f; co_yield {}; // 右へステップ for (int i = 0; i < 4; ++i) { obj.transform.position.x += 0.2f; co_yield {}; } // … 手を腰に当てる … // 再度タイムワープ for (int i = 0; i < 4; ++i) { obj.transform.Rotate(0.f, 90.f * i, 0.f); co_yield {}; } }
これは実質 Unity が使うハックと同じです。
co_yield は「次のフレーム」を意味します。co_await を使う方が複雑で、何を待つか、いつ準備完了か、どのスケジューラを使うかを決める必要があります。C++26 で execution が登場する予定ですが、現時点では generator アプローチが実用的です。
C++ の簡易 Unity‑style コルーチンランナー
以下は毎フレームすべてのアクティブコルーチンを1回だけ走らせる最小限のエグゼキュータです。
class effects_manager { public: void add(std::generator<std::monostate> effect) { _effects.push_back(std::move(effect)); _iterators.push_back(_effects.back().begin()); } void run() { // 完了したコルーチンを除去 int first = 0; for (; first != _effects.size() && _iterators[first] != _effects[first].end(); ++first); if (first != _effects.size()) { for (int i = first; ++i != _effects.size(); ) if (_iterators[i] != _effects[i].end()) { _effects[first] = std::move(_effects[i]); _iterators[first] = std::move(_iterators[i]); ++first; } _effects.erase(begin(_effects) + first, end(_effects)); _iterators.erase(begin(_iterators) + first, end(_iterators)); } // すべてのコルーチンを進める for (int i = 0; i < _effects.size(); ++i) ++_iterators[i]; } private: std::vector<std::generator<std::monostate>> _effects; using effect_iterator = decltype(std::declval<std::generator<std::monostate>>().begin()); std::vector<effect_iterator> _iterators; };
使用例:
effects_manager effects; effects.add(TimeWarp(object)); ... effects.run(); // 毎フレーム1回呼び出す
ボーナス:副作用ではなく描画データを返す
コルーチンから
Draw オブジェクトを返し、すべての描画を1度に集めることもできます。
struct Draw { Model model; Transform transform; }; std::generator<Draw> TimeWarp(const Model& model) { vec3 position{-1.f, 0.f, 0.f}; co_yield Draw{model, {.position = position}}; for (int i = 0; i < 4; ++i) { position.x += 0.2f; co_yield Draw{model, {.position = position}}; } // … }
ランナーは
Draw オブジェクトのベクタを生成します。
std::vector<Draw> run() { std::vector<Draw> draws; draws.reserve(_effects.size()); for (int i = 0; i < _effects.size(); ++i) { draws.push_back(*_iterators[i]); ++_iterators[i]; } return draws; }
TBB や他の並列ツールを使えば、ループを並列化できます。
std::vector<Draw> draws(_effects.size()); tbb::parallel_for(0zu, _effects.size(), [this, &draws](size_t i) { draws[i] = *_iterators[i]; ++_iterators[i]; }); return draws;
結論
C++ の
<generator> を使ったコルーチンは、手作りの状態機械を必要とせずに簡潔で読みやすい操作列を書けます。パターンは実装が簡単で、数十行程度のコードで済み、既存のゲームループに自然に統合できます。他の Fibonacci ジェネレータよりもずっと面白い使い道です!