Skip to content

Instantly share code, notes, and snippets.

@jacky860226
Created March 7, 2026 07:14
Show Gist options
  • Select an option

  • Save jacky860226/43618d78c28aa3ad7ad786c1d284463c to your computer and use it in GitHub Desktop.

Select an option

Save jacky860226/43618d78c28aa3ad7ad786c1d284463c to your computer and use it in GitHub Desktop.
Entity Component System (ECS)
#include <algorithm>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cstdint>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <memory>
#include <tuple>
#include <typeindex>
#include <unordered_map>
#include <vector>
// TBB Detection
#if __has_include(<tbb/parallel_for.h>) && __has_include(<tbb/blocked_range.h>)
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
#define ECS_USE_TBB
#endif
/**
* @brief 實體識別符配置模板。
* 允許透過模板參數控制 ID 和 Version 的位元數。
*
* @tparam Type 底層整數類型 (如 uint64_t, uint32_t)。
* @tparam IdBits ID 使用的位元數。
*/
template<typename Type, std::size_t IdBits>
struct EntityIdentifier {
using entity_type = Type;
using id_type = Type;
using version_type = Type;
static constexpr std::size_t id_bits = IdBits;
static constexpr std::size_t version_bits = sizeof(Type) * 8 - IdBits;
static_assert(id_bits > 0 && version_bits > 0, "ID and Version bits must be positive");
static constexpr Type id_mask = (Type(1) << id_bits) - 1;
static constexpr Type version_mask = (Type(1) << version_bits) - 1;
static constexpr std::size_t version_shift = id_bits;
static id_type to_id(entity_type e) { return e & id_mask; }
static version_type to_version(entity_type e) { return (e >> version_shift) & version_mask; }
static entity_type combine(id_type id, version_type version) {
return (id & id_mask) | ((version & version_mask) << version_shift);
}
};
// 使用 64 位元整數:40 位元用於 ID,24 位元用於版本 (Version)
using DefaultTraits = EntityIdentifier<uint64_t, 40>;
using Entity = DefaultTraits::entity_type;
using IDType = DefaultTraits::id_type;
using VersionType = DefaultTraits::version_type;
using EntityTraits = DefaultTraits;
constexpr Entity NullEntity = std::numeric_limits<Entity>::max();
/**
* @brief 使用觀察者模式 (Observer Pattern) 的簡單訊號 (Signal) 實作。
* @tparam Args 訊號傳遞的參數類型。
*/
template <typename... Args>
class Signal {
using Listener = std::function<void(Args...)>;
std::vector<Listener> listeners;
public:
/**
* @brief 連接監聽器 (Listener) 到訊號。
* @param listener 回呼函式。
*/
void connect(Listener listener) { listeners.push_back(listener); }
/**
* @brief 發布訊號給所有連接的監聽器。
* @param args 傳遞給監聽器的參數。
*/
void publish(Args... args) {
for (auto& listener : listeners) {
listener(args...);
}
}
};
/**
* @brief 儲存區的抽象基底類別 (Type Erasure)。
* 允許 Registry 透過基底指標管理不同類型的 Component 儲存區。
*/
class Registry; // Forward declaration
struct BaseStorage {
virtual ~BaseStorage() = default;
virtual void remove(Registry& registry, Entity e) = 0;
virtual bool has(Entity e) const = 0;
virtual size_t size() const = 0;
virtual const std::vector<Entity>& base_entities() const = 0;
};
/**
* @brief 稀疏集合 (Sparse Set) 實作。
* 這是 ECS 的核心資料結構,提供 O(1) 的存取、新增和刪除操作。
*
* @tparam T Component 的類型。
*/
template <typename T>
class SparseSet : public BaseStorage {
private:
std::vector<Entity> packed;
std::vector<T> payload;
/// 使用 deque 避免 resize 時的大量 copy
std::deque<size_t> sparse;
// Signals
Signal<Registry&, Entity> construction_signal;
Signal<Registry&, Entity> destruction_signal;
Signal<Registry&, Entity> update_signal;
public:
// Signal Accessors
auto& on_construct() { return construction_signal; }
auto& on_destroy() { return destruction_signal; }
auto& on_update() { return update_signal; }
bool has(Entity entity) const override {
IDType id = EntityTraits::to_id(entity);
return id < sparse.size() &&
sparse[id] != std::numeric_limits<size_t>::max() &&
packed[sparse[id]] == entity; // Version check
}
size_t index(Entity entity) const {
IDType id = EntityTraits::to_id(entity);
return (id < sparse.size()) ? sparse[id]
: std::numeric_limits<size_t>::max();
}
size_t size() const override { return packed.size(); }
const std::vector<Entity>& base_entities() const override { return packed; }
/**
* @brief 提供直接存取 payload 的介面,供 Group View 使用。
* @return payload 的參考。
*/
std::vector<T>& data() { return payload; }
/**
* @brief 建構並新增 Component 到實體。
* 注意:需要 Registry 參考來觸發 Signal。
*
* @param registry Registry 實例。
* @param entity 目標實體。
* @param component 要新增的 Component。
*/
void emplace(Registry& registry, Entity entity, T component) {
IDType id = EntityTraits::to_id(entity);
if (id >= sparse.size()) {
sparse.resize(id + 1, std::numeric_limits<size_t>::max());
}
size_t index = packed.size();
packed.push_back(entity);
payload.push_back(component);
sparse[id] = index;
// Trigger Signal
construction_signal.publish(registry, entity);
}
/**
* @brief 更新 Component 並觸發 Update Signal。
*
* @tparam Func 更新函式的類型。
* @param registry Registry 實例。
* @param entity 目標實體。
* @param func 用於更新 Component 的函式。
*/
template <typename Func>
void patch(Registry& registry, Entity entity, Func func) {
assert(has(entity));
IDType id = EntityTraits::to_id(entity);
func(payload[sparse[id]]);
update_signal.publish(registry, entity);
}
T& get(Entity entity) {
assert(has(entity));
IDType id = EntityTraits::to_id(entity);
return payload[sparse[id]];
}
/**
* @brief [Group 實作關鍵] 交換兩個實體的位置。
* 讓外部 (Group) 可以調整 Entity 在 packed array 中的順序,以維持 Group 的連續性。
*
* @param lhs 左手邊實體。
* @param rhs 右手邊實體。
*/
void swap_entities(Entity lhs, Entity rhs) {
if (lhs == rhs) return;
IDType lhs_id = EntityTraits::to_id(lhs);
IDType rhs_id = EntityTraits::to_id(rhs);
size_t lhs_idx = sparse[lhs_id];
size_t rhs_idx = sparse[rhs_id];
std::swap(packed[lhs_idx], packed[rhs_idx]);
std::swap(payload[lhs_idx], payload[rhs_idx]);
sparse[lhs_id] = rhs_idx;
sparse[rhs_id] = lhs_idx;
}
/**
* @brief 移除實體的 Component。
* 注意:需要 Registry 參考來觸發 Signal。
*
* @param registry Registry 實例。
* @param entity 目標實體。
*/
void remove(Registry& registry, Entity entity) override {
if (!has(entity)) return;
// Trigger Signal BEFORE destruction
destruction_signal.publish(registry, entity);
IDType id = EntityTraits::to_id(entity);
size_t index_to_remove = sparse[id];
size_t last_index = packed.size() - 1;
Entity last_entity = packed[last_index];
IDType last_id = EntityTraits::to_id(last_entity);
std::swap(packed[index_to_remove], packed[last_index]);
std::swap(payload[index_to_remove], payload[last_index]);
sparse[last_id] = index_to_remove;
sparse[id] = std::numeric_limits<size_t>::max();
packed.pop_back();
payload.pop_back();
}
};
/**
* @brief Component 類型 ID 產生器 (Family)。
* 使用靜態計數器為每個 Component 類型產生唯一的執行期 ID。
*/
struct ComponentFamily {
static size_t identifier() {
static size_t value = 0;
return value++;
}
};
/**
* @brief 取得特定 Component 類型的唯一 ID。
* @tparam T Component 類型。
*/
template <typename T>
struct ComponentType {
static size_t id() {
static size_t value = ComponentFamily::identifier();
return value;
}
};
/**
* @brief Group 類型 ID 產生器。
*/
struct GroupFamily {
static size_t identifier() {
static size_t value = 0;
return value++;
}
};
/**
* @brief 取得特定 Group 類型的唯一 ID。
* @tparam Ts Group 包含的 Component 類型列表。
*/
template <typename... Ts>
struct GroupType {
static size_t id() {
static size_t value = GroupFamily::identifier();
return value;
}
};
/**
* @brief Group 的抽象基底類別。
* 用於在 Registry 中統一管理不同類型的 Group。
*/
struct BaseGroup {
virtual ~BaseGroup() = default;
/**
* @brief 當某個 Entity 新增了 Component 時呼叫。
* 檢查該 Entity 是否滿足 Group 的條件,若滿足則將其加入 Group。
* @param e 目標實體。
*/
virtual void on_emplace(Entity e) = 0;
/**
* @brief 當某個 Entity 即將移除 Component 或被銷毀時呼叫。
* 若該 Entity 在 Group 中,將其移除。
* @param e 目標實體。
*/
virtual void on_remove(Entity e) = 0;
/**
* @brief 檢查此 Group 是否包含特定的 Component ID。
* @param component_id Component 的類型 ID。
* @return 若 Group 擁有此 Component 則回傳 true。
*/
virtual bool owns(size_t component_id) const = 0;
};
/**
* @brief 實體組件系統 (ECS) 的核心註冊表 (Registry)。
* 管理所有實體、組件儲存區 (Pools) 和群組 (Groups)。
*
* @note 讀取操作是執行緒安全的,但寫入操作 (結構性變更) 不是。
*/
class Registry {
private:
std::vector<VersionType> versions;
std::vector<IDType> free_ids;
/// 使用 deque 取代 vector,避免 resize 時的 pointer invalidation 和大量 copy
std::deque<std::unique_ptr<BaseStorage>> pools;
/// 儲存所有活躍的 Group
std::deque<std::unique_ptr<BaseGroup>> groups;
/// 優化:Component ID -> 相關 Group 的列表 (Adjacency List)
/// 讓 emplace/remove 不用遍歷所有 Group,複雜度從 O(G) 降為 O(G_related)
std::vector<std::vector<BaseGroup*>> component_to_groups;
void ensure_component_groups(size_t type_id) {
if (type_id >= component_to_groups.size()) {
component_to_groups.resize(type_id + 1);
}
}
public:
/**
* @brief 建立一個新的實體。
* 若有回收的 ID 則重複使用,否則產生新的 ID。
* @return 新建立的實體。
*/
Entity create() {
IDType id;
if (free_ids.empty()) {
id = (IDType)versions.size();
versions.push_back(0);
} else {
id = free_ids.back();
free_ids.pop_back();
}
return EntityTraits::combine(id, versions[id]);
}
/**
* @brief 銷毀一個實體。
* 移除該實體的所有 Component,並回收其 ID。
*
* @param entity 要銷毀的實體。
*/
void destroy(Entity entity) {
IDType id = EntityTraits::to_id(entity);
if (id >= versions.size() ||
EntityTraits::to_version(entity) != versions[id])
return;
// 1. 實際從所有 Pool 中移除,並同時通知相關 Group
for (size_t type_id = 0; type_id < pools.size(); ++type_id) {
auto& storage = pools[type_id];
if (storage && storage->has(entity)) {
// A. 通知相關 Group (優化:只通知相關的 Group)
if (type_id < component_to_groups.size()) {
for (auto* group : component_to_groups[type_id]) {
group->on_remove(entity);
}
}
// B. 從 Storage 移除
storage->remove(*this, entity);
}
}
versions[id]++;
free_ids.push_back(id);
}
/**
* @brief 移除實體的特定 Component。
* @tparam T Component 類型。
* @param entity 目標實體。
*/
template <typename T>
void remove(Entity entity) {
// 1. 通知相關 Group (優化:只通知相關的 Group)
size_t type_id = ComponentType<T>::id();
if (type_id < component_to_groups.size()) {
for (auto* group : component_to_groups[type_id]) {
group->on_remove(entity);
}
}
// 2. 實際移除 (呼叫帶 Registry 的版本以觸發 Signal)
get_storage<T>().remove(*this, entity);
}
/**
* @brief 取得特定 Component 類型的儲存區 (Sparse Set)。
* 若儲存區不存在則自動建立。
*
* @tparam T Component 類型。
* @return 該類型的 SparseSet 參考。
*/
template <typename T>
SparseSet<T>& get_storage() {
size_t type_id = ComponentType<T>::id();
if (type_id >= pools.size()) {
pools.resize(type_id + 1);
}
if (!pools[type_id]) {
pools[type_id] = std::make_unique<SparseSet<T>>();
}
// 這裡的 static_cast 是編譯期轉換,沒有執行期開銷
// 但 unique_ptr 的解參考 (*) 仍有微小開銷
// 為了極致效能,我們可以快取 raw pointer
return static_cast<SparseSet<T>&>(*pools[type_id]);
}
/**
* @brief 為實體新增一個 Component。
* 若實體已有該 Component,行為未定義 (通常會覆蓋或 assert)。
*
* @tparam T Component 類型。
* @param entity 目標實體。
* @param component 要新增的 Component 資料。
*/
template <typename T>
void emplace(Entity entity, T component) {
// 呼叫帶 Registry 的版本以觸發 Signal
get_storage<T>().emplace(*this, entity, component);
// 通知相關 Group 進行檢查與排序 (優化:只通知相關的 Group)
size_t type_id = ComponentType<T>::id();
if (type_id < component_to_groups.size()) {
for (auto* group : component_to_groups[type_id]) {
group->on_emplace(entity);
}
}
}
/**
* @brief 更新 (Patch) 實體的 Component。
* 這會觸發 Update Signal,適合用於修改現有的 Component。
*
* @tparam T Component 類型。
* @tparam Func 更新函式類型。
* @param entity 目標實體。
* @param func 用於修改 Component 的函式。
*/
template <typename T, typename Func>
void patch(Entity entity, Func func) {
get_storage<T>().patch(*this, entity, func);
}
/**
* @brief 群組 (Group) 實作 - 侵入式且持久化。
* 1. Group 物件是持久存在的 (Persistent)。
* 2. 它是侵入式的 (Intrusive),在 emplace/remove 時自動維護排序。
* 3. 呼叫 group() 只是回傳已存在的 Group 物件,時間複雜度 O(1)。
*
* @tparam Components Group 包含的 Component 類型列表。
*/
template <typename... Components>
class Group : public BaseGroup {
size_t length = 0;
std::tuple<SparseSet<Components>*...> storages;
std::vector<size_t> component_ids;
public:
Group(SparseSet<Components>*... args)
: storages(args...),
component_ids{ComponentType<Components>::id()...} {}
size_t size() const { return length; }
bool owns(size_t id) const override {
for (size_t cid : component_ids)
if (cid == id) return true;
return false;
}
/**
* @brief 當 Entity 獲得新 Component 時呼叫。
* 檢查是否滿足 Group 條件,若滿足則將其交換到 Group 的有效範圍內。
*/
void on_emplace(Entity e) override {
bool has_all = true;
std::apply([&](auto*... args) { has_all = (... && args->has(e)); },
storages);
if (has_all) {
auto* leader = std::get<0>(storages);
if (leader->index(e) < length) return;
std::apply(
[&](auto*... args) {
(args->swap_entities(args->base_entities()[length], e),
...);
},
storages);
length++;
}
}
/**
* @brief 當 Entity 即將失去 Component 時呼叫。
* 若 Entity 在 Group 內,將其交換出 Group 的有效範圍。
*/
void on_remove(Entity e) override {
auto* leader = std::get<0>(storages);
if (!leader->has(e)) return;
if (leader->index(e) < length) {
std::apply(
[&](auto*... args) {
(args->swap_entities(args->base_entities()[length - 1],
e),
...);
},
storages);
length--;
}
}
/**
* @brief 迭代 Group 中的所有實體。
* 這是最快的迭代方式,因為資料在記憶體中是連續的。
*
* @tparam Func 迭代函式類型,格式為 [](Entity e, Components&... args) {}
* @param func 迭代函式。
*/
template <typename Func>
void for_each(Func func) {
auto& entities = std::get<0>(storages)->base_entities();
auto payloads = std::make_tuple(
&std::get<SparseSet<Components>*>(storages)->data()...);
for (size_t i = 0; i < length; ++i) {
std::apply(
[&](auto*... args) { func(entities[i], (*args)[i]...); },
payloads);
}
}
#ifdef ECS_USE_TBB
/**
* @brief 平行迭代 Group 中的所有實體 (使用 TBB)。
*
* @tparam Func 迭代函式類型。
* @param func 迭代函式。
*/
template <typename Func>
void parallel_for_each(Func func) {
auto& entities = std::get<0>(storages)->base_entities();
auto payloads = std::make_tuple(
&std::get<SparseSet<Components>*>(storages)->data()...);
tbb::parallel_for(tbb::blocked_range<size_t>(0, length),
[&](const tbb::blocked_range<size_t>& r) {
for (size_t i = r.begin(); i != r.end(); ++i) {
std::apply(
[&](auto*... args) { func(entities[i], (*args)[i]...); },
payloads);
}
}
);
}
#endif
/**
* @brief 設定 Group 的長度 (供 Registry 初始化使用)。
* @param len 新的長度。
*/
void set_length(size_t len) { length = len; }
};
/**
* @brief 取得或建立一個 Group。
* Group 是一個持久化的資料結構,維護擁有特定 Component 組合的實體列表。
*
* @tparam Components Group 包含的 Component 類型列表。
* @return Group 的參考。
*/
template <typename... Components>
Group<Components...>& group() {
// 1. 檢查是否已經存在 (使用 Static Type ID 查找)
size_t group_id = GroupType<Components...>::id();
// 確保 groups vector 足夠大
if (group_id >= groups.size()) {
groups.resize(group_id + 1);
}
// 如果已經存在,直接回傳
if (groups[group_id]) {
// 這裡我們確信它是正確的型別,因為 ID 是唯一的
return static_cast<Group<Components...>&>(*groups[group_id]);
}
// 2. 建立新 Group
auto new_group = std::make_unique<Group<Components...>>(
&get_storage<Components>()...);
auto* ptr = new_group.get();
// 3. 初始排序 (Full Sort)
auto* leader = &get_storage<
typename std::tuple_element<0, std::tuple<Components...>>::type>();
size_t current = 0;
for (size_t i = 0; i < leader->size(); ++i) {
Entity entity = leader->base_entities()[i];
bool has_all = true;
std::apply(
[&](auto*... args) { has_all = (... && args->has(entity)); },
std::make_tuple(&get_storage<Components>()...));
if (has_all) {
if (current != i) {
// Swap to current in ALL pools
std::apply(
[&](auto*... args) {
(args->swap_entities(args->base_entities()[current],
entity),
...);
},
std::make_tuple(&get_storage<Components>()...));
}
current++;
}
}
ptr->set_length(current);
groups[group_id] = std::move(new_group);
// 4. 註冊到 Adjacency List (Component ID -> Group*) - C++17 Fold Expression
(..., (ensure_component_groups(ComponentType<Components>::id()),
component_to_groups[ComponentType<Components>::id()].push_back(ptr)));
return *ptr;
}
/**
* @brief 建立一個 View 來迭代擁有特定 Component 組合的實體。
* 使用 "Smallest Set Driving" 策略:找出數量最少的 Component Pool 作為驅動,
* 然後檢查其他 Pool 是否包含該實體。
*
* @tparam Components 要篩選的 Component 類型列表。
* @tparam Func 迭代函式類型。
* @param func 迭代函式。
*/
template <typename... Components, typename Func>
void view(Func func) {
if constexpr (sizeof...(Components) == 0) return;
// 優化:直接取得 Raw Pointer,避免在迴圈內重複呼叫 get_storage
// 這樣可以減少 unique_ptr 的解參考開銷,並讓編譯器更容易優化
std::tuple<SparseSet<Components>*...> storages =
std::make_tuple(&get_storage<Components>()...);
BaseStorage* driver = nullptr;
size_t min_size = std::numeric_limits<size_t>::max();
std::apply(
[&](auto*... args) {
auto check = [&](auto* s) {
if (s->size() < min_size) {
min_size = s->size();
driver = s;
}
};
(check(args), ...);
},
storages);
if (!driver || min_size == 0) return;
// 取得所有 payload 的 raw pointer (如果需要的話)
// 這裡我們只在 func 呼叫時才 get(),這會觸發 sparse lookup
// 對於 View 來說,這是無法避免的 (除非用 Group)
for (Entity entity : driver->base_entities()) {
bool has_all = true;
std::apply(
[&](auto*... args) { has_all = (... && args->has(entity)); },
storages);
if (has_all) {
// 這裡的 get() 呼叫是 inline 的,且 storages 已經是 raw pointer
func(entity, std::get<SparseSet<Components>*>(storages)->get(
entity)...);
}
}
}
#ifdef ECS_USE_TBB
/**
* @brief 建立一個平行 View 來迭代擁有特定 Component 組合的實體 (使用 TBB)。
*
* @tparam Components 要篩選的 Component 類型列表。
* @tparam Func 迭代函式類型。
* @param func 迭代函式。
*/
template <typename... Components, typename Func>
void parallel_view(Func func) {
if constexpr (sizeof...(Components) == 0) return;
std::tuple<SparseSet<Components>*...> storages =
std::make_tuple(&get_storage<Components>()...);
BaseStorage* driver = nullptr;
size_t min_size = std::numeric_limits<size_t>::max();
std::apply(
[&](auto*... args) {
auto check = [&](auto* s) {
if (s->size() < min_size) {
min_size = s->size();
driver = s;
}
};
(check(args), ...);
},
storages);
if (!driver || min_size == 0) return;
const auto& entities = driver->base_entities();
tbb::parallel_for(tbb::blocked_range<size_t>(0, entities.size()),
[&](const tbb::blocked_range<size_t>& r) {
for (size_t i = r.begin(); i != r.end(); ++i) {
Entity entity = entities[i];
bool has_all = true;
std::apply(
[&](auto*... args) { has_all = (... && args->has(entity)); },
storages);
if (has_all) {
func(entity, std::get<SparseSet<Components>*>(storages)->get(entity)...);
}
}
}
);
}
#endif
/**
* @brief 取得特定 Component 類型的 Construction Signal。
* @tparam T Component 類型。
* @return Signal 物件參考。
*/
template <typename T>
auto& on_construct() {
return get_storage<T>().on_construct();
}
/**
* @brief 取得特定 Component 類型的 Destruction Signal。
* @tparam T Component 類型。
* @return Signal 物件參考。
*/
template <typename T>
auto& on_destroy() {
return get_storage<T>().on_destroy();
}
/**
* @brief 取得特定 Component 類型的 Update Signal。
* @tparam T Component 類型。
* @return Signal 物件參考。
*/
template <typename T>
auto& on_update() {
return get_storage<T>().on_update();
}
};
/**
* @brief 範例 Component:位置。
*/
struct Position {
float x, y;
bool operator==(const Position& other) const {
return x == other.x && y == other.y;
}
};
/**
* @brief 範例 Component:速度。
*/
struct Velocity {
float dx, dy;
bool operator==(const Velocity& other) const {
return dx == other.dx && dy == other.dy;
}
};
// 簡單的測試框架
#define TEST(name) \
std::cout << "[TEST] " << name << " ... "; \
if (true)
#define ASSERT(cond) \
if (!(cond)) { \
std::cout << "FAILED\n Assertion failed: " << #cond << " at line " << __LINE__ << "\n"; \
std::exit(1); \
}
#define ASSERT_EQ(a, b) \
if ((a) != (b)) { \
std::cout << "FAILED\n Expected " << (a) << " == " << (b) << " at line " << __LINE__ << "\n"; \
std::exit(1); \
}
#define PASS() std::cout << "PASSED\n"
void test_entity_management() {
TEST("Entity Creation and Destruction") {
Registry registry;
Entity e1 = registry.create();
Entity e2 = registry.create();
ASSERT(EntityTraits::to_id(e1) == 0);
ASSERT(EntityTraits::to_id(e2) == 1);
ASSERT(EntityTraits::to_version(e1) == 0);
registry.destroy(e1);
Entity e3 = registry.create();
ASSERT(EntityTraits::to_id(e3) == 0); // Should reuse ID 0
ASSERT(EntityTraits::to_version(e3) == 1); // Version should increment
PASS();
}
}
void test_component_management() {
TEST("Component Emplace, Get, Remove") {
Registry registry;
Entity e = registry.create();
registry.emplace(e, Position{10.0f, 20.0f});
ASSERT(registry.get_storage<Position>().has(e));
Position& p = registry.get_storage<Position>().get(e);
ASSERT_EQ(p.x, 10.0f);
ASSERT_EQ(p.y, 20.0f);
registry.remove<Position>(e);
ASSERT(!registry.get_storage<Position>().has(e));
PASS();
}
TEST("Component Patch") {
Registry registry;
Entity e = registry.create();
registry.emplace(e, Position{10.0f, 20.0f});
bool signal_fired = false;
registry.on_update<Position>().connect([&](Registry&, Entity target) {
if (target == e) signal_fired = true;
});
registry.patch<Position>(e, [](Position& p) {
p.x += 5.0f;
});
Position& p = registry.get_storage<Position>().get(e);
ASSERT_EQ(p.x, 15.0f);
ASSERT(signal_fired);
PASS();
}
}
void test_view() {
TEST("View Iteration") {
Registry registry;
Entity e1 = registry.create();
Entity e2 = registry.create();
Entity e3 = registry.create();
registry.emplace(e1, Position{1, 1});
registry.emplace(e1, Velocity{1, 1});
registry.emplace(e2, Position{2, 2});
registry.emplace(e3, Position{3, 3});
registry.emplace(e3, Velocity{3, 3});
int count = 0;
registry.view<Position, Velocity>([&](Entity e, Position& p, Velocity& v) {
count++;
ASSERT(p.x == v.dx); // Based on setup
});
ASSERT_EQ(count, 2); // e1 and e3
PASS();
}
}
void test_group() {
TEST("Group Management") {
Registry registry;
Entity e1 = registry.create();
Entity e2 = registry.create();
Entity e3 = registry.create();
registry.emplace(e1, Position{1, 1});
registry.emplace(e1, Velocity{1, 1});
registry.emplace(e2, Position{2, 2}); // Only Position
// Create group
auto& group = registry.group<Position, Velocity>();
ASSERT_EQ(group.size(), 1); // Only e1 initially
// Add Velocity to e2, should enter group
registry.emplace(e2, Velocity{2, 2});
ASSERT_EQ(group.size(), 2);
// Create e3 with both, should enter group immediately
registry.emplace(e3, Position{3, 3});
registry.emplace(e3, Velocity{3, 3});
ASSERT_EQ(group.size(), 3);
// Remove Position from e1, should leave group
registry.remove<Position>(e1);
ASSERT_EQ(group.size(), 2);
// Destroy e3, should leave group
registry.destroy(e3);
ASSERT_EQ(group.size(), 1); // Only e2 remains
PASS();
}
}
void test_signals() {
TEST("Signals") {
Registry registry;
Entity e = registry.create();
int constructed = 0;
int destroyed = 0;
registry.on_construct<Position>().connect([&](Registry&, Entity) {
constructed++;
});
registry.on_destroy<Position>().connect([&](Registry&, Entity) {
destroyed++;
});
registry.emplace(e, Position{1, 1});
ASSERT_EQ(constructed, 1);
ASSERT_EQ(destroyed, 0);
registry.remove<Position>(e);
ASSERT_EQ(constructed, 1);
ASSERT_EQ(destroyed, 1);
PASS();
}
}
void test_performance() {
TEST("Performance Stress Test (1,000,000 Entities)") {
Registry registry;
const size_t count = 1000000;
std::vector<Entity> entities;
entities.reserve(count);
std::cout << "\n Creating " << count << " entities...\n";
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < count; ++i) {
Entity e = registry.create();
entities.push_back(e);
registry.emplace(e, Position{1.0f, 1.0f});
if (i % 2 == 0) {
registry.emplace(e, Velocity{0.1f, 0.1f});
}
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << " Creation time: " << diff.count() << " s\n";
// Prepare Group
auto& group = registry.group<Position, Velocity>();
std::cout << " Group size (Position + Velocity): " << group.size() << "\n";
// Test View Iteration
start = std::chrono::high_resolution_clock::now();
size_t view_count = 0;
registry.view<Position, Velocity>([&](Entity, Position& p, Velocity& v) {
p.x += v.dx;
view_count++;
});
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << " View iteration time: " << diff.count() << " s (" << view_count << " entities)\n";
// Test Group Iteration
start = std::chrono::high_resolution_clock::now();
size_t group_count = 0;
group.for_each([&](Entity, Position& p, Velocity& v) {
p.y += v.dy;
group_count++;
});
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << " Group iteration time: " << diff.count() << " s (" << group_count << " entities)\n";
ASSERT_EQ(view_count, 500000);
ASSERT_EQ(group_count, 500000);
#ifdef ECS_USE_TBB
// Test Parallel View Iteration
start = std::chrono::high_resolution_clock::now();
std::atomic<size_t> par_view_count = 0;
registry.parallel_view<Position, Velocity>([&](Entity, Position& p, Velocity& v) {
p.x += v.dx;
par_view_count++;
});
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << " Parallel View iteration time: " << diff.count() << " s (" << par_view_count << " entities)\n";
// Test Parallel Group Iteration
start = std::chrono::high_resolution_clock::now();
std::atomic<size_t> par_group_count = 0;
group.parallel_for_each([&](Entity, Position& p, Velocity& v) {
p.y += v.dy;
par_group_count++;
});
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << " Parallel Group iteration time: " << diff.count() << " s (" << par_group_count << " entities)\n";
ASSERT_EQ(par_view_count, 500000);
ASSERT_EQ(par_group_count, 500000);
#endif
// Test Destruction
std::cout << " Destroying all entities...\n";
start = std::chrono::high_resolution_clock::now();
for (Entity e : entities) {
registry.destroy(e);
}
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << " Destruction time: " << diff.count() << " s\n";
PASS();
}
}
int main() {
std::cout << "Running ECS Tests...\n====================\n";
test_entity_management();
test_component_management();
test_view();
test_group();
test_signals();
test_performance();
std::cout << "====================\nAll tests passed successfully!\n";
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment