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..28be603 --- /dev/null +++ b/src/engine/ComponentRegistry.h @@ -0,0 +1,85 @@ +#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; + +class ComponentRegistry { +public: + 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; + } + + template + [[nodiscard]] std::optional tryTypeOf() const noexcept { + const auto it = typeIds_.find(typeid(T)); + if (it == typeIds_.end()) { + return std::nullopt; + } + return it->second; + } + + template + [[nodiscard]] bool isRegistered() const noexcept { + return typeIds_.contains(typeid(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); + } + + 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..1cf75c1 --- /dev/null +++ b/src/engine/EcsCore.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include "engine/ComponentRegistry.h" +#include "engine/Entity.h" +#include "engine/EntityRegistry.h" + +namespace safecrowd::engine { + +class EcsCore { +public: + explicit EcsCore(std::size_t maxEntityCount = 4096) + : entityRegistry_(maxEntityCount) {} + + [[nodiscard]] Entity createEntity() { + return entityRegistry_.allocate(); + } + + void destroyEntity(Entity entity) { + componentRegistry_.notifyEntityDestroyed(entity); + entityRegistry_.release(entity); + } + + [[nodiscard]] bool isAlive(Entity entity) const noexcept { + return entityRegistry_.isAlive(entity); + } + + template + void addComponent(Entity entity, T component) { + Signature sig = entityRegistry_.signatureOf(entity); + const ComponentType typeId = componentRegistry_.getOrRegister(); + componentRegistry_.storageFor().insert(entity, std::move(component)); + + sig.set(typeId); + entityRegistry_.setSignature(entity, sig); + } + + 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); + } + + template + [[nodiscard]] T& getComponent(Entity entity) { + return componentRegistry_.storageFor().get(entity); + } + + template + [[nodiscard]] const T& getComponent(Entity entity) const { + return componentRegistry_.storageFor().get(entity); + } + + 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..a119b2a --- /dev/null +++ b/tests/EcsCoreTests.cpp @@ -0,0 +1,139 @@ +#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 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); + 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)); + + 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) { + 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_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); + + 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)); + + 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())); +}