From 8923005986cea87fa6cf1589d0190694e68677e8 Mon Sep 17 00:00:00 2001 From: learncold Date: Thu, 9 Apr 2026 02:04:40 +0900 Subject: [PATCH] tighten EngineWorld query boundary --- CMakeLists.txt | 2 + ...5\355\212\270 \352\265\254\354\241\260.md" | 2 +- ...4\353\260\234 \355\231\230\352\262\275.md" | 4 +- src/engine/EngineRuntime.cpp | 2 +- src/engine/EngineRuntime.h | 1 + src/engine/EngineSystem.h | 18 +-------- src/engine/EngineWorld.h | 38 +++++++++++++++++++ src/engine/WorldQuery.h | 8 +++- src/engine/internal/EngineWorldFactory.h | 16 ++++++++ tests/SystemSchedulerTests.cpp | 13 ++++--- tests/WorldQueryTests.cpp | 36 ++++++++++++------ uml/engine-ecs-core.puml | 3 ++ ...ecs-core.puml \355\225\264\354\204\244.md" | 2 + uml/engine-runtime-core.puml | 8 ++++ ...ime-core.puml \355\225\264\354\204\244.md" | 2 + 15 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 src/engine/EngineWorld.h create mode 100644 src/engine/internal/EngineWorldFactory.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0094216..580661b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -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 ) diff --git "a/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" "b/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" index 8f38620..b2c8f7c 100644 --- "a/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" +++ "b/docs/architecture/\355\224\204\353\241\234\354\240\235\355\212\270 \352\265\254\354\241\260.md" @@ -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)` 형태로 월드와 프레임 컨텍스트를 받는 범용 시스템 인터페이스 diff --git "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" index ea7b441..e5b6d73 100644 --- "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/[\354\226\221\354\213\235 5] \354\213\234\354\212\244\355\205\234 \352\265\254\354\241\260 \354\204\244\352\263\204 \353\260\217 \352\260\234\353\260\234 \355\231\230\352\262\275.md" @@ -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 @@ -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 도메인 지식 의존 금지 | 핵심 의존 방향은 아래 한 줄로 요약된다. diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 5a3803e..bc99e88 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -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_) { } diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 340e7d1..407e781 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -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" diff --git a/src/engine/EngineSystem.h b/src/engine/EngineSystem.h index c8d09d6..b0c6196 100644 --- a/src/engine/EngineSystem.h +++ b/src/engine/EngineSystem.h @@ -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; diff --git a/src/engine/EngineWorld.h b/src/engine/EngineWorld.h new file mode 100644 index 0000000..c31aab5 --- /dev/null +++ b/src/engine/EngineWorld.h @@ -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 diff --git a/src/engine/WorldQuery.h b/src/engine/WorldQuery.h index cc3885a..e4ad440 100644 --- a/src/engine/WorldQuery.h +++ b/src/engine/WorldQuery.h @@ -6,10 +6,10 @@ namespace safecrowd::engine { +class EngineWorld; + class WorldQuery { public: - explicit WorldQuery(EcsCore& core) : core_(core) {} - template [[nodiscard]] std::vector view() const { Signature required{}; @@ -50,6 +50,10 @@ class WorldQuery { } private: + friend class EngineWorld; + + explicit WorldQuery(EcsCore& core) : core_(core) {} + EcsCore& core_; }; diff --git a/src/engine/internal/EngineWorldFactory.h b/src/engine/internal/EngineWorldFactory.h new file mode 100644 index 0000000..4f0d001 --- /dev/null +++ b/src/engine/internal/EngineWorldFactory.h @@ -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 diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp index a40d6a7..5cc00f0 100644 --- a/tests/SystemSchedulerTests.cpp +++ b/tests/SystemSchedulerTests.cpp @@ -8,6 +8,7 @@ #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" #include "engine/SystemScheduler.h" +#include "engine/internal/EngineWorldFactory.h" namespace { @@ -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 log; scheduler.registerSystem( @@ -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 log; scheduler.registerSystem( @@ -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 log; scheduler.registerSystem( @@ -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(), {}); @@ -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(), @@ -187,7 +188,7 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { world, ctx); - const auto entities = safecrowd::engine::WorldQuery{core}.view(); + const auto entities = world.query().view(); SC_EXPECT_EQ(entities.size(), std::size_t{1}); } diff --git a/tests/WorldQueryTests.cpp b/tests/WorldQueryTests.cpp index 2bf469a..9c88492 100644 --- a/tests/WorldQueryTests.cpp +++ b/tests/WorldQueryTests.cpp @@ -1,6 +1,8 @@ #include "TestSupport.h" -#include "engine/WorldQuery.h" +#include + +#include "engine/internal/EngineWorldFactory.h" namespace { @@ -16,9 +18,15 @@ struct Velocity { } // namespace +static_assert(!std::is_constructible_v); +static_assert(!std::is_constructible_v); + 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(); @@ -29,24 +37,26 @@ SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) { core.addComponent(e2, Position{}); core.addComponent(e3, Velocity{}); - const auto result = query.view(); + const auto result = world.query().view(); 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(core.createEntity()); - const auto result = query.view(); + const auto result = world.query().view(); 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{}); @@ -55,30 +65,32 @@ SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { const auto e2 = core.createEntity(); core.addComponent(e2, Position{}); - const auto result = query.view(); + const auto result = world.query().view(); 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(e)); - SC_EXPECT_TRUE(!query.contains(e)); + SC_EXPECT_TRUE(world.query().contains(e)); + SC_EXPECT_TRUE(!world.query().contains(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(e); + const auto& pos = world.query().get(e); SC_EXPECT_NEAR(pos.x, 3.0f, 1e-6); SC_EXPECT_NEAR(pos.y, 4.0f, 1e-6); } diff --git a/uml/engine-ecs-core.puml b/uml/engine-ecs-core.puml index dd311a7..e592d23 100644 --- a/uml/engine-ecs-core.puml +++ b/uml/engine-ecs-core.puml @@ -18,6 +18,7 @@ package "engine::api" { +view() +contains(entity: Entity): bool +get(entity: Entity): T& + -WorldQuery(core: EcsCore&) } class WorldCommands { @@ -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 diff --git "a/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" "b/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" index e961ca6..a5783d0 100644 --- "a/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" +++ "b/uml/engine-ecs-core.puml \355\225\264\354\204\244.md" @@ -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 지원을 확장할 수 있다. diff --git a/uml/engine-runtime-core.puml b/uml/engine-runtime-core.puml index e02fa3f..300ec94 100644 --- a/uml/engine-runtime-core.puml +++ b/uml/engine-runtime-core.puml @@ -28,6 +28,7 @@ package "engine::api" { +view() +contains(entity: Entity): bool +get(entity: Entity): T& + -WorldQuery(core: EcsCore&) } class WorldResources { @@ -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. diff --git "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" index 26f247a..5ac673b 100644 --- "a/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" +++ "b/uml/engine-runtime-core.puml \355\225\264\354\204\244.md" @@ -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 최적화를 추가할 수 있다.