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
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ add_library(ecs_engine STATIC
src/engine/Entity.h
src/engine/EntityRegistry.h
src/engine/IComponentStorage.h
src/engine/PackedComponentStorage.h
src/engine/ComponentRegistry.h
src/engine/EcsCore.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 @@ -87,6 +89,7 @@ if (BUILD_TESTING)
tests/EngineRuntimeTests.cpp
tests/PackedComponentStorageTests.cpp
tests/SafeCrowdDomainTests.cpp
tests/EcsCoreTests.cpp
)

target_include_directories(safecrowd_tests
Expand Down
105 changes: 105 additions & 0 deletions src/engine/ComponentRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#pragma once

#include <memory>
#include <optional>
#include <stdexcept>
#include <typeindex>
#include <unordered_map>

#include "engine/EntityRegistry.h"
#include "engine/IComponentStorage.h"
#include "engine/PackedComponentStorage.h"

namespace safecrowd::engine {

using ComponentType = std::size_t;

// ComponentRegistry
//
// 컴포넌트 타입(C++ 타입)을 고유 ID(ComponentType)에 매핑하고,
// 타입별로 PackedComponentStorage<T> 인스턴스를 하나씩 보관한다.
//
// - 타입은 처음 addComponent 시 자동 등록된다(getOrRegister<T>).
// - entity가 삭제될 때 notifyEntityDestroyed()를 호출하면
// 등록된 모든 storage에서 해당 entity 데이터를 일괄 제거한다(cleanup flow).
class ComponentRegistry {
public:
// T를 레지스트리에 등록한다.
// 이미 등록된 경우 기존 ID를 그대로 반환한다.
// 처음 등록이면 PackedComponentStorage<T>를 함께 생성한다.
template <typename T>
ComponentType getOrRegister() {
const std::type_index key = typeid(T);

if (const auto it = typeIds_.find(key); it != typeIds_.end()) {
return it->second;
}

if (nextTypeId_ >= kMaxComponentTypes) {
throw std::runtime_error(
"ComponentRegistry: 최대 컴포넌트 타입 수를 초과했습니다.");
}

const ComponentType id = nextTypeId_++;
typeIds_.emplace(key, id);
storages_.emplace(key, std::make_unique<PackedComponentStorage<T>>());

return id;
}

// T의 ComponentType ID를 반환한다.
// 등록되지 않은 타입이면 std::nullopt를 반환한다(예외 없음).
template <typename T>
[[nodiscard]] std::optional<ComponentType> tryTypeOf() const noexcept {
const auto it = typeIds_.find(typeid(T));
if (it == typeIds_.end()) {
return std::nullopt;
}
return it->second;
}

// T가 레지스트리에 등록되어 있는지 확인한다.
template <typename T>
[[nodiscard]] bool isRegistered() const noexcept {
return typeIds_.contains(typeid(T));
}

// T의 PackedComponentStorage 참조를 반환한다.
// T가 등록되지 않은 경우 예외를 던진다.
template <typename T>
[[nodiscard]] PackedComponentStorage<T>& storageFor() {
const auto it = storages_.find(typeid(T));
if (it == storages_.end()) {
throw std::runtime_error(
"ComponentRegistry: 등록되지 않은 컴포넌트 타입입니다.");
}
return static_cast<PackedComponentStorage<T>&>(*it->second);
}

template <typename T>
[[nodiscard]] const PackedComponentStorage<T>& storageFor() const {
const auto it = storages_.find(typeid(T));
if (it == storages_.end()) {
throw std::runtime_error(
"ComponentRegistry: 등록되지 않은 컴포넌트 타입입니다.");
}
return static_cast<const PackedComponentStorage<T>&>(*it->second);
}

// EcsCore cleanup flow 진입점.
// entity가 destroyEntity()될 때 호출되며,
// 등록된 모든 storage에 entityDestroyed()를 전달해
// 해당 entity의 컴포넌트 데이터를 일괄 제거한다.
void notifyEntityDestroyed(Entity entity) {
for (auto& [key, storage] : storages_) {
storage->entityDestroyed(entity);
}
}

private:
std::unordered_map<std::type_index, ComponentType> typeIds_;
std::unordered_map<std::type_index, std::unique_ptr<IComponentStorage>> storages_;
ComponentType nextTypeId_{0};
};

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

#include <cstddef>

#include "engine/ComponentRegistry.h"
#include "engine/Entity.h"
#include "engine/EntityRegistry.h"

namespace safecrowd::engine {

// EcsCore
//
// ECS 저장 코어. EntityRegistry와 ComponentRegistry를 하나로 묶어
// 외부에서 raw 레지스트리를 직접 다루지 않고도 엔티티/컴포넌트를 조작하게 한다.
//
// 책임:
// - 엔티티 생성/소멸 (EntityRegistry 위임)
// - 컴포넌트 추가/제거 및 entity Signature 자동 갱신 (ComponentRegistry 위임)
// - 엔티티 소멸 시 cleanup flow 실행 (ComponentRegistry::notifyEntityDestroyed)
//
// 이 클래스는 domain 용어를 알지 않는다.
// "군중", "에이전트" 같은 개념은 domain 계층이 컴포넌트 타입으로 표현한다.
class EcsCore {
public:
explicit EcsCore(std::size_t maxEntityCount = 4096)
: entityRegistry_(maxEntityCount) {}

// ----------------------------------------------------------------
// 엔티티 생명주기
// ----------------------------------------------------------------

// 새 엔티티를 할당하고 핸들을 반환한다.
[[nodiscard]] Entity createEntity() {
return entityRegistry_.allocate();
}

// 엔티티와 그에 속한 모든 컴포넌트를 삭제한다.
//
// cleanup flow:
// 1. ComponentRegistry::notifyEntityDestroyed() → 등록된 모든 storage에
// entityDestroyed()를 호출해 컴포넌트 데이터를 제거
// 2. EntityRegistry::release() → 해당 슬롯을 free-list에 반환하고
// generation을 증가시켜 stale handle을 무효화
void destroyEntity(Entity entity) {
componentRegistry_.notifyEntityDestroyed(entity);
entityRegistry_.release(entity);
}

// 엔티티가 현재 살아있는지 확인한다.
[[nodiscard]] bool isAlive(Entity entity) const noexcept {
return entityRegistry_.isAlive(entity);
}

// ----------------------------------------------------------------
// 컴포넌트 조작
// ----------------------------------------------------------------

// 엔티티에 컴포넌트 T를 추가하고 signature를 갱신한다.
//
// T가 처음 추가되는 타입이면 ComponentRegistry에 자동 등록된다.
// 이미 해당 컴포넌트가 있는 경우 PackedComponentStorage::insert에서 예외 발생.
template <typename T>
void addComponent(Entity entity, T component) {
const ComponentType typeId = componentRegistry_.getOrRegister<T>();
componentRegistry_.storageFor<T>().insert(entity, std::move(component));

Signature sig = entityRegistry_.signatureOf(entity);
sig.set(typeId);
entityRegistry_.setSignature(entity, sig);
}

// 엔티티에서 컴포넌트 T를 제거하고 signature를 갱신한다.
//
// T가 등록되지 않았거나 해당 entity에 T가 없으면 조용히 무시한다.
template <typename T>
void removeComponent(Entity entity) {
const auto typeId = componentRegistry_.tryTypeOf<T>();
if (!typeId.has_value()) {
return;
}

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

storage.remove(entity);

Signature sig = entityRegistry_.signatureOf(entity);
sig.reset(typeId.value());
entityRegistry_.setSignature(entity, sig);
}

// 엔티티의 컴포넌트 T를 mutable 참조로 반환한다.
// T가 없으면 PackedComponentStorage::get에서 예외 발생.
template <typename T>
[[nodiscard]] T& getComponent(Entity entity) {
return componentRegistry_.storageFor<T>().get(entity);
}

template <typename T>
[[nodiscard]] const T& getComponent(Entity entity) const {
return componentRegistry_.storageFor<T>().get(entity);
}

// entity가 컴포넌트 T를 보유하고 있는지 확인한다.
// T가 한 번도 등록된 적 없으면 false를 반환한다.
template <typename T>
[[nodiscard]] bool hasComponent(Entity entity) const {
if (!componentRegistry_.isRegistered<T>()) {
return false;
}
return componentRegistry_.storageFor<T>().contains(entity);
}

// ----------------------------------------------------------------
// 내부 레지스트리 접근자
// ----------------------------------------------------------------

[[nodiscard]] EntityRegistry& entityRegistry() noexcept {
return entityRegistry_;
}
[[nodiscard]] const EntityRegistry& entityRegistry() const noexcept {
return entityRegistry_;
}
[[nodiscard]] ComponentRegistry& componentRegistry() noexcept {
return componentRegistry_;
}
[[nodiscard]] const ComponentRegistry& componentRegistry() const noexcept {
return componentRegistry_;
}

private:
EntityRegistry entityRegistry_;
ComponentRegistry componentRegistry_;
};

} // namespace safecrowd::engine
106 changes: 106 additions & 0 deletions tests/EcsCoreTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "TestSupport.h"

#include "engine/EcsCore.h"

namespace {

struct Position {
float x{0.0f};
float y{0.0f};
};

struct Velocity {
float vx{0.0f};
float vy{0.0f};
};

} // namespace

SC_TEST(EcsCore_CreateAndDestroyEntity) {
safecrowd::engine::EcsCore core;

const auto e = core.createEntity();
SC_EXPECT_TRUE(core.isAlive(e));

core.destroyEntity(e);
SC_EXPECT_TRUE(!core.isAlive(e));
}

SC_TEST(EcsCore_AddComponent_UpdatesSignatureAndData) {
safecrowd::engine::EcsCore core;
const auto e = core.createEntity();

SC_EXPECT_TRUE(!core.hasComponent<Position>(e));

core.addComponent(e, Position{1.0f, 2.0f});

SC_EXPECT_TRUE(core.hasComponent<Position>(e));

const auto& pos = core.getComponent<Position>(e);
SC_EXPECT_NEAR(pos.x, 1.0f, 1e-6);
SC_EXPECT_NEAR(pos.y, 2.0f, 1e-6);
}

SC_TEST(EcsCore_RemoveComponent_UpdatesSignature) {
safecrowd::engine::EcsCore core;
const auto e = core.createEntity();

core.addComponent(e, Position{3.0f, 4.0f});
SC_EXPECT_TRUE(core.hasComponent<Position>(e));

core.removeComponent<Position>(e);
SC_EXPECT_TRUE(!core.hasComponent<Position>(e));
}

SC_TEST(EcsCore_RemoveComponent_NonExistent_IsSafe) {
safecrowd::engine::EcsCore core;
const auto e = core.createEntity();

core.removeComponent<Position>(e);
SC_EXPECT_TRUE(!core.hasComponent<Position>(e));
}

SC_TEST(EcsCore_DestroyEntity_CleansUpAllComponents) {
safecrowd::engine::EcsCore core;
const auto e = core.createEntity();

core.addComponent(e, Position{5.0f, 6.0f});
core.addComponent(e, Velocity{1.0f, 0.0f});

SC_EXPECT_TRUE(core.hasComponent<Position>(e));
SC_EXPECT_TRUE(core.hasComponent<Velocity>(e));

core.destroyEntity(e);

SC_EXPECT_TRUE(!core.isAlive(e));
}

SC_TEST(EcsCore_EntityIndex_Reuse_DoesNotLeakComponents) {
safecrowd::engine::EcsCore core;

const auto e1 = core.createEntity();
core.addComponent(e1, Position{7.0f, 8.0f});

core.destroyEntity(e1);

const auto e2 = core.createEntity();
SC_EXPECT_TRUE(e1.index == e2.index);

SC_EXPECT_TRUE(!core.hasComponent<Position>(e2));
}

SC_TEST(EcsCore_MultipleComponents_IndependentSignatureBits) {
safecrowd::engine::EcsCore core;
const auto e = core.createEntity();

core.addComponent(e, Position{0.0f, 0.0f});
core.addComponent(e, Velocity{1.0f, 1.0f});

SC_EXPECT_TRUE(core.hasComponent<Position>(e));
SC_EXPECT_TRUE(core.hasComponent<Velocity>(e));

core.removeComponent<Position>(e);

SC_EXPECT_TRUE(!core.hasComponent<Position>(e));
SC_EXPECT_TRUE(core.hasComponent<Velocity>(e));
}
Loading