From acac8712c62fab3a290064afa9806475fe36df91 Mon Sep 17 00:00:00 2001 From: nircoe Date: Wed, 11 Feb 2026 20:41:46 +0200 Subject: [PATCH 1/5] [Core]: Implement entities manager --- include/gamecoe/entity/component_pool.hpp | 89 ++++++++++++--------- include/gamecoe/entity/sparse_set.hpp | 45 ++++++++--- tests/entity/component_pool_tests.cpp | 58 +------------- tests/entity/entity_tests.cpp | 18 +---- tests/entity/sparse_set_tests.cpp | 5 +- tests/integration/ecs_integration_tests.cpp | 2 - 6 files changed, 91 insertions(+), 126 deletions(-) diff --git a/include/gamecoe/entity/component_pool.hpp b/include/gamecoe/entity/component_pool.hpp index 9e2d0cd..aa6f147 100644 --- a/include/gamecoe/entity/component_pool.hpp +++ b/include/gamecoe/entity/component_pool.hpp @@ -10,69 +10,90 @@ namespace gamecoe { + class basic_component_pool + { + protected: + sparse_set m_entities; + + private: + virtual void do_reserve(std::size_t capacity) = 0; + + public: + virtual ~basic_component_pool() = default; + virtual void remove(entity e) = 0; + virtual void clear() = 0; + + bool contains(entity e) const noexcept { return m_entities.contains(e); } + std::size_t size() const noexcept { return m_entities.size(); } + bool empty() const noexcept { return m_entities.empty(); } + void reserve(std::size_t capacity) { m_entities.reserve(capacity); do_reserve(capacity); } + }; + template - class component_pool : private sparse_set + class component_pool : public basic_component_pool { std::vector m_components; - public: - using sparse_set::contains; - using sparse_set::size; - using sparse_set::empty; + void do_reserve(std::size_t capacity) override + { + m_components.reserve(capacity); + } + public: component_pool() noexcept = default; component_pool(const component_pool&) = delete; component_pool& operator=(const component_pool&) = delete; component_pool(component_pool&&) noexcept = default; component_pool& operator=(component_pool&&) noexcept = default; - ~component_pool() = default; + ~component_pool() override = default; - void reserve(std::size_t capacity) + void remove(entity e) override { - sparse_set::reserve(capacity); - m_components.reserve(capacity); + auto index = m_entities.index(e); + if(!index) return; + + std::uint32_t i = index.value(); + m_entities.erase_at(i); + + if (i != m_components.size() - 1) + m_components[i] = std::move(m_components.back()); + + m_components.pop_back(); + } + + void clear() override + { + m_entities.clear(); + m_components.clear(); } template - T& add(const entity &e, Args&&... args) + T& add(entity e, Args&&... args) { if(contains(e)) { assert(false && "component_pool::add(): entity already has this component"); - return m_components[sparse_set::index(e).value()]; + return m_components[m_entities.index(e).value()]; } - sparse_set::insert(e); + m_entities.insert(e); m_components.emplace_back(std::forward(args)...); return m_components.back(); } - void remove(const entity &e) - { - auto index = sparse_set::index(e); - if(!index) return; - - sparse_set::erase(e); - - if (index.value() < m_components.size() - 1) - m_components[index.value()] = std::move(m_components.back()); - - m_components.pop_back(); - } - - std::optional> get(const entity &e) + std::optional> get(entity e) { - auto index = sparse_set::index(e); + auto index = m_entities.index(e); if(!index) return std::nullopt; return std::ref(m_components[index.value()]); } - std::optional> get(const entity &e) const + std::optional> get(entity e) const { - auto index = sparse_set::index(e); + auto index = m_entities.index(e); if(!index) return std::nullopt; return std::cref(m_components[index.value()]); @@ -83,7 +104,7 @@ namespace gamecoe { auto size = m_components.size(); for(std::size_t i = 0; i < size; ++i) - func(sparse_set::get_entity_at_index(i), m_components[i]); + func(m_entities.get_entity_at_index(i), m_components[i]); } template @@ -91,13 +112,7 @@ namespace gamecoe { auto size = m_components.size(); for(std::size_t i = 0; i < size; ++i) - func(sparse_set::get_entity_at_index(i), m_components[i]); - } - - void clear() - { - sparse_set::clear(); - m_components.clear(); + func(m_entities.get_entity_at_index(i), m_components[i]); } // Iterators invalidated by add/remove (dense array reallocation) diff --git a/include/gamecoe/entity/sparse_set.hpp b/include/gamecoe/entity/sparse_set.hpp index 294068f..3dcdcbe 100644 --- a/include/gamecoe/entity/sparse_set.hpp +++ b/include/gamecoe/entity/sparse_set.hpp @@ -22,8 +22,8 @@ namespace gamecoe std::vector m_sparse; std::vector m_dense; - std::uint16_t index_in_page(const entity &e) const noexcept { return e.id() % PAGE_SIZE; } - std::uint32_t page_index(const entity &e) const noexcept { return e.id() / PAGE_SIZE; } + std::uint16_t index_in_page(entity e) const noexcept { return e.id() % PAGE_SIZE; } + std::uint32_t page_index(entity e) const noexcept { return e.id() / PAGE_SIZE; } std::uint32_t unpack_dense_index(std::uint32_t packed) const noexcept { return packed & entity::ID_MASK; } std::uint16_t unpack_generation(std::uint32_t packed) const noexcept { return packed >> entity::ID_BITS; } @@ -31,13 +31,6 @@ namespace gamecoe { return (generation << entity::ID_BITS) | dense_index; } - - protected: - entity get_entity_at_index(std::size_t index) const noexcept - { - assert(index < m_dense.size() && "sparse_set::get_entity_at_index(): index out of bounds"); - return m_dense[index]; - } public: sparse_set() noexcept = default; @@ -50,7 +43,7 @@ namespace gamecoe void reserve(std::size_t capacity) { m_dense.reserve(capacity); } - void insert(const entity &e) + void insert(entity e) { if (contains(e)) return; assert(m_dense.size() <= entity::MAX_ENTITIES && "sparse_set::insert(): max entities exceeded"); // sanity check @@ -70,7 +63,7 @@ namespace gamecoe m_dense.push_back(e); } - void erase(const entity &e) + void erase(entity e) { if (!contains(e)) return; @@ -89,14 +82,34 @@ namespace gamecoe (*swapped_page)[index_in_page(swapped)] = pack_dense_index(dense_index, swapped.generation()); } - std::optional index(const entity &e) const + void erase_at(std::uint32_t dense_index) + { + if (dense_index >= m_dense.size()) return; + + entity e = m_dense[dense_index]; + auto page_i = page_index(e); + auto i_in_page = index_in_page(e); + + entity swapped = m_dense.back(); + m_dense[dense_index] = swapped; + m_dense.pop_back(); + + (*m_sparse[page_i])[i_in_page] = TOMBSTONE; + + if (swapped == e) return; + + auto &swapped_page = m_sparse[page_index(swapped)]; + (*swapped_page)[index_in_page(swapped)] = pack_dense_index(dense_index, swapped.generation()); + } + + std::optional index(entity e) const { if (!contains(e)) return std::nullopt; return unpack_dense_index((*m_sparse[page_index(e)])[index_in_page(e)]); } - bool contains(const entity &e) const noexcept + bool contains(entity e) const noexcept { auto page_i = page_index(e); @@ -104,6 +117,12 @@ namespace gamecoe unpack_generation((*m_sparse[page_i])[index_in_page(e)]) == e.generation(); } + entity get_entity_at_index(std::size_t index) const noexcept + { + assert(index < m_dense.size() && "sparse_set::get_entity_at_index(): index out of bounds"); + return m_dense[index]; + } + void clear() { m_sparse.clear(); m_dense.clear(); } std::size_t size() const noexcept { return m_dense.size(); } bool empty() const noexcept { return m_dense.empty(); } diff --git a/tests/entity/component_pool_tests.cpp b/tests/entity/component_pool_tests.cpp index 7f55a0f..d2f0982 100644 --- a/tests/entity/component_pool_tests.cpp +++ b/tests/entity/component_pool_tests.cpp @@ -1,7 +1,6 @@ #include #include #include -#include using namespace gamecoe; @@ -34,46 +33,6 @@ struct MoveOnlyComponent MoveOnlyComponent &operator=(MoveOnlyComponent &&) noexcept = default; }; -// RAII type for lifecycle test -struct LifetimeTracker -{ - int *counter; - - LifetimeTracker(int *c) : counter(c) { ++(*counter); } - ~LifetimeTracker() { --(*counter); } - - // Copyable for swap-and-pop operations - LifetimeTracker(const LifetimeTracker &other) : counter(other.counter) { ++(*counter); } - LifetimeTracker &operator=(const LifetimeTracker &other) - { - if (this != &other) - { - if (counter) - --(*counter); - counter = other.counter; - ++(*counter); - } - return *this; - } - - // Moveable - LifetimeTracker(LifetimeTracker &&other) noexcept : counter(other.counter) - { - other.counter = nullptr; - } - LifetimeTracker &operator=(LifetimeTracker &&other) noexcept - { - if (this != &other) - { - if (counter) - --(*counter); - counter = other.counter; - other.counter = nullptr; - } - return *this; - } -}; - //============================================================================== // ComponentPoolTests - Component pool wrapper tests //============================================================================== @@ -129,21 +88,6 @@ TEST_F(ComponentPoolTests, AddMultiple) } } -TEST_F(ComponentPoolTests, ContainsCheck) -{ - auto e = entity::create(42, 0); - - EXPECT_FALSE(pool.contains(e)); - - pool.add(e, Position{1.0f, 2.0f, 3.0f}); - - EXPECT_TRUE(pool.contains(e)); - EXPECT_EQ(pool.size(), 1); - - // Note: Adding duplicate component triggers assertion in debug builds - // This is a programmer error, not a runtime behavior to test -} - //============================================================================== // Remove Components //============================================================================== @@ -292,3 +236,5 @@ TEST_F(ComponentPoolTests, ClearPool) for (std::uint32_t i = 0; i < 20; ++i) EXPECT_FALSE(pool.contains(entity::create(i, 0))); } + + diff --git a/tests/entity/entity_tests.cpp b/tests/entity/entity_tests.cpp index 5a48551..2af813c 100644 --- a/tests/entity/entity_tests.cpp +++ b/tests/entity/entity_tests.cpp @@ -16,7 +16,7 @@ class EntityTests : public ::testing::Test // Test helper: custom hash for unordered containers struct entity_hash { - std::size_t operator()(const entity &e) const noexcept + std::size_t operator()(entity e) const noexcept { return std::hash{}(e.id()) ^ (std::hash{}(e.generation()) << 1); @@ -66,22 +66,6 @@ TEST_F(EntityTests, InvalidEntity) EXPECT_EQ(invalid, entity::invalid()); // Two invalid entities are equal } -TEST_F(EntityTests, IdGenerationAccessors) -{ - // Test various combinations - auto e1 = entity::create(0, 0); - EXPECT_EQ(e1.id(), 0); - EXPECT_EQ(e1.generation(), 0); - - auto e2 = entity::create(1024, 42); - EXPECT_EQ(e2.id(), 1024); - EXPECT_EQ(e2.generation(), 42); - - auto e3 = entity::create(100000, 1000); - EXPECT_EQ(e3.id(), 100000); - EXPECT_EQ(e3.generation(), 1000); -} - //============================================================================== // Bit Packing and Layout //============================================================================== diff --git a/tests/entity/sparse_set_tests.cpp b/tests/entity/sparse_set_tests.cpp index 65f6f5a..f5ff13a 100644 --- a/tests/entity/sparse_set_tests.cpp +++ b/tests/entity/sparse_set_tests.cpp @@ -1,7 +1,6 @@ #include #include #include -#include using namespace gamecoe; @@ -111,6 +110,8 @@ TEST_F(SparseSetTests, EraseNonExistent) auto e2 = entity::create(20, 0); set.insert(e1); + EXPECT_EQ(set.size(), 1); + set.erase(e2); // e2 not in set EXPECT_EQ(set.size(), 1); // Size unchanged @@ -223,10 +224,12 @@ TEST_F(SparseSetTests, MoveSemantics) set1.insert(e1); set1.insert(e2); + EXPECT_EQ(set1.size(), 2); // Move constructor sparse_set set2(std::move(set1)); EXPECT_EQ(set2.size(), 2); + EXPECT_EQ(set1.size(), 0); EXPECT_TRUE(set2.contains(e1)); EXPECT_TRUE(set2.contains(e2)); diff --git a/tests/integration/ecs_integration_tests.cpp b/tests/integration/ecs_integration_tests.cpp index 0c7abf5..3531cac 100644 --- a/tests/integration/ecs_integration_tests.cpp +++ b/tests/integration/ecs_integration_tests.cpp @@ -1,9 +1,7 @@ #include #include -#include #include #include -#include using namespace gamecoe; From af7a7ca27df7da115651fceb5147240790e904a3 Mon Sep 17 00:00:00 2001 From: nircoe Date: Fri, 13 Feb 2026 16:46:41 +0200 Subject: [PATCH 2/5] Implement entities, need to start working on tests --- cmake/gamecoe_config.hpp.in | 1 + include/gamecoe/entity/component_pool.hpp | 14 +-- include/gamecoe/entity/entities.hpp | 147 ++++++++++++++++++++++ include/gamecoe/entity/entity.hpp | 24 ++-- src/gamecoe/entity/entities.cpp | 70 +++++++++++ 5 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 include/gamecoe/entity/entities.hpp create mode 100644 src/gamecoe/entity/entities.cpp diff --git a/cmake/gamecoe_config.hpp.in b/cmake/gamecoe_config.hpp.in index a4f5885..e311a71 100644 --- a/cmake/gamecoe_config.hpp.in +++ b/cmake/gamecoe_config.hpp.in @@ -37,6 +37,7 @@ // gamecoe toolkit #define GAMECOE_USE_LOGCOE @GAMECOE_USE_LOGCOE@ #define GAMECOE_USE_SOUNDCOE @GAMECOE_USE_SOUNDCOE@ +#define GAMECOE_USE_TESTCOE @GAMECOE_USE_TESTCOE@ #if GAMECOE_USE_SOUNDCOE #include diff --git a/include/gamecoe/entity/component_pool.hpp b/include/gamecoe/entity/component_pool.hpp index aa6f147..694f716 100644 --- a/include/gamecoe/entity/component_pool.hpp +++ b/include/gamecoe/entity/component_pool.hpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include #include namespace gamecoe @@ -83,20 +81,20 @@ namespace gamecoe return m_components.back(); } - std::optional> get(entity e) + T& get(entity e) { auto index = m_entities.index(e); - if(!index) return std::nullopt; + assert(index && "component_pool::get(): entity does not exist in the pool"); - return std::ref(m_components[index.value()]); + return m_components[index.value()]; } - std::optional> get(entity e) const + const T& get(entity e) const { auto index = m_entities.index(e); - if(!index) return std::nullopt; + assert(index && "component_pool::get(): entity does not exist in the pool"); - return std::cref(m_components[index.value()]); + return m_components[index.value()]; } template diff --git a/include/gamecoe/entity/entities.hpp b/include/gamecoe/entity/entities.hpp new file mode 100644 index 0000000..1214f41 --- /dev/null +++ b/include/gamecoe/entity/entities.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gamecoe +{ + class entities + { + static std::atomic s_component_id; + + std::vector> m_pools; + std::vector m_recycle_ids; + std::vector m_generations; + + std::atomic m_current_entity_id; + + // Returns static and unique id for component T + template + static std::uint32_t component_id() + { + static std::uint32_t s_componentT_id = s_component_id++; + return s_componentT_id; + } + + // Returns a pointer to the realtime type component_pool of the T component, creates a new pool if not exists + template + component_pool* get_pool() + { + std::uint32_t comp_id = component_id(); + + if (comp_id >= m_pools.size()) m_pools.resize(comp_id + 1); + if (!m_pools[comp_id]) m_pools[comp_id] = std::make_unique>(); + + return static_cast*>(m_pools[comp_id].get()); + } + + public: + entities() = default; + entities(const entities&) = delete; + entities(entities&&) = delete; + entities &operator=(const entities&) = delete; + entities &operator=(entities&&) = delete; + + ~entities() = default; + + // Creates an entity (may be recycled id) + entity create(); + + // Destroy an entity + void destroy(entity e); + + // Destroys all entities + void clear(); + + // Returns if the entity handle given is valid + bool valid(entity e) const; + + // Reserves capacity for a number of entities + void reserve(std::size_t capacity); + + // Returns the number of alive entities + std::size_t size() const; + + // Emplaces a new T component to entity e + template + T& add_component(entity e, Args&&... args) + { + assert(valid(e) && "entities::add_component(): entity is not valid"); + + auto pool = get_pool(); + return pool->add(e, std::forward(args)...); + } + + // Returns if entity e has a component T + template + bool has_component(entity e) const + { + if (!valid(e)) return false; + + std::uint32_t comp_id = component_id(); + if (comp_id >= m_pools.size() || !m_pools[comp_id]) return false; + + return m_pools[comp_id]->contains(e); + } + + // Removes component T from entity e + template + void remove_component(entity e) + { + if (!has_component(e)) return; + + m_pools[component_id()]->remove(e); + } + + // Returns a pointer to component T of entity e (`nullptr` if entity doesn't have T component), + // also note that the pointer may invalidated by any `add_component` call + template + T* get_component(entity e) + { + if (!has_component(e)) return nullptr; + + auto pool = static_cast*>(m_pools[component_id()].get()); + return &(pool->get(e)); + } + + // Returns a pointer to component T of entity e (`nullptr` if entity doesn't have T component), + // also note that the pointer may invalidated by any `add_component` call + template + const T* get_component(entity e) const + { + if (!has_component(e)) return nullptr; + + auto pool = static_cast*>(m_pools[component_id()].get()); + return &(pool->get(e)); + } + + // Iterates over all entities and their components and run func() on each of them + template + void for_each(Func &&func) + { + std::uint32_t comp_id = component_id(); + if (comp_id >= m_pools.size() || !m_pools[comp_id]) return; + + auto pool = static_cast*>(m_pools[comp_id].get()); + pool->for_each(std::forward(func)); + } + + // Iterates over all entities and their components and run func() on each of them + template + void for_each(Func &&func) const + { + std::uint32_t comp_id = component_id(); + if (comp_id >= m_pools.size() || !m_pools[comp_id]) return; + + auto pool = static_cast*>(m_pools[comp_id].get()); + pool->for_each(std::forward(func)); + } + }; +} // namespace gamecoe diff --git a/include/gamecoe/entity/entity.hpp b/include/gamecoe/entity/entity.hpp index 6e9af32..2e1d419 100644 --- a/include/gamecoe/entity/entity.hpp +++ b/include/gamecoe/entity/entity.hpp @@ -7,6 +7,8 @@ namespace gamecoe { + class entities; + struct entity { static constexpr std::uint8_t ID_BITS = 20; @@ -18,16 +20,6 @@ namespace gamecoe static constexpr entity invalid() noexcept { return entity{std::numeric_limits::max()}; } - static entity create(std::uint32_t id, std::uint16_t generation) - { - assert(id <= MAX_ENTITIES && "entity::create(): entity id exceeds maximum"); - assert(generation <= MAX_GENERATIONS && "entity::create(): entity generation exceeds maximum"); - // Bit layout (id-major ensures default operator<=> orders by id first, generation second) - // [31..................12][11..............0] - // [ 20-bit id ][12-bit generation] - return entity((id << GEN_BITS) | generation); - } - auto operator<=>(const entity &other) const noexcept = default; bool operator==(const entity &other) const noexcept = default; @@ -35,10 +27,22 @@ namespace gamecoe std::uint32_t id() const noexcept { return m_handle >> GEN_BITS; } std::uint16_t generation() const noexcept { return m_handle & GEN_MASK; } + + friend class entities; private: std::uint32_t m_handle; explicit constexpr entity(std::uint32_t value) noexcept : m_handle(value) { } + + static entity create(std::uint32_t id, std::uint16_t generation) + { + assert(id <= MAX_ENTITIES && "entity::create(): entity id exceeds maximum"); + assert(generation <= MAX_GENERATIONS && "entity::create(): entity generation exceeds maximum"); + // Bit layout (id-major ensures default operator<=> orders by id first, generation second) + // [31..................12][11..............0] + // [ 20-bit id ][12-bit generation] + return entity((id << GEN_BITS) | generation); + } }; } // namespace gamecoe diff --git a/src/gamecoe/entity/entities.cpp b/src/gamecoe/entity/entities.cpp new file mode 100644 index 0000000..a22d1e9 --- /dev/null +++ b/src/gamecoe/entity/entities.cpp @@ -0,0 +1,70 @@ +#include "gamecoe/entity/entity.hpp" +#include +#include +#include + +namespace gamecoe +{ + std::atomic entities::s_component_id{0}; + + entity entities::create() + { + std::uint32_t id; + std::uint16_t generation; + + if (m_recycle_ids.empty()) + { + id = m_current_entity_id.load(); + assert(id <= entity::MAX_ENTITIES && "entities::create(): entity limit reached"); + m_current_entity_id++; + generation = 0; + m_generations.push_back(generation); + } + else + { + id = m_recycle_ids.back(); + m_recycle_ids.pop_back(); + generation = m_generations[id]; + } + + return entity::create(id, generation); + } + + void entities::destroy(entity e) + { + if (!valid(e)) return; + + for (auto &pool : m_pools) if (pool) pool->remove(e); + + m_generations[e.id()]++; + m_recycle_ids.push_back(e.id()); + } + + void entities::clear() + { + m_pools.clear(); + m_recycle_ids.clear(); + m_generations.clear(); + m_current_entity_id = 0; + } + + bool entities::valid(entity e) const + { + return e.id() < m_generations.size() && m_generations[e.id()] == e.generation(); + } + + void entities::reserve(std::size_t capacity) + { + m_generations.reserve(capacity); + m_recycle_ids.reserve(capacity); + + for (auto &pool : m_pools) if (pool) pool->reserve(capacity); + } + + std::size_t entities::size() const + { + assert(static_cast(m_current_entity_id.load()) >= m_recycle_ids.size()); + return static_cast(m_current_entity_id.load()) - m_recycle_ids.size(); + } + +} // namespace gamecoe From 4903747c16246c9d57955abc733175f5242bfa35 Mon Sep 17 00:00:00 2001 From: nircoe Date: Sat, 14 Feb 2026 17:39:33 +0200 Subject: [PATCH 3/5] Adding tests --- include/gamecoe/entity/entity.hpp | 14 +- tests/CMakeLists.txt | 2 +- tests/entity/component_pool_tests.cpp | 156 ++++++------ tests/entity/entities_tests.cpp | 264 ++++++++++++++++++++ tests/entity/entity_tests.cpp | 10 +- tests/entity/sparse_set_tests.cpp | 45 ++++ tests/integration/ecs_integration_tests.cpp | 203 --------------- tests/main.cpp | 5 +- 8 files changed, 407 insertions(+), 292 deletions(-) create mode 100644 tests/entity/entities_tests.cpp delete mode 100644 tests/integration/ecs_integration_tests.cpp diff --git a/include/gamecoe/entity/entity.hpp b/include/gamecoe/entity/entity.hpp index 2e1d419..d4a6ba2 100644 --- a/include/gamecoe/entity/entity.hpp +++ b/include/gamecoe/entity/entity.hpp @@ -23,18 +23,9 @@ namespace gamecoe auto operator<=>(const entity &other) const noexcept = default; bool operator==(const entity &other) const noexcept = default; - bool valid() const noexcept { return *this != invalid(); } - std::uint32_t id() const noexcept { return m_handle >> GEN_BITS; } std::uint16_t generation() const noexcept { return m_handle & GEN_MASK; } - friend class entities; - - private: - std::uint32_t m_handle; - - explicit constexpr entity(std::uint32_t value) noexcept : m_handle(value) { } - static entity create(std::uint32_t id, std::uint16_t generation) { assert(id <= MAX_ENTITIES && "entity::create(): entity id exceeds maximum"); @@ -44,5 +35,10 @@ namespace gamecoe // [ 20-bit id ][12-bit generation] return entity((id << GEN_BITS) | generation); } + + private: + std::uint32_t m_handle; + + explicit constexpr entity(std::uint32_t value) noexcept : m_handle(value) { } }; } // namespace gamecoe diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d47155a..79c2921 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,7 +3,7 @@ add_executable(gamecoe_tests entity/entity_tests.cpp entity/sparse_set_tests.cpp entity/component_pool_tests.cpp - integration/ecs_integration_tests.cpp + entity/entities_tests.cpp ) target_link_libraries(gamecoe_tests diff --git a/tests/entity/component_pool_tests.cpp b/tests/entity/component_pool_tests.cpp index d2f0982..7c28977 100644 --- a/tests/entity/component_pool_tests.cpp +++ b/tests/entity/component_pool_tests.cpp @@ -26,13 +26,56 @@ struct MoveOnlyComponent MoveOnlyComponent(int val) : data(std::make_unique(val)) {} - // Move-only (no copy) MoveOnlyComponent(const MoveOnlyComponent &) = delete; MoveOnlyComponent &operator=(const MoveOnlyComponent &) = delete; MoveOnlyComponent(MoveOnlyComponent &&) noexcept = default; MoveOnlyComponent &operator=(MoveOnlyComponent &&) noexcept = default; }; +// RAII type for lifecycle test +struct LifetimeTracker +{ + int *counter; + + LifetimeTracker(int *c) : counter(c) { ++(*counter); } + ~LifetimeTracker() + { + if (counter) + --(*counter); + } + + LifetimeTracker(const LifetimeTracker &other) : counter(other.counter) + { + if (counter) + ++(*counter); + } + LifetimeTracker &operator=(const LifetimeTracker &other) + { + if (this != &other) + { + if (counter) --(*counter); + counter = other.counter; + if (counter) ++(*counter); + } + return *this; + } + + LifetimeTracker(LifetimeTracker &&other) noexcept : counter(other.counter) + { + other.counter = nullptr; + } + LifetimeTracker &operator=(LifetimeTracker &&other) noexcept + { + if (this != &other) + { + if (counter) --(*counter); + counter = other.counter; + other.counter = nullptr; + } + return *this; + } +}; + //============================================================================== // ComponentPoolTests - Component pool wrapper tests //============================================================================== @@ -47,7 +90,7 @@ class ComponentPoolTests : public ::testing::Test // Add and Get Components //============================================================================== -TEST_F(ComponentPoolTests, AddComponent) +TEST_F(ComponentPoolTests, AddAndGetComponent) { auto e = entity::create(42, 0); @@ -56,20 +99,17 @@ TEST_F(ComponentPoolTests, AddComponent) EXPECT_TRUE(pool.contains(e)); EXPECT_EQ(pool.size(), 1); EXPECT_FALSE(pool.empty()); + EXPECT_EQ(pool.get(e), pos); - auto retrieved = pool.get(e); - EXPECT_TRUE(retrieved.has_value()); - EXPECT_EQ(retrieved->get(), pos); - EXPECT_EQ(retrieved->get().x, 1.0f); - EXPECT_EQ(retrieved->get().y, 2.0f); - EXPECT_EQ(retrieved->get().z, 3.0f); + // Modify and verify changes persist + pool.get(e).x = 99.0f; + EXPECT_EQ(pool.get(e).x, 99.0f); } TEST_F(ComponentPoolTests, AddMultiple) { std::vector entities; - // Add 50 components for (std::uint32_t i = 0; i < 50; ++i) { auto e = entity::create(i, 0); @@ -79,34 +119,14 @@ TEST_F(ComponentPoolTests, AddMultiple) EXPECT_EQ(pool.size(), 50); - // Verify all are accessible for (std::size_t i = 0; i < entities.size(); ++i) - { - auto retrieved = pool.get(entities[i]); - EXPECT_TRUE(retrieved.has_value()); - EXPECT_EQ(retrieved->get().x, static_cast(i)); - } + EXPECT_EQ(pool.get(entities[i]).x, static_cast(i)); } //============================================================================== // Remove Components //============================================================================== -TEST_F(ComponentPoolTests, RemoveComponent) -{ - auto e = entity::create(42, 0); - - pool.add(e, Position{1.0f, 2.0f, 3.0f}); - pool.remove(e); - - EXPECT_FALSE(pool.contains(e)); - EXPECT_EQ(pool.size(), 0); - EXPECT_TRUE(pool.empty()); - - auto retrieved = pool.get(e); - EXPECT_FALSE(retrieved.has_value()); -} - TEST_F(ComponentPoolTests, RemoveSwapAndPop) { auto e1 = entity::create(10, 0); @@ -117,7 +137,6 @@ TEST_F(ComponentPoolTests, RemoveSwapAndPop) pool.add(e2, Position{2.0f, 0.0f, 0.0f}); pool.add(e3, Position{3.0f, 0.0f, 0.0f}); - // Remove middle entity (e2) pool.remove(e2); EXPECT_EQ(pool.size(), 2); @@ -125,33 +144,14 @@ TEST_F(ComponentPoolTests, RemoveSwapAndPop) EXPECT_FALSE(pool.contains(e2)); EXPECT_TRUE(pool.contains(e3)); - // Verify e3's component data is still correct (swapped to e2's position) - auto e3_comp = pool.get(e3); - EXPECT_TRUE(e3_comp.has_value()); - EXPECT_EQ(e3_comp->get().x, 3.0f); + // Verify e3's component data is still correct after swap + EXPECT_EQ(pool.get(e3).x, 3.0f); } //============================================================================== -// Mutable and Const Access +// Const Access //============================================================================== -TEST_F(ComponentPoolTests, GetMutable) -{ - auto e = entity::create(42, 0); - - pool.add(e, Position{1.0f, 2.0f, 3.0f}); - - auto comp = pool.get(e); - EXPECT_TRUE(comp.has_value()); - - // Modify component - comp->get().x = 99.0f; - - // Verify changes persist - auto retrieved = pool.get(e); - EXPECT_EQ(retrieved->get().x, 99.0f); -} - TEST_F(ComponentPoolTests, GetConst) { auto e = entity::create(42, 0); @@ -159,13 +159,10 @@ TEST_F(ComponentPoolTests, GetConst) pool.add(e, Position{1.0f, 2.0f, 3.0f}); const auto &const_pool = pool; - auto comp = const_pool.get(e); - - EXPECT_TRUE(comp.has_value()); - EXPECT_EQ(comp->get().x, 1.0f); + const Position &comp = const_pool.get(e); - // Verify const reference wrapper - static_assert(std::is_same_v>>); + EXPECT_EQ(comp.x, 1.0f); + static_assert(std::is_same_v); } //============================================================================== @@ -176,7 +173,6 @@ TEST_F(ComponentPoolTests, ForEachIteration) { std::vector entities; - // Add 10 components for (std::uint32_t i = 0; i < 10; ++i) { auto e = entity::create(i, 0); @@ -184,13 +180,13 @@ TEST_F(ComponentPoolTests, ForEachIteration) pool.add(e, Position{static_cast(i), 0.0f, 0.0f}); } - // Iterate using for_each int count = 0; pool.for_each([&count, &entities](entity e, Position &pos) - { + { EXPECT_EQ(e, entities[count]); EXPECT_EQ(pos.x, static_cast(count)); - ++count; }); + ++count; + }); EXPECT_EQ(count, 10); } @@ -204,15 +200,37 @@ TEST_F(ComponentPoolTests, PerfectForwarding) component_pool move_pool; auto e = entity::create(42, 0); - // Add move-only component using perfect forwarding MoveOnlyComponent comp(123); move_pool.add(e, std::move(comp)); EXPECT_TRUE(move_pool.contains(e)); + EXPECT_EQ(*move_pool.get(e).data, 123); +} + +//============================================================================== +// Component Lifecycle (RAII) +//============================================================================== + +TEST_F(ComponentPoolTests, ComponentLifecycle) +{ + component_pool trackers; + int counter = 0; - auto retrieved = move_pool.get(e); - EXPECT_TRUE(retrieved.has_value()); - EXPECT_EQ(*retrieved->get().data, 123); + auto e1 = entity::create(1, 0); + auto e2 = entity::create(2, 0); + auto e3 = entity::create(3, 0); + + trackers.add(e1, LifetimeTracker{&counter}); + trackers.add(e2, LifetimeTracker{&counter}); + trackers.add(e3, LifetimeTracker{&counter}); + + EXPECT_EQ(counter, 3); + + trackers.remove(e2); + EXPECT_EQ(counter, 2); + + trackers.clear(); + EXPECT_EQ(counter, 0); } //============================================================================== @@ -221,7 +239,6 @@ TEST_F(ComponentPoolTests, PerfectForwarding) TEST_F(ComponentPoolTests, ClearPool) { - // Add components for (std::uint32_t i = 0; i < 20; ++i) pool.add(entity::create(i, 0), Position{0.0f, 0.0f, 0.0f}); @@ -232,9 +249,6 @@ TEST_F(ComponentPoolTests, ClearPool) EXPECT_EQ(pool.size(), 0); EXPECT_TRUE(pool.empty()); - // Verify entities no longer have components for (std::uint32_t i = 0; i < 20; ++i) EXPECT_FALSE(pool.contains(entity::create(i, 0))); } - - diff --git a/tests/entity/entities_tests.cpp b/tests/entity/entities_tests.cpp new file mode 100644 index 0000000..c87e5c4 --- /dev/null +++ b/tests/entity/entities_tests.cpp @@ -0,0 +1,264 @@ +#include +#include +#include + +using namespace gamecoe; + +//============================================================================== +// Test Component Types +//============================================================================== + +struct Position +{ + float x, y, z; +}; + +struct Velocity +{ + float dx, dy, dz; +}; + +//============================================================================== +// EntitiesTests - Entities manager tests +//============================================================================== + +class EntitiesTests : public ::testing::Test +{ +protected: + entities mgr; +}; + +//============================================================================== +// Entity Lifecycle +//============================================================================== + +TEST_F(EntitiesTests, CreateAndValid) +{ + entity e = mgr.create(); + + EXPECT_TRUE(mgr.valid(e)); + EXPECT_TRUE(e != entity::invalid()); + EXPECT_EQ(mgr.size(), 1); +} + +TEST_F(EntitiesTests, CreateMultiple) +{ + entity e0 = mgr.create(); + entity e1 = mgr.create(); + entity e2 = mgr.create(); + + EXPECT_EQ(mgr.size(), 3); + EXPECT_TRUE(mgr.valid(e0)); + EXPECT_TRUE(mgr.valid(e1)); + EXPECT_TRUE(mgr.valid(e2)); + + // IDs should be distinct + EXPECT_NE(e0, e1); + EXPECT_NE(e1, e2); + + // Handles should be ordered by creation (ID-major layout) + EXPECT_LT(e0, e1); + EXPECT_LT(e1, e2); +} + +TEST_F(EntitiesTests, DestroyEntity) +{ + entity e = mgr.create(); + EXPECT_EQ(mgr.size(), 1); + + mgr.destroy(e); + + EXPECT_FALSE(mgr.valid(e)); + EXPECT_EQ(mgr.size(), 0); +} + +TEST_F(EntitiesTests, RecycleId) +{ + entity e0 = mgr.create(); + std::uint32_t original_id = e0.id(); + std::uint16_t original_gen = e0.generation(); + + mgr.destroy(e0); + + entity e1 = mgr.create(); + + // Same ID recycled, generation incremented + EXPECT_EQ(e1.id(), original_id); + EXPECT_EQ(e1.generation(), original_gen + 1); + + // Old handle is stale, new one is valid + EXPECT_FALSE(mgr.valid(e0)); + EXPECT_TRUE(mgr.valid(e1)); +} + +TEST_F(EntitiesTests, ClearEntities) +{ + entity e0 = mgr.create(); + entity e1 = mgr.create(); + entity e2 = mgr.create(); + + mgr.clear(); + + EXPECT_EQ(mgr.size(), 0); + EXPECT_FALSE(mgr.valid(e0)); + EXPECT_FALSE(mgr.valid(e1)); + EXPECT_FALSE(mgr.valid(e2)); +} + +//============================================================================== +// Component Operations +//============================================================================== + +TEST_F(EntitiesTests, AddHasGetRemoveComponent) +{ + entity e = mgr.create(); + + // Add + mgr.add_component(e, Position{1.0f, 2.0f, 3.0f}); + EXPECT_TRUE(mgr.has_component(e)); + + // Get (mutable) + Position *pos = mgr.get_component(e); + EXPECT_NE(pos, nullptr); + EXPECT_EQ(pos->x, 1.0f); + + // Modify and verify + pos->x = 99.0f; + EXPECT_EQ(mgr.get_component(e)->x, 99.0f); + + // Remove + mgr.remove_component(e); + EXPECT_FALSE(mgr.has_component(e)); + EXPECT_EQ(mgr.get_component(e), nullptr); + + // Entity still valid after component removal + EXPECT_TRUE(mgr.valid(e)); +} + +TEST_F(EntitiesTests, GetComponentConst) +{ + entity e = mgr.create(); + mgr.add_component(e, Position{5.0f, 6.0f, 7.0f}); + + const entities &const_mgr = mgr; + const Position *pos = const_mgr.get_component(e); + + EXPECT_NE(pos, nullptr); + EXPECT_EQ(pos->x, 5.0f); + static_assert(std::is_same_v); +} + +TEST_F(EntitiesTests, GetComponentNullptr) +{ + entity e = mgr.create(); + entity invalid = entity::invalid(); + + // Entity without the component + EXPECT_EQ(mgr.get_component(e), nullptr); + + // Invalid entity handle + EXPECT_EQ(mgr.get_component(invalid), nullptr); +} + +TEST_F(EntitiesTests, DestroyRemovesAllComponents) +{ + entity e = mgr.create(); + + mgr.add_component(e, Position{1.0f, 0.0f, 0.0f}); + mgr.add_component(e, Velocity{0.1f, 0.0f, 0.0f}); + + EXPECT_TRUE(mgr.has_component(e)); + EXPECT_TRUE(mgr.has_component(e)); + + mgr.destroy(e); + + // Both components should be gone along with the entity + EXPECT_FALSE(mgr.valid(e)); + EXPECT_FALSE(mgr.has_component(e)); + EXPECT_FALSE(mgr.has_component(e)); +} + +//============================================================================== +// Iteration +//============================================================================== + +TEST_F(EntitiesTests, ForEach) +{ + for (int i = 0; i < 5; ++i) + { + entity e = mgr.create(); + mgr.add_component(e, Position{static_cast(i), 0.0f, 0.0f}); + } + + // Mutable for_each + int count = 0; + mgr.for_each([&count]([[maybe_unused]] entity e, Position &pos) + { + pos.x += 10.0f; + ++count; + }); + EXPECT_EQ(count, 5); + + // Const for_each — verify mutations persisted + const entities &const_mgr = mgr; + float sum = 0.0f; + const_mgr.for_each([&sum]([[maybe_unused]] entity e, const Position &pos) + { + sum += pos.x; + }); + // 0+10 + 1+10 + 2+10 + 3+10 + 4+10 = 60 + EXPECT_EQ(sum, 60.0f); +} + +//============================================================================== +// Bulk Operations Performance +//============================================================================== + +TEST_F(EntitiesTests, BulkOperations) +{ + const std::size_t NUM_ENTITIES = 1000; + + mgr.reserve(NUM_ENTITIES); + + auto start = std::chrono::high_resolution_clock::now(); + + for (std::size_t i = 0; i < NUM_ENTITIES; ++i) + { + entity e = mgr.create(); + mgr.add_component(e, Position{static_cast(i), 0.0f, 0.0f}); + mgr.add_component(e, Velocity{1.0f, 0.0f, 0.0f}); + } + + int count = 0; + mgr.for_each([&count]([[maybe_unused]] entity e, Position &pos) + { + pos.x += 1.0f; + ++count; + }); + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + EXPECT_EQ(count, NUM_ENTITIES); + EXPECT_EQ(mgr.size(), NUM_ENTITIES); + EXPECT_LT(duration.count(), 10); // Should complete in < 10ms +} + +//============================================================================== +// Capacity +//============================================================================== + +TEST_F(EntitiesTests, Reserve) +{ + mgr.reserve(1000); + + // Reserve doesn't create entities + EXPECT_EQ(mgr.size(), 0); + + // Normal creation still works after reserve + entity e = mgr.create(); + EXPECT_TRUE(mgr.valid(e)); + EXPECT_EQ(mgr.size(), 1); +} + + diff --git a/tests/entity/entity_tests.cpp b/tests/entity/entity_tests.cpp index 2af813c..bc99805 100644 --- a/tests/entity/entity_tests.cpp +++ b/tests/entity/entity_tests.cpp @@ -34,7 +34,7 @@ TEST_F(EntityTests, CreateValidEntity) EXPECT_EQ(e.id(), 42); EXPECT_EQ(e.generation(), 5); - EXPECT_TRUE(e.valid()); + EXPECT_TRUE(e != entity::invalid()); } TEST_F(EntityTests, CreateBoundaryValues) @@ -43,26 +43,26 @@ TEST_F(EntityTests, CreateBoundaryValues) auto max_id_entity = entity::create(entity::MAX_ENTITIES, 0); EXPECT_EQ(max_id_entity.id(), entity::MAX_ENTITIES); EXPECT_EQ(max_id_entity.generation(), 0); - EXPECT_TRUE(max_id_entity.valid()); + EXPECT_TRUE(max_id_entity != entity::invalid()); // Test maximum generation (4,094) auto max_gen_entity = entity::create(0, entity::MAX_GENERATIONS); EXPECT_EQ(max_gen_entity.id(), 0); EXPECT_EQ(max_gen_entity.generation(), entity::MAX_GENERATIONS); - EXPECT_TRUE(max_gen_entity.valid()); + EXPECT_TRUE(max_gen_entity != entity::invalid()); // Test both at max auto max_both = entity::create(entity::MAX_ENTITIES, entity::MAX_GENERATIONS); EXPECT_EQ(max_both.id(), entity::MAX_ENTITIES); EXPECT_EQ(max_both.generation(), entity::MAX_GENERATIONS); - EXPECT_TRUE(max_both.valid()); + EXPECT_TRUE(max_both != entity::invalid()); } TEST_F(EntityTests, InvalidEntity) { auto invalid = entity::invalid(); - EXPECT_FALSE(invalid.valid()); + EXPECT_TRUE(invalid == entity::invalid()); EXPECT_EQ(invalid, entity::invalid()); // Two invalid entities are equal } diff --git a/tests/entity/sparse_set_tests.cpp b/tests/entity/sparse_set_tests.cpp index f5ff13a..b577257 100644 --- a/tests/entity/sparse_set_tests.cpp +++ b/tests/entity/sparse_set_tests.cpp @@ -241,6 +241,51 @@ TEST_F(SparseSetTests, MoveSemantics) EXPECT_TRUE(set3.contains(e2)); } +//============================================================================== +// Erase At (by dense index) +//============================================================================== + +TEST_F(SparseSetTests, EraseAtSwapAndPop) +{ + auto e1 = entity::create(10, 0); + auto e2 = entity::create(20, 0); + auto e3 = entity::create(30, 0); + + set.insert(e1); + set.insert(e2); + set.insert(e3); + + // Erase middle entity by index (e2 is at dense index 1) + set.erase_at(1); + + EXPECT_EQ(set.size(), 2); + EXPECT_TRUE(set.contains(e1)); + EXPECT_FALSE(set.contains(e2)); + EXPECT_TRUE(set.contains(e3)); + + // e3 should have been swapped to index 1 (e2's old position) + EXPECT_EQ(set.index(e1).value(), 0); + EXPECT_EQ(set.index(e3).value(), 1); + + // Erase last element by index (no swap needed) + set.erase_at(1); // e3 is now at index 1 (last) + + EXPECT_EQ(set.size(), 1); + EXPECT_TRUE(set.contains(e1)); + EXPECT_FALSE(set.contains(e3)); +} + +TEST_F(SparseSetTests, EraseAtOutOfBounds) +{ + auto e = entity::create(42, 0); + set.insert(e); + + set.erase_at(5); // Out of bounds, should be no-op + + EXPECT_EQ(set.size(), 1); + EXPECT_TRUE(set.contains(e)); +} + //============================================================================== // Capacity Management //============================================================================== diff --git a/tests/integration/ecs_integration_tests.cpp b/tests/integration/ecs_integration_tests.cpp deleted file mode 100644 index 3531cac..0000000 --- a/tests/integration/ecs_integration_tests.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include -#include -#include -#include - -using namespace gamecoe; - -//============================================================================== -// Test Component Types -//============================================================================== - -struct Position -{ - float x, y, z; -}; - -struct Velocity -{ - float dx, dy, dz; -}; - -struct Health -{ - int value; -}; - -// RAII type for lifecycle test -struct LifetimeTracker -{ - int *counter; - - LifetimeTracker(int *c) : counter(c) { ++(*counter); } - ~LifetimeTracker() - { - if (counter) - --(*counter); - } - - // Copyable for swap-and-pop - LifetimeTracker(const LifetimeTracker &other) : counter(other.counter) - { - if (counter) - ++(*counter); - } - LifetimeTracker &operator=(const LifetimeTracker &other) - { - if (this != &other) - { - if (counter) - --(*counter); - counter = other.counter; - if (counter) - ++(*counter); - } - return *this; - } - - // Moveable - LifetimeTracker(LifetimeTracker &&other) noexcept : counter(other.counter) - { - other.counter = nullptr; - } - LifetimeTracker &operator=(LifetimeTracker &&other) noexcept - { - if (this != &other) - { - if (counter) - --(*counter); - counter = other.counter; - other.counter = nullptr; - } - return *this; - } -}; - -//============================================================================== -// ECSIntegrationTests - ECS component interaction tests -//============================================================================== - -class ECSIntegrationTests : public ::testing::Test -{ -protected: - component_pool positions; - component_pool velocities; - component_pool healths; -}; - -//============================================================================== -// Multi-Component Entity -//============================================================================== - -TEST_F(ECSIntegrationTests, MultiComponentEntity) -{ - auto e = entity::create(42, 0); - - // Add 3 different component types to same entity - positions.add(e, Position{1.0f, 2.0f, 3.0f}); - velocities.add(e, Velocity{0.1f, 0.2f, 0.3f}); - healths.add(e, Health{100}); - - // Verify all components are accessible - EXPECT_TRUE(positions.contains(e)); - EXPECT_TRUE(velocities.contains(e)); - EXPECT_TRUE(healths.contains(e)); - - auto pos = positions.get(e); - auto vel = velocities.get(e); - auto hp = healths.get(e); - - EXPECT_TRUE(pos.has_value()); - EXPECT_TRUE(vel.has_value()); - EXPECT_TRUE(hp.has_value()); - - EXPECT_EQ(pos->get().x, 1.0f); - EXPECT_EQ(vel->get().dx, 0.1f); - EXPECT_EQ(hp->get().value, 100); -} - -//============================================================================== -// Component Lifecycle (RAII) -//============================================================================== - -TEST_F(ECSIntegrationTests, ComponentLifecycle) -{ - component_pool trackers; - int counter = 0; - - auto e1 = entity::create(1, 0); - auto e2 = entity::create(2, 0); - auto e3 = entity::create(3, 0); - - // Add components (counter should increment) - trackers.add(e1, LifetimeTracker{&counter}); - trackers.add(e2, LifetimeTracker{&counter}); - trackers.add(e3, LifetimeTracker{&counter}); - - EXPECT_EQ(counter, 3); - - // Remove e2 (counter should decrement) - trackers.remove(e2); - EXPECT_EQ(counter, 2); - - // Clear all (counter should go to 0) - trackers.clear(); - EXPECT_EQ(counter, 0); -} - -//============================================================================== -// Bulk Operations Performance -//============================================================================== - -TEST_F(ECSIntegrationTests, BulkOperations) -{ - const std::size_t NUM_ENTITIES = 1000; - - auto start = std::chrono::high_resolution_clock::now(); - - // Add 1000 entities with components - for (std::uint32_t i = 0; i < NUM_ENTITIES; ++i) - { - auto e = entity::create(i, 0); - positions.add(e, Position{static_cast(i), 0.0f, 0.0f}); - } - - // Iterate all entities - int count = 0; - positions.for_each([&count](entity e, Position &pos) - { - pos.x += 1.0f; // Simple transform - ++count; }); - - auto end = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end - start); - - EXPECT_EQ(count, NUM_ENTITIES); - EXPECT_LT(duration.count(), 10); // Should complete in < 10ms - - // Verify all entities were updated - for (std::uint32_t i = 0; i < NUM_ENTITIES; ++i) - { - auto e = entity::create(i, 0); - auto pos = positions.get(e); - EXPECT_TRUE(pos.has_value()); - EXPECT_EQ(pos->get().x, static_cast(i) + 1.0f); - } -} - -//============================================================================== -// Invalid Entity Handling -//============================================================================== - -TEST_F(ECSIntegrationTests, InvalidEntityHandling) -{ - auto invalid = entity::invalid(); - - // Operations on invalid entity should be safe - EXPECT_FALSE(positions.contains(invalid)); - EXPECT_FALSE(positions.get(invalid).has_value()); - - // Remove should be no-op - positions.remove(invalid); - EXPECT_EQ(positions.size(), 0); -} diff --git a/tests/main.cpp b/tests/main.cpp index 3ddd2de..0dabc50 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -15,7 +15,7 @@ int printHelp() std::cout << " EntityTests - Entity handle tests" << std::endl; std::cout << " SparseSetTests - Sparse set data structure tests" << std::endl; std::cout << " ComponentPoolTests - Component pool wrapper tests" << std::endl; - std::cout << " ECSIntegrationTests - ECS component interaction tests" << std::endl; + std::cout << " EntitiesTests - Entities manager tests" << std::endl; std::cout << std::endl; std::cout << "Example usage:" << std::endl; std::cout << " ./gamecoe_tests --suite=EntityTests" << std::endl; @@ -30,8 +30,7 @@ int main(int argc, char **argv) std::cout << "====================================================" << std::endl; std::cout << std::endl; std::cout << "Comprehensive testing for gamecoe." << std::endl; - std::cout << "Testing Entity Module: entity, sparse_set, component_pool" << std::endl; - std::cout << "Testing Integration Module: Multi-component entities, RAII, bulk operations" << std::endl; + std::cout << "Testing Entity Module: entity, sparse_set, component_pool, entities" << std::endl; std::cout << std::endl; testcoe::init(&argc, argv); From 44dbe9f00d081d74b24d5046aacc3bdbae549692 Mon Sep 17 00:00:00 2001 From: nircoe Date: Sat, 14 Feb 2026 17:54:35 +0200 Subject: [PATCH 4/5] Remove std::atomic --- include/gamecoe/entity/entities.hpp | 5 ++--- include/gamecoe/entity/entity.hpp | 2 -- src/gamecoe/entity/entities.cpp | 11 +++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/include/gamecoe/entity/entities.hpp b/include/gamecoe/entity/entities.hpp index 1214f41..4efe64c 100644 --- a/include/gamecoe/entity/entities.hpp +++ b/include/gamecoe/entity/entities.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -14,13 +13,13 @@ namespace gamecoe { class entities { - static std::atomic s_component_id; + static std::uint32_t s_component_id; std::vector> m_pools; std::vector m_recycle_ids; std::vector m_generations; - std::atomic m_current_entity_id; + std::uint32_t m_current_entity_id{0}; // Returns static and unique id for component T template diff --git a/include/gamecoe/entity/entity.hpp b/include/gamecoe/entity/entity.hpp index d4a6ba2..ee1b104 100644 --- a/include/gamecoe/entity/entity.hpp +++ b/include/gamecoe/entity/entity.hpp @@ -7,8 +7,6 @@ namespace gamecoe { - class entities; - struct entity { static constexpr std::uint8_t ID_BITS = 20; diff --git a/src/gamecoe/entity/entities.cpp b/src/gamecoe/entity/entities.cpp index a22d1e9..ed935ae 100644 --- a/src/gamecoe/entity/entities.cpp +++ b/src/gamecoe/entity/entities.cpp @@ -1,11 +1,10 @@ -#include "gamecoe/entity/entity.hpp" +#include #include #include -#include namespace gamecoe { - std::atomic entities::s_component_id{0}; + std::uint32_t entities::s_component_id{0}; entity entities::create() { @@ -14,7 +13,7 @@ namespace gamecoe if (m_recycle_ids.empty()) { - id = m_current_entity_id.load(); + id = m_current_entity_id; assert(id <= entity::MAX_ENTITIES && "entities::create(): entity limit reached"); m_current_entity_id++; generation = 0; @@ -63,8 +62,8 @@ namespace gamecoe std::size_t entities::size() const { - assert(static_cast(m_current_entity_id.load()) >= m_recycle_ids.size()); - return static_cast(m_current_entity_id.load()) - m_recycle_ids.size(); + assert(static_cast(m_current_entity_id) >= m_recycle_ids.size()); + return static_cast(m_current_entity_id) - m_recycle_ids.size(); } } // namespace gamecoe From 014c002bd1c39ca4da58ecc2152d57c9d0712bda Mon Sep 17 00:00:00 2001 From: nircoe Date: Sat, 14 Feb 2026 18:06:20 +0200 Subject: [PATCH 5/5] CI pipeline fixes --- include/gamecoe/entity/sparse_set.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/gamecoe/entity/sparse_set.hpp b/include/gamecoe/entity/sparse_set.hpp index 3dcdcbe..43d99ef 100644 --- a/include/gamecoe/entity/sparse_set.hpp +++ b/include/gamecoe/entity/sparse_set.hpp @@ -59,7 +59,7 @@ namespace gamecoe page->fill(TOMBSTONE); } - (*page)[index_in_page(e)] = pack_dense_index(m_dense.size(), e.generation()); + (*page)[index_in_page(e)] = pack_dense_index(static_cast(m_dense.size()), e.generation()); m_dense.push_back(e); }