🖥️

C++でEntity Component Systemを実装してみる

2023/09/29に公開

はじめに

CPUの進化により、データの処理速度は毎年驚異的な速度で上がっています。それに対し、データをメモリから受け取る速度はここ十数年で数倍程度にしか進化してません。
その結果、プログラムの実行時間の大部分がデータ取得の待機時間に費やされるようになってしまいました。
そこで、キャッシュメモリとCPUの特性を生かし、データ取得の待機時間を減らすことでパフォーマンスの向上を図ろうとしたのがEntityComponentSystem(ECS) です。

今回は、そんなECSをC++で実装してみたので、その基本的な概念から作り方まで解説していこうと思います。

必要な前提知識

この記事はC++を前提に書いてあるので、ある程度のC++の知識があると読みやすいと思います。
また、途中でコンポーネント指向やオブジェクトプール等のデザインパターンが出てくるので、必要に応じて調べながら読んでいただけると嬉しいです。

キャッシュメモリとCPUの特性について

前述したように、データをメインメモリから受け取る速度はあまり進化してないのですが、少しでもこれを改善するための仕組みがPCにはいくつか搭載されています。
そのうちの一つがキャッシュメモリです

キャッシュメモリとは、よく使われるデータや今後使用される可能性が高いデータにすぐアクセスできるようにするために、CPU内部に設けられた高速なメモリのことです。
CPUはメインメモリからデータを取得する際、毎回決まった量(キャッシュライン) のデータをまとめてキャッシュメモリに渡します。

そのため、可能な限り必要なデータを一か所にまとめておくことで、メインメモリにデータを取得しに行く回数を減らすことができます。

コンポーネント指向のデータを連続させてみよう

データを連続することによる恩恵を確かめるために、ゲーム開発で広く使われるコンポーネント指向を改造し、データを連続させてみましょう。

シンプルなコンポーネント指向

まずは、継承とメモリの動的確保を用いて作られた、一般的なコンポーネント指向のメモリの様子をみてみましょう。

一般的なコンポーネント指向ではメモリの動的確保を使いコンポーネントを生成するため、データがあちこちに散らばってしまい、キャッシュラインを有効活用できないことが分かります。

サンプルコード

シンプルなコンポーネント指向.h
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <array>

class GameObject;
class IComponent
{
public:
	virtual void Update() {}
	void SetOwner(GameObject* a_pObject)
	{
		m_pOwner = a_pObject;
	}
protected:
	GameObject* m_pOwner = nullptr;
};

class GameObject
{
public:
	template<typename CompType>
	std::weak_ptr<CompType> AddComponent()
	{
		std::shared_ptr<CompType> newComp = std::make_shared<CompType>();
		m_umComponents[typeid(CompType)] = newComp;
		newComp->SetOwner(this);
		return newComp;
	}
	template<typename CompType>
	std::weak_ptr<CompType> GetComponent()
	{
		return std::static_pointer_cast<CompType>(m_umComponents[typeid(CompType)]);
	}
	void Update()
	{
		for (auto&& spComp : m_umComponents)
		{
			spComp.second->Update();
		}
	}
private:
	std::unordered_map<std::type_index, std::shared_ptr<IComponent>> m_umComponents;
};

データが連続するコンポーネント指向

キャッシュメモリとCPUの特性を踏まえると、オブジェクトやコンポーネントはデータが連続するコンテナで管理するほうがいいということが分かります。
コンポーネントの管理主をGameObjectからComponentArrayに変更し、インスタンスもメモリの動的確保ではなくデータが連続するコンテナ(std::vector)に直接追加する形にしてみましょう。

このようにすることで、メモリ上でデータが連続し、キャッシュライン上に効率よくデータを乗せることができるようになります。

ComponentArray

コンポーネントを種類ごとにstd::vector配列で扱うためのクラスです。
同じコンテナで管理できるように、空のインターフェースクラスを実装しています。

また、コンポーネントの管理主がComponentArrayに変わったため、ComponentArrayから更新関数を実行できるようにしました。

ComponentArray
// ComponentArrayの基底クラス
class IComponentArray
{
public:
	virtual void Update() {}
};

// コンポーネントを種類ごとに管理するコンテナクラス
template<typename CompType>
class ComponentArray :public IComponentArray
{
public:
	// このクラスが管理するコンポーネントの更新関数を実行
	void Update()override
	{
		for (auto&& comp : m_vComponents)
		{
			comp->Update();
		}
	}
	// コンポーネントを追加し、追加したコンポーネントを返す
	CompType* AddComponent()
	{
		m_vComponents.emplace_back();
		return &m_vComponents.back();
	}
private:
	// このクラスが管理するすべてのコンポーネントを格納するコンテナ
	std::vector<CompType> m_vComponents;
};

// 全てのコンポーネントを型ごとに格納するコンテナ
std::unordered_map<std::type_index, std::shared_ptr<IComponentArray>> umTypeToComponents;

void Update()
{
	// 全てのコンポーネントの更新関数を実行
	for (auto&& comps : umTypeToComponents)
	{
		comps.second->Update();
	}
}

サンプルコード

データが連続するコンポーネント指向.h

#include <memory>
#include <unordered_map>
#include <typeindex>
#include <array>
#include <vector>
class GameObject;

// コンポーネントの基底クラス
class IComponent
{
public:
	virtual void Update() {}
	// 持ち主のポインタをセットする関数
	void SetOwner(GameObject* a_pObject)
	{
		m_pOwner = a_pObject;
	}
protected:
	GameObject* m_pOwner = nullptr;
};

// ComponentArrayの基底クラス
class IComponentArray
{
public:
	virtual void Update() {}
};

// コンポーネントを種類ごとに管理するコンテナクラス
	class GameObject;

	// コンポーネントの基底クラス
	class IComponent
	{
	public:
		virtual void Update() {}
		// 持ち主のポインタをセットする関数
		void SetOwner(GameObject* a_pObject)
		{
			m_pOwner = a_pObject;
		}
	protected:
		GameObject* m_pOwner = nullptr;
	};

	// ComponentArrayの基底クラス
	class IComponentArray
	{
	public:
		virtual void Update() {}
	};

	static size_t m_nextCompTypeID = 0;

	// コンポーネントを種類ごとに管理するコンテナクラス
	template<typename CompType>
	class ComponentArray :public IComponentArray
	{
	public:
		ComponentArray()
		{
			m_vComponents.reserve(100000000);
		}
		// このクラスが管理するコンポーネントの更新関数を全て実行する
		void Update() override
		{
			for (auto&& comp : m_vComponents)
			{
				comp.Update();
			}
		}
		// コンポーネントを追加し、追加したコンポーネントを返す
		CompType* AddComponent()
		{
			m_vComponents.emplace_back();
			return &m_vComponents.back();
		}
	private:
		std::vector<CompType> m_vComponents;
		// このコンテナクラスが管理するコンポーネントが持つ一意なID
		// 詳細はシンプルなECSのArchetypeにて説明します
		static size_t m_compTypeID;
	public:
		// CompTypeIDを取得する関数
		static inline const size_t GetID()
		{
			// この関数を初めて呼んだ時にIDを発行
			if (!ComponentArray<CompType>::m_compTypeID)
			{
				ComponentArray<CompType>::m_compTypeID = ++m_nextCompTypeID;
			}
			return ComponentArray<CompType>::m_compTypeID;
		}
	};

	template<typename CompType>
	size_t ComponentArray<CompType>::m_compTypeID = 0;

	// コンポーネントを管理するクラス
	class ComponentManager
	{
	public:
		// 全てのコンポーネントの更新関数を実行する
		void Update()
		{
			for (auto&& compArray : m_umTypeToCompArray)
			{
				compArray.second->Update();
			}
		}

		template<typename CompType>
		CompType* AddComponent()
		{
			size_t type = ComponentArray<CompType>::GetID();
			if (m_umTypeToCompArray.find(type) == m_umTypeToCompArray.end())
			{
				m_umTypeToCompArray[type] = std::make_shared<ComponentArray<CompType>>();
			}
			return std::static_pointer_cast<ComponentArray<CompType>>(m_umTypeToCompArray[type])->AddComponent();
		}

	private:
		std::unordered_map<size_t, std::shared_ptr<IComponentArray>> m_umTypeToCompArray;
		static size_t m_nextCompTypeID;
	private:
		ComponentManager() {}
	public:
		static ComponentManager& Instance()
		{
			static ComponentManager instance;
			return instance;
		}
	};

	// ゲームオブジェクト1つとそれに紐づくコンポーネントを管理するクラス
	class GameObject
	{
	public:
		// ComponentManagerにコンポーネントを追加し、このクラスと紐づける
		template<typename CompType>
		CompType* AddComponent()
		{
			size_t type = ComponentArray<CompType>::GetID();
			CompType* pComp = ComponentManager::Instance().AddComponent<CompType>();
			if (m_vTypeToComp.size() <= type)
			{
				m_vTypeToComp.resize(type + 1, nullptr);
			}
			m_vTypeToComp[type] = static_cast<void*>(pComp);
			pComp->SetOwner(this);
			return pComp;
		}
		template<typename CompType>
		CompType* GetComponent()
		{
			return static_cast<CompType*>(m_vTypeToComp[ComponentArray<CompType>::GetID()]);
		}
	private:
		std::vector<void*> m_vTypeToComp;
	};

実行速度を比べてみる

シンプルなコンポーネント指向とデータが連続するコンポーネント指向で同じ処理を行い、実行速度を調べると、以下のような結果が出ました。

シンプルなコンポーネント指向:      10749584000ns
データが連続するコンポーネント指向:555962100ns

このように、データを連続させるだけで実行速度に約20倍の差が出ることが分かります。

実行したコードはこちら

速度比較で使ったコード
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <array>
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <array>
#include <vector>

#include <string>
#include <sstream>
#include <chrono>
#include <functional>
#include <iostream>
#include <windows.h>

// 速度計測用関数
namespace utl
{
	inline void ProfilingFunction(const std::string& a_msg, const std::function<void()>& a_func)
	{
		std::chrono::system_clock::time_point point1, point2;
		std::chrono::system_clock::duration dis;
		point1 = std::chrono::system_clock::now();

		a_func();

		point2 = std::chrono::system_clock::now();
		dis = point2 - point1;
		auto nanoseconds = std::chrono::duration_cast<std::chrono::nanoseconds>(dis).count();
		std::ostringstream oss;
		oss << a_msg.c_str() << nanoseconds << " ns\n---------------\n";
		std::string durationString = oss.str();
		std::wstring wideString(durationString.begin(), durationString.end());
		OutputDebugString(wideString.c_str());
		std::cout << nanoseconds << std::endl;
	}
}


// シンプルなコンポーネント指向
namespace NormalComponent
{
	class GameObject;
	class IComponent
	{
	public:
		virtual void Update() {}
		void SetOwner(GameObject* a_pObject)
		{
			m_pOwner = a_pObject;
		}
	protected:
		GameObject* m_pOwner = nullptr;
	};

	class GameObject
	{
	public:
		template<typename CompType>
		std::weak_ptr<CompType> AddComponent()
		{
			std::shared_ptr<CompType> newComp = std::make_shared<CompType>();
			m_umComponents[typeid(CompType)] = newComp;
			newComp->SetOwner(this);
			return newComp;
		}
		template<typename CompType>
		std::weak_ptr<CompType> GetComponent()
		{
			return std::static_pointer_cast<CompType>(m_umComponents[typeid(CompType)]);
		}
		void Update()
		{
			for (auto&& spComp : m_umComponents)
			{
				spComp.second->Update();
			}
		}
	private:
		std::unordered_map<std::type_index, std::shared_ptr<IComponent>> m_umComponents;
	};

	class TestComponent1 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent2 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent3 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent4 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent5 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent6 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent7 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent8 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent9 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent10 :public IComponent
	{
	public:
		int a = 0;
	};

	class RigidbodyComponent :public IComponent
	{
	public:
		void Update()override
		{
			TestComponent1* pTest1 = m_pOwner->GetComponent<TestComponent1>().lock().get();
			TestComponent2* pTest2 = m_pOwner->GetComponent<TestComponent2>().lock().get();
			TestComponent3* pTest3 = m_pOwner->GetComponent<TestComponent3>().lock().get();
			TestComponent4* pTest4 = m_pOwner->GetComponent<TestComponent4>().lock().get();
			TestComponent5* pTest5 = m_pOwner->GetComponent<TestComponent5>().lock().get();
			TestComponent6* pTest6 = m_pOwner->GetComponent<TestComponent6>().lock().get();
			TestComponent7* pTest7 = m_pOwner->GetComponent<TestComponent7>().lock().get();
			TestComponent8* pTest8 = m_pOwner->GetComponent<TestComponent8>().lock().get();
			TestComponent9* pTest9 = m_pOwner->GetComponent<TestComponent9>().lock().get();
			TestComponent10* pTest10 = m_pOwner->GetComponent<TestComponent10>().lock().get();
			a = pTest1->a +
				pTest2->a +
				pTest3->a +
				pTest4->a +
				pTest5->a +
				pTest6->a +
				pTest7->a +
				pTest8->a +
				pTest9->a +
				pTest10->a;
		}
		int a = 0;
	};


}



// データが連続するコンポーネント指向
namespace DataComponent
{
	class GameObject;

	// コンポーネントの基底クラス
	class IComponent
	{
	public:
		virtual void Update() {}
		// 持ち主のポインタをセットする関数
		void SetOwner(GameObject* a_pObject)
		{
			m_pOwner = a_pObject;
		}
	protected:
		GameObject* m_pOwner = nullptr;
	};

	// ComponentArrayの基底クラス
	class IComponentArray
	{
	public:
		virtual void Update() {}
	};

	static size_t m_nextCompTypeID = 0;

	// コンポーネントを種類ごとに管理するコンテナクラス
	template<typename CompType>
	class ComponentArray :public IComponentArray
	{
	public:
		ComponentArray()
		{
			m_vComponents.reserve(100000000);
		}
		// このクラスが管理するコンポーネントの更新関数を全て実行する
		void Update() override
		{
			for (auto&& comp : m_vComponents)
			{
				comp.Update();
			}
		}
		// コンポーネントを追加し、追加したコンポーネントを返す
		CompType* AddComponent()
		{
			m_vComponents.emplace_back();
			return &m_vComponents.back();
		}
	private:
		std::vector<CompType> m_vComponents;
		// このコンテナクラスが管理するコンポーネントが持つ一意なID
		static size_t m_compTypeID;
	public:
		// CompTypeIDを取得する関数
		static inline const size_t GetID()
		{
			// この関数を初めて読んだ時にIDを発行
			if (!ComponentArray<CompType>::m_compTypeID)
			{
				ComponentArray<CompType>::m_compTypeID = ++m_nextCompTypeID;
			}
			return ComponentArray<CompType>::m_compTypeID;
		}
	};

	template<typename CompType>
	size_t ComponentArray<CompType>::m_compTypeID = 0;

	// コンポーネントを管理するクラス
	class ComponentManager
	{
	public:
		// 全てのコンポーネントの更新関数を実行する
		void Update()
		{
			for (auto&& compArray : m_umTypeToCompArray)
			{
				compArray.second->Update();
			}
		}

		template<typename CompType>
		CompType* AddComponent()
		{
			size_t type = ComponentArray<CompType>::GetID();
			if (m_umTypeToCompArray.find(type) == m_umTypeToCompArray.end())
			{
				m_umTypeToCompArray[type] = std::make_shared<ComponentArray<CompType>>();
			}
			return std::static_pointer_cast<ComponentArray<CompType>>(m_umTypeToCompArray[type])->AddComponent();
		}

	private:
		std::unordered_map<size_t, std::shared_ptr<IComponentArray>> m_umTypeToCompArray;
		static size_t m_nextCompTypeID;
	private:
		ComponentManager() {}
	public:
		static ComponentManager& Instance()
		{
			static ComponentManager instance;
			return instance;
		}
	};

	// ゲームオブジェクト1つとそれに紐づくコンポーネントを管理するクラス
	class GameObject
	{
	public:
		// ComponentManagerにコンポーネントを追加し、このクラスと紐づける
		template<typename CompType>
		CompType* AddComponent()
		{
			size_t type = ComponentArray<CompType>::GetID();
			CompType* pComp = ComponentManager::Instance().AddComponent<CompType>();
			if (m_vTypeToComp.size() <= type)
			{
				m_vTypeToComp.resize(type + 1, nullptr);
			}
			m_vTypeToComp[type] = static_cast<void*>(pComp);
			pComp->SetOwner(this);
			return pComp;
		}
		template<typename CompType>
		CompType* GetComponent()
		{
			return static_cast<CompType*>(m_vTypeToComp[ComponentArray<CompType>::GetID()]);
		}
	private:
		std::vector<void*> m_vTypeToComp;
		//std::unordered_map<std::type_index, void*> m_umTypeToComp;
	};

	class TestComponent1 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent2 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent3 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent4 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent5 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent6 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent7 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent8 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent9 :public IComponent
	{
	public:
		int a = 0;
	};

	class TestComponent10 :public IComponent
	{
	public:
		int a = 0;
	};

	class RigidbodyComponent :public IComponent
	{
	public:
		void Update()override
		{
			TestComponent1* pTest1 = m_pOwner->GetComponent<TestComponent1>();
			TestComponent2* pTest2 = m_pOwner->GetComponent<TestComponent2>();
			TestComponent3* pTest3 = m_pOwner->GetComponent<TestComponent3>();
			TestComponent4* pTest4 = m_pOwner->GetComponent<TestComponent4>();
			TestComponent5* pTest5 = m_pOwner->GetComponent<TestComponent5>();
			TestComponent6* pTest6 = m_pOwner->GetComponent<TestComponent6>();
			TestComponent7* pTest7 = m_pOwner->GetComponent<TestComponent7>();
			TestComponent8* pTest8 = m_pOwner->GetComponent<TestComponent8>();
			TestComponent9* pTest9 = m_pOwner->GetComponent<TestComponent9>();
			TestComponent10* pTest10 = m_pOwner->GetComponent<TestComponent10>();
			a = pTest1->a +
				pTest2->a +
				pTest3->a +
				pTest4->a +
				pTest5->a +
				pTest6->a +
				pTest7->a +
				pTest8->a +
				pTest9->a +
				pTest10->a;
		}
		int a = 0;
	};
}


constexpr size_t TestNum = 100000000;

int main()
{
	{
		std::vector<NormalComponent::GameObject> vNormalObjects;
		vNormalObjects.reserve(TestNum);
		for (size_t i = 0; i < TestNum; i++)
		{
			vNormalObjects.emplace_back();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent1>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent2>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent3>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent4>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent5>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent6>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent7>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent8>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent9>();
			vNormalObjects.back().AddComponent<NormalComponent::TestComponent10>();
			vNormalObjects.back().AddComponent<NormalComponent::RigidbodyComponent>();
		}	
		utl::ProfilingFunction("normal", [&]()
			{
				for (auto&& obj : vNormalObjects)
				{
					obj.Update();
				}
			}
		);		
	}

	{
		std::vector<DataComponent::GameObject> vDataObjects;
		vDataObjects.reserve(TestNum);
		for (size_t i = 0; i < TestNum; i++)
		{
			vDataObjects.emplace_back();
			vDataObjects.back().AddComponent<DataComponent::TestComponent1>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent2>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent3>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent4>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent5>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent6>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent7>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent8>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent9>();
			vDataObjects.back().AddComponent<DataComponent::TestComponent10>();
			vDataObjects.back().AddComponent<DataComponent::RigidbodyComponent>();
		}
		utl::ProfilingFunction("Data", [&]()
			{
				DataComponent::ComponentManager::Instance().Update();
			}
		);		
	}

	return 0;
}

シンプルなECS

前節までで、データが連続するコンテナでコンポーネントを管理するようにしたことで、効率的にデータをキャッシュラインに乗せることができるようになりました。
今度は、コンポーネントのインスタンスの管理だけでなく、コンポーネントを紐づける役割もComponentManagerに任せてみましょう。

Entity

同じ添え字を持つコンポーネントを1つのオブジェクトとして扱うことで、コンポーネント同士を紐づけることができます。この添え字のように、コンポーネント同士を紐づける一意な数字のことをEntityと呼びます。

Entityでコンポーネントを紐づけてオブジェクトを表すサンプルコード
// コンポーネントを管理するコンテナ
std::vector<Rigidbody> vRigidbody;
std::vector<Transform> vTransform;
std::vector<Status> vStatus;

// エンティティの宣言
size_t player = 0;
size_t enemy = 1;

// エンティティを使いゲームオブジェクトを表現している様子
vTransform[player].m_x += vRigidbody[player].m_x;
vTransform[enemy].m_x += vRigidbody[enemy].m_x;

vStatus[player].m_hp = 50;
vStatus[player].m_attack = 10;

vStatus[enemy].m_hp = 20;
vStatus[enemy].m_attack = 5;

vStatus[player].m_hp -= vStatus[enemy].m_attack;
vStatus[enemy].m_hp -= vStatus[player].m_attack;

このようにすることで、GameObjectが必要なくなり、オブジェクトをEntityとComponentに分割することができました。

また、Entityの値がそのまま配列の添え字となるため、このままでは無限に配列の要素が増え続けてしまいます。
そのため、サンプルコードでは削除したEntityをリサイクルする機能を付けてみました。

Component

コンポーネントはデータが連続するコンポーネント指向と同じように、std::vectorで直接管理します。
今回はObjectPoolパターンを使い、コンポーネントのインスタンスを使いまわせるようにしているので、ポインタの扱いには注意してください。

template<typename CompType>
class ComponentPool :public IComponentPool
{
public:
	// コンテナのメモリを確保
	ComponentPool(const size_t a_size)
		:m_vComponents(a_size)
	{
	}
	// コンポーネントを追加
	inline CompType* AddComponent(const size_t a_entity)
	{
		if (m_vComponents.size() < a_entity)
		{
			m_vComponents.resize(a_entity,CompType());
		}
		m_vComponents[a_entity] = CompType();
		return &m_vComponents[a_entity];
	}
	// コンポーネントを取得する
	inline CompType* GetComponent(const size_t a_entity)noexcept
	{
		// エンティティが有効なら
		if (m_vComponents.size() >= a_entity)
		{
			return &m_vComponents[a_entity];
		}
		return nullptr;
	}
private:
	std::vector<CompType> m_vComponents;
};

System

さらに、データの処理もComponentManagerに任せてみましょう
Entityをそのまま添え字として使うことで、以下のように処理を実装することができます。

添え字を使ったデータアクセス
std::vector<Rigidbody> vRigidbody;
std::vector<Transform> vTransform;

void TestFunction()
{
	// TransformとRigidbody両方と紐づくEntityのリスト
	std::vector<size_t> vEntities;	

	for (auto&& entity : vEntities)
	{
		vTransform[entity].m_x += vRigidbody[entity].m_x;
	}
}

サンプルコードでは、関数ポインタと可変長テンプレートを使うことにより、より簡単に処理を実装できるようにしました。

このように、コンポーネントから切り離した処理のことをSystemと呼びます。

Archetype

最後に、Entityがどのコンポーネントを持ってるか確認するためにArchetypeを実装してみましょう。
コンポーネントの種類ごとに一意なIDを発行することで、エンティティが持つコンポーネントの組み合わせをビットフラグで表現することができるようになります。
このビットフラグがArchetypeです。

まずは、コンポーネントの種類ごとに一意なIDを発行してみましょう

コンポーネントの種類ごとに一意なIDを発行する
static size_t nextCompTypeID = 0;

template<typename CompType>
class ComponentPool :public IComponentPool
{

	~~ コンポーネントを管理する処理 ~~
		
private:
	//このクラスが管理するコンポーネントの一意なID
	static size_t m_compTypeID;
public:
	//このクラスが管理するコンポーネントの一意なIDを取得する
	static inline const size_t GetID()
	{
		//この関数が初めて呼ばれたときにIDを発行する
		if (!ComponentPool<CompType>::m_compTypeID)
		{
			ComponentPool<CompType>::m_compTypeID = ++nextCompTypeID;
		}
		return ComponentPool<CompType>::m_compTypeID;
	}
};
// compTypeIDの初期化
template<typename CompType>
size_t ECSManager::ComponentPool<CompType>::m_compTypeID = 0;

次に、ComponentのIDを使い、Entityがどのコンポーネントと紐づいているか確認できるようにしてみましょう。
コンポーネントを追加する際にComponentの一意なIDを受け取り、自身のアーキタイプに追加します。

// bitsetのテンプレート引数はコンポーネントの種類の数より多くする必要がある
using Archetype = std::bitset<256>;

// エンティティとアーキタイプを紐づけるコンテナ
std::vector<Archetype> vEntityToArchetype;

// コンポーネントを追加する
template<typename CompType>
CompType* AddComponent(const size_t a_entity)
{

	~~ コンポーネントを追加する処理 ~~
	
	// コンポーネントの一意なIDを取得
	size_t type = ComponentPool<CompType>::GetID();
	
	Archetype arch;
	// コンポーネントを追加する前のアーキタイプを取得
	if (vEntityToArchetype.size() > a_entity)
	{
		arch = vEntityToArchetype[a_entity];
	}
	else
	{
		vEntityToArchetype.resize(a_entity + 1, Archetype());
	}

	// エンティティのアーキタイプに追加するコンポーネントのタイプを登録
	arch.set(type);
	
	// 変更したアーキタイプをセット
	vEntityToArchetype[a_entity] = arch;
}

まとめ

このように、オブジェクトをEntity、Component、Systemの3つに分解して管理するのがEntity Component Systemです。

サンプルコード

シンプルなECS
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <array>
#include <vector>
#include <bitset>
#include <functional>

using Archetype = std::bitset<128>;

class ECSManager
{
public:
	// エンティティを新規作成
	inline const size_t GenerateEntity()
	{
		size_t nEntity;
		// リサイクル待ちエンティティがあればそこから1つ取り出す
		if (m_vRecycleEntities.size())
		{
			nEntity = m_vRecycleEntities.back();
			m_vRecycleEntities.pop_back();
		}
		// 無ければ新規発行
		else
		{
			nEntity = ++m_nextID;
		}
		// エンティティを有効にする
		if (m_vEntityToActive.size() < nEntity)
		{
			m_vEntityToActive.resize(nEntity + 1, false);
		}
		m_vEntityToActive[nEntity] = true;
		// 生成したエンティティを返す
		return nEntity;
	}

	// コンポーネントを追加する
	template<typename CompType>
	CompType* AddComponent(const size_t a_entity)
	{
		// コンポーネントの型のIDを取得
		size_t type = ComponentPool<CompType>::GetID();
		// 初めて生成するコンポーネントなら
		if (!m_umTypeToComponents[type])
		{
			// ComponentPoolを実体化する
			m_umTypeToComponents[type] = std::make_shared<ComponentPool<CompType>>(4096);
		}
		// 追加するコンポーネントを格納するコンテナクラスを取得
		std::shared_ptr<ComponentPool<CompType>> spCompPool = std::static_pointer_cast<ComponentPool<CompType>>(m_umTypeToComponents[type]);
		// コンポーネントを追加し取得
		CompType* pResultComp = spCompPool->AddComponent(a_entity);

		// コンポーネントを追加する前のアーキタイプを取得
		Archetype arch;
		if (m_vEntityToArchetype.size() > a_entity)
		{
			arch = m_vEntityToArchetype[a_entity];
		}
		else
		{
			m_vEntityToArchetype.resize(a_entity + 1, Archetype());
		}
		// 前アーキタイプのエンティティリストからエンティティを削除
		m_umArchToEntities[arch].Remove(a_entity);
		// アーキタイプを編集
		arch.set(type);
		// 新しいアーキタイプをセット
		m_umArchToEntities[arch].Add(a_entity);
		m_vEntityToArchetype[a_entity] = arch;
		// 追加したコンポーネントを返す
		return pResultComp;
	}

	// コンポーネントを削除する
	template<typename CompType>
	void RemvoeComponent(const size_t a_entity)
	{
		// コンポーネントの型のIDを取得
		size_t type = ComponentPool<CompType>::GetID();

		Archetype arch;
		// コンポーネントを削除する前のアーキタイプを取得
		if (m_vEntityToArchetype.size() > a_entity)
		{
			arch = m_vEntityToArchetype[a_entity];
		}
		else
		{
			return;
		}
		// エンティティが削除するコンポーネントを持ってなかったらreturn
		if (!arch.test(type))
		{
			return;
		}
		// 前アーキタイプのエンティティリストからエンティティを削除
		m_umArchToEntities[arch].Remove(a_entity);
		// アーキタイプを編集
		arch.reset(type);
		// 新しいアーキタイプをセット
		m_umArchToEntities[arch].Add(a_entity);
		m_vEntityToArchetype[a_entity] = arch;
	}

	// エンティティを無効にする
	inline void RemoveEntity(const size_t a_entity)
	{
		m_vEntityToActive[a_entity] = false;
		m_umArchToEntities[m_vEntityToArchetype[a_entity]].Remove(a_entity);
		m_vRecycleEntities.emplace_back(a_entity);
	}

	// 処理を実行する
	template<typename ...CompType>
	void RunFunction(std::function<void(CompType&...)> a_func)
	{
		// 処理に必要なコンポーネントのアーキタイプを取得
		Archetype arch;
		(arch.set(ComponentPool<CompType>::GetID()), ...);
		// 処理に必要なアーキタイプを含むアーキタイプを持つエンティティのリストを検索
		for (auto&& entities : m_umArchToEntities)
		{
			if ((entities.first & arch) == arch)
			{
				for (auto&& entity : entities.second.GetEntities())
				{
					a_func(std::static_pointer_cast<ComponentPool<CompType>>(m_umTypeToComponents[ComponentPool<CompType>::GetID()])->m_vComponents[entity]...);
				}
			}
		}
	}

	// エンティティを管理するためのコンテナクラス
	class EntityContainer
	{
	public:
		// エンティティを追加
		inline void Add(const size_t a_entity)
		{
			m_vEntities.emplace_back(a_entity);
			if (m_vEntityToIndex.size() < a_entity)
			{
				m_vEntityToIndex.resize(a_entity + 1);
			}
			m_vEntityToIndex[a_entity] = m_vEntities.size() - 1;
		}
		// エンティティを削除
		inline void Remove(const size_t a_entity)
		{
			if (m_vEntityToIndex.size() < a_entity)
			{
				return;
			}
			size_t backIndex = m_vEntities.size() - 1;
			size_t backEntity = m_vEntities.back();
			size_t removeIndex = m_vEntityToIndex[a_entity];
			// 削除する要素が最後の要素でなければ
			if (a_entity != m_vEntities.back())
			{
				m_vEntities[removeIndex] = backEntity;
				m_vEntityToIndex[backIndex] = removeIndex;
			}
			// 最後尾のEntityを削除
			m_vEntities.pop_back();
		}
	private:
		std::vector<size_t> m_vEntities;
		std::vector<size_t> m_vEntityToIndex;
	public:
		inline const std::vector<size_t>& GetEntities()const noexcept
		{
			return m_vEntities;
		}
	};

	// ComponentPoolを同じコンテナで扱うための基底クラス
	class IComponentPool
	{
	public:
	private:
	};

	// コンポーネントを管理するコンテナクラス
	template<typename CompType>
	class ComponentPool :public IComponentPool
	{
	public:
		// コンテナのメモリを確保
		ComponentPool(const size_t a_size)
			:m_vComponents(a_size)
		{
		}
		// コンポーネントを追加
		inline CompType* AddComponent(const size_t a_entity)
		{
			if (m_vComponents.size() < a_entity)
			{
				m_vComponents.resize(a_entity, CompType());
			}
			m_vComponents[a_entity] = CompType();
			return &m_vComponents[a_entity];
		}

		// コンポーネントを取得する
		inline CompType* GetComponent(const size_t a_entity)noexcept
		{
			// エンティティが有効なら
			if (m_vComponents.size() >= a_entity)
			{
				return &m_vComponents[a_entity];
			}
			return nullptr;
		}
	private:
		friend class ECSManager;
		// コンポーネントのインスタンスを管理するコンテナ
		std::vector<CompType> m_vComponents;
		// このコンテナクラスが管理するコンポーネントが持つ一意なID
		static size_t m_compTypeID;
	public:
		// CompTypeIDを取得する関数
		static inline const size_t GetID()
		{
			// この関数を初めて読んだ時にIDを発行
			if (!ComponentPool<CompType>::m_compTypeID)
			{
				ComponentPool<CompType>::m_compTypeID = ++ECSManager::m_nextCcompTypeID;
			}
			return ComponentPool<CompType>::m_compTypeID;
		}
	};

private:
	// コンポーネントをタイプ別で管理するコンテナ
	std::unordered_map<size_t, std::shared_ptr<IComponentPool>> m_umTypeToComponents;
	// エンティティとアーキタイプを紐づけるコンテナ
	std::vector<Archetype> m_vEntityToArchetype;
	// エンティティと有効フラグを紐づけるコンテナ
	std::vector<bool> m_vEntityToActive;
	// エンティティをアーキタイプごとに分割したコンテナ
	std::unordered_map<Archetype, EntityContainer> m_umArchToEntities;
	// 次に生成するエンティティのID
	size_t m_nextID = 0;
	// 再利用待ちのEntityのリスト
	std::vector<size_t> m_vRecycleEntities;
	// 次に発行するCompTypeID
	static size_t m_nextCcompTypeID;
public:

};

// スタティック変数の初期化
size_t ECSManager::m_nextCcompTypeID = 0;
template<typename CompType>
size_t ECSManager::ComponentPool<CompType>::m_compTypeID = 0;

おわりに

今回の記事では、メモリの基本的な仕様とそれを活かしたパフォーマンス向上、基本的なECSについて解説しました。
皆さんもECSを使ってオブジェクトを沢山生成してみて下さい!

神戸電子専門学校ゲーム技術研究部

Discussion