diff --git a/CMakeLists.txt b/CMakeLists.txt index 358de67..641992f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ set(SAFECROWD_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") add_library(ecs_engine STATIC src/engine/Entity.h src/engine/EntityRegistry.h + src/engine/IComponentStorage.h src/engine/EngineConfig.h src/engine/EngineRuntime.h src/engine/EngineState.h @@ -44,6 +45,7 @@ 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 @@ -80,6 +82,7 @@ if (BUILD_TESTING) tests/EngineRegistryTests.cpp tests/FrameClockTests.cpp tests/EngineRuntimeTests.cpp + tests/PackedComponentStorageTests.cpp tests/SafeCrowdDomainTests.cpp ) diff --git a/src/engine/IComponentStorage.h b/src/engine/IComponentStorage.h new file mode 100644 index 0000000..9ef30bf --- /dev/null +++ b/src/engine/IComponentStorage.h @@ -0,0 +1,14 @@ +#pragma once + +#include "engine/Entity.h" + +namespace safecrowd::engine { + +class IComponentStorage { +public: + virtual ~IComponentStorage() = default; + + virtual void entityDestroyed(Entity entity) = 0; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/PackedComponentStorage.h b/src/engine/PackedComponentStorage.h new file mode 100644 index 0000000..362be98 --- /dev/null +++ b/src/engine/PackedComponentStorage.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "engine/IComponentStorage.h" + +namespace safecrowd::engine { + +template +class PackedComponentStorage final : public IComponentStorage { +public: + void insert(Entity entity, const T& component) { + insertImpl(entity, component); + } + + void insert(Entity entity, T&& component) { + insertImpl(entity, std::move(component)); + } + + void remove(Entity entity) { + const std::size_t removedIndex = indexOf(entity); + const std::size_t lastIndex = components_.size() - 1; + const Entity lastEntity = entities_[lastIndex]; + + if (removedIndex != lastIndex) { + components_[removedIndex] = std::move(components_[lastIndex]); + entities_[removedIndex] = lastEntity; + entityToIndex_[lastEntity] = removedIndex; + } + + components_.pop_back(); + entities_.pop_back(); + entityToIndex_.erase(entity); + } + + [[nodiscard]] bool contains(Entity entity) const noexcept { + return entity.isValid() && entityToIndex_.contains(entity); + } + + [[nodiscard]] T& get(Entity entity) { + return components_[indexOf(entity)]; + } + + [[nodiscard]] const T& get(Entity entity) const { + return components_[indexOf(entity)]; + } + + void entityDestroyed(Entity entity) override { + if (contains(entity)) { + remove(entity); + } + } + + [[nodiscard]] std::size_t size() const noexcept { + return components_.size(); + } + +private: + struct EntityHash { + [[nodiscard]] std::size_t operator()(const Entity& entity) const noexcept { + const auto packed = + (static_cast(entity.generation) << 32U) | entity.index; + return std::hash{}(packed); + } + }; + + template + void insertImpl(Entity entity, U&& component) { + if (!entity.isValid()) { + throw std::invalid_argument("Invalid entity handle."); + } + + if (contains(entity)) { + throw std::invalid_argument("Component already exists for entity."); + } + + const std::size_t index = components_.size(); + components_.push_back(std::forward(component)); + + try { + entities_.push_back(entity); + } catch (...) { + components_.pop_back(); + throw; + } + + try { + entityToIndex_.emplace(entity, index); + } catch (...) { + entities_.pop_back(); + components_.pop_back(); + throw; + } + } + + [[nodiscard]] std::size_t indexOf(Entity entity) const { + if (!entity.isValid()) { + throw std::invalid_argument("Invalid entity handle."); + } + + const auto it = entityToIndex_.find(entity); + if (it == entityToIndex_.end()) { + throw std::invalid_argument("Component not found for entity."); + } + + return it->second; + } + + std::vector components_; + std::vector entities_; + std::unordered_map entityToIndex_; +}; + +} // namespace safecrowd::engine diff --git a/tests/PackedComponentStorageTests.cpp b/tests/PackedComponentStorageTests.cpp new file mode 100644 index 0000000..528d804 --- /dev/null +++ b/tests/PackedComponentStorageTests.cpp @@ -0,0 +1,114 @@ +#include +#include + +#include "TestSupport.h" + +#include "engine/IComponentStorage.h" +#include "engine/PackedComponentStorage.h" + +namespace { + +struct TestComponent { + int value{0}; +}; + +} // namespace + +SC_TEST(PackedComponentStorageStoresAndRemovesComponentsDensely) { + using safecrowd::engine::Entity; + using safecrowd::engine::PackedComponentStorage; + + PackedComponentStorage storage; + + const Entity first{1, 0}; + const Entity second{2, 0}; + const Entity third{3, 0}; + + storage.insert(first, TestComponent{10}); + storage.insert(second, TestComponent{20}); + storage.insert(third, TestComponent{30}); + + SC_EXPECT_TRUE(storage.contains(first)); + SC_EXPECT_TRUE(storage.contains(second)); + SC_EXPECT_TRUE(storage.contains(third)); + SC_EXPECT_EQ(storage.size(), static_cast(3)); + SC_EXPECT_EQ(storage.get(second).value, 20); + + storage.remove(second); + + SC_EXPECT_TRUE(storage.contains(first)); + SC_EXPECT_TRUE(!storage.contains(second)); + SC_EXPECT_TRUE(storage.contains(third)); + SC_EXPECT_EQ(storage.size(), static_cast(2)); + SC_EXPECT_EQ(storage.get(first).value, 10); + SC_EXPECT_EQ(storage.get(third).value, 30); + + bool threwOnMissingComponent = false; + try { + static_cast(storage.get(second)); + } catch (const std::invalid_argument&) { + threwOnMissingComponent = true; + } + + SC_EXPECT_TRUE(threwOnMissingComponent); +} + +SC_TEST(PackedComponentStorageRejectsInvalidOrDuplicateEntities) { + using safecrowd::engine::Entity; + using safecrowd::engine::PackedComponentStorage; + + PackedComponentStorage storage; + const Entity entity{7, 1}; + + storage.insert(entity, 42); + + bool threwOnDuplicateInsert = false; + try { + storage.insert(entity, 99); + } catch (const std::invalid_argument&) { + threwOnDuplicateInsert = true; + } + + SC_EXPECT_TRUE(threwOnDuplicateInsert); + + bool threwOnInvalidInsert = false; + try { + storage.insert(Entity::invalid(), 1); + } catch (const std::invalid_argument&) { + threwOnInvalidInsert = true; + } + + SC_EXPECT_TRUE(threwOnInvalidInsert); + + bool threwOnMissingRemove = false; + try { + storage.remove(Entity{99, 1}); + } catch (const std::invalid_argument&) { + threwOnMissingRemove = true; + } + + SC_EXPECT_TRUE(threwOnMissingRemove); +} + +SC_TEST(IComponentStorageEntityDestroyedRemovesStoredComponent) { + using safecrowd::engine::Entity; + using safecrowd::engine::IComponentStorage; + using safecrowd::engine::PackedComponentStorage; + + auto storage = std::make_unique>(); + PackedComponentStorage* typedStorage = storage.get(); + IComponentStorage& baseStorage = *storage; + + const Entity entity{12, 2}; + const Entity survivor{13, 2}; + + typedStorage->insert(entity, 5); + typedStorage->insert(survivor, 6); + + baseStorage.entityDestroyed(entity); + baseStorage.entityDestroyed(Entity{111, 0}); + + SC_EXPECT_TRUE(!typedStorage->contains(entity)); + SC_EXPECT_TRUE(typedStorage->contains(survivor)); + SC_EXPECT_EQ(typedStorage->get(survivor), 6); +}