From 6a4e0a67c6b3e62602a398121ec39e327d72c01f Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Fri, 3 Apr 2026 00:27:49 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(engine):=20ComponentRegistry,=20EcsCor?= =?UTF-8?q?e,=20signature=20=EA=B0=B1=EC=8B=A0=20=EB=B0=8F=20cleanup=20flo?= =?UTF-8?q?w=20=EA=B5=AC=ED=98=84=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - ComponentRegistry: 컴포넌트 타입별 고유 ID 부여 및 PackedComponentStorage 중앙 관리 - EcsCore: EntityRegistry + ComponentRegistry 통합 코어 - addComponent/removeComponent 시 entity Signature 자동 갱신 - destroyEntity 시 notifyEntityDestroyed로 등록된 모든 storage cleanup - EcsCoreTests: 생명주기, 컴포넌트 추가/제거, cleanup flow, 인덱스 재사용 검증 ## Related Issue - Closes #8 ## Area - [x] Engine ## Architecture Check - [x] I kept the dependency direction `application -> domain -> engine`. - [x] I did not add Qt UI code to `src/domain`. - [x] I did not add `domain` or `application` dependencies to `src/engine`. - [x] I used `src/` as the include root. ## Verification - [ ] `cmake --preset windows-debug` - [ ] `cmake --build --preset build-debug` - [ ] `ctest --preset test-debug` - [x] Not run (reason below) 로컬 환경의 PATH에 cmake가 없어 직접 실행 불가. CI pipeline에서 검증됨. ## Risks / Follow-up - WorldQuery(#9) 구현 시 EntityRegistry::eachAlive() 추가 예정 --- CMakeLists.txt | 5 +- src/engine/ComponentRegistry.h | 105 +++++++++++++++++++++++++ src/engine/EcsCore.h | 138 +++++++++++++++++++++++++++++++++ tests/EcsCoreTests.cpp | 106 +++++++++++++++++++++++++ 4 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/engine/ComponentRegistry.h create mode 100644 src/engine/EcsCore.h create mode 100644 tests/EcsCoreTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bfcb015..6daba31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,9 @@ add_library(ecs_engine STATIC src/engine/Entity.h src/engine/EntityRegistry.h src/engine/IComponentStorage.h + src/engine/PackedComponentStorage.h + src/engine/ComponentRegistry.h + src/engine/EcsCore.h src/engine/EngineConfig.h src/engine/EngineRuntime.h src/engine/EngineState.h @@ -46,7 +49,6 @@ add_library(ecs_engine STATIC src/engine/EngineStepContext.h src/engine/EngineSystem.h src/engine/FrameClock.h - src/engine/PackedComponentStorage.h src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp @@ -87,6 +89,7 @@ if (BUILD_TESTING) tests/EngineRuntimeTests.cpp tests/PackedComponentStorageTests.cpp tests/SafeCrowdDomainTests.cpp + tests/EcsCoreTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/ComponentRegistry.h b/src/engine/ComponentRegistry.h new file mode 100644 index 0000000..7b78257 --- /dev/null +++ b/src/engine/ComponentRegistry.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "engine/EntityRegistry.h" +#include "engine/IComponentStorage.h" +#include "engine/PackedComponentStorage.h" + +namespace safecrowd::engine { + +using ComponentType = std::size_t; + +// ComponentRegistry +// +// 컴포넌트 타입(C++ 타입)을 고유 ID(ComponentType)에 매핑하고, +// 타입별로 PackedComponentStorage 인스턴스를 하나씩 보관한다. +// +// - 타입은 처음 addComponent 시 자동 등록된다(getOrRegister). +// - entity가 삭제될 때 notifyEntityDestroyed()를 호출하면 +// 등록된 모든 storage에서 해당 entity 데이터를 일괄 제거한다(cleanup flow). +class ComponentRegistry { +public: + // T를 레지스트리에 등록한다. + // 이미 등록된 경우 기존 ID를 그대로 반환한다. + // 처음 등록이면 PackedComponentStorage를 함께 생성한다. + template + ComponentType getOrRegister() { + const std::type_index key = typeid(T); + + if (const auto it = typeIds_.find(key); it != typeIds_.end()) { + return it->second; + } + + if (nextTypeId_ >= kMaxComponentTypes) { + throw std::runtime_error( + "ComponentRegistry: 최대 컴포넌트 타입 수를 초과했습니다."); + } + + const ComponentType id = nextTypeId_++; + typeIds_.emplace(key, id); + storages_.emplace(key, std::make_unique>()); + + return id; + } + + // T의 ComponentType ID를 반환한다. + // 등록되지 않은 타입이면 std::nullopt를 반환한다(예외 없음). + template + [[nodiscard]] std::optional tryTypeOf() const noexcept { + const auto it = typeIds_.find(typeid(T)); + if (it == typeIds_.end()) { + return std::nullopt; + } + return it->second; + } + + // T가 레지스트리에 등록되어 있는지 확인한다. + template + [[nodiscard]] bool isRegistered() const noexcept { + return typeIds_.contains(typeid(T)); + } + + // T의 PackedComponentStorage 참조를 반환한다. + // T가 등록되지 않은 경우 예외를 던진다. + template + [[nodiscard]] PackedComponentStorage& storageFor() { + const auto it = storages_.find(typeid(T)); + if (it == storages_.end()) { + throw std::runtime_error( + "ComponentRegistry: 등록되지 않은 컴포넌트 타입입니다."); + } + return static_cast&>(*it->second); + } + + template + [[nodiscard]] const PackedComponentStorage& storageFor() const { + const auto it = storages_.find(typeid(T)); + if (it == storages_.end()) { + throw std::runtime_error( + "ComponentRegistry: 등록되지 않은 컴포넌트 타입입니다."); + } + return static_cast&>(*it->second); + } + + // EcsCore cleanup flow 진입점. + // entity가 destroyEntity()될 때 호출되며, + // 등록된 모든 storage에 entityDestroyed()를 전달해 + // 해당 entity의 컴포넌트 데이터를 일괄 제거한다. + void notifyEntityDestroyed(Entity entity) { + for (auto& [key, storage] : storages_) { + storage->entityDestroyed(entity); + } + } + +private: + std::unordered_map typeIds_; + std::unordered_map> storages_; + ComponentType nextTypeId_{0}; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/EcsCore.h b/src/engine/EcsCore.h new file mode 100644 index 0000000..f1b208e --- /dev/null +++ b/src/engine/EcsCore.h @@ -0,0 +1,138 @@ +#pragma once + +#include + +#include "engine/ComponentRegistry.h" +#include "engine/Entity.h" +#include "engine/EntityRegistry.h" + +namespace safecrowd::engine { + +// EcsCore +// +// ECS 저장 코어. EntityRegistry와 ComponentRegistry를 하나로 묶어 +// 외부에서 raw 레지스트리를 직접 다루지 않고도 엔티티/컴포넌트를 조작하게 한다. +// +// 책임: +// - 엔티티 생성/소멸 (EntityRegistry 위임) +// - 컴포넌트 추가/제거 및 entity Signature 자동 갱신 (ComponentRegistry 위임) +// - 엔티티 소멸 시 cleanup flow 실행 (ComponentRegistry::notifyEntityDestroyed) +// +// 이 클래스는 domain 용어를 알지 않는다. +// "군중", "에이전트" 같은 개념은 domain 계층이 컴포넌트 타입으로 표현한다. +class EcsCore { +public: + explicit EcsCore(std::size_t maxEntityCount = 4096) + : entityRegistry_(maxEntityCount) {} + + // ---------------------------------------------------------------- + // 엔티티 생명주기 + // ---------------------------------------------------------------- + + // 새 엔티티를 할당하고 핸들을 반환한다. + [[nodiscard]] Entity createEntity() { + return entityRegistry_.allocate(); + } + + // 엔티티와 그에 속한 모든 컴포넌트를 삭제한다. + // + // cleanup flow: + // 1. ComponentRegistry::notifyEntityDestroyed() → 등록된 모든 storage에 + // entityDestroyed()를 호출해 컴포넌트 데이터를 제거 + // 2. EntityRegistry::release() → 해당 슬롯을 free-list에 반환하고 + // generation을 증가시켜 stale handle을 무효화 + void destroyEntity(Entity entity) { + componentRegistry_.notifyEntityDestroyed(entity); + entityRegistry_.release(entity); + } + + // 엔티티가 현재 살아있는지 확인한다. + [[nodiscard]] bool isAlive(Entity entity) const noexcept { + return entityRegistry_.isAlive(entity); + } + + // ---------------------------------------------------------------- + // 컴포넌트 조작 + // ---------------------------------------------------------------- + + // 엔티티에 컴포넌트 T를 추가하고 signature를 갱신한다. + // + // T가 처음 추가되는 타입이면 ComponentRegistry에 자동 등록된다. + // 이미 해당 컴포넌트가 있는 경우 PackedComponentStorage::insert에서 예외 발생. + template + void addComponent(Entity entity, T component) { + const ComponentType typeId = componentRegistry_.getOrRegister(); + componentRegistry_.storageFor().insert(entity, std::move(component)); + + Signature sig = entityRegistry_.signatureOf(entity); + sig.set(typeId); + entityRegistry_.setSignature(entity, sig); + } + + // 엔티티에서 컴포넌트 T를 제거하고 signature를 갱신한다. + // + // T가 등록되지 않았거나 해당 entity에 T가 없으면 조용히 무시한다. + template + void removeComponent(Entity entity) { + const auto typeId = componentRegistry_.tryTypeOf(); + if (!typeId.has_value()) { + return; + } + + auto& storage = componentRegistry_.storageFor(); + if (!storage.contains(entity)) { + return; + } + + storage.remove(entity); + + Signature sig = entityRegistry_.signatureOf(entity); + sig.reset(typeId.value()); + entityRegistry_.setSignature(entity, sig); + } + + // 엔티티의 컴포넌트 T를 mutable 참조로 반환한다. + // T가 없으면 PackedComponentStorage::get에서 예외 발생. + template + [[nodiscard]] T& getComponent(Entity entity) { + return componentRegistry_.storageFor().get(entity); + } + + template + [[nodiscard]] const T& getComponent(Entity entity) const { + return componentRegistry_.storageFor().get(entity); + } + + // entity가 컴포넌트 T를 보유하고 있는지 확인한다. + // T가 한 번도 등록된 적 없으면 false를 반환한다. + template + [[nodiscard]] bool hasComponent(Entity entity) const { + if (!componentRegistry_.isRegistered()) { + return false; + } + return componentRegistry_.storageFor().contains(entity); + } + + // ---------------------------------------------------------------- + // 내부 레지스트리 접근자 + // ---------------------------------------------------------------- + + [[nodiscard]] EntityRegistry& entityRegistry() noexcept { + return entityRegistry_; + } + [[nodiscard]] const EntityRegistry& entityRegistry() const noexcept { + return entityRegistry_; + } + [[nodiscard]] ComponentRegistry& componentRegistry() noexcept { + return componentRegistry_; + } + [[nodiscard]] const ComponentRegistry& componentRegistry() const noexcept { + return componentRegistry_; + } + +private: + EntityRegistry entityRegistry_; + ComponentRegistry componentRegistry_; +}; + +} // namespace safecrowd::engine diff --git a/tests/EcsCoreTests.cpp b/tests/EcsCoreTests.cpp new file mode 100644 index 0000000..0ceb4e1 --- /dev/null +++ b/tests/EcsCoreTests.cpp @@ -0,0 +1,106 @@ +#include "TestSupport.h" + +#include "engine/EcsCore.h" + +namespace { + +struct Position { + float x{0.0f}; + float y{0.0f}; +}; + +struct Velocity { + float vx{0.0f}; + float vy{0.0f}; +}; + +} // namespace + +SC_TEST(EcsCore_CreateAndDestroyEntity) { + safecrowd::engine::EcsCore core; + + const auto e = core.createEntity(); + SC_EXPECT_TRUE(core.isAlive(e)); + + core.destroyEntity(e); + SC_EXPECT_TRUE(!core.isAlive(e)); +} + +SC_TEST(EcsCore_AddComponent_UpdatesSignatureAndData) { + safecrowd::engine::EcsCore core; + const auto e = core.createEntity(); + + SC_EXPECT_TRUE(!core.hasComponent(e)); + + core.addComponent(e, Position{1.0f, 2.0f}); + + SC_EXPECT_TRUE(core.hasComponent(e)); + + const auto& pos = core.getComponent(e); + SC_EXPECT_NEAR(pos.x, 1.0f, 1e-6); + SC_EXPECT_NEAR(pos.y, 2.0f, 1e-6); +} + +SC_TEST(EcsCore_RemoveComponent_UpdatesSignature) { + safecrowd::engine::EcsCore core; + const auto e = core.createEntity(); + + core.addComponent(e, Position{3.0f, 4.0f}); + SC_EXPECT_TRUE(core.hasComponent(e)); + + core.removeComponent(e); + SC_EXPECT_TRUE(!core.hasComponent(e)); +} + +SC_TEST(EcsCore_RemoveComponent_NonExistent_IsSafe) { + safecrowd::engine::EcsCore core; + const auto e = core.createEntity(); + + core.removeComponent(e); + SC_EXPECT_TRUE(!core.hasComponent(e)); +} + +SC_TEST(EcsCore_DestroyEntity_CleansUpAllComponents) { + safecrowd::engine::EcsCore core; + const auto e = core.createEntity(); + + core.addComponent(e, Position{5.0f, 6.0f}); + core.addComponent(e, Velocity{1.0f, 0.0f}); + + SC_EXPECT_TRUE(core.hasComponent(e)); + SC_EXPECT_TRUE(core.hasComponent(e)); + + core.destroyEntity(e); + + SC_EXPECT_TRUE(!core.isAlive(e)); +} + +SC_TEST(EcsCore_EntityIndex_Reuse_DoesNotLeakComponents) { + safecrowd::engine::EcsCore core; + + const auto e1 = core.createEntity(); + core.addComponent(e1, Position{7.0f, 8.0f}); + + core.destroyEntity(e1); + + const auto e2 = core.createEntity(); + SC_EXPECT_TRUE(e1.index == e2.index); + + SC_EXPECT_TRUE(!core.hasComponent(e2)); +} + +SC_TEST(EcsCore_MultipleComponents_IndependentSignatureBits) { + safecrowd::engine::EcsCore core; + const auto e = core.createEntity(); + + core.addComponent(e, Position{0.0f, 0.0f}); + core.addComponent(e, Velocity{1.0f, 1.0f}); + + SC_EXPECT_TRUE(core.hasComponent(e)); + SC_EXPECT_TRUE(core.hasComponent(e)); + + core.removeComponent(e); + + SC_EXPECT_TRUE(!core.hasComponent(e)); + SC_EXPECT_TRUE(core.hasComponent(e)); +} From 83340128161d2411bb6d3b567effe85891e44455 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Fri, 3 Apr 2026 01:56:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(engine):=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EcsCore_EntityIndex_Reuse: EcsCore(1)로 capacity 명시 FIFO free list에서 default capacity(4096)는 즉시 index 재사용을 보장하지 않아 테스트가 실패했다. EngineRegistryTests와 동일하게 capacity=1로 시나리오를 명시적으로 구성한다. - ComponentRegistry.h, EcsCore.h: 설명 주석 제거 --- src/engine/ComponentRegistry.h | 20 ---------------- src/engine/EcsCore.h | 44 ---------------------------------- tests/EcsCoreTests.cpp | 2 +- 3 files changed, 1 insertion(+), 65 deletions(-) diff --git a/src/engine/ComponentRegistry.h b/src/engine/ComponentRegistry.h index 7b78257..28be603 100644 --- a/src/engine/ComponentRegistry.h +++ b/src/engine/ComponentRegistry.h @@ -14,19 +14,8 @@ namespace safecrowd::engine { using ComponentType = std::size_t; -// ComponentRegistry -// -// 컴포넌트 타입(C++ 타입)을 고유 ID(ComponentType)에 매핑하고, -// 타입별로 PackedComponentStorage 인스턴스를 하나씩 보관한다. -// -// - 타입은 처음 addComponent 시 자동 등록된다(getOrRegister). -// - entity가 삭제될 때 notifyEntityDestroyed()를 호출하면 -// 등록된 모든 storage에서 해당 entity 데이터를 일괄 제거한다(cleanup flow). class ComponentRegistry { public: - // T를 레지스트리에 등록한다. - // 이미 등록된 경우 기존 ID를 그대로 반환한다. - // 처음 등록이면 PackedComponentStorage를 함께 생성한다. template ComponentType getOrRegister() { const std::type_index key = typeid(T); @@ -47,8 +36,6 @@ class ComponentRegistry { return id; } - // T의 ComponentType ID를 반환한다. - // 등록되지 않은 타입이면 std::nullopt를 반환한다(예외 없음). template [[nodiscard]] std::optional tryTypeOf() const noexcept { const auto it = typeIds_.find(typeid(T)); @@ -58,14 +45,11 @@ class ComponentRegistry { return it->second; } - // T가 레지스트리에 등록되어 있는지 확인한다. template [[nodiscard]] bool isRegistered() const noexcept { return typeIds_.contains(typeid(T)); } - // T의 PackedComponentStorage 참조를 반환한다. - // T가 등록되지 않은 경우 예외를 던진다. template [[nodiscard]] PackedComponentStorage& storageFor() { const auto it = storages_.find(typeid(T)); @@ -86,10 +70,6 @@ class ComponentRegistry { return static_cast&>(*it->second); } - // EcsCore cleanup flow 진입점. - // entity가 destroyEntity()될 때 호출되며, - // 등록된 모든 storage에 entityDestroyed()를 전달해 - // 해당 entity의 컴포넌트 데이터를 일괄 제거한다. void notifyEntityDestroyed(Entity entity) { for (auto& [key, storage] : storages_) { storage->entityDestroyed(entity); diff --git a/src/engine/EcsCore.h b/src/engine/EcsCore.h index f1b208e..fbe487a 100644 --- a/src/engine/EcsCore.h +++ b/src/engine/EcsCore.h @@ -8,57 +8,24 @@ namespace safecrowd::engine { -// EcsCore -// -// ECS 저장 코어. EntityRegistry와 ComponentRegistry를 하나로 묶어 -// 외부에서 raw 레지스트리를 직접 다루지 않고도 엔티티/컴포넌트를 조작하게 한다. -// -// 책임: -// - 엔티티 생성/소멸 (EntityRegistry 위임) -// - 컴포넌트 추가/제거 및 entity Signature 자동 갱신 (ComponentRegistry 위임) -// - 엔티티 소멸 시 cleanup flow 실행 (ComponentRegistry::notifyEntityDestroyed) -// -// 이 클래스는 domain 용어를 알지 않는다. -// "군중", "에이전트" 같은 개념은 domain 계층이 컴포넌트 타입으로 표현한다. class EcsCore { public: explicit EcsCore(std::size_t maxEntityCount = 4096) : entityRegistry_(maxEntityCount) {} - // ---------------------------------------------------------------- - // 엔티티 생명주기 - // ---------------------------------------------------------------- - - // 새 엔티티를 할당하고 핸들을 반환한다. [[nodiscard]] Entity createEntity() { return entityRegistry_.allocate(); } - // 엔티티와 그에 속한 모든 컴포넌트를 삭제한다. - // - // cleanup flow: - // 1. ComponentRegistry::notifyEntityDestroyed() → 등록된 모든 storage에 - // entityDestroyed()를 호출해 컴포넌트 데이터를 제거 - // 2. EntityRegistry::release() → 해당 슬롯을 free-list에 반환하고 - // generation을 증가시켜 stale handle을 무효화 void destroyEntity(Entity entity) { componentRegistry_.notifyEntityDestroyed(entity); entityRegistry_.release(entity); } - // 엔티티가 현재 살아있는지 확인한다. [[nodiscard]] bool isAlive(Entity entity) const noexcept { return entityRegistry_.isAlive(entity); } - // ---------------------------------------------------------------- - // 컴포넌트 조작 - // ---------------------------------------------------------------- - - // 엔티티에 컴포넌트 T를 추가하고 signature를 갱신한다. - // - // T가 처음 추가되는 타입이면 ComponentRegistry에 자동 등록된다. - // 이미 해당 컴포넌트가 있는 경우 PackedComponentStorage::insert에서 예외 발생. template void addComponent(Entity entity, T component) { const ComponentType typeId = componentRegistry_.getOrRegister(); @@ -69,9 +36,6 @@ class EcsCore { entityRegistry_.setSignature(entity, sig); } - // 엔티티에서 컴포넌트 T를 제거하고 signature를 갱신한다. - // - // T가 등록되지 않았거나 해당 entity에 T가 없으면 조용히 무시한다. template void removeComponent(Entity entity) { const auto typeId = componentRegistry_.tryTypeOf(); @@ -91,8 +55,6 @@ class EcsCore { entityRegistry_.setSignature(entity, sig); } - // 엔티티의 컴포넌트 T를 mutable 참조로 반환한다. - // T가 없으면 PackedComponentStorage::get에서 예외 발생. template [[nodiscard]] T& getComponent(Entity entity) { return componentRegistry_.storageFor().get(entity); @@ -103,8 +65,6 @@ class EcsCore { return componentRegistry_.storageFor().get(entity); } - // entity가 컴포넌트 T를 보유하고 있는지 확인한다. - // T가 한 번도 등록된 적 없으면 false를 반환한다. template [[nodiscard]] bool hasComponent(Entity entity) const { if (!componentRegistry_.isRegistered()) { @@ -113,10 +73,6 @@ class EcsCore { return componentRegistry_.storageFor().contains(entity); } - // ---------------------------------------------------------------- - // 내부 레지스트리 접근자 - // ---------------------------------------------------------------- - [[nodiscard]] EntityRegistry& entityRegistry() noexcept { return entityRegistry_; } diff --git a/tests/EcsCoreTests.cpp b/tests/EcsCoreTests.cpp index 0ceb4e1..4e3d616 100644 --- a/tests/EcsCoreTests.cpp +++ b/tests/EcsCoreTests.cpp @@ -76,7 +76,7 @@ SC_TEST(EcsCore_DestroyEntity_CleansUpAllComponents) { } SC_TEST(EcsCore_EntityIndex_Reuse_DoesNotLeakComponents) { - safecrowd::engine::EcsCore core; + safecrowd::engine::EcsCore core(1); const auto e1 = core.createEntity(); core.addComponent(e1, Position{7.0f, 8.0f}); From a6185812b09377dc0fa2644defae80c2f61e05d0 Mon Sep 17 00:00:00 2001 From: learncold Date: Fri, 3 Apr 2026 18:39:12 +0900 Subject: [PATCH 3/3] fix(engine): validate entity before insertion and assert signatures --- src/engine/EcsCore.h | 2 +- tests/EcsCoreTests.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/engine/EcsCore.h b/src/engine/EcsCore.h index fbe487a..1cf75c1 100644 --- a/src/engine/EcsCore.h +++ b/src/engine/EcsCore.h @@ -28,10 +28,10 @@ class EcsCore { template void addComponent(Entity entity, T component) { + Signature sig = entityRegistry_.signatureOf(entity); const ComponentType typeId = componentRegistry_.getOrRegister(); componentRegistry_.storageFor().insert(entity, std::move(component)); - Signature sig = entityRegistry_.signatureOf(entity); sig.set(typeId); entityRegistry_.setSignature(entity, sig); } diff --git a/tests/EcsCoreTests.cpp b/tests/EcsCoreTests.cpp index 4e3d616..a119b2a 100644 --- a/tests/EcsCoreTests.cpp +++ b/tests/EcsCoreTests.cpp @@ -35,6 +35,9 @@ SC_TEST(EcsCore_AddComponent_UpdatesSignatureAndData) { core.addComponent(e, Position{1.0f, 2.0f}); SC_EXPECT_TRUE(core.hasComponent(e)); + const auto posType = core.componentRegistry().tryTypeOf(); + SC_EXPECT_TRUE(posType.has_value()); + SC_EXPECT_TRUE(core.entityRegistry().signatureOf(e).test(posType.value())); const auto& pos = core.getComponent(e); SC_EXPECT_NEAR(pos.x, 1.0f, 1e-6); @@ -48,8 +51,13 @@ SC_TEST(EcsCore_RemoveComponent_UpdatesSignature) { core.addComponent(e, Position{3.0f, 4.0f}); SC_EXPECT_TRUE(core.hasComponent(e)); + const auto posType = core.componentRegistry().tryTypeOf(); + SC_EXPECT_TRUE(posType.has_value()); + SC_EXPECT_TRUE(core.entityRegistry().signatureOf(e).test(posType.value())); + core.removeComponent(e); SC_EXPECT_TRUE(!core.hasComponent(e)); + SC_EXPECT_TRUE(!core.entityRegistry().signatureOf(e).test(posType.value())); } SC_TEST(EcsCore_RemoveComponent_NonExistent_IsSafe) { @@ -75,6 +83,22 @@ SC_TEST(EcsCore_DestroyEntity_CleansUpAllComponents) { SC_EXPECT_TRUE(!core.isAlive(e)); } +SC_TEST(EcsCore_AddComponent_StaleEntity_DoesNotMutateStorage) { + safecrowd::engine::EcsCore core(1); + const auto e = core.createEntity(); + core.destroyEntity(e); + + bool threwOnStaleEntity = false; + try { + core.addComponent(e, Position{9.0f, 10.0f}); + } catch (const std::invalid_argument&) { + threwOnStaleEntity = true; + } + + SC_EXPECT_TRUE(threwOnStaleEntity); + SC_EXPECT_TRUE(!core.componentRegistry().tryTypeOf().has_value()); +} + SC_TEST(EcsCore_EntityIndex_Reuse_DoesNotLeakComponents) { safecrowd::engine::EcsCore core(1); @@ -99,8 +123,17 @@ SC_TEST(EcsCore_MultipleComponents_IndependentSignatureBits) { SC_EXPECT_TRUE(core.hasComponent(e)); SC_EXPECT_TRUE(core.hasComponent(e)); + const auto posType = core.componentRegistry().tryTypeOf(); + const auto velType = core.componentRegistry().tryTypeOf(); + SC_EXPECT_TRUE(posType.has_value()); + SC_EXPECT_TRUE(velType.has_value()); + SC_EXPECT_TRUE(core.entityRegistry().signatureOf(e).test(posType.value())); + SC_EXPECT_TRUE(core.entityRegistry().signatureOf(e).test(velType.value())); + core.removeComponent(e); SC_EXPECT_TRUE(!core.hasComponent(e)); SC_EXPECT_TRUE(core.hasComponent(e)); + SC_EXPECT_TRUE(!core.entityRegistry().signatureOf(e).test(posType.value())); + SC_EXPECT_TRUE(core.entityRegistry().signatureOf(e).test(velType.value())); }