Skip to content
Closed
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,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
Expand Down Expand Up @@ -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
Expand Down
104 changes: 104 additions & 0 deletions src/engine/ComponentRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#pragma once

#include <cstddef>
#include <memory>
#include <stdexcept>
#include <typeindex>
#include <unordered_map>
#include <vector>

#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 <typename T>
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<PackedComponentStorage<T>>();
}

template <typename T>
[[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<T>() during initialization.");
}
return it->second;
}

template <typename T>
[[nodiscard]] PackedComponentStorage<T>& storage() {
const ComponentType id = componentType<T>();

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<PackedComponentStorage<T>>();
}

return *static_cast<PackedComponentStorage<T>*>(storages_[id].get());
}

template <typename T>
[[nodiscard]] const PackedComponentStorage<T>& storage() const {
const ComponentType id = componentType<T>();

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<const PackedComponentStorage<T>*>(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<std::type_index, ComponentType> typeToId_;
ComponentType nextId_{0};
std::vector<std::unique_ptr<IComponentStorage>> storages_;
};

} // namespace safecrowd::engine

120 changes: 120 additions & 0 deletions src/engine/EcsCore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#pragma once

#include <cstddef>
#include <stdexcept>

#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 <typename T>
void registerType() {
componentRegistry_.registerType<T>();
}

template <typename T>
[[nodiscard]] ComponentType componentType() const {
// ComponentRegistry::componentType<T>() throws if not registered.
return componentRegistry_.componentType<T>();
}

template <typename T>
void addComponent(Entity entity, const T& component) {
// Must be explicitly registered; do not auto-register.
const ComponentType id = componentRegistry_.componentType<T>();

auto& storage = componentRegistry_.storage<T>();
storage.insert(entity, component);

auto signature = entityRegistry_.signatureOf(entity);
signature.set(id);
entityRegistry_.setSignature(entity, signature);
}

template <typename T>
void addComponent(Entity entity, T&& component) {
const ComponentType id = componentRegistry_.componentType<T>();

auto& storage = componentRegistry_.storage<T>();
storage.insert(entity, std::move(component));

auto signature = entityRegistry_.signatureOf(entity);
signature.set(id);
entityRegistry_.setSignature(entity, signature);
}

template <typename T>
void removeComponent(Entity entity) {
const ComponentType id = componentRegistry_.componentType<T>();

auto& storage = componentRegistry_.storage<T>();
storage.remove(entity);

auto signature = entityRegistry_.signatureOf(entity);
signature.reset(id);
entityRegistry_.setSignature(entity, signature);
}

template <typename T>
[[nodiscard]] bool containsComponent(Entity entity) {
// Must be explicitly registered; throws when called with an unregistered type.
(void)componentRegistry_.componentType<T>();

auto& storage = componentRegistry_.storage<T>();
return storage.contains(entity);
}

template <typename T>
[[nodiscard]] T& getComponent(Entity entity) {
(void)componentRegistry_.componentType<T>();
return componentRegistry_.storage<T>().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

1 change: 1 addition & 0 deletions src/engine/EngineRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ void EngineRuntime::pause() {
}

void EngineRuntime::stop() {
world_.shutdown();
frameClock_.reset();
stats_ = {};
stats_.state = EngineState::Stopped;
Expand Down
16 changes: 16 additions & 0 deletions src/engine/EngineSystem.h
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
76 changes: 76 additions & 0 deletions tests/EcsCoreTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "TestSupport.h"

#include <exception>

#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<Position>(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<Position>();

const auto entity = core.createEntity();
auto sig0 = core.signatureOf(entity);
SC_EXPECT_TRUE(sig0.none());

core.addComponent<Position>(entity, Position{5});
auto sig1 = core.signatureOf(entity);
SC_EXPECT_EQ(sig1.count(), 1U);

core.removeComponent<Position>(entity);
auto sig2 = core.signatureOf(entity);
SC_EXPECT_TRUE(sig2.none());
}

SC_TEST(EcsCoreDestroysComponentsOnDestroyEntity) {
safecrowd::engine::EcsCore core;
core.registerType<Position>();

const auto entity = core.createEntity();
core.addComponent<Position>(entity, Position{42});
SC_EXPECT_TRUE(core.containsComponent<Position>(entity));

core.destroyEntity(entity);
SC_EXPECT_TRUE(!core.isAlive(entity));
SC_EXPECT_TRUE(!core.containsComponent<Position>(entity));
}

SC_TEST(EngineRuntimeStopShutsDownEcsCore) {
safecrowd::engine::EngineRuntime runtime;
auto& ecs = runtime.world().ecsCore();

ecs.registerType<Position>();
const auto entity = ecs.createEntity();
ecs.addComponent<Position>(entity, Position{7});
SC_EXPECT_TRUE(ecs.containsComponent<Position>(entity));

runtime.stop();

SC_EXPECT_TRUE(!ecs.isAlive(entity));
SC_EXPECT_TRUE(!ecs.containsComponent<Position>(entity));
}

Loading