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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ add_library(ecs_engine STATIC
src/engine/EcsCore.h
src/engine/EngineConfig.h
src/engine/EngineRuntime.h
src/engine/EngineWorld.h
src/engine/EngineState.h
src/engine/EngineStats.h
src/engine/EngineStepContext.h
Expand All @@ -58,6 +59,7 @@ add_library(ecs_engine STATIC
src/engine/EntityRegistry.cpp
src/engine/EngineRuntime.cpp
src/engine/FrameClock.cpp
src/engine/internal/EngineWorldFactory.h
src/engine/SystemScheduler.cpp
)

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/프로젝트 구조.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Project/
- `EngineRuntime` : 엔진 상태와 실행 루프를 관리하는 진입점
- `Entity` : `index + generation` 기반 식별자. 재사용된 entity ID의 stale reference를 막기 위한 기본 핸들
- `EngineWorld` : query / resources / commands를 통해 월드 상태에 접근하는 공유 파사드
- `WorldQuery`, `WorldResources`, `WorldCommands` : 컴포넌트 조회·값 접근 / 리소스 접근 / 구조 변경 요청을 분리한 접근면
- `WorldQuery`, `WorldResources`, `WorldCommands` : 컴포넌트 조회·값 접근 / 리소스 접근 / 구조 변경 요청을 분리한 접근면. 상위 계층은 `WorldQuery`를 직접 생성하지 않고 `EngineWorld::query()`로 획득한다.
- `WorldCommands` : 엔티티 생성/삭제와 컴포넌트 추가/제거 같은 구조 변경을 즉시 ECS에 반영하지 않고 command buffer에 기록하며, 실제 반영은 runtime이 phase 경계에서 수행
- `FrameClock` : 고정 timestep 누적과 catch-up step 계산 담당
- `EngineSystem` : `update(world, stepContext)` 형태로 월드와 프레임 컨텍스트를 받는 범용 시스템 인터페이스
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ flowchart LR
FC["FrameClock"]
ER["EntityRegistry"]
PCS["PackedComponentStorage"]
SYS["EngineSystem / WorldQuery / CommandBuffer"]
SYS["EngineWorld / EngineSystem / CommandBuffer"]
end

QT["Qt6 Widgets"] --> Application
Expand All @@ -130,7 +130,7 @@ flowchart LR
| --- | --- | --- | --- |
| `application` | `MainWindow`, Qt signal/slot, 향후 import/scenario/result UI | 사용자 입력 수집, 도메인 서비스 호출, 상태 표시 | ECS 내부 로직과 위험 계산식을 직접 가지지 않음 |
| `domain` | `SafeCrowdDomain`, `DxfImportService`, `FacilityLayoutBuilder`, `ImportValidationService`, 향후 시나리오/위험/추천 모듈 | SafeCrowd 문제 영역 규칙, 공간 구조 정규화, 실행 가능성 검토, 위험 지표 정의 | Qt UI 의존 금지 |
| `engine` | `EngineRuntime`, `FrameClock`, `EntityRegistry`, `ComponentRegistry`, `PackedComponentStorage`, `WorldQuery`, `CommandBuffer` | 범용 ECS runtime, fixed timestep, world 상태 관리, 시스템 실행 기반 제공 | SafeCrowd 도메인 지식 의존 금지 |
| `engine` | `EngineRuntime`, `EngineWorld`, `FrameClock`, `EntityRegistry`, `ComponentRegistry`, `PackedComponentStorage`, `WorldQuery`(`EngineWorld::query()` 경유), `CommandBuffer` | 범용 ECS runtime, fixed timestep, world 상태 관리, 시스템 실행 기반 제공 | SafeCrowd 도메인 지식 의존 금지 |

핵심 의존 방향은 아래 한 줄로 요약된다.

Expand Down
2 changes: 1 addition & 1 deletion src/engine/EngineRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ EngineConfig normalizeConfig(EngineConfig config) {
EngineRuntime::EngineRuntime(EngineConfig config)
: config_(normalizeConfig(config)),
scheduler_(core_, buffer_),
world_(core_, buffer_),
world_(EngineWorld::ConstructionToken{}, core_, buffer_),
frameClock_(config_) {
}

Expand Down
1 change: 1 addition & 0 deletions src/engine/EngineRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "engine/EngineConfig.h"
#include "engine/EngineStats.h"
#include "engine/EngineSystem.h"
#include "engine/EngineWorld.h"
#include "engine/FrameClock.h"
#include "engine/SystemDescriptor.h"
#include "engine/SystemScheduler.h"
Expand Down
18 changes: 1 addition & 17 deletions src/engine/EngineSystem.h
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
#pragma once

#include "engine/CommandBuffer.h"
#include "engine/EngineWorld.h"
#include "engine/EngineStepContext.h"
#include "engine/WorldQuery.h"

namespace safecrowd::engine {

class EngineWorld {
public:
EngineWorld() = delete;
explicit EngineWorld(EcsCore& core, CommandBuffer& buffer)
: query_(core), commands_(buffer) {}

[[nodiscard]] WorldQuery& query() noexcept { return query_; }
[[nodiscard]] const WorldQuery& query() const noexcept { return query_; }
[[nodiscard]] WorldCommands& commands() noexcept { return commands_; }

private:
WorldQuery query_;
WorldCommands commands_;
};

class EngineSystem {
public:
virtual ~EngineSystem() = default;
Expand Down
38 changes: 38 additions & 0 deletions src/engine/EngineWorld.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include "engine/CommandBuffer.h"
#include "engine/WorldQuery.h"

namespace safecrowd::engine {

namespace internal {
class EngineWorldFactory;
}

class EngineWorld {
public:
[[nodiscard]] WorldQuery& query() noexcept { return query_; }
[[nodiscard]] const WorldQuery& query() const noexcept { return query_; }
[[nodiscard]] WorldCommands& commands() noexcept { return commands_; }

private:
class ConstructionToken {
private:
ConstructionToken() = default;

friend class EngineRuntime;
friend class internal::EngineWorldFactory;
};

EngineWorld() = delete;
explicit EngineWorld(ConstructionToken, EcsCore& core, CommandBuffer& buffer)
: query_(core), commands_(buffer) {}

friend class EngineRuntime;
friend class internal::EngineWorldFactory;

WorldQuery query_;
WorldCommands commands_;
};

} // namespace safecrowd::engine
8 changes: 6 additions & 2 deletions src/engine/WorldQuery.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

namespace safecrowd::engine {

class EngineWorld;

class WorldQuery {
public:
explicit WorldQuery(EcsCore& core) : core_(core) {}

template <typename... Ts>
[[nodiscard]] std::vector<Entity> view() const {
Signature required{};
Expand Down Expand Up @@ -50,6 +50,10 @@ class WorldQuery {
}

private:
friend class EngineWorld;

explicit WorldQuery(EcsCore& core) : core_(core) {}

EcsCore& core_;
};

Expand Down
16 changes: 16 additions & 0 deletions src/engine/internal/EngineWorldFactory.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include "engine/EngineWorld.h"

namespace safecrowd::engine::internal {

class EngineWorldFactory {
public:
EngineWorldFactory() = delete;

[[nodiscard]] static EngineWorld create(EcsCore& core, CommandBuffer& buffer) {
return EngineWorld(EngineWorld::ConstructionToken{}, core, buffer);
}
};

} // namespace safecrowd::engine::internal
13 changes: 7 additions & 6 deletions tests/SystemSchedulerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "engine/CommandBuffer.h"
#include "engine/EcsCore.h"
#include "engine/SystemScheduler.h"
#include "engine/internal/EngineWorldFactory.h"

namespace {

Expand Down Expand Up @@ -67,7 +68,7 @@ SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) {

safecrowd::engine::EcsCore dummyCore;
safecrowd::engine::CommandBuffer dummyBuffer;
safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer};
auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer);

std::vector<int> log;
scheduler.registerSystem(
Expand Down Expand Up @@ -105,7 +106,7 @@ SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) {

safecrowd::engine::EcsCore dummyCore;
safecrowd::engine::CommandBuffer dummyBuffer;
safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer};
auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer);

std::vector<int> log;
scheduler.registerSystem(
Expand Down Expand Up @@ -134,7 +135,7 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) {

safecrowd::engine::EcsCore dummyCore;
safecrowd::engine::CommandBuffer dummyBuffer;
safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer};
auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer);

std::vector<int> log;
scheduler.registerSystem(
Expand All @@ -157,7 +158,7 @@ SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) {
safecrowd::engine::EcsCore core;
safecrowd::engine::CommandBuffer buffer;
safecrowd::engine::SystemScheduler scheduler{core, buffer};
safecrowd::engine::EngineWorld world{core, buffer};
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

std::size_t configuredCount = 0;
scheduler.registerSystem(std::make_unique<ConfigureSpawnTagSystem>(), {});
Expand All @@ -174,7 +175,7 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) {
safecrowd::engine::CommandBuffer buffer;
safecrowd::engine::SystemScheduler scheduler{core, buffer};

safecrowd::engine::EngineWorld world{core, buffer};
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

scheduler.registerSystem(
std::make_unique<SpawnTagSystem>(),
Expand All @@ -187,7 +188,7 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) {
world,
ctx);

const auto entities = safecrowd::engine::WorldQuery{core}.view<Tag>();
const auto entities = world.query().view<Tag>();
SC_EXPECT_EQ(entities.size(), std::size_t{1});
}

Expand Down
36 changes: 24 additions & 12 deletions tests/WorldQueryTests.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "TestSupport.h"

#include "engine/WorldQuery.h"
#include <type_traits>

#include "engine/internal/EngineWorldFactory.h"

namespace {

Expand All @@ -16,9 +18,15 @@ struct Velocity {

} // namespace

static_assert(!std::is_constructible_v<safecrowd::engine::WorldQuery, safecrowd::engine::EcsCore&>);
static_assert(!std::is_constructible_v<safecrowd::engine::EngineWorld,
safecrowd::engine::EcsCore&,
safecrowd::engine::CommandBuffer&>);

SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) {
safecrowd::engine::EcsCore core;
safecrowd::engine::WorldQuery query{core};
safecrowd::engine::CommandBuffer buffer;
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

const auto e1 = core.createEntity();
const auto e2 = core.createEntity();
Expand All @@ -29,24 +37,26 @@ SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) {
core.addComponent(e2, Position{});
core.addComponent(e3, Velocity{});

const auto result = query.view<Position, Velocity>();
const auto result = world.query().view<Position, Velocity>();
SC_EXPECT_EQ(result.size(), std::size_t{1});
SC_EXPECT_TRUE(result[0] == e1);
}

SC_TEST(WorldQuery_ViewReturnsEmptyIfTypeNotRegistered) {
safecrowd::engine::EcsCore core;
safecrowd::engine::WorldQuery query{core};
safecrowd::engine::CommandBuffer buffer;
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

static_cast<void>(core.createEntity());

const auto result = query.view<Position>();
const auto result = world.query().view<Position>();
SC_EXPECT_TRUE(result.empty());
}

SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) {
safecrowd::engine::EcsCore core;
safecrowd::engine::WorldQuery query{core};
safecrowd::engine::CommandBuffer buffer;
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

const auto e1 = core.createEntity();
core.addComponent(e1, Position{});
Expand All @@ -55,30 +65,32 @@ SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) {
const auto e2 = core.createEntity();
core.addComponent(e2, Position{});

const auto result = query.view<Position>();
const auto result = world.query().view<Position>();
SC_EXPECT_EQ(result.size(), std::size_t{1});
SC_EXPECT_TRUE(result[0] == e2);
}

SC_TEST(WorldQuery_ContainsReflectsComponentPresence) {
safecrowd::engine::EcsCore core;
safecrowd::engine::WorldQuery query{core};
safecrowd::engine::CommandBuffer buffer;
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

const auto e = core.createEntity();
core.addComponent(e, Position{});

SC_EXPECT_TRUE(query.contains<Position>(e));
SC_EXPECT_TRUE(!query.contains<Velocity>(e));
SC_EXPECT_TRUE(world.query().contains<Position>(e));
SC_EXPECT_TRUE(!world.query().contains<Velocity>(e));
}

SC_TEST(WorldQuery_GetReturnsComponentRef) {
safecrowd::engine::EcsCore core;
safecrowd::engine::WorldQuery query{core};
safecrowd::engine::CommandBuffer buffer;
auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer);

const auto e = core.createEntity();
core.addComponent(e, Position{3.0f, 4.0f});

const auto& pos = query.get<Position>(e);
const auto& pos = world.query().get<Position>(e);
SC_EXPECT_NEAR(pos.x, 3.0f, 1e-6);
SC_EXPECT_NEAR(pos.y, 4.0f, 1e-6);
}
3 changes: 3 additions & 0 deletions uml/engine-ecs-core.puml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package "engine::api" {
+view<T...>()
+contains<T>(entity: Entity): bool
+get<T>(entity: Entity): T&
-WorldQuery(core: EcsCore&)
}

class WorldCommands {
Expand Down Expand Up @@ -111,6 +112,8 @@ end note
note bottom of WorldQuery
The first implementation may iterate live entities
and filter by signature without extra caches.
Upper layers obtain it through EngineWorld::query()
instead of constructing it directly.
Higher-level query caching can be added later
without changing the facade.
end note
Expand Down
2 changes: 2 additions & 0 deletions uml/engine-ecs-core.puml 해설.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
## `EngineWorld`
- 개요: ECS 코어에 접근하는 상위 파사드다.
- 목적: runtime과 상위 계층이 raw registry 대신 동일한 진입점을 쓰도록 만든다.
- 접근 경계: `WorldQuery` 같은 ECS 읽기 면도 직접 생성하지 않고 `EngineWorld::query()`를 통해서만 꺼내 쓴다.
- 유의사항: 편의 객체처럼 보이더라도, 실제로는 계층 경계를 지키는 핵심 요소다.
- 후속 개선 사항: 읽기/쓰기 분리 facade나 phase-aware facade로 나눌 수 있다.

## `WorldQuery`
- 개요: ECS 컴포넌트 조회와 값 접근용 API다.
- 목적: 필요한 컴포넌트 조합을 읽고 개별 엔티티의 컴포넌트 값을 조회하거나 갱신하게 한다.
- 접근 경계: 상위 계층은 raw `EcsCore`를 받아 `WorldQuery`를 직접 만들지 않고, `EngineWorld`가 제공하는 query facade를 사용한다.
- 유의사항: 초기에는 단순 signature 필터로 충분하지만, query 중 엔티티 생성/삭제나 컴포넌트 추가/제거 같은 구조 변경은 허용하지 않는 원칙이 중요하다.
- 후속 개선 사항: cached query, typed iterator, optional access 지원을 확장할 수 있다.

Expand Down
8 changes: 8 additions & 0 deletions uml/engine-runtime-core.puml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package "engine::api" {
+view<T...>()
+contains<T>(entity: Entity): bool
+get<T>(entity: Entity): T&
-WorldQuery(core: EcsCore&)
}

class WorldResources {
Expand Down Expand Up @@ -180,6 +181,13 @@ note bottom of WorldCommands
while iterating queries.
end note

note bottom of WorldQuery
Runtime and systems access query capabilities
through EngineWorld::query().
Raw ECS-backed construction stays behind
the engine facade boundary.
end note

note right of DeterministicRng
Repeatable seed streams are derived per run
so scenario batches can be reproduced.
Expand Down
2 changes: 2 additions & 0 deletions uml/engine-runtime-core.puml 해설.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
## `EngineWorld`
- 개요: query, resources, commands를 묶은 월드 파사드다.
- 목적: 외부 계층이 raw storage를 직접 만지지 않고도 월드에 접근할 수 있게 한다.
- 접근 경계: `WorldQuery`는 외부에서 raw `EcsCore`로 직접 생성하지 않고 `EngineWorld::query()`를 통해 획득한다.
- 유의사항: 단순 편의 객체가 아니라 엔진 접근 규칙을 강제하는 경계로 다뤄야 한다.
- 후속 개선 사항: 읽기/쓰기 권한 분리 facade로 확장할 수 있다.

## `WorldQuery`
- 개요: 엔티티와 컴포넌트 조회 및 값 접근 면이다.
- 목적: 시스템이 필요한 컴포넌트 집합을 안전하게 순회하고 조회하거나 기존 컴포넌트 값을 갱신하게 한다.
- 접근 경계: runtime과 시스템은 `EngineWorld`가 가진 query facade를 사용하고, raw ECS 기반 생성 경로는 엔진 내부 구현 세부로 남긴다.
- 유의사항: 초기 구현은 전체 live entity 순회로 시작해도 되지만, query 중 entity 생성/삭제나 component 추가/제거 같은 구조 변경은 허용하면 안 된다.
- 후속 개선 사항: query cache, signature index, iterator 최적화를 추가할 수 있다.

Expand Down
Loading