2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C++】enttライブラリで実務レベルのECSを

2
Posted at

前回記事ではECS/Entity Component SystemのメリットやC++での実現方法について紹介しました。

実際の開発現場でECSを使用する場合、イベントシステム効率的なイテレーションSystem の設計など、より実務的な観点を考慮する必要があります。

本記事ではEnttというライブラリを使用して、実務で通用するECSの構築方法を解説します。

前提知識

  • C++の基礎文法を知っている
  • ECSを知っている(知らない方は前回記事をご参照下さい)

Enttとは

Entt/enttは、C++向けの高速・軽量・柔軟の三拍子がそろったECSライブラリです。

自前のでECSを実装するよりも安全で高速なため、ゲームエンジンやリアルタイムアプリケーションで広く使用されています。

EnTTライブラリのリポジトリ👇

導入方法

EnTTライブラリはヘッダーオンリーですので非常に簡単に導入が可能です。
特に特殊なこともないので、以下の記事をご参照ください。
余談ですが公開しているサンプルリポジトリはサブモジュールで管理を行っています。

ヘッダーオンリーライブラリの導入方法👇

サブモジュールでの管理方法👇

公開リポジトリでのプロジェクトから見たインクルードパスは$(ProjectDir)..\External\entt\single_includeとなっております。

スクリーンショット (322).png

サンプルコード

本記事のサンプルコードはGithubにてすべて公開しております。
最後の方のサンプルコードは長くなりすぎるため、重要な点のみを抜き出して紹介しますので、適宜リポジトリの方をご参照ください。

最も単純なECSサンプル

main.cpp
//--------------------------------------------------------
//! @file	main.cpp
//! @brief	EnTTの基本的な使い方を示すサンプルコード
//! @author つきの
//--------------------------------------------------------
#include <iostream>
#include <entt/entt.hpp>
//--------------------------------------------------------
//! @struct	Position
//! @brief	位置を表すコンポーネント
//--------------------------------------------------------
struct Position {
	float x;
	float y;
};

//--------------------------------------------------------
//! @struct	Velocity
//! @brief	速度を表すコンポーネント
//--------------------------------------------------------
struct Velocity {
	float x;
	float y;
};

// エントリポイント
int main() {
	// Registry を作成
	entt::registry registry;

	// Entity を作成
	entt::entity entity = registry.create();

	// Component を付与
	registry.emplace<Position>(entity, 0.0f, 0.0f);
	registry.emplace<Velocity>(entity, 1.0f, 0.5f);

	// Position と Velocity を持つ Entity を走査
	const auto view = registry.view<Position, Velocity>();

	// Entity を更新
	view.each([](auto e, auto& pos, auto& vel) {
		// 位置を更新
		pos.x += vel.x;
		pos.y += vel.y;

		// 更新後の位置を表示
		std::cout << "Entity " << static_cast<int>(e)
			<< " Position : (" << pos.x << ", " << pos.y << ")\n";
		});

	// プログラムの終了
	return 0;
}

Entityの作成方法

entt::registry内のcreate()関数を呼ぶことで、Entityを生成することが出来ます。
EnTTでも、EntityはあくまでただのIDであり、データやふるまいを持ちません。

main.cpp
	entt::entity entity = registry.create();

Componentの付与方法

entt::registry内のemplace<T>()関数を呼ぶことで、EntityComponentを付与することが出来ます。

引数 説明
entity entt::entity コンポーネントを追加する対象のエンティティ
0.0f float Position.x の初期値
0.0f float Position.y の初期値

第二引数以降はTに渡される引数となっています。

main.cpp
	registry.emplace<Position>(entity, 0.0f, 0.0f);

特定のComponentを持つEntityを取得

entt::registry内のview<>()関数を呼ぶことで、テンプレート内のすべてのComponentを持つすべてのEntityが取得できます。

main.cpp
	const auto view = registry.view<Position, Velocity>();

viewからComponentに処理を加える

view内のeach()関数を使用することで、Componentに対して処理を行うことが出来ます。

each()には処理を行うラムダ式を渡します。

ラムダ式の引数は以下の通りです。

引数 説明
entity entt::entity 対象エンティティの ID。省略可能(受け取らない書き方もできる)。
component1 T1& または const T1& view<...> に指定した最初のコンポーネント。参照で渡される。
component2 T2& または const T2& 2 つ目のコンポーネント。参照で渡される。
componentN TN& または const TN& N 番目のコンポーネント。参照で渡される。

第二引数からは、Componentの分だけ順番に渡していきます。

Systemを関数で作るサンプル

main.cpp
//--------------------------------------------------------
//! @file   main.cpp
//! @brief  EnTTでSystemを設計する最小サンプル
//! @author つきの
//--------------------------------------------------------
#include <iostream>
#include <entt/entt.hpp>

//--------------------------------------------------------
//! @struct Position
//! @brief 位置を表すコンポーネント
//--------------------------------------------------------
struct Position {
	float x;
	float y;
};

//--------------------------------------------------------
//! @struct Velocity
//! @brief  速度を表すコンポーネント
//--------------------------------------------------------
struct Velocity {
	float x;
	float y;
};

//--------------------------------------------------------
//! @brief	 位置更新システム
//! @param	 registry [in,out] EnTTのレジストリ
//! @details Position と Velocity を持つ Entity の位置を更新する
//--------------------------------------------------------
void UpdatePositionSystem(entt::registry& registry) {
	// Position と Velocity を持つ Entity を走査
	const auto view = registry.view<Position, Velocity>();
	// Entity を更新
	view.each([](auto entity, Position& pos, const Velocity& vel) {
		pos.x += vel.x;
		pos.y += vel.y;
		// 更新後の位置を表示
		std::cout << "Entity " << static_cast<int>(entity)
			<< " Position : (" << pos.x << ", " << pos.y << ")\n";
		});
}

// エントリポイント
int main() {
	// Registry を作成
	entt::registry registry;

	// Entity を作成
	entt::entity entity = registry.create();
	
	// Component を付与
	registry.emplace<Position>(entity, 0.0f, 0.0f);
	registry.emplace<Velocity>(entity, 1.0f, 0.5f);

	// 位置更新システムを実行
	UpdatePositionSystem(registry);

	// プログラムの終了
	return 0;
}

Systemの組み込み

entt::registryを引数に渡すことで、内部でviewを呼んで処理を施すことで、責務分離がさらに美しくなります。

main.cpp
//--------------------------------------------------------
//! @brief	 位置更新システム
//! @param	 registry [in,out] EnTTのレジストリ
//! @details Position と Velocity を持つ Entity の位置を更新する
//--------------------------------------------------------
void UpdatePositionSystem(entt::registry& registry) {
	// Position と Velocity を持つ Entity を走査
	const auto view = registry.view<Position, Velocity>();
	// Entity を更新
	view.each([](auto entity, Position& pos, const Velocity& vel) {
		pos.x += vel.x;
		pos.y += vel.y;
		// 更新後の位置を表示
		std::cout << "Entity " << static_cast<int>(entity)
			<< " Position : (" << pos.x << ", " << pos.y << ")\n";
		});
}

GameLoopサンプル

main.cpp
//--------------------------------------------------------
//! @file   main.cpp
//! @brief  EnTTでGameLoopを構築する最小サンプル
//! @author つきの
//--------------------------------------------------------
#include <iostream>
#include <entt/entt.hpp>
#include <chrono>
#include <thread>

//--------------------------------------------------------
//! @struct Position
//! @brief 位置を表すコンポーネント
//--------------------------------------------------------
struct Position {
	float x;
	float y;
};

//--------------------------------------------------------
//! @struct Velocity
//! @brief 速度を表すコンポーネント
//--------------------------------------------------------
struct Velocity {
	float x;
	float y;
};

//--------------------------------------------------------
//! @brief   位置更新システム
//! @param   registry [in,out] EnTTのレジストリ
//! @details Position と Velocity を持つ Entity の位置を更新する
//--------------------------------------------------------
void UpdatePositionSystem(entt::registry& registry) {
	// Position と Velocity を持つ Entity を走査
	const auto view = registry.view<Position, Velocity>();
	// Entity を更新
	view.each([](auto entity, Position& pos, const Velocity& vel) {
		pos.x += vel.x;
		pos.y += vel.y;
		// 更新後の位置を表示
		std::cout << "Entity " << static_cast<int>(entity)
			<< " Position : (" << pos.x << ", " << pos.y << ")\n";
		});
}

//--------------------------------------------------------
//! @brief GameLoop
//! @param registry [in,out] EnTTのレジストリ
//! @details 毎フレームSystemを実行する
//--------------------------------------------------------
void GameLoop(entt::registry& registry) {
	// この関数内でのみ時間リテラルを有効にする
	using namespace std::chrono_literals;
	// 簡易的なゲームループ(5フレーム分)
	for (int frame = 0; frame < 5; ++frame) {
		std::cout << "---- Frame " << frame << " ----\n";

		// System を実行
		UpdatePositionSystem(registry);

		// 簡易的なフレーム待機(60fps 相当)
		std::this_thread::sleep_for(16ms);
	}
}

// エントリポイント
int main() {
	// Registry を作成
	entt::registry registry;

	// Entity を作成
	entt::entity entity = registry.create();
	registry.emplace<Position>(entity, 0.0f, 0.0f);
	registry.emplace<Velocity>(entity, 1.0f, 0.5f);

	// GameLoopを疑似的に行う
	GameLoop(registry);

	// プログラムの終了
	return 0;
}

フレーム待機

std::this_thread::空間のsleep_for()関数を呼ぶことで、スレッド(GameLoop)を停止させることができます。
一秒が1000msで、60fpsの場合の1フレームの長さは

1000 ÷ 60 = 16.666....

ということで、本記事では16ms間停止させます。

main.cpp
        // 簡易的なフレーム待機(60fps 相当)
		std::this_thread::sleep_for(16ms);

システムのクラス化サンプル

main.cpp
//--------------------------------------------------------
//! @file   main.cpp
//! @brief  System をクラス化した最小 ECS サンプル(複数 System 版)
//! @author つきの
//--------------------------------------------------------
#include <iostream>
#include <vector>
#include <entt/entt.hpp>
#include <chrono>
#include <thread>

//--------------------------------------------------------
//! @struct Position
//! @brief  位置を表すコンポーネント
//--------------------------------------------------------
struct Position {
	float x;
	float y;
};

//--------------------------------------------------------
//! @struct Velocity
//! @brief  速度を表すコンポーネント
//--------------------------------------------------------
struct Velocity {
	float x;
	float y;
};

//--------------------------------------------------------
//! @class ISystem
//! @brief 全ての System の基底クラス
//--------------------------------------------------------
class ISystem {
public:
	//--------------------------------------------------------
	//! @brief デストラクタ
	//--------------------------------------------------------
	virtual ~ISystem() = default;

	//--------------------------------------------------------
	//! @brief System の更新処理
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	virtual void Update(entt::registry& registry) = 0;
};

//--------------------------------------------------------
//! @class MovementSystem
//! @brief Position と Velocity を使って位置を更新する System
//--------------------------------------------------------
class MovementSystem : public ISystem {
public:
	//--------------------------------------------------------
	//! @brief 移動処理の更新
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	void Update(entt::registry& registry) override {
		// PositionとVelocityを持つEntity を取得
		const auto view = registry.view<Position, Velocity>();

		// 各Entityの位置を速度分だけ更新
		view.each([](auto entity, Position& pos, const Velocity& vel) {
			pos.x += vel.x;
			pos.y += vel.y;
			});
	}
};

//--------------------------------------------------------
//! @class LogSystem
//! @brief Position を使って位置をログ出力する System
//--------------------------------------------------------
class LogSystem : public ISystem {
public:
	//--------------------------------------------------------
	//! @brief ログ出力処理の更新
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	void Update(entt::registry& registry) override {
		// Positionを持つEntity を取得
		const auto view = registry.view<Position>();

		// 各Entityの位置をログ出力
		view.each([](auto entity, const Position& pos) {
			std::cout << "Entity " << static_cast<int>(entity)
				<< " Render Position : (" << pos.x << ", " << pos.y << ")\n";
			});
	}
};

//--------------------------------------------------------
//! @class GameLoop
//! @brief System をまとめて実行するループ
//--------------------------------------------------------
class GameLoop {
public:
	//--------------------------------------------------------
	//! @brief System を追加
	//! @param system [in] 追加する System のポインタ
	//--------------------------------------------------------
	void AddSystem(ISystem* system) {
		systems_.push_back(system);
	}

	//--------------------------------------------------------
	//! @brief GameLoop の実行
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	void Run(entt::registry& registry) {
		// 時間リテラルを使用するためのusing宣言
		using namespace std::chrono_literals;

		// 簡単なループでSystemを更新
		for (int frame = 0; frame < 5; ++frame) {
			std::cout << "---- Frame " << frame << " ----\n";

			// 各Systemを更新
			for (auto* system : systems_) {
				system->Update(registry);
			}

			// フレームレートを制限するために少し待つ
			std::this_thread::sleep_for(16ms);
		}
	}

private:
	std::vector<ISystem*> systems_; // System のリスト
};

// エントリポイント
int main() {
	// EnTT のレジストリを作成
	entt::registry registry;

	// Entity 作成
	auto entity = registry.create();
	registry.emplace<Position>(entity, 0.0f, 0.0f);
	registry.emplace<Velocity>(entity, 1.0f, 0.5f);

	// GameLoop と System の準備
	GameLoop loop;
	MovementSystem movement;
	LogSystem render;

	// SystemをGameLoopに追加
	loop.AddSystem(&movement);
	loop.AddSystem(&render);

	// 実行
	loop.Run(registry);

	//プログラムの終了
	return 0;
}

システムの抽象クラス

Update()関数を純粋仮想関数として持つことで、継承したクラスのUpdate()アップキャストでまとめて処理を行うことができます。

main.cpp
//--------------------------------------------------------
//! @class ISystem
//! @brief 全ての System の基底クラス
//--------------------------------------------------------
class ISystem {
public:
	//--------------------------------------------------------
	//! @brief デストラクタ
	//--------------------------------------------------------
	virtual ~ISystem() = default;

	//--------------------------------------------------------
	//! @brief System の更新処理
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	virtual void Update(entt::registry& registry) = 0;
};
main.cpp
//--------------------------------------------------------
//! @class GameLoop
//! @brief System をまとめて実行するループ
//--------------------------------------------------------
class GameLoop {
public:
	//--------------------------------------------------------
	//! @brief System を追加
	//! @param system [in] 追加する System のポインタ
	//--------------------------------------------------------
	void AddSystem(ISystem* system) {
		systems_.push_back(system);
	}

	//--------------------------------------------------------
	//! @brief GameLoop の実行
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	void Run(entt::registry& registry) {
		// 時間リテラルを使用するためのusing宣言
		using namespace std::chrono_literals;

		// 簡単なループでSystemを更新
		for (int frame = 0; frame < 5; ++frame) {
			std::cout << "---- Frame " << frame << " ----\n";

			// 各Systemを更新
			for (auto* system : systems_) {
				system->Update(registry);
			}

			// フレームレートを制限するために少し待つ
			std::this_thread::sleep_for(16ms);
		}
	}

private:
	std::vector<ISystem*> systems_; // System のリスト
};

ファイルの分割サンプル

04のサンプルをファイル分割したものです。
実際の開発ではpch.hなどを入れたり、エンジン層を作ったりといったことを行います。

私の方針としましては、#includeを行うヘッダーファイルincludeディレクトリへ入れ、プロパティ追加のインクルードディレクトリに入れる手法を推奨します。

File.png

スクリーンショット (2).png

システムマネージャーの実装サンプル

prioritysystemの構造体を作ることで、マネージャー追加時にソートを行い、システムの優先順位を指定できます。

UpdateAll()SystemManagerが持つことで、責務分離も明確になります。

SystemManager.hpp
//--------------------------------------------------------
//! @file   SystemManager.hpp
//! @brief  Systemの登録・実行を管理するクラス定義
//! @author つきの
//--------------------------------------------------------
#pragma once
#include <vector>
#include <algorithm>
#include <entt/entt.hpp>
class ISystem;	// 前方宣言
//--------------------------------------------------------
//! @class SystemManager
//! @brief System の登録と実行順序(優先度)を管理するクラス
//--------------------------------------------------------
class SystemManager {
public:
	//--------------------------------------------------------
	// System を登録する
	//! @param system   [in] 登録する System
	//! @param priority [in] 実行優先度(小さいほど先に実行)
	//--------------------------------------------------------
	void AddSystem(ISystem* system, int priority);

	//--------------------------------------------------------
	// 登録された System を優先度順に実行する
	//! @param registry [in,out] EnTT のレジストリ
	//--------------------------------------------------------
	void UpdateAll(entt::registry& registry);

private:
	//--------------------------------------------------------
	//! @struct Systemと優先度をまとめた構造体
	//--------------------------------------------------------
	struct SystemEntry {
		int priority;		// 実行優先度
		ISystem* system;	// System のポインタ
	};

	std::vector<SystemEntry> systems_;	// 登録された System とその優先度のリスト
};
SystemManager.cpp
//--------------------------------------------------------
//! @file   SystemManager.cpp
//! @brief  SystemManagerクラスの実装
//! @author つきの
//--------------------------------------------------------
#include <Core/SystemManager.hpp>
#include <Systems/ISystem.hpp>
//--------------------------------------------------------
//! @brief Systemを追加し、優先度順にソートする
//--------------------------------------------------------
void SystemManager::AddSystem(ISystem* system, int priority) {
	// System と優先度をリストに追加
	systems_.push_back({ priority, system });

	// 優先度の昇順でソート
	std::sort(systems_.begin(), systems_.end(),
		[](const SystemEntry& a, const SystemEntry& b) {
			return a.priority < b.priority;
		});
}

//--------------------------------------------------------
//! @brief 登録されたSystemを優先度順に実行
//--------------------------------------------------------
void SystemManager::UpdateAll(entt::registry& registry) {
	// 優先度順に System を実行
	for (auto& entry : systems_) {
		entry.system->Update(registry);
	}
}

DeltaTimeの実装サンプル

過去にDeltaTimeの仕組みと取得法について解説しているので、実装方法の詳細はそちらをご覧ください。

GameLoop.cpp
//--------------------------------------------------------
//! @file   GameLoop.cpp
//! @brief  GameLoop クラスの実装
//! @author つきの
//--------------------------------------------------------
#include <Core/GameLoop.hpp>
#include <chrono>
#include <thread>
#include <iostream>
//--------------------------------------------------------
//! @brief ゲームループの実行を行う関数
//--------------------------------------------------------
void GameLoop::Run(entt::registry& registry, SystemManager& systemManager) {
	// 時間リテラルを使用するための名前空間
	using namespace std::chrono_literals;

	// 長いので型エイリアスを使用して簡略化
	using clock = std::chrono::steady_clock;
	// ゲームループ開始前の時間を記録
	auto last = clock::now();

	// 簡単なゲームループ(5フレーム分だけ)
	for (int frame = 0; frame < 5; ++frame) {
		//--------------------------------------------------------
		// ここで deltaTime を計算する
		//--------------------------------------------------------
		auto now = clock::now();	// 現在の時間を取得
		float deltaTime = std::chrono::duration<float>(now - last).count();
		last = now;	// 現在の時間を last に保存

		// デバッグ用に deltaTime を表示
		std::cout << "---- Frame " << frame << " (dt=" << deltaTime << ") ----\n";

		// SystemManager に実行を委譲
		systemManager.UpdateAll(registry, deltaTime);

		// フレームレートを制限するために少し待つ(例: 60 FPS なら約16ms)
		std::this_thread::sleep_for(16ms);
	}
}

移動処理にDeltaTimeを乗算することで、フレームレートに左右されない移動処理を行うことができます。

MovementSystem.cpp
//--------------------------------------------------------
//! @file	MovementSystem.cpp
//! @brief	座標を更新するSystemのクラス定義
//! @author つきの
//--------------------------------------------------------
#include <Systems/MovementSystem.hpp>
#include <Components/Position.hpp>
#include <Components/Velocity.hpp>
//--------------------------------------------------------
//! @brief 移動処理の更新
//--------------------------------------------------------
void MovementSystem::Update(entt::registry& registry, float deltaTime) {
	// PositionとVelocityを持つEntity を取得
	const auto view = registry.view<Position, Velocity>();

	// 各Entityの位置を速度分だけ更新
	view.each([deltaTime](auto entity, Position& pos, const Velocity& vel) {
		pos.x += vel.x * deltaTime;
		pos.y += vel.y * deltaTime;
		});
}

Transformと階層構造実装サンプル

Transform.hpp
//--------------------------------------------------------
//! @file	Transform.hpp
//! @brief	2DTransform(位置・回転・スケール + 親子関係)
//! @author つきの
//--------------------------------------------------------
#pragma once
#include <vector>
//--------------------------------------------------------
//! @struct Transform
//! @brief 2DTransform(位置・回転・スケール + 親子関係)
//--------------------------------------------------------
struct Transform {
	// ローカル座標系 
	float x = 0.0f;		   // ローカルx座標
	float y = 0.0f;		   // ローカルy座標
	float rotation = 0.0f; // ラジアン
	float scaleX = 1.0f;   // ローカルxスケール
	float scaleY = 1.0f;   // ローカルyスケール

	// 親子関係 
	Transform* parent = nullptr;		// 親Transformへのポインタ
	std::vector<Transform*> children;	// 子Transformへのポインタリスト

	// ワールド変換キャッシュ
	bool dirty = true;			// ワールド変換が最新かどうかのフラグ
	float worldX = 0.0f;		// ワールドx座標
	float worldY = 0.0f;		// ワールドy座標
	float worldRotation = 0.0f;	// ワールド回転
	float worldScaleX = 1.0f;	// ワールドxスケール
	float worldScaleY = 1.0f;	// ワールドyスケール

	//--------------------------------------------------------
	//! @brief 親を設定する(子リストへの登録も行う)
	//! @param newParent 新しい親Transformへのポインタ
	//--------------------------------------------------------
	void SetParent(Transform* newParent) {
		// 既存の親から子リストを削除
		parent = newParent;

		// 新しい親の子リストに自身を追加
		if (parent) {
			parent->children.push_back(this);
		}

		// ワールド変換を更新する必要がある
		dirty = true;
	}

	//--------------------------------------------------------
	//! @brief 自身と子の worldTransform を更新する(再帰)
	//--------------------------------------------------------
	void UpdateWorldTransform() {
		if (parent) {
			// 親の world を継承
			worldX = parent->worldX + x;
			worldY = parent->worldY + y;
			worldRotation = parent->worldRotation + rotation;
			worldScaleX = parent->worldScaleX * scaleX;
			worldScaleY = parent->worldScaleY * scaleY;
		}
		else {
			// 親がいない場合は local = world
			worldX = x;
			worldY = y;
			worldRotation = rotation;
			worldScaleX = scaleX;
			worldScaleY = scaleY;
		}

		dirty = false;

		// 子も再帰的に更新
		for (auto* child : children) {
			// 子のワールド変換も更新する必要がある
			child->dirty = true;
			// 子のワールド変換を更新
			child->UpdateWorldTransform();
		}
	}
};

親子関係

SetParent()で親のポインタを渡すことで、Transform内部に親子関係を持つことができます。

OOPではCompositeパターンなどでGamaObjectが階層構造を持つことが多いですが、ECSではTransformが階層を持つという方式が一般的です。

他のComponentで階層を確認したい場合は、viewTransformをテンプレートに入れれば参照することが可能です。

Transform.hpp
	//--------------------------------------------------------
	//! @brief 親を設定する(子リストへの登録も行う)
	//! @param newParent 新しい親Transformへのポインタ
	//--------------------------------------------------------
	void SetParent(Transform* newParent) {
		// 既存の親から子リストを削除
		parent = newParent;

		// 新しい親の子リストに自身を追加
		if (parent) {
			parent->children.push_back(this);
		}

		// ワールド変換を更新する必要がある
		dirty = true;
	}

親への再帰的な追従

ゲームオブジェクトが階層構造(親子関係)を持つ場合、 子オブジェクトのワールド座標は親のワールド座標を基準に計算する必要があります。

親の位置を基準に、子のローカル位置を足し合わせることで、子のワールド座標を計算することができます。

dirtyフラグがtrueなら更新が必要、falseであれば更新済みとすることで、無駄な計算を避けることができます。

Transform.hpp
	//--------------------------------------------------------
	//! @brief 自身と子の worldTransform を更新する(再帰)
	//--------------------------------------------------------
	void UpdateWorldTransform() {
		if (parent) {
			// 親の world を継承
			worldX = parent->worldX + x;
			worldY = parent->worldY + y;
			worldRotation = parent->worldRotation + rotation;
			worldScaleX = parent->worldScaleX * scaleX;
			worldScaleY = parent->worldScaleY * scaleY;
		}
		else {
			// 親がいない場合は local = world
			worldX = x;
			worldY = y;
			worldRotation = rotation;
			worldScaleX = scaleX;
			worldScaleY = scaleY;
		}

		dirty = false;

		// 子も再帰的に更新
		for (auto* child : children) {
			// 子のワールド変換も更新する必要がある
			child->dirty = true;
			// 子のワールド変換を更新
			child->UpdateWorldTransform();
		}
	}

UpdateWorldTransform()関数内で子オブジェクトのUpdateWorldTransform()を呼ぶことで、再帰的にすべての子Transformの位置が漏れなく更新されます。

Transform.cpp
        // 子も再帰的に更新
		for (auto* child : children) {
			// 子のワールド変換も更新する必要がある
			child->dirty = true;
			// 子のワールド変換を更新
			child->UpdateWorldTransform();
		}

イベント駆動ECSの実装サンプル

本実装ではsubscribe()で受け取るコールバック関数を指定、publish()関数で処理をキューに積み、dispatch()でキューに積まれたイベントを発火させています。

即時発火ではなくdispatch()方式にすることで、フレームの最後に発火させるまでComponentの状態が安定します。

また、Systemの疎結合を維持することもできます。

EventBus.hpp
//--------------------------------------------------------
//! @file   EventBus.hpp
//! @brief  シンプルなイベントバス(publish / subscribe / dispatch)
//! @author つきの
//--------------------------------------------------------
#pragma once
#include <unordered_map>
#include <vector>
#include <functional>
#include <typeindex>
#include <any>
//--------------------------------------------------------
//! @class EventBus
//! @brief シンプルなイベントバス(publish / subscribe / dispatch)
//--------------------------------------------------------
class EventBus {
public:
	//--------------------------------------------------------
	//! @brief  Event を発行(キューに積む)
	//! @tparam Event 発行するイベントの型
	//! @param  event [in] 発行するイベントのインスタンス
	//--------------------------------------------------------
	template<typename Event>
	void publish(const Event event) {
		// イベントを std::any に変換してキューに積む
		events_[typeid(Event)].push_back(event);
	}

	//--------------------------------------------------------
	//! @brief Event を購読(コールバック登録)
	//! @tparam Event 購読するイベントの型
	//! @param  callback [in] イベントを受け取るコールバック関数(引数は const Event&)
	//--------------------------------------------------------
	template<typename Event>
	void subscribe(std::function<void(const Event&)> callback) {
		// コールバックを std::function<void(const std::any&)> に変換して登録
		auto& vec = subscribers_[typeid(Event)];

		// 登録されたコールバックは std::anyをEventにキャストして呼び出す
		vec.push_back([callback](const std::any& e) {
			callback(std::any_cast<const Event&>(e));
			});
	}

	//--------------------------------------------------------
	//! @brief 1フレーム分のEventを配信
	//--------------------------------------------------------
	void dispatch() {
		//--------------------------------------------------------
		// イベントキューを走査して、登録されたコールバックを呼び出す
		//--------------------------------------------------------
		for (auto& [type, eventList] : events_) {
			// イベントの型に対応する購読者リストを取得
			auto subIt = subscribers_.find(type);
			// 購読者がいない場合はスキップ
			if (subIt == subscribers_.end()) continue;
			// 各イベントに対して、登録されたコールバックを呼び出す
			for (auto& e : eventList) {
				// 登録されたコールバックを呼び出す
				for (auto& callback : subIt->second) {
					callback(e);
				}
			}
		}
		// 配信後はイベントキューをクリア
		events_.clear();
	}

private:
	// Event キュー
	std::unordered_map<std::type_index, std::vector<std::any>> events_;

	// 購読者リスト
	std::unordered_map<std::type_index,
		std::vector<std::function<void(const std::any&)>>> subscribers_;
};

EventSystem

EventSystem.hpp
//--------------------------------------------------------
//! @file   EventSystem.hpp
//! @brief  EventBus.dispatch() を呼ぶだけの System
//! @author つきの
//--------------------------------------------------------
#pragma once
#include <Events/EventBus.hpp>
//--------------------------------------------------------
//! @class EventSystem
//! @brief EventBus.dispatch() を呼ぶだけの System
//--------------------------------------------------------
class EventSystem {
public:
	//--------------------------------------------------------
	//! @brief コンストラクタ
	//! @param bus [in] EventBusのインスタンスへの参照
	//--------------------------------------------------------
	EventSystem(EventBus& bus) : bus_(bus) {}

	//--------------------------------------------------------
	//! @brief EventBus.dispatch() を呼ぶだけの更新処理
	//! @param registry [in,out] EnTT のレジストリ(使用しない)
	//--------------------------------------------------------
	void Update() {
		bus_.dispatch();
	}

private:
	EventBus& bus_;
};
GameLoop.cpp
    // 簡単なゲームループ(5フレーム分だけ)
    for (int frame = 0; frame < 5; ++frame) {
	//--------------------------------------------------------
	// ここで deltaTime を計算する
	//--------------------------------------------------------
	auto now = clock::now();	// 現在の時間を取得
	float deltaTime = std::chrono::duration<float>(now - last).count();
	last = now;	// 現在の時間を last に保存

	// デバッグ用に deltaTime を表示
	std::cout << "---- Frame " << frame << " (dt=" << deltaTime << ") ----\n";

	// SystemManager に実行を委譲
	systemManager.UpdateAll(registry, deltaTime);
	eventSystem.Update(); // dispatch

	// フレームレートを制限するために少し待つ(例: 60 FPS なら約16ms)
	std::this_thread::sleep_for(16ms);
}

SystemManagerを更新した後にEventSystemUpdate内で分離して発火させることで、イベントの一斉発火が最後に行われることを保証します。

総括

  • EnTTライブラリを使用することで、効率的で高速なキャッシュへのアクセスが、わかりやすいAPIで使用することができる
  • SystemManagerEventBusで責務分離を行っていくことで実務レベルのECS設計を行うことができる
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?