diff --git a/CMakeLists.txt b/CMakeLists.txt index 641992f..cf1672f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,8 @@ set(SAFECROWD_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") add_library(ecs_engine STATIC src/engine/Entity.h src/engine/EntityRegistry.h + src/engine/ComponentRegistry.h + src/engine/EcsCore.h src/engine/IComponentStorage.h src/engine/EngineConfig.h src/engine/EngineRuntime.h @@ -81,6 +83,7 @@ if (BUILD_TESTING) tests/TestSupport.h tests/EngineRegistryTests.cpp tests/FrameClockTests.cpp + tests/EcsCoreTests.cpp tests/EngineRuntimeTests.cpp tests/PackedComponentStorageTests.cpp tests/SafeCrowdDomainTests.cpp diff --git a/src/engine/ComponentRegistry.h b/src/engine/ComponentRegistry.h new file mode 100644 index 0000000..3f01aae --- /dev/null +++ b/src/engine/ComponentRegistry.h @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "engine/Entity.h" +#include "engine/EntityRegistry.h" +#include "engine/IComponentStorage.h" +#include "engine/PackedComponentStorage.h" + +namespace safecrowd::engine { + +using ComponentType = std::size_t; + +class ComponentRegistry { +public: + template + void registerType() { + const std::type_index key{typeid(T)}; + + if (const auto it = typeToId_.find(key); it != typeToId_.end()) { + return; // idempotent + } + + if (nextId_ >= kMaxComponentTypes) { + throw std::overflow_error("Component type limit exceeded."); + } + + const ComponentType id = nextId_++; + typeToId_.emplace(key, id); + + if (storages_.size() < (id + 1U)) { + storages_.resize(id + 1U); + } + + storages_[id] = std::make_unique>(); + } + + template + [[nodiscard]] ComponentType componentType() const { + const std::type_index key{typeid(T)}; + const auto it = typeToId_.find(key); + if (it == typeToId_.end()) { + throw std::logic_error("Component type not registered. Call registerType() during initialization."); + } + return it->second; + } + + template + [[nodiscard]] PackedComponentStorage& storage() { + const ComponentType id = componentType(); + + if (id >= storages_.size() || storages_[id] == nullptr) { + // Storage may be cleared during shutdown; recreate lazily while type ids remain valid. + if (id >= storages_.size()) { + storages_.resize(id + 1U); + } + storages_[id] = std::make_unique>(); + } + + return *static_cast*>(storages_[id].get()); + } + + template + [[nodiscard]] const PackedComponentStorage& storage() const { + const ComponentType id = componentType(); + + if (id >= storages_.size() || storages_[id] == nullptr) { + // Lazily creating storages inside a const method is not possible without breaking const-correctness. + // For const access, assume shutdown clears storages by resetting the pointers but keeps them recreated on non-const access. + // Call the non-const storage() when you need access across shutdown. + throw std::logic_error("Component storage is not available (did it get cleared by shutdown?)."); + } + + return *static_cast*>(storages_[id].get()); + } + + void entityDestroyed(Entity entity) { + for (auto& storage : storages_) { + if (storage != nullptr) { + storage->entityDestroyed(entity); + } + } + } + + // Clears stored component data to release memory, while preserving type IDs. + void shutdown() noexcept { + for (auto& storage : storages_) { + storage.reset(); + } + } + +private: + std::unordered_map typeToId_; + ComponentType nextId_{0}; + std::vector> storages_; +}; + +} // namespace safecrowd::engine + diff --git a/src/engine/EcsCore.h b/src/engine/EcsCore.h new file mode 100644 index 0000000..579bfa9 --- /dev/null +++ b/src/engine/EcsCore.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#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), + maxEntityCount_(maxEntityCount) {} + + ~EcsCore() { + // Destructors must not throw. + try { + shutdown(); + } catch (...) { + } + } + + void shutdown() { + componentRegistry_.shutdown(); + entityRegistry_ = EntityRegistry(maxEntityCount_); + } + + [[nodiscard]] Entity createEntity() { + return entityRegistry_.allocate(); + } + + void destroyEntity(Entity entity) { + // Important order: call storage cleanup with the original generation, + // then release the entity handle (which increments generation). + componentRegistry_.entityDestroyed(entity); + entityRegistry_.release(entity); + } + + template + void registerType() { + componentRegistry_.registerType(); + } + + template + [[nodiscard]] ComponentType componentType() const { + // ComponentRegistry::componentType() throws if not registered. + return componentRegistry_.componentType(); + } + + template + void addComponent(Entity entity, const T& component) { + // Must be explicitly registered; do not auto-register. + const ComponentType id = componentRegistry_.componentType(); + + auto& storage = componentRegistry_.storage(); + storage.insert(entity, component); + + auto signature = entityRegistry_.signatureOf(entity); + signature.set(id); + entityRegistry_.setSignature(entity, signature); + } + + template + void addComponent(Entity entity, T&& component) { + const ComponentType id = componentRegistry_.componentType(); + + auto& storage = componentRegistry_.storage(); + storage.insert(entity, std::move(component)); + + auto signature = entityRegistry_.signatureOf(entity); + signature.set(id); + entityRegistry_.setSignature(entity, signature); + } + + template + void removeComponent(Entity entity) { + const ComponentType id = componentRegistry_.componentType(); + + auto& storage = componentRegistry_.storage(); + storage.remove(entity); + + auto signature = entityRegistry_.signatureOf(entity); + signature.reset(id); + entityRegistry_.setSignature(entity, signature); + } + + template + [[nodiscard]] bool containsComponent(Entity entity) { + // Must be explicitly registered; throws when called with an unregistered type. + (void)componentRegistry_.componentType(); + + auto& storage = componentRegistry_.storage(); + return storage.contains(entity); + } + + template + [[nodiscard]] T& getComponent(Entity entity) { + (void)componentRegistry_.componentType(); + return componentRegistry_.storage().get(entity); + } + + [[nodiscard]] bool isAlive(Entity entity) const noexcept { + return entityRegistry_.isAlive(entity); + } + + [[nodiscard]] Signature signatureOf(Entity entity) const { + return entityRegistry_.signatureOf(entity); + } + +private: + EntityRegistry entityRegistry_; + ComponentRegistry componentRegistry_; + std::size_t maxEntityCount_{4096}; +}; + +} // namespace safecrowd::engine + diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 1dfc923..f844e2e 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -48,6 +48,7 @@ void EngineRuntime::pause() { } void EngineRuntime::stop() { + world_.shutdown(); frameClock_.reset(); stats_ = {}; stats_.state = EngineState::Stopped; diff --git a/src/engine/EngineSystem.h b/src/engine/EngineSystem.h index 943b151..9efac7e 100644 --- a/src/engine/EngineSystem.h +++ b/src/engine/EngineSystem.h @@ -1,10 +1,26 @@ #pragma once +#include "engine/EcsCore.h" #include "engine/EngineStepContext.h" namespace safecrowd::engine { class EngineWorld { +public: + [[nodiscard]] EcsCore& ecsCore() noexcept { + return ecsCore_; + } + + [[nodiscard]] const EcsCore& ecsCore() const noexcept { + return ecsCore_; + } + + void shutdown() { + ecsCore_.shutdown(); + } + +private: + EcsCore ecsCore_{}; }; class EngineSystem { diff --git a/tests/EcsCoreTests.cpp b/tests/EcsCoreTests.cpp new file mode 100644 index 0000000..411a94d --- /dev/null +++ b/tests/EcsCoreTests.cpp @@ -0,0 +1,76 @@ +#include "TestSupport.h" + +#include + +#include "engine/EngineRuntime.h" +#include "engine/EcsCore.h" + +namespace { + +struct Position { + int x{0}; +}; + +} // namespace + +SC_TEST(EcsCoreThrowsOnAddingUnregisteredComponentType) { + safecrowd::engine::EcsCore core; + const auto entity = core.createEntity(); + + bool threw = false; + try { + core.addComponent(entity, Position{1}); + } catch (const std::logic_error&) { + threw = true; + } catch (const std::exception&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +} + +SC_TEST(EcsCoreUpdatesEntitySignatureOnAddRemove) { + safecrowd::engine::EcsCore core; + core.registerType(); + + const auto entity = core.createEntity(); + auto sig0 = core.signatureOf(entity); + SC_EXPECT_TRUE(sig0.none()); + + core.addComponent(entity, Position{5}); + auto sig1 = core.signatureOf(entity); + SC_EXPECT_EQ(sig1.count(), 1U); + + core.removeComponent(entity); + auto sig2 = core.signatureOf(entity); + SC_EXPECT_TRUE(sig2.none()); +} + +SC_TEST(EcsCoreDestroysComponentsOnDestroyEntity) { + safecrowd::engine::EcsCore core; + core.registerType(); + + const auto entity = core.createEntity(); + core.addComponent(entity, Position{42}); + SC_EXPECT_TRUE(core.containsComponent(entity)); + + core.destroyEntity(entity); + SC_EXPECT_TRUE(!core.isAlive(entity)); + SC_EXPECT_TRUE(!core.containsComponent(entity)); +} + +SC_TEST(EngineRuntimeStopShutsDownEcsCore) { + safecrowd::engine::EngineRuntime runtime; + auto& ecs = runtime.world().ecsCore(); + + ecs.registerType(); + const auto entity = ecs.createEntity(); + ecs.addComponent(entity, Position{7}); + SC_EXPECT_TRUE(ecs.containsComponent(entity)); + + runtime.stop(); + + SC_EXPECT_TRUE(!ecs.isAlive(entity)); + SC_EXPECT_TRUE(!ecs.containsComponent(entity)); +} +