ECS(エンティティ・コンポーネント・システム)
オブジェクト指向の継承で肥大化したゲームロジックに悩む人へ。データとふるまいを分離するECSの内部構造を押さえれば、数万体を高速に回すデータ指向設計が書けます。
- ECSはIDだけのエンティティ、データのコンポーネント、振る舞いのシステムへ三分割し、継承でなく合成で組む。処理をデータから切り離し、システムが対象を横断する。
- 格納は同じ構成を連続配列へまとめるアーキタイプと、部品ごとに密配列と索引を持つスパースセットが主流。前者は反復、後者は追加削除が速く、どちらもCPUキャッシュを生かす。
- システムは必要なコンポーネントの組でクエリし、マッチしたエンティティ群に対して線形にループする。仮想関数呼び出しやポインタ追跡が消え、キャッシュミスと分岐予測ミスが激減するため、継承ベースのゲームループより桁で速くなりうる。
継承ツリーの限界から、データとふるまいの分離へ
伝統的なゲームエンジンは、GameObject を基底クラスに Enemy、FlyingEnemy、Player と継承ツリーを伸ばして世界を表現してきました。しかしこの設計はすぐに破綻します。「飛べる敵」と「毒を持つ敵」の両方が欲しくなった瞬間、単一継承では表現できず、菱形継承やコピー&ペーストに逃げることになります。これはダイヤモンド継承問題であり、ふるまいがクラス階層に固定されるがゆえの構造的な硬直です。
ECS(Entity Component System)は発想を反転させます。継承(is-a)をやめ、合成(has-a) で組み立てる。ゲーム世界の各要素を、次の3つに分解します。
- エンティティ(Entity): それ自体はデータもふるまいも持たない、ただの一意なID(多くは整数)。「そこに何かが存在する」という以上の意味を持たない。
- コンポーネント(Component): 位置
Position、速度Velocity、体力Healthといった純粋なデータの塊。メソッド(ふるまい)を持たないのが原則で、POD(Plain Old Data)に近い。あるエンティティは複数のコンポーネントを「持つ」。 - システム(System): 特定のコンポーネントの組を持つエンティティ群に対してふるまいを実行するロジック。データを持たず、コンポーネント配列を横断的に処理する。
「飛べて毒を持つ敵」は、エンティティに Flying コンポーネントと Poison コンポーネントを付け足すだけで表現できます。クラス階層を一切いじる必要がありません。ふるまいがデータから切り離されているため、組み合わせ爆発が階層の爆発を引き起こさない——これが継承ベース設計に対するECSの第一の利点です。
データ指向設計:なぜ「メモリの並び」が速度を決めるのか
ECSの真価は、設計の柔軟さだけでなく性能にあります。その根拠は現代CPUのメモリ階層にあります。CPUコアの演算速度に対しメインメモリのレイテンシは桁違いに遅く、その差をキャッシュ(L1/L2/L3)が埋めています。メモリはキャッシュライン(典型的に64バイト)単位でまとめて読み込まれるため、次に使うデータが直前のデータの隣に並んでいれば、ほぼタダで読めます。逆に、あちこちに散らばったオブジェクトをポインタでたどると、毎回キャッシュミスが起き、コアはメモリ待ちで空回りします。
オブジェクト指向の GameObject はヒープ上に個別に確保され、std::vector<GameObject*> はポインタの配列でしかありません。位置だけを更新したくても、体力もAI状態もテクスチャ参照も同じオブジェクトに同居しており、位置以外の不要なデータまでキャッシュラインに載ってしまう。これがオブジェクト指向ゲームループのキャッシュ効率を下げる根本原因です。
データ指向設計(Data-Oriented Design、DOD)は逆を行きます。同じ種類のデータを連続した配列にまとめて並べる。位置更新システムが触るのは Position 配列と Velocity 配列だけになり、それらがメモリ上で連続していれば、キャッシュラインは有効データで満たされ、ハードウェアプリフェッチャも先読みを的中させます(メモリ配置とキャッシュ局所性の一般論はプログラミングの解説トップでも扱っています)。
配列の持ち方:AoS と SoA
AoS (Array of Structs): [ {x,y,hp,ai}, {x,y,hp,ai}, ... ]
位置だけ欲しくても hp や ai まで一緒にキャッシュへ載る
SoA (Struct of Arrays): x[]:[x0,x1,...] y[]:[y0,y1,...] hp[]:[...]
位置更新は x[], y[] だけを連続に舐める → キャッシュ効率が最大
ECSの格納方式は本質的にSoA寄りで、システムが使う成分だけを密に走査する
格納方式(1)アーキタイプ:同じ構成を連続配列に束ねる
横にスクロール
コンポーネントを実際にどうメモリへ並べるか。代表的な方式がアーキタイプ(Archetype)です。Unity DOTS や Bevy が採用しています。
アーキタイプとは、同一のコンポーネント集合を持つエンティティのグループです。Position+Velocity を持つエンティティ群は1つのアーキタイプを成し、Position+Velocity+Health を持つ群は別のアーキタイプになります。各アーキタイプは、コンポーネントごとの連続配列(チャンク)を保持します。
アーキタイプ A = {Position, Velocity}
Position: [P0, P1, P2, P3, ...] ← 連続
Velocity: [V0, V1, V2, V3, ...] ← 連続
アーキタイプ B = {Position, Velocity, Health}
Position: [P0, P1, ...] Velocity: [V0, V1, ...] Health: [H0, H1, ...]
同じ行インデックス i が1体のエンティティ。i 番目の全成分がそろって並ぶ
この方式の長所は反復(イテレーション)が最速なことです。あるアーキタイプに属するエンティティは、コンポーネントが完全に連続配列で並んでいるため、システムは配列を先頭から線形に舐めるだけで済みます。分岐も間接参照もありません。
短所は構造変化のコストです。エンティティにコンポーネントを1つ追加・削除すると、それは所属アーキタイプが変わることを意味し、旧アーキタイプの配列から新アーキタイプの配列へ全成分を丸ごとコピーして移動する必要があります。頻繁にコンポーネントを付け外しするゲームロジックでは、このアーキタイプ間移動が無視できないコストになります。
格納方式(2)スパースセット:追加削除を軽くする
もう1つの主要方式がスパースセット(Sparse Set)で、EnTT や flecs(内部で両方式を併用)が採用します。コンポーネントの種類ごとに、以下の2つの配列を持ちます。
スパースセット(コンポーネント Position の例)
dense(密配列) : [ Position 実データ を詰めて連続に格納 ]
dense_entities : [ そのデータを持つエンティティID を同順で並べる ]
sparse(疎配列) : entityID をキーに dense 内の位置を引くインデックス表
追加: dense の末尾に push、sparse[entityID] にその位置を書く → O(1)
削除: 末尾要素を穴に swap して pop(swap-and-pop)、sparse を更新 → O(1)
反復: dense を先頭から線形に走査(実データが連続なのでキャッシュ良好)
スパースセットは、実データを詰めた dense 配列(連続なので反復時のキャッシュ効率が高い)と、エンティティIDから dense 内位置を引く sparse 配列(穴あきでよい)を組み合わせます。追加は末尾へ、削除は「末尾要素を穴へ入れ替えて pop する(swap-and-pop)」ことで、いずれも償却 O(1) で済みます。
アーキタイプがコンポーネントの追加削除で配列間移動を強いられたのに対し、スパースセットは1つのコンポーネントの付け外しがそのコンポーネントのセットだけで完結するため、構造変化が圧倒的に軽い。反面、複数コンポーネントを同時に要求するクエリでは、各セットの dense を突き合わせる必要があり、アーキタイプほど反復が一枚岩の連続にはなりません(もっとも swap-and-pop により削除順が並びを乱すため、順序に依存する処理には注意が要ります)。
| 観点 | アーキタイプ | スパースセット |
|---|---|---|
| データ配置 | 同一構成のエンティティを成分ごと連続配列に束ねる | コンポーネント種類ごとに dense+sparse を持つ |
| 反復(クエリ) | 最速。マッチ群を線形に舐めるだけ | 速いが複数成分の突き合わせが要る |
| コンポーネント追加削除 | アーキタイプ間の全成分コピーで重い | そのセット内で swap-and-pop、償却O(1)で軽い |
| メモリ断片化 | 少ない(チャンクに密に詰まる) | sparse 配列に穴が空きうる |
| 向く場面 | 構成が安定し反復が支配的な大規模処理 | コンポーネントの付け外しが頻繁な動的な世界 |
| 採用例 | Unity DOTS、Bevy | EnTT、flecs(両方式を併用) |
システムのクエリと、キャッシュ効率が生む速度差
システムは自分が必要とするコンポーネントの組を宣言してクエリします。たとえば移動システムは「Position と Velocity を両方持つエンティティすべて」を要求します。ECSはこの条件にマッチするエンティティ集合を返し、システムはそれを線形にループするだけです。
移動システムの疑似コード
query = world.query<Position, Velocity>() // 両方を持つ集合を要求
for (pos, vel) in query: // マッチ群を連続走査
pos.x += vel.x * dt
pos.y += vel.y * dt
// アーキタイプ方式なら、この集合は成分ごとに連続配列で並んでいるため
// ループ本体はポインタ追跡も仮想関数呼び出しもなく、ほぼ純演算になる
ここでオブジェクト指向の for each obj: obj->update() と比較すると差が際立ちます。仮想関数 update() は vtable を経由する間接呼び出しであり、呼び先が実行時までわからないため分岐予測ミスを誘発し、命令キャッシュも汚します。加えて各 obj はヒープに散在するのでデータキャッシュミスが頻発する。ECSのループはこのいずれも起こしません。マッチしたコンポーネントが連続配列で並び、呼び出しがインライン化され、プリフェッチが効く——結果として、数万〜数十万体を毎フレーム処理するようなワークロードで、継承ベースのゲームループに対し桁違いの実効スループットが出ることがあります。この性質はSIMD化とも相性がよく、連続配列は複数エンティティをまとめてベクトル演算しやすい(描画側の大量処理はグラフィックスの解説トップも参照)。
ECSの利点は「同じ処理を大量のエンティティへ一様に適用する」ときに最大化されます。逆に、エンティティ数が少ない、あるいは1体ごとに複雑で分岐だらけの固有ロジックを走らせる場合、データ指向のメリットは薄く、素直なオブジェクト指向の方が読みやすいこともあります。また、コンポーネント間の関係(親子・参照)の表現、順序依存の処理、単発イベントの扱いは、素朴なECSが不得意とする領域で、フレームワークごとに工夫(リレーション機能、コマンドバッファなど)が要ります。「弾幕・群衆・パーティクルは強い、複雑な状態機械1個は微妙」という肌感が実務の目安です。
- 三分割: エンティティ=ID、コンポーネント=データ、システム=ふるまい。継承(is-a)ではなく合成(has-a)で組む。ふるまいをデータから分離する点がOOPとの本質的差。
- 速さの理由: データ指向設計により同種データを連続配列に並べ、CPUキャッシュライン(〜64B)とプリフェッチを効かせる。仮想関数の間接呼び出し・分岐予測ミス・ポインタ追跡によるキャッシュミスを排除する。
- アーキタイプ: 同一コンポーネント構成を成分ごと連続配列に束ねる。反復は最速だが、コンポーネント追加削除でアーキタイプ間の全成分コピーが発生する。
- スパースセット: コンポーネント種類ごとに dense+sparse を持ち、追加削除は swap-and-pop で償却O(1)。構造変化に強いが、複数成分クエリは突き合わせが要る。
- クエリ: システムは必要なコンポーネントの組を宣言し、マッチ集合を線形ループする。トレードオフは「反復速度(アーキタイプ)」対「構造変化コスト(スパースセット)」。
まとめ
ECSは、ゲームオブジェクトをエンティティ(ID)・コンポーネント(データ)・システム(ふるまい)に三分割し、継承の代わりに合成で組み立てるデータ指向アーキテクチャです。設計面では、コンポーネントを付け外しするだけで機能を組み合わせられ、継承ツリーの組み合わせ爆発やダイヤモンド継承問題から解放されます。性能面では、同種データを連続配列に並べることでCPUキャッシュとプリフェッチを最大限に活かし、仮想関数の間接呼び出しやポインタ追跡が生むキャッシュミス・分岐予測ミスを排除します。格納方式は、反復が最速だが構造変化が重いアーキタイプ(Unity DOTS、Bevy)と、追加削除が swap-and-pop で軽いスパースセット(EnTT、flecs)の2系統が主流で、どちらも「システムが触る成分だけを密に走査する」というSoA的発想を共有します。システムは必要なコンポーネントの組でクエリし、マッチ群を線形に処理する。大量の同種エンティティを毎フレーム一様に更新するワークロードでは、この構造が継承ベースのゲームループに対し桁違いの実効性能をもたらします。一方で、少数の複雑な固有ロジックや関係・順序の表現は素朴なECSの不得意領域であり、適用対象を見極めることが、データ指向設計を武器として使いこなす鍵になります。
ゲーム開発の記事ガイド
ECS(エンティティ・コンポーネント・システム)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ゲーム開発
比較で見る軸
難易度: advanced / カテゴリ: ゲーム開発 / タグ数: 6
導入後に効く点
格納は同じ構成を連続配列へまとめるアーキタイプと、部品ごとに密配列と索引を持つスパースセットが主流。前者は反復、後者は追加削除が速く、どちらもCPUキャッシュを生かす。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ゲーム開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ゲーム開発 / ECS」に近いか確認する。
- 強みである「ECSはIDだけのエンティティ、データのコンポーネント、振る舞いのシステムへ三分割し、継承でなく合成で組む。処理をデータから切り離し、システムが対象を横断する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。