2019年もそろそろ終わってしまうと思うと、月日って早いですね。
Advent Calendarも1日目の投稿通知が来て、「やべぇ」ってなりました。
###概要
普段Unity使っていて、GameObjectやComponentの仕組み便利だなと、調べて行き着いたのがEntityComponentSystem(ECS)
これをC++でやろうと学びながら設計した内容とかを書こうかなと思います。
ここ数ヶ月取り組んでいますが、Unityやっぱすげぇわ...
#Entity Component System
ECSは継承よりコンポジションを使った設計で、
Entityはプレイヤーや敵、建物、地形などゲーム上にあるオブジェクト
Componentはそのオブジェクト(Entity)に追加する機能です。
Entity単体では描画もされないし、衝突もしないです。
描画や衝突させるにはComponentを追加しないといけない訳で、それがRendererやColliderです。
#構成するクラス
##Object
using InstanceID = unsigned int;
class Object
{
private:
InstanceID _InstanceID;
public:
Object();
virtual ~Object();
}
EntityやComponentの基底クラスとしてObjectクラスを設定しています。
削除処理とか全体に共通した処理書くならObjectが便利かな
InstanceIDは被らない様にユニークなID
##Entity
class IEntity:public Object
{
private:
public:
IEntity();
virtual ~IEntity();
}
template<class Type>
class Entity:public IEntity
{
private:
public:
Entity();
virtual ~Entity();
}
IEntityクラスはEntityManagerで管理する為に用意した抽象クラスです。
それと共にComponentとの関わりもここで定義しています。
EntityはGameObjectの基底クラスだったりしてます。
##Component
class IComponent:public Object
{
private:
public:
IComponent();
virtual ~IComponent();
}
template<class Type>
class Component:public IComponent
{
private:
public:
Component();
virtual ~Component();
}
IComponentクラスはComponentManagerで管理する為に用意した抽象クラスです。
#Managerとの関わり
##Object
using InstanceID = unsigned int;
class Object
{
private:
InstanceID _InstanceID;
public:
Object();
virtual ~Object();
virtual void Destroy(); //削除する
virtual void OnDestroy(); //削除された時
}
class ObjectManager
{
using ObjectIndex = std::map<InstanceID,std::shared_ptr<Object>>;
using DestroyIndex = std::vector<InstanceID>;
private:
static ObjectIndex _ObjectIndex; //Objectの管理
static DestroyIndex _DestroyIndex; //削除ObjectのInstanceID
public:
static InstanceID AttachID(); //InstanceIDを割り振る
static std::weak_ptr<Object> GetInstance(InstanceID id); //idからIndexを探査して返す
}
削除系の処理もここで用意しています。
これを設定する事によって、EntityやComponentの削除も一緒にできて扱いやすい設計になりました。
InstanceIDを設定する際に、ObjectIndexのkey値から一致しない物を戻り値として設定しています。
ObjectIndexで管理しているポインタをアップキャストしてEntityやComponentの各マネージャーで管理しています。
##Entity
using EntityID = InstanceID;
class IEntity:public Object
{
private:
std::weak_ptr<IEntity> _self; //EntityManagerで管理されてるポインタ
public:
IEntity();
virtual ~IEntity();
EntityID GetEntityID(); //InstanceIDをEntityIDに変換したのを返す
std::weak_ptr<IEntity> GetEntity(); //_selfを返す
//ComponentManagerとの関連
public:
template<typename Type> std::weak_ptr<Type> AddComponent();
template<typename Type> std::weak_ptr<Type> GetComponent();
template<typename Type> void DestroyComponent();
}
_selfを持たせたのは、EntityManagerを探査して管理してるポインタ取得するのが面倒だったので設定してます。
AddComponentやGetComponentは下記のComponentManagerと関連していて、自身のEntityID(InstanceID)を使ってComponentManagerの関数を呼んでいます。
class EntityManager
{
using EntityIndex = std::unorder_map<EntityID,std::weak_ptr<IEntity>>;
privte:
static EntityIndex _EntityIndex; //Entityの管理
public:
static std::weak_ptr<IEntity> CreateEntity(IEntity* instance); //instanceをIndexに追加し管理
static std::weak_ptr<IEntity> GetEntity(EntityID id); //idからIndexを探査して返す
static void ReleaseEntity(EntityID id); //idからIndexを探査して削除
static void Create();
static void Release();
}
ここでEntityを管理しています。
Objectを継承したのみでは、EntityなのかObjectなのか判別がつかないので、EntityManagerを通して判別つける様な設計にしました。Entityを管理しているので、IEntityを通して、Entity全体への共通的な処理を流すことも可能になります。
##Component
class IComponent:public Object
{
private:
EntityID _OwnerID; //所属してるEntity
std::weak_ptr<IComponent> _self; //ComponentManagerで管理してるポインタ
public:
IComponent();
virtual ~IComponent();
}
Componentは付加されているEntityのID(EntityID)を所持しています。
これを設定することで、Entityが持っている別なComponentにComponentManagerを通して取得する事が出来ます。
class ComponentManager
{
using ComponentTypeID = unsigned int;
using Components = std::list<std::weak_ptr<IComponent>>;
using EntityComponents = std::unordered_map<EntityID, std::shared_ptr<Components>>;
using ComponentTypeIndex = std::unordered_map<ComponentTypeID,std::shared_ptr<Components>>;
private:
static EntityComponents _EntityComponents; //Entityに付加されたComponentの管理
static ComponentTypeIndex _ComponentTypeIndex; //Component毎のinstance群
public:
static ComponentTypeID AttachComponentTypeID(); //一つのComponentクラスに対して識別するためのIDを返す
template<typename Type>
static std::weak_ptr<Type> AddComponent(EntityID OwnerID); //EntityにComponentを追加する
template<typename Type>
static std::weak_ptr<Type> GetComponent(EntityID id); //Entityに付加されたComponentを取得
template<typename Type>
static void ReleaseComponent(EntityID id); //Entityに付加されたComponentの削除
}
ComponentTypeIDはTransformやColliderなどのクラス単位のIDです。
これによって、ComponentTypeIndex内で各Componentのインスタンスを管理しています。
このEntityやComponentを継承してGameObject,Transform,Rendererなどを設計しています。
DOTS ECSとの違い
UnityのDOTS ECSとは構造が違っていると思います。
EntityはComponentDataというデータを持っていて、ComponentDataを使うComponentSystemにデータを使った動作を定義するようです。
##余談
Managerでインスタンスをスマートポインタとかで管理してるけど、内部でコンストラクタ呼ぶものだから、インスタンスのコンストラクタをプライベートに設定できないのが気に食わない。EntityとかManager通さず自由に作れちゃうじゃん!ってなってる。