この記事について
Entity Component System(以後ECS)について解説します。また、ECSのC++での実装EnTTについて紹介します。
Entity Component Systemについて
Entity Component Systemは設計パターンの一つです。継承よりも委譲を優先する原則に従い、
エンティティ(ゲームシーンを構成する敵キャラ、ドア、弾丸、etc...)を部品を組み合わせることで実装できるようにします。この手法は長い継承関係による不透明な実装を回避して、設計をクリーンにします。一方で、実行時のオーバーヘッドという短所もあります。
ECSは以下の3つの要素からなります。
- Entity
コンポーネントを追加するコンテナです。通常、階層構造をとっています。(通常、Entityは SubEntityを持ちます。) - Component
オブジェクトの振る舞い、見た目、データを定義するクラスです。 - System
SystemはEntityとComponentを利用して、データに基づいた振る舞いとロジックを保持します。
UMLで表すとこんな感じです。
Entityに対して複数のComponentが紐付けられます。
Systemは、Entityを介してEntityの構成要素のComponentにアクセスできます。また、Entityを介さずに全てのComponentに対してループできたりします。
ECSは従来のOOP(オブジェクト指向)のアプローチに比べて以下のメリットがあります。
- EntityはポインタではなくIDとして保持できます。このことによりダングリングポインタ(指している先が無効な状態になっているポインタ)が発生しません。
- 外部に状態を保持しやすなります。状態を読み込むときにポインタを再構築する必要がありません。
- メモリ上のデータの位置を再配置できます。
- ポインタを使用しないので、ネットワークを介してEntityを送受信できます。
やってみよう!!
上を読んでみてECSで遊んでみたくなったと思います。早速、C++でのECSの実装のEnTTを使ってECSを実装してみましょう。
# include <entt/entt.hpp>
# include <cstdint>
struct position {
float x;
float y;
};
struct velocity {
float dx;
float dy;
};
// Systemに当たる部分。Entityの位置を更新する。
void update_position(std::uint64_t dt, entt::registry ®istry) {
registry.view<position, velocity>().each([dt](auto &pos, auto &vel) {
// gets all the components of the view at once ...
pos.x += vel.dx * dt;
pos.y += vel.dy * dt;
// ...
});
}
int main() {
entt::registry registry;
std::uint64_t dt = 16;
for(auto i = 0; i < 10; ++i) {
auto entity = registry.create();
registry.assign<position>(entity, i * 1.f, i * 1.f);
if(i % 2 == 0) { registry.assign<velocity>(entity, i * .1f, i * .1f); }
}
update_position(dt, registry);
}
描画したいときは新しいSystemとComponentを追加すればできます。
struct Drawable {
Image image; // 描画に使う画像
};
void update_drawing(entt::registry& registry)
{
registry.view<Drawable, const position>().each([dt](auto &pos, auto &vel) {
// 描画に使うロジック
});
}