From e97301e4420bd178b4df0e4b404b88b4820276d7 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Sat, 11 Apr 2026 08:04:51 +0900 Subject: [PATCH 1/5] [Engine] add ResourceStore and WorldResources --- CMakeLists.txt | 2 + src/engine/EngineRuntime.cpp | 2 +- src/engine/EngineRuntime.h | 2 + src/engine/EngineSystem.h | 1 - src/engine/EngineWorld.h | 13 ++-- src/engine/ResourceStore.h | 81 ++++++++++++++++++++++++ src/engine/internal/EngineWorldFactory.h | 5 +- tests/EngineRuntimeTests.cpp | 23 +++++++ tests/ResourceStoreTests.cpp | 47 ++++++++++++++ tests/SystemSchedulerTests.cpp | 21 ++++-- tests/WorldQueryTests.cpp | 16 +++-- 11 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 src/engine/ResourceStore.h create mode 100644 tests/ResourceStoreTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 749365c..e1d16d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_library(ecs_engine STATIC src/engine/EngineStepContext.h src/engine/EngineSystem.h src/engine/FrameClock.h + src/engine/ResourceStore.h src/engine/WorldQuery.h src/engine/CommandBuffer.h src/engine/UpdatePhase.h @@ -128,6 +129,7 @@ if (BUILD_TESTING) tests/CommandBufferTests.cpp tests/SystemSchedulerTests.cpp tests/EngineIntegrationTests.cpp + tests/ResourceStoreTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index bc99e88..c1486a1 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_(EngineWorld::ConstructionToken{}, core_, buffer_), + world_(EngineWorld::ConstructionToken{}, core_, resources_, buffer_), frameClock_(config_) { } diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 407e781..6c7d823 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -10,6 +10,7 @@ #include "engine/EngineSystem.h" #include "engine/EngineWorld.h" #include "engine/FrameClock.h" +#include "engine/ResourceStore.h" #include "engine/SystemDescriptor.h" #include "engine/SystemScheduler.h" @@ -39,6 +40,7 @@ class EngineRuntime { EngineConfig config_; EngineStats stats_; EcsCore core_; + ResourceStore resources_; CommandBuffer buffer_; SystemScheduler scheduler_; EngineWorld world_; diff --git a/src/engine/EngineSystem.h b/src/engine/EngineSystem.h index b0c6196..1ea062b 100644 --- a/src/engine/EngineSystem.h +++ b/src/engine/EngineSystem.h @@ -4,7 +4,6 @@ #include "engine/EngineStepContext.h" namespace safecrowd::engine { - class EngineSystem { public: virtual ~EngineSystem() = default; diff --git a/src/engine/EngineWorld.h b/src/engine/EngineWorld.h index c31aab5..f1db8e9 100644 --- a/src/engine/EngineWorld.h +++ b/src/engine/EngineWorld.h @@ -1,6 +1,7 @@ #pragma once #include "engine/CommandBuffer.h" +#include "engine/ResourceStore.h" #include "engine/WorldQuery.h" namespace safecrowd::engine { @@ -13,6 +14,8 @@ class EngineWorld { public: [[nodiscard]] WorldQuery& query() noexcept { return query_; } [[nodiscard]] const WorldQuery& query() const noexcept { return query_; } + [[nodiscard]] WorldResources& resources() noexcept { return resources_; } + [[nodiscard]] const WorldResources& resources() const noexcept { return resources_; } [[nodiscard]] WorldCommands& commands() noexcept { return commands_; } private: @@ -25,14 +28,16 @@ class EngineWorld { }; EngineWorld() = delete; - explicit EngineWorld(ConstructionToken, EcsCore& core, CommandBuffer& buffer) - : query_(core), commands_(buffer) {} + explicit EngineWorld(ConstructionToken, EcsCore& core, ResourceStore& resources, + CommandBuffer& buffer) + : query_(core), resources_(resources), commands_(buffer) {} friend class EngineRuntime; friend class internal::EngineWorldFactory; - WorldQuery query_; - WorldCommands commands_; + WorldQuery query_; + WorldResources resources_; + WorldCommands commands_; }; } // namespace safecrowd::engine diff --git a/src/engine/ResourceStore.h b/src/engine/ResourceStore.h new file mode 100644 index 0000000..6ea0361 --- /dev/null +++ b/src/engine/ResourceStore.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace safecrowd::engine { + +class ResourceStore { +public: + template + void set(T resource) { + resources_[std::type_index(typeid(T))] = std::move(resource); + } + + template + [[nodiscard]] bool contains() const { + return resources_.contains(std::type_index(typeid(T))); + } + + template + [[nodiscard]] T& get() { + return getInternal(); + } + + template + [[nodiscard]] const T& get() const { + return getInternal(); + } + +private: + template + [[nodiscard]] T& getInternal() const { + const auto it = resources_.find(std::type_index(typeid(T))); + if (it == resources_.end()) { + throw std::runtime_error("Requested resource is not present in ResourceStore"); + } + + auto* value = std::any_cast(&it->second); + if (value == nullptr) { + throw std::runtime_error("Stored resource type does not match requested type"); + } + + return *value; + } + + mutable std::unordered_map resources_; +}; + +class WorldResources { +public: + explicit WorldResources(ResourceStore& store) + : store_(store) {} + + template + void set(T resource) { + store_.set(std::move(resource)); + } + + template + [[nodiscard]] bool contains() const { + return store_.contains(); + } + + template + [[nodiscard]] T& get() { + return store_.get(); + } + + template + [[nodiscard]] const T& get() const { + return store_.get(); + } + +private: + ResourceStore& store_; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/internal/EngineWorldFactory.h b/src/engine/internal/EngineWorldFactory.h index 4f0d001..b22306b 100644 --- a/src/engine/internal/EngineWorldFactory.h +++ b/src/engine/internal/EngineWorldFactory.h @@ -8,8 +8,9 @@ class EngineWorldFactory { public: EngineWorldFactory() = delete; - [[nodiscard]] static EngineWorld create(EcsCore& core, CommandBuffer& buffer) { - return EngineWorld(EngineWorld::ConstructionToken{}, core, buffer); + [[nodiscard]] static EngineWorld create(EcsCore& core, ResourceStore& resources, + CommandBuffer& buffer) { + return EngineWorld(EngineWorld::ConstructionToken{}, core, resources, buffer); } }; diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index e87969e..3b82a94 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -10,6 +10,9 @@ namespace { struct Marker {}; +struct SharedCounter { + int value{0}; +}; class UpdateCounterSystem : public safecrowd::engine::EngineSystem { public: @@ -64,6 +67,16 @@ class RecordPhaseSystem : public safecrowd::engine::EngineSystem { } }; +class ResourceSetupSystem : public safecrowd::engine::EngineSystem { +public: + void configure(safecrowd::engine::EngineWorld& world) override { + world.resources().set(SharedCounter{7}); + } + + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + } +}; + } // namespace SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { @@ -259,3 +272,13 @@ SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) { SC_EXPECT_EQ(stats.fixedStepIndex, 0ULL); SC_EXPECT_NEAR(stats.alpha, 0.0, 1e-9); } + +SC_TEST(EngineRuntime_WorldResources_AccessibleThroughEngineWorld) { + safecrowd::engine::EngineRuntime runtime; + + runtime.addSystem(std::make_unique()); + runtime.initialize(); + + SC_EXPECT_TRUE(runtime.world().resources().contains()); + SC_EXPECT_EQ(runtime.world().resources().get().value, 7); +} diff --git a/tests/ResourceStoreTests.cpp b/tests/ResourceStoreTests.cpp new file mode 100644 index 0000000..7eb8d6f --- /dev/null +++ b/tests/ResourceStoreTests.cpp @@ -0,0 +1,47 @@ +#include "TestSupport.h" + +#include +#include + +#include "engine/ResourceStore.h" + +namespace { + +struct SampleResource { + int value{0}; +}; + +} // namespace + +SC_TEST(ResourceStore_SetGetAndContainsWork) { + safecrowd::engine::ResourceStore store; + + store.set(SampleResource{42}); + + SC_EXPECT_TRUE(store.contains()); + SC_EXPECT_EQ(store.get().value, 42); +} + +SC_TEST(ResourceStore_GetMissingResourceThrows) { + safecrowd::engine::ResourceStore store; + + bool threw = false; + try { + (void)store.get(); + } catch (const std::runtime_error&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +} + +SC_TEST(WorldResources_DelegatesToUnderlyingStore) { + safecrowd::engine::ResourceStore store; + safecrowd::engine::WorldResources resources{store}; + + resources.set(std::string{"seeded"}); + + SC_EXPECT_TRUE(resources.contains()); + SC_EXPECT_EQ(resources.get(), std::string{"seeded"}); + SC_EXPECT_EQ(store.get(), std::string{"seeded"}); +} diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp index 5cc00f0..8681127 100644 --- a/tests/SystemSchedulerTests.cpp +++ b/tests/SystemSchedulerTests.cpp @@ -7,6 +7,7 @@ #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" +#include "engine/ResourceStore.h" #include "engine/SystemScheduler.h" #include "engine/internal/EngineWorldFactory.h" @@ -67,8 +68,10 @@ SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { safecrowd::engine::SystemScheduler scheduler{core, buffer}; safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; safecrowd::engine::CommandBuffer dummyBuffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -105,8 +108,10 @@ SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { safecrowd::engine::SystemScheduler scheduler{core, buffer}; safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; safecrowd::engine::CommandBuffer dummyBuffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -134,8 +139,10 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { safecrowd::engine::SystemScheduler scheduler{core, buffer}; safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::ResourceStore dummyResources; safecrowd::engine::CommandBuffer dummyBuffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(dummyCore, dummyBuffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + dummyCore, dummyResources, dummyBuffer); std::vector log; scheduler.registerSystem( @@ -156,9 +163,11 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; safecrowd::engine::SystemScheduler scheduler{core, buffer}; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + core, resources, buffer); std::size_t configuredCount = 0; scheduler.registerSystem(std::make_unique(), {}); @@ -172,10 +181,12 @@ SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) { SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; safecrowd::engine::SystemScheduler scheduler{core, buffer}; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create( + core, resources, buffer); scheduler.registerSystem( std::make_unique(), diff --git a/tests/WorldQueryTests.cpp b/tests/WorldQueryTests.cpp index 9c88492..1fca7b9 100644 --- a/tests/WorldQueryTests.cpp +++ b/tests/WorldQueryTests.cpp @@ -21,12 +21,14 @@ struct Velocity { static_assert(!std::is_constructible_v); static_assert(!std::is_constructible_v); SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const auto e1 = core.createEntity(); const auto e2 = core.createEntity(); @@ -44,8 +46,9 @@ SC_TEST(WorldQuery_ViewFiltersEntitiesBySignature) { SC_TEST(WorldQuery_ViewReturnsEmptyIfTypeNotRegistered) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); static_cast(core.createEntity()); @@ -55,8 +58,9 @@ SC_TEST(WorldQuery_ViewReturnsEmptyIfTypeNotRegistered) { SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const auto e1 = core.createEntity(); core.addComponent(e1, Position{}); @@ -72,8 +76,9 @@ SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { SC_TEST(WorldQuery_ContainsReflectsComponentPresence) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const auto e = core.createEntity(); core.addComponent(e, Position{}); @@ -84,8 +89,9 @@ SC_TEST(WorldQuery_ContainsReflectsComponentPresence) { SC_TEST(WorldQuery_GetReturnsComponentRef) { safecrowd::engine::EcsCore core; + safecrowd::engine::ResourceStore resources; safecrowd::engine::CommandBuffer buffer; - auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, buffer); + auto world = safecrowd::engine::internal::EngineWorldFactory::create(core, resources, buffer); const auto e = core.createEntity(); core.addComponent(e, Position{3.0f, 4.0f}); From 9336bdf29175c92d6ae9d5bad4fb0a6192694e67 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Sat, 11 Apr 2026 23:54:06 +0900 Subject: [PATCH 2/5] [Engine] reset world resources on runtime restart Reset ResourceStore during initialize/stop so world resources do not leak across runs. Add regression tests covering stop/reset and reinitialize/reset behavior. --- src/engine/EngineRuntime.cpp | 2 ++ tests/EngineRuntimeTests.cpp | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index c1486a1..8f94624 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -36,6 +36,7 @@ void EngineRuntime::addSystem(std::unique_ptr system, void EngineRuntime::initialize() { frameClock_.reset(); core_ = EcsCore{}; + resources_ = ResourceStore{}; buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Ready; @@ -70,6 +71,7 @@ void EngineRuntime::pause() { void EngineRuntime::stop() { frameClock_.reset(); core_ = EcsCore{}; + resources_ = ResourceStore{}; buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Stopped; diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index 3b82a94..7c2a45f 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -282,3 +282,25 @@ SC_TEST(EngineRuntime_WorldResources_AccessibleThroughEngineWorld) { SC_EXPECT_TRUE(runtime.world().resources().contains()); SC_EXPECT_EQ(runtime.world().resources().get().value, 7); } + +SC_TEST(EngineRuntime_Stop_ClearsWorldResourcesBeforeNextRun) { + safecrowd::engine::EngineRuntime runtime; + + runtime.world().resources().set(SharedCounter{11}); + SC_EXPECT_TRUE(runtime.world().resources().contains()); + + runtime.stop(); + + SC_EXPECT_TRUE(!runtime.world().resources().contains()); +} + +SC_TEST(EngineRuntime_Initialize_ClearsExistingWorldResources) { + safecrowd::engine::EngineRuntime runtime; + + runtime.world().resources().set(SharedCounter{13}); + SC_EXPECT_TRUE(runtime.world().resources().contains()); + + runtime.initialize(); + + SC_EXPECT_TRUE(!runtime.world().resources().contains()); +} From a97e162b7fc9f4121f182c98512f17847e4b614a Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Sun, 12 Apr 2026 00:34:32 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[Docs]=20=EB=B0=9C=ED=91=9C=20=EB=8C=80?= =?UTF-8?q?=EB=B3=B8=20=EB=AC=B8=EC=84=9C=20=EC=9C=84=EC=B9=98=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\355\221\234_\354\264\210\354\225\210.md" | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) 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/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" index 3589f83..a8d2f69 100644 --- "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" @@ -87,5 +87,46 @@ run 요약, variation 요약, 비교, 누적 기록의 분리는, 개별 실행 # 슬라이드 10 일부 User Story의 Acceptance Criteria가 변경되어 Sprint 별 배치도 조금씩 변경되었습니다. -# 슬라이드 11 - 구현내용 -(추가 예정) \ No newline at end of file +# 슬라이드 11 - Engine 파트 구현 범위 +이제 구현 내용 중에서 제가 담당한 Engine 파트를 간단히 설명드리겠습니다. +저희 프로젝트는 `application -> domain -> engine` 계층 구조를 유지하고 있는데, 여기서 Engine은 SafeCrowd에서 범용 ECS 실행 기반을 담당합니다. +제가 구현한 범위는 크게 다섯 가지로, `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler`입니다. +이 작업은 한 번에 큰 덩어리로 프로젝트에 구현한 것이 아니라, 이슈 단위로 쪼개서 브랜치와 PR 흐름으로 진행했습니다. +예를 들어 이슈 `#8`에서는 `ComponentRegistry`와 `EcsCore`를, 이슈 `#9`에서는 `WorldQuery`를, 이슈 `#10`에서는 `CommandBuffer`를, 이슈 `#12`에서는 `EngineRuntime`을, 그리고 이슈 `#47`에서는 `SystemScheduler`를 구현했습니다. +즉 엔티티와 컴포넌트를 저장하고 관리하는 코어부터, 시스템이 월드를 읽고 변경하는 방식, 그리고 실제 실행 루프와 스케줄링까지 단계적으로 확장한 것입니다. +이번 구현의 목표는 단순히 데이터를 담는 자료구조를 만드는 것이 아니라, 이후 시뮬레이션 로직이 안정적으로 올라갈 수 있는 실행 환경을 먼저 구축하는 것이었습니다. + +# 슬라이드 12 - ECS 실행 구조 설계 +구조를 보면 먼저 `EcsCore`가 엔티티 생성과 삭제, 컴포넌트 추가와 제거를 통합 관리합니다. +이때 컴포넌트가 추가되거나 제거되면 엔티티의 `Signature` 비트셋이 자동으로 갱신되도록 해서, 각 엔티티가 어떤 컴포넌트 조합을 가지고 있는지 일관되게 추적할 수 있도록 했습니다. +그 위에서 `WorldQuery`는 시스템이 필요한 엔티티만 조회할 수 있는 읽기 전용 인터페이스를 제공합니다. +반대로 구조 변경은 즉시 반영하지 않고, `CommandBuffer`와 `WorldCommands`를 통해 먼저 명령으로 기록한 뒤 나중에 한 번에 반영하도록 했습니다. +이렇게 읽기와 쓰기를 분리하도록 구현한 이유는, 시스템 실행 중 월드 구조를 바로 바꾸면 순회 안정성이 깨질 수 있기 때문입니다. + +# 슬라이드 13 - Runtime과 Scheduler 구현 및 검증 +이 구조 위에서 `EngineRuntime`은 전체 실행을 담당하는 최소 오케스트레이터 역할을 합니다. +프레임 시간을 누적한 뒤 `fixed timestep` 기준으로 필요한 만큼 simulation step을 반복 실행하고, 각 step마다 시스템이 호출되도록 구성했습니다. +그리고 `SystemScheduler`에서는 시스템마다 `phase`와 `order`를 지정할 수 있게 해서, `Startup`, `PreSimulation`, `FixedSimulation`, `PostSimulation`, `RenderSync` 같은 단계별 실행 순서를 제어할 수 있도록 했습니다. +또 각 phase가 끝나는 경계에서 `CommandBuffer`를 flush하도록 하여, 시스템이 실행되는 도중에는 구조 변경이 쌓이기만 하고 실제 반영은 일관된 시점에 일어나도록 했습니다. +마지막으로 이 구현은 `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler` 각각에 대해 테스트 코드를 작성해 생명주기 관리, 조회 필터링, deferred mutation, fixed-step 실행, phase 순서 보장을 검증했습니다. +정리하면, SafeCrowd에서 engine 파트 구현은 이후 도메인 시뮬레이션 로직을 안정적으로 올릴 수 있도록 ECS의 저장 구조와 실행 구조를 함께 구축한 작업이라고 말씀드릴 수 있습니다. + +# 슬라이드 14 - 2DMap +도메인 계층의 구현 내용을 설명하기에 앞서 계층의 역할에 대해 설명하겠습니다. 도메인 계층은 시설 레이아웃 정의와 에이전트 이동 로직 등 시뮬레이션의 핵심 규칙과 데이터 구조를 포함하는 계층입니다. 시뮬레이션의 토대가 되는 Map은 FacilityLayout2D, Zone2D, Connection2D, Barrier2D, SpawnZone2D로 이루어져 있습니다. +FacilityLayout2D는 전체 시뮬레이션의 최상위 컨테이너입니다. +Zone2D는 보행자가 이동할 수 있는 구역, +Connection2D는 구역과 구역을 잇는 통로, +Barrier2D는 에이전트의 이동을 막는 고정 장애물, +SpawnZone2D 에이전트의 생성 지점 역할을 수행합니다. +출구는 Zone2D의 하위 속성으로 지정되고, 출구와 연결된 Connection2D 객체도 출구 속성을 갖습니다. 이 요소들은 에이전트의 경로 계산에 사용되고, 에이전트가 exitzone에 도달하면 도착한 것으로 판단하게 됩니다. + +# 슬라이드 15 - Agent +스폰된 에이전트는 Position, Velocity, Goal, Agent 컴포넌트를 가집니다. +Position은 에이전트의 좌표, Velocity는 에이전트의 이동 방향과 속도, Goal은 목적지와 도착 여부, Agent는 에이전트 개인의 물리적 크기나 이동 능력치 등의 고유 속성을 정의합니다. +에이전트는 방향 벡터에 maxspeed 값을 곱해 이동 속도를 정합니다. 만약, 에이전트가 목적지에 도달했다면 goal의 도착 여부 값을 true로 설정하고 속도를 0으로 조정합니다. +충돌 여부 탐지를 위해서는 이동 경로상에 있는 벽 데이터를 가져와 다음 프레임 위치가 벽과 충돌이 발생하는지 계산합니다. 충돌이 발생하지 않은 경우, 속도를 그대로 유지하고 충돌이 발생한 경우, 벽의 법선 벡터를 구해 벽을 따라 이동하도록 처리합니다. +각 에이전트는 goal에 도착할 때까지 이 과정을 반복합니다. + +# 슬라이드 16 - Import module +이 슬라이드는 Domain 계층에서 import 모듈이 도면 파일을 시뮬레이션에 사용할 수 있는 구조로 바꾸는 과정을 보여줍니다. 먼저 DxfImportService가 DXF 도면을 읽어 필요한 선과 도형 정보를 가져옵니다. 이 원본 정보는 RawImportModel에 한 번 보관해서, 나중에 문제가 생겼을 때 어떤 도면 요소에서 나온 것인지 추적할 수 있게 합니다. 그다음 CanonicalGeometry 단계에서 도면의 여러 요소를 벽, 출구, 보행 가능 영역처럼 공통된 의미로 정리합니다. 이후 FacilityLayoutBuilder가 이 정보를 방, 복도, 연결 통로 같은 시뮬레이션용 공간 구조로 변환합니다. 마지막으로 ImportValidationService가 출구 누락이나 끊긴 동선 같은 문제를 검사하고, ImportResult가 최종 결과와 문제 목록을 함께 넘겨서 사용자가 확인한 뒤 다음 단계로 넘어가게 합니다. + From 5cb1e1f0e05c22ebb5dbd55182189df508f73e60 Mon Sep 17 00:00:00 2001 From: learncold <166647091+learncold@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:16:54 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Delete=20docs/=EC=A0=9C=EC=B6=9C=EC=9A=A9/?= =?UTF-8?q?=EC=A2=85=ED=95=A9=EC=84=A4=EA=B3=84/Pathfinder=5F=EB=B0=98?= =?UTF-8?q?=EC=98=81=5F=EB=B0=9C=ED=91=9C=5F=EC=B4=88=EC=95=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\355\221\234_\354\264\210\354\225\210.md" | 132 ------------------ 1 file changed, 132 deletions(-) delete mode 100644 "docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" 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/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" deleted file mode 100644 index a8d2f69..0000000 --- "a/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" +++ /dev/null @@ -1,132 +0,0 @@ -# 슬라이드 1 -2팀 SafeCrowd: 군중 안전 시뮬레이터의 발표를 시작하겠습니다. - -# 슬라이드 2 -저희는 지난주까지 기존 시뮬레이터의 문제 정의와 저희 소프트웨어의 차별점을 발표했었습니다. -교수님께서 차별점 이전에 기존 범용 시뮬레이터의 기능들이 커버 되는 것이 우선이라고 피드백 주셔서 그 점 반영한 내용부터 발표하겠습니다. - -# 슬라이드 3 -반영한 내용은 다음과 같이 6가지로 나눌 수 있습니다. - -공간 입력과 토폴로지는 도면을 시뮬레이션 가능한 구조로 바꾸는 범위이고, -인원 모델과 이동 제약은 사람 유형별 이동 특성과 사용 가능한 경로를 정하는 범위입니다. -행동과 운영 제어는 상황에 따라 행동을 바꾸는 규칙과 운영 이벤트를 다루는 범위이고, -실행과 재현성은 반복 실험과 비교가 가능하도록 실행을 관리하는 범위입니다. -위험 측정과 결과 기록은 실행 중 무엇을 측정하고 어떤 결과를 남길지에 대한 범위이고, -결과 시각화와 비교는 그 결과를 화면으로 해석하는 범위입니다. - -# 슬라이드 4 - 공간 입력과 토폴로지 -공간 입력과 토폴로지 범위입니다. - -먼저 room-door connectivity 검토는, 방이 존재하는지만 보는 것이 아니라 그 방이 실제로 문을 통해 다른 공간이나 출구와 연결되는지까지 미리 확인하는 구조입니다. -그래서 단절 구역이나 출구 누락을 실행 전에 잡아낼 수 있습니다. - -정적 장애물과 동적 위험 구역 방해 요소의 분리, 처음부터 고정된 장애물과 상황에 따라 동적으로 생기거나 변화하는 위험 요소를 다르게 다룬다는 뜻입니다. - -계단 / 경사 / 에스컬레이터 / 복도의 공통 connector + modifier 구조는, 이 요소들을 모두 공간을 잇는 연결 통로로 보고 경사, 방향, 속도 같은 특성을 추가해 처리하는 방식입니다. - -ControlZone은 방, 문, 연결부를 하나의 구역으로 묶어서 한 번에 통제할 수 있게 하는 구조입니다. -그래서 실제 운영 시나리오처럼 특정 구역 전체를 제어할 수 있습니다. - -# 슬라이드 5 - 인원 모델과 이동 제약 -인원 모델과 이동 제약 범위입니다. - -프로필별 usable connector restriction은, 사람 유형에 따라 사용할 수 있는 문, 계단, 연결 통로를 다르게 둘 수 있다는 의미입니다. -즉 모든 사람이 같은 경로를 쓸 수 있다고 가정하지 않는 구조입니다. - -출구 선택 비용은 단순히 가장 가까운 출구를 고르는 것이 아니라, 지금 이동하는 시간, 문 앞 대기, 남은 경로를 함께 고려해 출구를 선택하는 방식입니다. - -그룹 분포, 보조 대피, 시야 조건을 반영한 인원 모델 확장은, 개인 단위 이동만이 아니라 동행 그룹, 도움이 필요한 사람, 시야가 나쁜 상황까지 사람 모델에 포함했다는 뜻입니다. - -마지막 문장은, 사람 이동이 단순 공간 구조만이 아니라 주변 환경 조건의 영향도 받도록 입력 구조를 확장했다는 의미입니다. - -# 슬라이드 6 - 행동과 운영 제어 -행동과 운영 제어 범위입니다. - -단순 on/off 이벤트 대신 행동 시퀀스 제어는, 이벤트를 그냥 켜고 끄는 것이 아니라 사람의 행동이 어떤 순서로 바뀌는지까지 포함해 제어한다는 뜻입니다. - -시간 조건과 상태 조건을 포함하는 Trigger 구조는, 특정 시간이 되었을 때뿐 아니라 특정 상황이 발생했을 때도 행동 전환이 일어나도록 만드는 구조입니다. - -Occupant Tag 구조는, 직원, 통제 대상, 유도 완료 집단처럼 사람들을 상태나 역할별로 구분해서 서로 다르게 제어할 수 있게 하는 방식입니다. - -보조 대피 행동 규칙과 역할 제어는, 도움이 필요한 사람과 지원 인력을 단순 속도 차이로 처리하는 것이 아니라 역할과 행동 규칙의 조합으로 제어할 수 있게 열어 두었다는 뜻입니다. - -# 슬라이드 7 - 실행과 재현성 -실행과 재현성 범위입니다. - -scenario, run, variation 단위의 실행 구조는, 시뮬레이션을 한 번 돌리고 끝내는 것이 아니라 시나리오, 개별 실행, 반복 변형 결과를 구분해서 관리하는 방식입니다. - -처음부터 배치된 사람만 다루는 것이 아니라 실행 도중 계속 들어오는 인원까지 함께 반영할 수 있습니다. - -seed, 샘플링 규칙, variation 식별자 기반 재현성 확보는, 같은 조건이면 같은 결과를 다시 재현할 수 있도록 난수와 실행 조건을 함께 관리하는 구조입니다. - -반복 실행과 variation 요약은, 여러 번 돌린 결과의 경향과 변동 폭을 파악할 수 있는 기능입니다. - -ScenarioBatchRunner 기반 배치 실행과 비교 실행은, 여러 시나리오와 variation을 한 번에 실행하고 그 결과를 비교하는 과정을 하나의 실행 흐름으로 정리했다는 의미입니다. - -# 슬라이드 8 - 위험 측정과 결과 기록 -위험 측정과 결과 기록 범위입니다. - -결과를 하나의 숫자로 끝내지 않고 문 단위, 공간 단위, 측정 구역 단위, 전체 요약 단위로 나누어 남깁니다. - -병목, 정체, 출구 처리량, 구역별 밀도와 혼잡의 계층형 기록 구조는, 위험을 전체 평균으로만 보지 않고 어디에서 어떤 문제가 얼마나 발생했는지를 단계적으로 확인할 수 있게 하는 구조입니다. - -run 요약, variation 요약, 비교, 누적 기록의 분리는, 개별 실행 결과와 반복 실행 요약, 시나리오 비교 결과, 전체 종합 결과를 서로 구분해서 관리한다는 뜻입니다. - -선택적 occupant history 기반 개체 단위 기록은, 필요할 때는 특정 사람이나 집단의 이동 이력을 따로 남겨서 자세히 분석할 수 있게 한다는 의미입니다. - -마지막으로, 단순 대피 시간뿐 아니라 여러 위험 유형을 구역별로 비교할 수 있게 합니다. - -# 슬라이드 9 - 결과 시각화와 비교 -결과 시각화와 비교 범위입니다. - -밀도, 혼잡, 대피 시간, 출구 처리량, 구역별 위험 상태의 동일 공간 기준 비교는, 서로 다른 시나리오 결과를 같은 공간 위에서 겹쳐 보듯이 비교할 수 있게 한다는 의미입니다. - -재생, 비교, 근거 확인, 외부 공유까지 이어지는 결과 활용 구조는, 결과를 본 뒤 왜 그런 결과가 나왔는지 확인하고, 다른 안과 비교하고, 마지막에는 보고용 자료로 활용할 수 있게 이어지는 흐름이라는 의미입니다. - -# 슬라이드 10 -일부 User Story의 Acceptance Criteria가 변경되어 Sprint 별 배치도 조금씩 변경되었습니다. - -# 슬라이드 11 - Engine 파트 구현 범위 -이제 구현 내용 중에서 제가 담당한 Engine 파트를 간단히 설명드리겠습니다. -저희 프로젝트는 `application -> domain -> engine` 계층 구조를 유지하고 있는데, 여기서 Engine은 SafeCrowd에서 범용 ECS 실행 기반을 담당합니다. -제가 구현한 범위는 크게 다섯 가지로, `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler`입니다. -이 작업은 한 번에 큰 덩어리로 프로젝트에 구현한 것이 아니라, 이슈 단위로 쪼개서 브랜치와 PR 흐름으로 진행했습니다. -예를 들어 이슈 `#8`에서는 `ComponentRegistry`와 `EcsCore`를, 이슈 `#9`에서는 `WorldQuery`를, 이슈 `#10`에서는 `CommandBuffer`를, 이슈 `#12`에서는 `EngineRuntime`을, 그리고 이슈 `#47`에서는 `SystemScheduler`를 구현했습니다. -즉 엔티티와 컴포넌트를 저장하고 관리하는 코어부터, 시스템이 월드를 읽고 변경하는 방식, 그리고 실제 실행 루프와 스케줄링까지 단계적으로 확장한 것입니다. -이번 구현의 목표는 단순히 데이터를 담는 자료구조를 만드는 것이 아니라, 이후 시뮬레이션 로직이 안정적으로 올라갈 수 있는 실행 환경을 먼저 구축하는 것이었습니다. - -# 슬라이드 12 - ECS 실행 구조 설계 -구조를 보면 먼저 `EcsCore`가 엔티티 생성과 삭제, 컴포넌트 추가와 제거를 통합 관리합니다. -이때 컴포넌트가 추가되거나 제거되면 엔티티의 `Signature` 비트셋이 자동으로 갱신되도록 해서, 각 엔티티가 어떤 컴포넌트 조합을 가지고 있는지 일관되게 추적할 수 있도록 했습니다. -그 위에서 `WorldQuery`는 시스템이 필요한 엔티티만 조회할 수 있는 읽기 전용 인터페이스를 제공합니다. -반대로 구조 변경은 즉시 반영하지 않고, `CommandBuffer`와 `WorldCommands`를 통해 먼저 명령으로 기록한 뒤 나중에 한 번에 반영하도록 했습니다. -이렇게 읽기와 쓰기를 분리하도록 구현한 이유는, 시스템 실행 중 월드 구조를 바로 바꾸면 순회 안정성이 깨질 수 있기 때문입니다. - -# 슬라이드 13 - Runtime과 Scheduler 구현 및 검증 -이 구조 위에서 `EngineRuntime`은 전체 실행을 담당하는 최소 오케스트레이터 역할을 합니다. -프레임 시간을 누적한 뒤 `fixed timestep` 기준으로 필요한 만큼 simulation step을 반복 실행하고, 각 step마다 시스템이 호출되도록 구성했습니다. -그리고 `SystemScheduler`에서는 시스템마다 `phase`와 `order`를 지정할 수 있게 해서, `Startup`, `PreSimulation`, `FixedSimulation`, `PostSimulation`, `RenderSync` 같은 단계별 실행 순서를 제어할 수 있도록 했습니다. -또 각 phase가 끝나는 경계에서 `CommandBuffer`를 flush하도록 하여, 시스템이 실행되는 도중에는 구조 변경이 쌓이기만 하고 실제 반영은 일관된 시점에 일어나도록 했습니다. -마지막으로 이 구현은 `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler` 각각에 대해 테스트 코드를 작성해 생명주기 관리, 조회 필터링, deferred mutation, fixed-step 실행, phase 순서 보장을 검증했습니다. -정리하면, SafeCrowd에서 engine 파트 구현은 이후 도메인 시뮬레이션 로직을 안정적으로 올릴 수 있도록 ECS의 저장 구조와 실행 구조를 함께 구축한 작업이라고 말씀드릴 수 있습니다. - -# 슬라이드 14 - 2DMap -도메인 계층의 구현 내용을 설명하기에 앞서 계층의 역할에 대해 설명하겠습니다. 도메인 계층은 시설 레이아웃 정의와 에이전트 이동 로직 등 시뮬레이션의 핵심 규칙과 데이터 구조를 포함하는 계층입니다. 시뮬레이션의 토대가 되는 Map은 FacilityLayout2D, Zone2D, Connection2D, Barrier2D, SpawnZone2D로 이루어져 있습니다. -FacilityLayout2D는 전체 시뮬레이션의 최상위 컨테이너입니다. -Zone2D는 보행자가 이동할 수 있는 구역, -Connection2D는 구역과 구역을 잇는 통로, -Barrier2D는 에이전트의 이동을 막는 고정 장애물, -SpawnZone2D 에이전트의 생성 지점 역할을 수행합니다. -출구는 Zone2D의 하위 속성으로 지정되고, 출구와 연결된 Connection2D 객체도 출구 속성을 갖습니다. 이 요소들은 에이전트의 경로 계산에 사용되고, 에이전트가 exitzone에 도달하면 도착한 것으로 판단하게 됩니다. - -# 슬라이드 15 - Agent -스폰된 에이전트는 Position, Velocity, Goal, Agent 컴포넌트를 가집니다. -Position은 에이전트의 좌표, Velocity는 에이전트의 이동 방향과 속도, Goal은 목적지와 도착 여부, Agent는 에이전트 개인의 물리적 크기나 이동 능력치 등의 고유 속성을 정의합니다. -에이전트는 방향 벡터에 maxspeed 값을 곱해 이동 속도를 정합니다. 만약, 에이전트가 목적지에 도달했다면 goal의 도착 여부 값을 true로 설정하고 속도를 0으로 조정합니다. -충돌 여부 탐지를 위해서는 이동 경로상에 있는 벽 데이터를 가져와 다음 프레임 위치가 벽과 충돌이 발생하는지 계산합니다. 충돌이 발생하지 않은 경우, 속도를 그대로 유지하고 충돌이 발생한 경우, 벽의 법선 벡터를 구해 벽을 따라 이동하도록 처리합니다. -각 에이전트는 goal에 도착할 때까지 이 과정을 반복합니다. - -# 슬라이드 16 - Import module -이 슬라이드는 Domain 계층에서 import 모듈이 도면 파일을 시뮬레이션에 사용할 수 있는 구조로 바꾸는 과정을 보여줍니다. 먼저 DxfImportService가 DXF 도면을 읽어 필요한 선과 도형 정보를 가져옵니다. 이 원본 정보는 RawImportModel에 한 번 보관해서, 나중에 문제가 생겼을 때 어떤 도면 요소에서 나온 것인지 추적할 수 있게 합니다. 그다음 CanonicalGeometry 단계에서 도면의 여러 요소를 벽, 출구, 보행 가능 영역처럼 공통된 의미로 정리합니다. 이후 FacilityLayoutBuilder가 이 정보를 방, 복도, 연결 통로 같은 시뮬레이션용 공간 구조로 변환합니다. 마지막으로 ImportValidationService가 출구 누락이나 끊긴 동선 같은 문제를 검사하고, ImportResult가 최종 결과와 문제 목록을 함께 넘겨서 사용자가 확인한 뒤 다음 단계로 넘어가게 합니다. - From bbec4c30086419b6ba3d8c2f626b3d2061094876 Mon Sep 17 00:00:00 2001 From: learncold Date: Mon, 13 Apr 2026 18:22:45 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Pathfinder=20=EC=A1=B0=EC=82=AC=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EB=8C=80=EB=B3=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\355\221\234_\354\264\210\354\225\210.md" | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 "docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" 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/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" new file mode 100644 index 0000000..40ca017 --- /dev/null +++ "b/docs/\354\240\234\354\266\234\354\232\251/\354\242\205\355\225\251\354\204\244\352\263\204/Pathfinder_\353\260\230\354\230\201_\353\260\234\355\221\234_\354\264\210\354\225\210.md" @@ -0,0 +1,149 @@ +# 슬라이드 1 +2팀 SafeCrowd: 군중 안전 시뮬레이터의 발표를 시작하겠습니다. + +# 슬라이드 2 +저희는 지난주까지 기존 시뮬레이터의 문제 정의와 저희 소프트웨어의 차별점을 발표했었습니다. +교수님께서 차별점 이전에 기존 범용 시뮬레이터의 기능들이 커버 되는 것이 우선이라고 피드백 주셔서 그 점 반영한 내용부터 발표하겠습니다. + +# 슬라이드 3 +반영한 내용은 다음과 같이 6가지로 나눌 수 있습니다. + +공간 입력과 토폴로지는 도면을 시뮬레이션 가능한 구조로 바꾸는 범위이고, +인원 모델과 이동 제약은 사람 유형별 이동 특성과 사용 가능한 경로를 정하는 범위입니다. +행동과 운영 제어는 상황에 따라 행동을 바꾸는 규칙과 운영 이벤트를 다루는 범위이고, +실행과 재현성은 반복 실험과 비교가 가능하도록 실행을 관리하는 범위입니다. +위험 측정과 결과 기록은 실행 중 무엇을 측정하고 어떤 결과를 남길지에 대한 범위이고, +결과 시각화와 비교는 그 결과를 화면으로 해석하는 범위입니다. + +# 슬라이드 4 - 공간 입력과 토폴로지 +공간 입력과 토폴로지 범위입니다. + +먼저 room-door connectivity 검토는, 방이 존재하는지만 보는 것이 아니라 그 방이 실제로 문을 통해 다른 공간이나 출구와 연결되는지까지 미리 확인하는 구조입니다. +그래서 단절 구역이나 출구 누락을 실행 전에 잡아낼 수 있습니다. + +정적 장애물과 동적 위험 구역 방해 요소의 분리, 처음부터 고정된 장애물과 상황에 따라 동적으로 생기거나 변화하는 위험 요소를 다르게 다룬다는 뜻입니다. + +계단 / 경사 / 에스컬레이터 / 복도의 공통 connector + modifier 구조는, 이 요소들을 모두 공간을 잇는 연결 통로로 보고 경사, 방향, 속도 같은 특성을 추가해 처리하는 방식입니다. + +ControlZone은 방, 문, 연결부를 하나의 구역으로 묶어서 한 번에 통제할 수 있게 하는 구조입니다. +그래서 실제 운영 시나리오처럼 특정 구역 전체를 제어할 수 있습니다. + +# 슬라이드 5 - 인원 모델과 이동 제약 +인원 모델과 이동 제약 범위입니다. + +프로필별 usable connector restriction은, 사람 유형에 따라 사용할 수 있는 문, 계단, 연결 통로를 다르게 둘 수 있다는 의미입니다. +즉 모든 사람이 같은 경로를 쓸 수 있다고 가정하지 않는 구조입니다. + +출구 선택 비용은 단순히 가장 가까운 출구를 고르는 것이 아니라, 지금 이동하는 시간, 문 앞 대기, 남은 경로를 함께 고려해 출구를 선택하는 방식입니다. + +그룹 분포, 보조 대피, 시야 조건을 반영한 인원 모델 확장은, 개인 단위 이동만이 아니라 동행 그룹, 도움이 필요한 사람, 시야가 나쁜 상황까지 사람 모델에 포함했다는 뜻입니다. + +마지막 문장은, 사람 이동이 단순 공간 구조만이 아니라 주변 환경 조건의 영향도 받도록 입력 구조를 확장했다는 의미입니다. + +# 슬라이드 6 - 행동과 운영 제어 +행동과 운영 제어 범위입니다. + +단순 on/off 이벤트 대신 행동 시퀀스 제어는, 이벤트를 그냥 켜고 끄는 것이 아니라 사람의 행동이 어떤 순서로 바뀌는지까지 포함해 제어한다는 뜻입니다. + +시간 조건과 상태 조건을 포함하는 Trigger 구조는, 특정 시간이 되었을 때뿐 아니라 특정 상황이 발생했을 때도 행동 전환이 일어나도록 만드는 구조입니다. + +Occupant Tag 구조는, 직원, 통제 대상, 유도 완료 집단처럼 사람들을 상태나 역할별로 구분해서 서로 다르게 제어할 수 있게 하는 방식입니다. + +보조 대피 행동 규칙과 역할 제어는, 도움이 필요한 사람과 지원 인력을 단순 속도 차이로 처리하는 것이 아니라 역할과 행동 규칙의 조합으로 제어할 수 있게 열어 두었다는 뜻입니다. + +# 슬라이드 7 - 실행과 재현성 +실행과 재현성 범위입니다. + +scenario, run, variation 단위의 실행 구조는, 시뮬레이션을 한 번 돌리고 끝내는 것이 아니라 시나리오, 개별 실행, 반복 변형 결과를 구분해서 관리하는 방식입니다. + +처음부터 배치된 사람만 다루는 것이 아니라 실행 도중 계속 들어오는 인원까지 함께 반영할 수 있습니다. + +seed, 샘플링 규칙, variation 식별자 기반 재현성 확보는, 같은 조건이면 같은 결과를 다시 재현할 수 있도록 난수와 실행 조건을 함께 관리하는 구조입니다. + +반복 실행과 variation 요약은, 여러 번 돌린 결과의 경향과 변동 폭을 파악할 수 있는 기능입니다. + +ScenarioBatchRunner 기반 배치 실행과 비교 실행은, 여러 시나리오와 variation을 한 번에 실행하고 그 결과를 비교하는 과정을 하나의 실행 흐름으로 정리했다는 의미입니다. + +# 슬라이드 8 - 위험 측정과 결과 기록 +위험 측정과 결과 기록 범위입니다. + +결과를 하나의 숫자로 끝내지 않고 문 단위, 공간 단위, 측정 구역 단위, 전체 요약 단위로 나누어 남깁니다. + +병목, 정체, 출구 처리량, 구역별 밀도와 혼잡의 계층형 기록 구조는, 위험을 전체 평균으로만 보지 않고 어디에서 어떤 문제가 얼마나 발생했는지를 단계적으로 확인할 수 있게 하는 구조입니다. + +run 요약, variation 요약, 비교, 누적 기록의 분리는, 개별 실행 결과와 반복 실행 요약, 시나리오 비교 결과, 전체 종합 결과를 서로 구분해서 관리한다는 뜻입니다. + +선택적 occupant history 기반 개체 단위 기록은, 필요할 때는 특정 사람이나 집단의 이동 이력을 따로 남겨서 자세히 분석할 수 있게 한다는 의미입니다. + +마지막으로, 단순 대피 시간뿐 아니라 여러 위험 유형을 구역별로 비교할 수 있게 합니다. + +# 슬라이드 9 - 결과 시각화와 비교 +결과 시각화와 비교 범위입니다. + +밀도, 혼잡, 대피 시간, 출구 처리량, 구역별 위험 상태의 동일 공간 기준 비교는, 서로 다른 시나리오 결과를 같은 공간 위에서 겹쳐 보듯이 비교할 수 있게 한다는 의미입니다. + +재생, 비교, 근거 확인, 외부 공유까지 이어지는 결과 활용 구조는, 결과를 본 뒤 왜 그런 결과가 나왔는지 확인하고, 다른 안과 비교하고, 마지막에는 보고용 자료로 활용할 수 있게 이어지는 흐름이라는 의미입니다. + +# 슬라이드 10 +일부 User Story의 Acceptance Criteria가 변경되어 Sprint 별 배치도 조금씩 변경되었습니다. + +# 슬라이드 11 - Engine 파트 구현 범위 +이제 구현 내용 중에서 제가 담당한 Engine 파트를 간단히 설명드리겠습니다. +저희 프로젝트는 `application -> domain -> engine` 계층 구조를 유지하고 있는데, 여기서 Engine은 SafeCrowd에서 범용 ECS 실행 기반을 담당합니다. +제가 구현한 범위는 크게 다섯 가지로, `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler`입니다. +이 작업은 한 번에 큰 덩어리로 프로젝트에 구현한 것이 아니라, 이슈 단위로 쪼개서 브랜치와 PR 흐름으로 진행했습니다. +예를 들어 이슈 `#8`에서는 `ComponentRegistry`와 `EcsCore`를, 이슈 `#9`에서는 `WorldQuery`를, 이슈 `#10`에서는 `CommandBuffer`를, 이슈 `#12`에서는 `EngineRuntime`을, 그리고 이슈 `#47`에서는 `SystemScheduler`를 구현했습니다. +즉 엔티티와 컴포넌트를 저장하고 관리하는 코어부터, 시스템이 월드를 읽고 변경하는 방식, 그리고 실제 실행 루프와 스케줄링까지 단계적으로 확장한 것입니다. +이번 구현의 목표는 단순히 데이터를 담는 자료구조를 만드는 것이 아니라, 이후 시뮬레이션 로직이 안정적으로 올라갈 수 있는 실행 환경을 먼저 구축하는 것이었습니다. + +# 슬라이드 12 - ECS 실행 구조 설계 +구조를 보면 먼저 `EcsCore`가 엔티티 생성과 삭제, 컴포넌트 추가와 제거를 통합 관리합니다. +이때 컴포넌트가 추가되거나 제거되면 엔티티의 `Signature` 비트셋이 자동으로 갱신되도록 해서, 각 엔티티가 어떤 컴포넌트 조합을 가지고 있는지 일관되게 추적할 수 있도록 했습니다. +그 위에서 `WorldQuery`는 시스템이 필요한 엔티티만 조회할 수 있는 읽기 전용 인터페이스를 제공합니다. +반대로 구조 변경은 즉시 반영하지 않고, `CommandBuffer`와 `WorldCommands`를 통해 먼저 명령으로 기록한 뒤 나중에 한 번에 반영하도록 했습니다. +이렇게 읽기와 쓰기를 분리하도록 구현한 이유는, 시스템 실행 중 월드 구조를 바로 바꾸면 순회 안정성이 깨질 수 있기 때문입니다. + +# 슬라이드 13 - Runtime과 Scheduler 구현 및 검증 +이 구조 위에서 `EngineRuntime`은 전체 실행을 담당하는 최소 오케스트레이터 역할을 합니다. +프레임 시간을 누적한 뒤 `fixed timestep` 기준으로 필요한 만큼 simulation step을 반복 실행하고, 각 step마다 시스템이 호출되도록 구성했습니다. +그리고 `SystemScheduler`에서는 시스템마다 `phase`와 `order`를 지정할 수 있게 해서, `Startup`, `PreSimulation`, `FixedSimulation`, `PostSimulation`, `RenderSync` 같은 단계별 실행 순서를 제어할 수 있도록 했습니다. +또 각 phase가 끝나는 경계에서 `CommandBuffer`를 flush하도록 하여, 시스템이 실행되는 도중에는 구조 변경이 쌓이기만 하고 실제 반영은 일관된 시점에 일어나도록 했습니다. +마지막으로 이 구현은 `EcsCore`, `WorldQuery`, `CommandBuffer`, `EngineRuntime`, `SystemScheduler` 각각에 대해 테스트 코드를 작성해 생명주기 관리, 조회 필터링, deferred mutation, fixed-step 실행, phase 순서 보장을 검증했습니다. +정리하면, SafeCrowd에서 engine 파트 구현은 이후 도메인 시뮬레이션 로직을 안정적으로 올릴 수 있도록 ECS의 저장 구조와 실행 구조를 함께 구축한 작업이라고 말씀드릴 수 있습니다. + +# 슬라이드 14 - 2DMap +도메인 계층의 구현 내용을 설명하기에 앞서 계층의 역할에 대해 설명하겠습니다. 도메인 계층은 시설 레이아웃 정의와 에이전트 이동 로직 등 시뮬레이션의 핵심 규칙과 데이터 구조를 포함하는 계층입니다. 시뮬레이션의 토대가 되는 Map은 FacilityLayout2D, Zone2D, Connection2D, Barrier2D, SpawnZone2D로 이루어져 있습니다. +FacilityLayout2D는 전체 시뮬레이션의 최상위 컨테이너입니다. +Zone2D는 보행자가 이동할 수 있는 구역, +Connection2D는 구역과 구역을 잇는 통로, +Barrier2D는 에이전트의 이동을 막는 고정 장애물, +SpawnZone2D 에이전트의 생성 지점 역할을 수행합니다. +출구는 Zone2D의 하위 속성으로 지정되고, 출구와 연결된 Connection2D 객체도 출구 속성을 갖습니다. 이 요소들은 에이전트의 경로 계산에 사용되고, 에이전트가 exitzone에 도달하면 도착한 것으로 판단하게 됩니다. + +# 슬라이드 15 - Agent +스폰된 에이전트는 Position, Velocity, Goal, Agent 컴포넌트를 가집니다. +Position은 에이전트의 좌표, Velocity는 에이전트의 이동 방향과 속도, Goal은 목적지와 도착 여부, Agent는 에이전트 개인의 물리적 크기나 이동 능력치 등의 고유 속성을 정의합니다. +에이전트는 방향 벡터에 maxspeed 값을 곱해 이동 속도를 정합니다. 만약, 에이전트가 목적지에 도달했다면 goal의 도착 여부 값을 true로 설정하고 속도를 0으로 조정합니다. +충돌 여부 탐지를 위해서는 이동 경로상에 있는 벽 데이터를 가져와 다음 프레임 위치가 벽과 충돌이 발생하는지 계산합니다. 충돌이 발생하지 않은 경우, 속도를 그대로 유지하고 충돌이 발생한 경우, 벽의 법선 벡터를 구해 벽을 따라 이동하도록 처리합니다. +각 에이전트는 goal에 도착할 때까지 이 과정을 반복합니다. + +# 슬라이드 16 - Import module +이 슬라이드는 Domain 계층에서 import 모듈이 도면 파일을 시뮬레이션에 사용할 수 있는 구조로 바꾸는 과정을 보여줍니다. 먼저 DxfImportService가 DXF 도면을 읽어 필요한 선과 도형 정보를 가져옵니다. 이 원본 정보는 RawImportModel에 한 번 보관해서, 나중에 문제가 생겼을 때 어떤 도면 요소에서 나온 것인지 추적할 수 있게 합니다. 그다음 CanonicalGeometry 단계에서 도면의 여러 요소를 벽, 출구, 보행 가능 영역처럼 공통된 의미로 정리합니다. 이후 FacilityLayoutBuilder가 이 정보를 방, 복도, 연결 통로 같은 시뮬레이션용 공간 구조로 변환합니다. 마지막으로 ImportValidationService가 출구 누락이나 끊긴 동선 같은 문제를 검사하고, ImportResult가 최종 결과와 문제 목록을 함께 넘겨서 사용자가 확인한 뒤 다음 단계로 넘어가게 합니다. + +# 슬라이드 17 - 구현내용 - UI +이번 슬라이드에서는 Application, UI 구현 내용을 설명드리겠습니다. + +먼저 요구 사항으로는, 처음 맵을 만들 때 사용자가 기본 도구의 위치를 쉽게 찾을 수 있어야 하고, 현재 작업이나 진행 상태가 화면에 보였으면 좋겠고, +또 도면을 import할 수 있는 기능이 필요하다는 점이 있었습니다. + +이 요구를 반영해서, 맵 에디터를 처음 켰을 때 바로 사용할 수 있는 기본 도구 4가지를 화면 왼쪽에 배치했습니다. +또 사용자가 현재 진행 상황을 직관적으로 확인할 수 있도록 우측 속성 영역에 progress bar를 두었습니다. +그리고 메뉴바의 File - Import를 통해 도면 파일을 불러올 수 있도록 UI 흐름도 추가했습니다. + +즉, 이번 UI 구현은 단순히 화면을 꾸민 것이 아니라, 사용자가 처음 진입했을 때 바로 도구를 찾고, 현재 상태를 확인하고, +외부 도면을 가져오는 기본 작업 흐름이 자연스럽게 이어지도록 구성한 것입니다. + +아직 구현 예정인 부분도 있습니다. +예를 들어 결과 지표를 확인하는 전용 창과, 대안 시뮬레이션을 설정하는 창은 후속 작업으로 추가할 예정입니다. + +정리하면, 현재 Application UI는 기본 편집 도구 배치, 진행 상태 표시, 그리고 import 진입 경로를 우선 반영한 상태라고 볼 수 있습니다. \ No newline at end of file