Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ 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
src/engine/EngineStats.h
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
Expand Down Expand Up @@ -80,6 +82,7 @@ if (BUILD_TESTING)
tests/EngineRegistryTests.cpp
tests/FrameClockTests.cpp
tests/EngineRuntimeTests.cpp
tests/PackedComponentStorageTests.cpp
tests/SafeCrowdDomainTests.cpp
)

Expand Down
14 changes: 14 additions & 0 deletions src/engine/IComponentStorage.h
Original file line number Diff line number Diff line change
@@ -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
120 changes: 120 additions & 0 deletions src/engine/PackedComponentStorage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#pragma once

#include <cstddef>
#include <cstdint>
#include <functional>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>

#include "engine/IComponentStorage.h"

namespace safecrowd::engine {

template <typename T>
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<std::uint64_t>(entity.generation) << 32U) | entity.index;
return std::hash<std::uint64_t>{}(packed);
}
};

template <typename U>
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<U>(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<T> components_;
std::vector<Entity> entities_;
std::unordered_map<Entity, std::size_t, EntityHash> entityToIndex_;
};

} // namespace safecrowd::engine
114 changes: 114 additions & 0 deletions tests/PackedComponentStorageTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include <memory>
#include <stdexcept>

#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<TestComponent> 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<std::size_t>(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<std::size_t>(2));
SC_EXPECT_EQ(storage.get(first).value, 10);
SC_EXPECT_EQ(storage.get(third).value, 30);

bool threwOnMissingComponent = false;
try {
static_cast<void>(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<int> 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<int>>();
PackedComponentStorage<int>* 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);
}
Loading