Created
March 7, 2026 07:14
-
-
Save jacky860226/43618d78c28aa3ad7ad786c1d284463c to your computer and use it in GitHub Desktop.
Entity Component System (ECS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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