diff --git a/CMakeLists.txt b/CMakeLists.txt index 3247c35..358de67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,8 @@ endfunction() set(SAFECROWD_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") add_library(ecs_engine STATIC + src/engine/Entity.h + src/engine/EntityRegistry.h src/engine/EngineConfig.h src/engine/EngineRuntime.h src/engine/EngineState.h @@ -42,6 +44,7 @@ add_library(ecs_engine STATIC src/engine/EngineStepContext.h src/engine/EngineSystem.h src/engine/FrameClock.h + src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp ) @@ -74,6 +77,7 @@ if (BUILD_TESTING) add_executable(safecrowd_tests tests/TestMain.cpp tests/TestSupport.h + tests/EngineRegistryTests.cpp tests/FrameClockTests.cpp tests/EngineRuntimeTests.cpp tests/SafeCrowdDomainTests.cpp diff --git a/src/engine/Entity.h b/src/engine/Entity.h new file mode 100644 index 0000000..c25335b --- /dev/null +++ b/src/engine/Entity.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace safecrowd::engine { + +using EntityIndex = std::uint32_t; +using EntityGeneration = std::uint32_t; + +struct Entity { + static constexpr EntityIndex invalidIndex = std::numeric_limits::max(); + + EntityIndex index{invalidIndex}; + EntityGeneration generation{0}; + + [[nodiscard]] constexpr bool isValid() const noexcept { + return index != invalidIndex; + } + + [[nodiscard]] static constexpr Entity invalid() noexcept { + return {}; + } + + auto operator<=>(const Entity&) const = default; +}; + +inline std::ostream& operator<<(std::ostream& stream, const Entity& entity) { + stream << "Entity{index=" << entity.index << ", generation=" << entity.generation << "}"; + return stream; +} + +} // namespace safecrowd::engine \ No newline at end of file diff --git a/src/engine/EntityRegistry.cpp b/src/engine/EntityRegistry.cpp new file mode 100644 index 0000000..7476d90 --- /dev/null +++ b/src/engine/EntityRegistry.cpp @@ -0,0 +1,95 @@ +#include "engine/EntityRegistry.h" + +#include +#include +#include + +namespace safecrowd::engine { +namespace { + +std::string describeEntity(Entity entity) { + std::ostringstream stream; + stream << entity; + return stream.str(); +} + +} // namespace + +EntityRegistry::EntityRegistry(std::size_t maxEntityCount) + : entries_(maxEntityCount) { + freeIndices_.resize(maxEntityCount); + + for (std::size_t index = 0; index < maxEntityCount; ++index) { + freeIndices_[index] = static_cast(index); + } +} + +Entity EntityRegistry::allocate() { + if (freeIndices_.empty()) { + throw std::overflow_error("EntityRegistry capacity exhausted."); + } + + const EntityIndex index = freeIndices_.front(); + freeIndices_.pop_front(); + + Entry& entry = entries_[index]; + entry.alive = true; + entry.signature.reset(); + + return Entity{index, entry.generation}; +} + +void EntityRegistry::release(Entity entity) { + Entry& entry = entryFor(entity); + entry.alive = false; + entry.signature.reset(); + ++entry.generation; + freeIndices_.push_back(entity.index); +} + +bool EntityRegistry::isAlive(Entity entity) const noexcept { + if (!entity.isValid()) { + return false; + } + + const auto index = static_cast(entity.index); + if (index >= entries_.size()) { + return false; + } + + const Entry& entry = entries_[index]; + return entry.alive && entry.generation == entity.generation; +} + +void EntityRegistry::setSignature(Entity entity, Signature signature) { + Entry& entry = entryFor(entity); + entry.signature = signature; +} + +Signature EntityRegistry::signatureOf(Entity entity) const { + return entryFor(entity).signature; +} + +const EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) const { + if (!entity.isValid()) { + throw std::invalid_argument("Invalid entity handle."); + } + + const auto index = static_cast(entity.index); + if (index >= entries_.size()) { + throw std::out_of_range("Entity index is out of range."); + } + + const Entry& entry = entries_[index]; + if (!entry.alive || entry.generation != entity.generation) { + throw std::invalid_argument("Stale or dead entity handle: " + describeEntity(entity)); + } + + return entry; +} + +EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) { + return const_cast(std::as_const(*this).entryFor(entity)); +} + +} // namespace safecrowd::engine \ No newline at end of file diff --git a/src/engine/EntityRegistry.h b/src/engine/EntityRegistry.h new file mode 100644 index 0000000..392d487 --- /dev/null +++ b/src/engine/EntityRegistry.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include "engine/Entity.h" + +namespace safecrowd::engine { + +inline constexpr std::size_t kMaxComponentTypes = 64; +using Signature = std::bitset; + +class EntityRegistry { +public: + explicit EntityRegistry(std::size_t maxEntityCount = 4096); + + [[nodiscard]] Entity allocate(); + void release(Entity entity); + [[nodiscard]] bool isAlive(Entity entity) const noexcept; + void setSignature(Entity entity, Signature signature); + [[nodiscard]] Signature signatureOf(Entity entity) const; + +private: + struct Entry { + EntityGeneration generation{0}; + bool alive{false}; + Signature signature{}; + }; + + [[nodiscard]] const Entry& entryFor(Entity entity) const; + [[nodiscard]] Entry& entryFor(Entity entity); + + std::vector entries_; + std::deque freeIndices_; +}; + +} // namespace safecrowd::engine \ No newline at end of file diff --git a/tests/EngineRegistryTests.cpp b/tests/EngineRegistryTests.cpp new file mode 100644 index 0000000..b5aa753 --- /dev/null +++ b/tests/EngineRegistryTests.cpp @@ -0,0 +1,59 @@ +#include + +#include "TestSupport.h" + +#include "engine/EntityRegistry.h" + +SC_TEST(EntityRegistryReusesIndexWithNewGeneration) { + safecrowd::engine::EntityRegistry registry(1); + + const auto first = registry.allocate(); + SC_EXPECT_TRUE(registry.isAlive(first)); + + registry.release(first); + SC_EXPECT_TRUE(!registry.isAlive(first)); + + const auto second = registry.allocate(); + SC_EXPECT_EQ(second.index, first.index); + SC_EXPECT_TRUE(second.generation > first.generation); + SC_EXPECT_TRUE(registry.isAlive(second)); +} + +SC_TEST(EntityRegistryRejectsStaleEntityHandles) { + safecrowd::engine::EntityRegistry registry(1); + + const auto entity = registry.allocate(); + registry.release(entity); + + bool threwOnRelease = false; + try { + registry.release(entity); + } catch (const std::invalid_argument&) { + threwOnRelease = true; + } + + SC_EXPECT_TRUE(threwOnRelease); + + bool threwOnSignatureRead = false; + try { + static_cast(registry.signatureOf(entity)); + } catch (const std::invalid_argument&) { + threwOnSignatureRead = true; + } + + SC_EXPECT_TRUE(threwOnSignatureRead); +} + +SC_TEST(EntityRegistryStoresSignaturesPerLiveEntity) { + safecrowd::engine::EntityRegistry registry(1); + + const auto entity = registry.allocate(); + safecrowd::engine::Signature signature; + signature.set(1); + signature.set(5); + + registry.setSignature(entity, signature); + + const auto stored = registry.signatureOf(entity); + SC_EXPECT_EQ(stored, signature); +} \ No newline at end of file