From 4883a0b98bb12def0d9c26f0f2fdf8b0719186b9 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Tue, 7 Apr 2026 22:56:20 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Engine]=20EngineRuntime=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=EC=98=A4=EC=BC=80=EC=8A=A4=ED=8A=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EngineWorld에 WorldQuery/WorldCommands 노출 추가 - EngineRuntime에 EcsCore, CommandBuffer, systems_ 소유권 추가 - addSystem, initialize, stepFrame 고정 스텝 루프 및 CommandBuffer flush 구현 - EngineRuntimeTests: 시스템 update 호출 및 CommandBuffer flush 검증 테스트 추가 --- src/engine/EngineRuntime.cpp | 23 +++++++++++++++ src/engine/EngineRuntime.h | 9 ++++++ src/engine/EngineSystem.h | 14 ++++++++++ tests/EngineRuntimeTests.cpp | 54 ++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 1dfc923..8615c61 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -23,14 +23,23 @@ EngineConfig normalizeConfig(EngineConfig config) { EngineRuntime::EngineRuntime(EngineConfig config) : config_(normalizeConfig(config)), + world_(core_, buffer_), frameClock_(config_) { } +void EngineRuntime::addSystem(std::unique_ptr system) { + systems_.push_back(std::move(system)); +} + void EngineRuntime::initialize() { frameClock_.reset(); stats_ = {}; stats_.state = EngineState::Ready; ++runIndex_; + + for (auto& system : systems_) { + system->configure(world_); + } } void EngineRuntime::play() { @@ -67,6 +76,20 @@ void EngineRuntime::stepFrame(double deltaSeconds) { frameClock_.consumeFixedStep(); ++stats_.fixedStepIndex; ++stats_.fixedStepsThisFrame; + + const EngineStepContext ctx{ + .frameIndex = stats_.frameIndex, + .fixedStepIndex = stats_.fixedStepIndex, + .alpha = frameClock_.alpha(), + .runIndex = runIndex_, + .derivedSeed = 0, + }; + + for (auto& system : systems_) { + system->update(world_, ctx); + } + + buffer_.flush(core_); } stats_.alpha = frameClock_.alpha(); diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 0da63c9..05c5336 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -1,7 +1,11 @@ #pragma once #include +#include +#include +#include "engine/CommandBuffer.h" +#include "engine/EcsCore.h" #include "engine/EngineConfig.h" #include "engine/EngineStats.h" #include "engine/EngineSystem.h" @@ -13,6 +17,8 @@ class EngineRuntime { public: explicit EngineRuntime(EngineConfig config = {}); + void addSystem(std::unique_ptr system); + void initialize(); void play(); void pause(); @@ -29,9 +35,12 @@ class EngineRuntime { private: EngineConfig config_; EngineStats stats_; + EcsCore core_; + CommandBuffer buffer_; EngineWorld world_; FrameClock frameClock_; std::uint64_t runIndex_{0}; + std::vector> systems_; }; } // namespace safecrowd::engine diff --git a/src/engine/EngineSystem.h b/src/engine/EngineSystem.h index 943b151..c8d09d6 100644 --- a/src/engine/EngineSystem.h +++ b/src/engine/EngineSystem.h @@ -1,10 +1,24 @@ #pragma once +#include "engine/CommandBuffer.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 { diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index b40db19..178a4b9 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -1,7 +1,31 @@ #include "TestSupport.h" +#include + #include "engine/EngineRuntime.h" +namespace { + +struct Marker {}; + +class UpdateCounterSystem : public safecrowd::engine::EngineSystem { +public: + int& count; + explicit UpdateCounterSystem(int& c) : count(c) {} + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + ++count; + } +}; + +class SpawnMarkerSystem : public safecrowd::engine::EngineSystem { +public: + void update(safecrowd::engine::EngineWorld& world, const safecrowd::engine::EngineStepContext&) override { + world.commands().spawnEntity(Marker{}); + } +}; + +} // namespace + SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, @@ -20,6 +44,36 @@ SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { SC_EXPECT_NEAR(stats.alpha, 0.0, 1e-9); } +SC_TEST(EngineRuntime_RegisteredSystem_UpdateCalledOnFixedStep) { + int count = 0; + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem(std::make_unique(count)); + runtime.play(); + runtime.stepFrame(0.50); // 2 fixed steps + + SC_EXPECT_EQ(count, 2); +} + +SC_TEST(EngineRuntime_WorldCommands_FlushedAfterEachFixedStep) { + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem(std::make_unique()); + runtime.play(); + runtime.stepFrame(0.25); // 1 fixed step + + const auto entities = runtime.world().query().view(); + SC_EXPECT_EQ(entities.size(), std::size_t{1}); +} + SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, From 34636228cf12410491d6182e92e3deb95260289e Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Tue, 7 Apr 2026 23:17:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[Engine]=20SystemScheduler=20phase=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EB=A3=A8=ED=94=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UpdatePhase, TriggerPolicy, SystemDescriptor 선언 - SystemScheduler: registerSystem(phase/order 정렬), configure, executePhase(phase 경계 flush) 구현 - EngineRuntime: systems_ 벡터를 SystemScheduler로 교체, addSystem에 descriptor 기본값 추가 - SystemSchedulerTests: phase 순서, order 정렬, phase 격리, flush 검증 (4개) --- CMakeLists.txt | 6 ++ src/engine/EngineRuntime.cpp | 16 ++--- src/engine/EngineRuntime.h | 22 +++--- src/engine/SystemDescriptor.h | 14 ++++ src/engine/SystemScheduler.cpp | 34 +++++++++ src/engine/SystemScheduler.h | 33 +++++++++ src/engine/TriggerPolicy.h | 11 +++ src/engine/UpdatePhase.h | 13 ++++ tests/SystemSchedulerTests.cpp | 122 +++++++++++++++++++++++++++++++++ 9 files changed, 251 insertions(+), 20 deletions(-) create mode 100644 src/engine/SystemDescriptor.h create mode 100644 src/engine/SystemScheduler.cpp create mode 100644 src/engine/SystemScheduler.h create mode 100644 src/engine/TriggerPolicy.h create mode 100644 src/engine/UpdatePhase.h create mode 100644 tests/SystemSchedulerTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cebed69..840077f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,9 +51,14 @@ add_library(ecs_engine STATIC src/engine/FrameClock.h src/engine/WorldQuery.h src/engine/CommandBuffer.h + src/engine/UpdatePhase.h + src/engine/TriggerPolicy.h + src/engine/SystemDescriptor.h + src/engine/SystemScheduler.h src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp + src/engine/SystemScheduler.cpp ) target_include_directories(ecs_engine @@ -112,6 +117,7 @@ if (BUILD_TESTING) tests/FacilityLayoutBuilderTests.cpp tests/WorldQueryTests.cpp tests/CommandBufferTests.cpp + tests/SystemSchedulerTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 8615c61..133a7d3 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -23,12 +23,14 @@ EngineConfig normalizeConfig(EngineConfig config) { EngineRuntime::EngineRuntime(EngineConfig config) : config_(normalizeConfig(config)), + scheduler_(core_, buffer_), world_(core_, buffer_), frameClock_(config_) { } -void EngineRuntime::addSystem(std::unique_ptr system) { - systems_.push_back(std::move(system)); +void EngineRuntime::addSystem(std::unique_ptr system, + SystemDescriptor descriptor) { + scheduler_.registerSystem(std::move(system), descriptor); } void EngineRuntime::initialize() { @@ -37,9 +39,7 @@ void EngineRuntime::initialize() { stats_.state = EngineState::Ready; ++runIndex_; - for (auto& system : systems_) { - system->configure(world_); - } + scheduler_.configure(world_); } void EngineRuntime::play() { @@ -85,11 +85,7 @@ void EngineRuntime::stepFrame(double deltaSeconds) { .derivedSeed = 0, }; - for (auto& system : systems_) { - system->update(world_, ctx); - } - - buffer_.flush(core_); + scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx); } stats_.alpha = frameClock_.alpha(); diff --git a/src/engine/EngineRuntime.h b/src/engine/EngineRuntime.h index 05c5336..340e7d1 100644 --- a/src/engine/EngineRuntime.h +++ b/src/engine/EngineRuntime.h @@ -2,7 +2,6 @@ #include #include -#include #include "engine/CommandBuffer.h" #include "engine/EcsCore.h" @@ -10,6 +9,8 @@ #include "engine/EngineStats.h" #include "engine/EngineSystem.h" #include "engine/FrameClock.h" +#include "engine/SystemDescriptor.h" +#include "engine/SystemScheduler.h" namespace safecrowd::engine { @@ -17,7 +18,8 @@ class EngineRuntime { public: explicit EngineRuntime(EngineConfig config = {}); - void addSystem(std::unique_ptr system); + void addSystem(std::unique_ptr system, + SystemDescriptor descriptor = {}); void initialize(); void play(); @@ -33,14 +35,14 @@ class EngineRuntime { std::uint64_t runIndex() const noexcept; private: - EngineConfig config_; - EngineStats stats_; - EcsCore core_; - CommandBuffer buffer_; - EngineWorld world_; - FrameClock frameClock_; - std::uint64_t runIndex_{0}; - std::vector> systems_; + EngineConfig config_; + EngineStats stats_; + EcsCore core_; + CommandBuffer buffer_; + SystemScheduler scheduler_; + EngineWorld world_; + FrameClock frameClock_; + std::uint64_t runIndex_{0}; }; } // namespace safecrowd::engine diff --git a/src/engine/SystemDescriptor.h b/src/engine/SystemDescriptor.h new file mode 100644 index 0000000..110c8bc --- /dev/null +++ b/src/engine/SystemDescriptor.h @@ -0,0 +1,14 @@ +#pragma once + +#include "engine/TriggerPolicy.h" +#include "engine/UpdatePhase.h" + +namespace safecrowd::engine { + +struct SystemDescriptor { + UpdatePhase phase{UpdatePhase::FixedSimulation}; + int order{0}; + TriggerPolicy triggerPolicy{TriggerPolicy::FixedStep}; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.cpp b/src/engine/SystemScheduler.cpp new file mode 100644 index 0000000..7478c9c --- /dev/null +++ b/src/engine/SystemScheduler.cpp @@ -0,0 +1,34 @@ +#include "engine/SystemScheduler.h" + +#include + +namespace safecrowd::engine { + +void SystemScheduler::registerSystem(std::unique_ptr system, + SystemDescriptor descriptor) { + entries_.push_back({std::move(system), descriptor}); + std::stable_sort(entries_.begin(), entries_.end(), [](const Entry& a, const Entry& b) { + if (a.descriptor.phase != b.descriptor.phase) { + return static_cast(a.descriptor.phase) < static_cast(b.descriptor.phase); + } + return a.descriptor.order < b.descriptor.order; + }); +} + +void SystemScheduler::configure(EngineWorld& world) { + for (auto& e : entries_) { + e.system->configure(world); + } +} + +void SystemScheduler::executePhase(UpdatePhase phase, EngineWorld& world, + const EngineStepContext& ctx) { + for (auto& e : entries_) { + if (e.descriptor.phase == phase) { + e.system->update(world, ctx); + } + } + buffer_.flush(core_); +} + +} // namespace safecrowd::engine diff --git a/src/engine/SystemScheduler.h b/src/engine/SystemScheduler.h new file mode 100644 index 0000000..bb31810 --- /dev/null +++ b/src/engine/SystemScheduler.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "engine/EcsCore.h" +#include "engine/EngineSystem.h" +#include "engine/SystemDescriptor.h" +#include "engine/UpdatePhase.h" + +namespace safecrowd::engine { + +class SystemScheduler { +public: + SystemScheduler(EcsCore& core, CommandBuffer& buffer) + : core_(core), buffer_(buffer) {} + + void registerSystem(std::unique_ptr system, SystemDescriptor descriptor); + void configure(EngineWorld& world); + void executePhase(UpdatePhase phase, EngineWorld& world, const EngineStepContext& ctx); + +private: + struct Entry { + std::unique_ptr system; + SystemDescriptor descriptor; + }; + + EcsCore& core_; + CommandBuffer& buffer_; + std::vector entries_; +}; + +} // namespace safecrowd::engine diff --git a/src/engine/TriggerPolicy.h b/src/engine/TriggerPolicy.h new file mode 100644 index 0000000..6f101d6 --- /dev/null +++ b/src/engine/TriggerPolicy.h @@ -0,0 +1,11 @@ +#pragma once + +namespace safecrowd::engine { + +enum class TriggerPolicy { + EveryFrame, + FixedStep, + Interval, +}; + +} // namespace safecrowd::engine diff --git a/src/engine/UpdatePhase.h b/src/engine/UpdatePhase.h new file mode 100644 index 0000000..7fd694f --- /dev/null +++ b/src/engine/UpdatePhase.h @@ -0,0 +1,13 @@ +#pragma once + +namespace safecrowd::engine { + +enum class UpdatePhase { + Startup, + PreSimulation, + FixedSimulation, + PostSimulation, + RenderSync, +}; + +} // namespace safecrowd::engine diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp new file mode 100644 index 0000000..4cfe98c --- /dev/null +++ b/tests/SystemSchedulerTests.cpp @@ -0,0 +1,122 @@ +#include "TestSupport.h" + +#include +#include + +#include "engine/CommandBuffer.h" +#include "engine/EcsCore.h" +#include "engine/SystemScheduler.h" + +namespace { + +class RecordingSystem : public safecrowd::engine::EngineSystem { +public: + std::vector& log; + int id; + explicit RecordingSystem(std::vector& l, int id) : log(l), id(id) {} + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + log.push_back(id); + } +}; + +struct Tag {}; + +class SpawnTagSystem : public safecrowd::engine::EngineSystem { +public: + void update(safecrowd::engine::EngineWorld& world, + const safecrowd::engine::EngineStepContext&) override { + world.commands().spawnEntity(Tag{}); + } +}; + +} // namespace + +SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, .order = 0}); + scheduler.registerSystem( + std::make_unique(log, 2), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase(safecrowd::engine::UpdatePhase::PreSimulation, world, ctx); + scheduler.executePhase(safecrowd::engine::UpdatePhase::PostSimulation, world, ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{2}); + SC_EXPECT_EQ(log[0], 2); + SC_EXPECT_EQ(log[1], 1); +} + +SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 10), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 1}); + scheduler.registerSystem( + std::make_unique(log, 20), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{2}); + SC_EXPECT_EQ(log[0], 20); + SC_EXPECT_EQ(log[1], 10); +} + +SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EcsCore dummyCore; + safecrowd::engine::CommandBuffer dummyBuffer; + safecrowd::engine::EngineWorld world{dummyCore, dummyBuffer}; + + std::vector log; + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + + SC_EXPECT_EQ(log.size(), std::size_t{0}); +} + +SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + safecrowd::engine::EngineWorld world{core, buffer}; + + scheduler.registerSystem( + std::make_unique(), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); + + const safecrowd::engine::EngineStepContext ctx{}; + scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + + const auto entities = safecrowd::engine::WorldQuery{core}.view(); + SC_EXPECT_EQ(entities.size(), std::size_t{1}); +} From afa413d12d58028a627c52fa8a2064366aeeba4d Mon Sep 17 00:00:00 2001 From: learncold Date: Wed, 8 Apr 2026 01:50:27 +0900 Subject: [PATCH 3/3] [Engine] Fix scheduler runtime integration --- src/engine/EngineRuntime.cpp | 42 ++++++++- src/engine/SystemScheduler.cpp | 41 +++++++- src/engine/SystemScheduler.h | 4 +- tests/EngineRuntimeTests.cpp | 166 +++++++++++++++++++++++++++++++++ tests/SystemSchedulerTests.cpp | 105 +++++++++++++++++++-- 5 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 133a7d3..5a3803e 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -35,11 +35,22 @@ void EngineRuntime::addSystem(std::unique_ptr system, void EngineRuntime::initialize() { frameClock_.reset(); + core_ = EcsCore{}; + buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Ready; ++runIndex_; scheduler_.configure(world_); + + const EngineStepContext startupCtx{ + .frameIndex = stats_.frameIndex, + .fixedStepIndex = stats_.fixedStepIndex, + .alpha = 0.0, + .runIndex = runIndex_, + .derivedSeed = 0, + }; + scheduler_.executeStartup(world_, startupCtx); } void EngineRuntime::play() { @@ -58,6 +69,8 @@ void EngineRuntime::pause() { void EngineRuntime::stop() { frameClock_.reset(); + core_ = EcsCore{}; + buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Stopped; } @@ -67,17 +80,33 @@ void EngineRuntime::stepFrame(double deltaSeconds) { initialize(); } + if (stats_.state == EngineState::Paused) { + stats_.fixedStepsThisFrame = 0; + return; + } + frameClock_.beginFrame(deltaSeconds); ++stats_.frameIndex; stats_.fixedStepsThisFrame = 0; + EngineStepContext ctx{ + .frameIndex = stats_.frameIndex, + .fixedStepIndex = stats_.fixedStepIndex, + .alpha = frameClock_.alpha(), + .runIndex = runIndex_, + .derivedSeed = 0, + }; + + scheduler_.executePhase(UpdatePhase::PreSimulation, TriggerPolicy::EveryFrame, + world_, ctx); + while (frameClock_.shouldRunFixedStep()) { frameClock_.consumeFixedStep(); ++stats_.fixedStepIndex; ++stats_.fixedStepsThisFrame; - const EngineStepContext ctx{ + ctx = EngineStepContext{ .frameIndex = stats_.frameIndex, .fixedStepIndex = stats_.fixedStepIndex, .alpha = frameClock_.alpha(), @@ -85,10 +114,19 @@ void EngineRuntime::stepFrame(double deltaSeconds) { .derivedSeed = 0, }; - scheduler_.executePhase(UpdatePhase::FixedSimulation, world_, ctx); + scheduler_.executePhase(UpdatePhase::FixedSimulation, TriggerPolicy::FixedStep, + world_, ctx); } + ctx.alpha = frameClock_.alpha(); + scheduler_.executePhase(UpdatePhase::PostSimulation, TriggerPolicy::EveryFrame, + world_, ctx); + stats_.alpha = frameClock_.alpha(); + + ctx.alpha = stats_.alpha; + scheduler_.executePhase(UpdatePhase::RenderSync, TriggerPolicy::EveryFrame, + world_, ctx); } EngineWorld& EngineRuntime::world() noexcept { diff --git a/src/engine/SystemScheduler.cpp b/src/engine/SystemScheduler.cpp index 7478c9c..ef9845b 100644 --- a/src/engine/SystemScheduler.cpp +++ b/src/engine/SystemScheduler.cpp @@ -1,11 +1,35 @@ #include "engine/SystemScheduler.h" #include +#include namespace safecrowd::engine { +namespace { + +void validateDescriptor(const SystemDescriptor& descriptor) { + if (descriptor.triggerPolicy == TriggerPolicy::Interval) { + throw std::invalid_argument("TriggerPolicy::Interval is not supported yet."); + } + + if (descriptor.phase == UpdatePhase::FixedSimulation && + descriptor.triggerPolicy != TriggerPolicy::FixedStep) { + throw std::invalid_argument( + "FixedSimulation systems must use TriggerPolicy::FixedStep."); + } + + if (descriptor.phase != UpdatePhase::FixedSimulation && + descriptor.phase != UpdatePhase::Startup && + descriptor.triggerPolicy != TriggerPolicy::EveryFrame) { + throw std::invalid_argument( + "Frame phases must use TriggerPolicy::EveryFrame."); + } +} + +} // namespace void SystemScheduler::registerSystem(std::unique_ptr system, SystemDescriptor descriptor) { + validateDescriptor(descriptor); entries_.push_back({std::move(system), descriptor}); std::stable_sort(entries_.begin(), entries_.end(), [](const Entry& a, const Entry& b) { if (a.descriptor.phase != b.descriptor.phase) { @@ -18,13 +42,24 @@ void SystemScheduler::registerSystem(std::unique_ptr system, void SystemScheduler::configure(EngineWorld& world) { for (auto& e : entries_) { e.system->configure(world); + buffer_.flush(core_); } } -void SystemScheduler::executePhase(UpdatePhase phase, EngineWorld& world, - const EngineStepContext& ctx) { +void SystemScheduler::executeStartup(EngineWorld& world, const EngineStepContext& ctx) { + for (auto& e : entries_) { + if (e.descriptor.phase == UpdatePhase::Startup) { + e.system->update(world, ctx); + } + } + buffer_.flush(core_); +} + +void SystemScheduler::executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy, + EngineWorld& world, const EngineStepContext& ctx) { for (auto& e : entries_) { - if (e.descriptor.phase == phase) { + if (e.descriptor.phase == phase && + e.descriptor.triggerPolicy == triggerPolicy) { e.system->update(world, ctx); } } diff --git a/src/engine/SystemScheduler.h b/src/engine/SystemScheduler.h index bb31810..a6fec7a 100644 --- a/src/engine/SystemScheduler.h +++ b/src/engine/SystemScheduler.h @@ -17,7 +17,9 @@ class SystemScheduler { void registerSystem(std::unique_ptr system, SystemDescriptor descriptor); void configure(EngineWorld& world); - void executePhase(UpdatePhase phase, EngineWorld& world, const EngineStepContext& ctx); + void executeStartup(EngineWorld& world, const EngineStepContext& ctx); + void executePhase(UpdatePhase phase, TriggerPolicy triggerPolicy, + EngineWorld& world, const EngineStepContext& ctx); private: struct Entry { diff --git a/tests/EngineRuntimeTests.cpp b/tests/EngineRuntimeTests.cpp index 178a4b9..e87969e 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -1,6 +1,9 @@ #include "TestSupport.h" +#include +#include #include +#include #include "engine/EngineRuntime.h" @@ -24,6 +27,43 @@ class SpawnMarkerSystem : public safecrowd::engine::EngineSystem { } }; +class ConfigureSpawnMarkerSystem : public safecrowd::engine::EngineSystem { +public: + void configure(safecrowd::engine::EngineWorld& world) override { + world.commands().spawnEntity(Marker{}); + } + + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + } +}; + +class ConfigureObserveMarkerSystem : public safecrowd::engine::EngineSystem { +public: + std::size_t& count; + + explicit ConfigureObserveMarkerSystem(std::size_t& c) : count(c) {} + + void configure(safecrowd::engine::EngineWorld& world) override { + count = world.query().view().size(); + } + + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + } +}; + +class RecordPhaseSystem : public safecrowd::engine::EngineSystem { +public: + std::vector& log; + int marker; + + explicit RecordPhaseSystem(std::vector& l, int value) + : log(l), marker(value) {} + + void update(safecrowd::engine::EngineWorld&, const safecrowd::engine::EngineStepContext&) override { + log.push_back(marker); + } +}; + } // namespace SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { @@ -74,6 +114,132 @@ SC_TEST(EngineRuntime_WorldCommands_FlushedAfterEachFixedStep) { SC_EXPECT_EQ(entities.size(), std::size_t{1}); } +SC_TEST(EngineRuntime_ConfigureCommands_AreVisibleToLaterSystems) { + std::size_t configuredMarkerCount = 0; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem(std::make_unique()); + runtime.addSystem(std::make_unique(configuredMarkerCount)); + + runtime.play(); + + SC_EXPECT_EQ(configuredMarkerCount, std::size_t{1}); + SC_EXPECT_EQ(runtime.world().query().view().size(), std::size_t{1}); +} + +SC_TEST(EngineRuntime_ExecutesStartupAndFramePhases) { + std::vector log; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem( + std::make_unique(log, 10), + {.phase = safecrowd::engine::UpdatePhase::Startup, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 20), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 30), + {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::FixedStep}); + runtime.addSystem( + std::make_unique(log, 40), + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + runtime.addSystem( + std::make_unique(log, 50), + {.phase = safecrowd::engine::UpdatePhase::RenderSync, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); + + runtime.play(); + runtime.stepFrame(0.50); + + SC_EXPECT_EQ(log.size(), std::size_t{6}); + SC_EXPECT_EQ(log[0], 10); + SC_EXPECT_EQ(log[1], 20); + SC_EXPECT_EQ(log[2], 30); + SC_EXPECT_EQ(log[3], 30); + SC_EXPECT_EQ(log[4], 40); + SC_EXPECT_EQ(log[5], 50); +} + +SC_TEST(EngineRuntime_Stop_ClearsWorldAndPendingCommandsBeforeNextRun) { + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem(std::make_unique()); + runtime.play(); + runtime.stepFrame(0.25); + SC_EXPECT_EQ(runtime.world().query().view().size(), std::size_t{1}); + + runtime.world().commands().spawnEntity(Marker{}); + runtime.stop(); + + SC_EXPECT_EQ(runtime.world().query().view().size(), std::size_t{0}); + + runtime.play(); + runtime.stepFrame(0.25); + + SC_EXPECT_EQ(runtime.world().query().view().size(), std::size_t{1}); +} + +SC_TEST(EngineRuntime_PausedRuntime_DoesNotAdvanceSimulation) { + int count = 0; + + safecrowd::engine::EngineRuntime runtime({ + .fixedDeltaTime = 0.25, + .maxCatchUpSteps = 4, + .baseSeed = 1, + }); + + runtime.addSystem(std::make_unique(count)); + runtime.play(); + runtime.stepFrame(0.25); + runtime.pause(); + runtime.stepFrame(1.00); + + const auto& pausedStats = runtime.stats(); + SC_EXPECT_EQ(count, 1); + SC_EXPECT_EQ(pausedStats.frameIndex, 1ULL); + SC_EXPECT_EQ(pausedStats.fixedStepIndex, 1ULL); + SC_EXPECT_EQ(pausedStats.fixedStepsThisFrame, 0U); + + runtime.play(); + runtime.stepFrame(0.25); + SC_EXPECT_EQ(count, 2); +} + +SC_TEST(EngineRuntime_AddSystem_RejectsUnsupportedIntervalTriggerPolicy) { + int count = 0; + safecrowd::engine::EngineRuntime runtime; + + bool threw = false; + try { + runtime.addSystem( + std::make_unique(count), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + } catch (const std::exception&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +} + SC_TEST(EngineRuntimePauseAndStopResetLifecycleState) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, diff --git a/tests/SystemSchedulerTests.cpp b/tests/SystemSchedulerTests.cpp index 4cfe98c..a40d6a7 100644 --- a/tests/SystemSchedulerTests.cpp +++ b/tests/SystemSchedulerTests.cpp @@ -1,5 +1,7 @@ #include "TestSupport.h" +#include +#include #include #include @@ -30,6 +32,32 @@ class SpawnTagSystem : public safecrowd::engine::EngineSystem { } }; +class ConfigureSpawnTagSystem : public safecrowd::engine::EngineSystem { +public: + void configure(safecrowd::engine::EngineWorld& world) override { + world.commands().spawnEntity(Tag{}); + } + + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + } +}; + +class ConfigureObserveTagSystem : public safecrowd::engine::EngineSystem { +public: + std::size_t& count; + + explicit ConfigureObserveTagSystem(std::size_t& c) : count(c) {} + + void configure(safecrowd::engine::EngineWorld& world) override { + count = world.query().view().size(); + } + + void update(safecrowd::engine::EngineWorld&, + const safecrowd::engine::EngineStepContext&) override { + } +}; + } // namespace SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { @@ -44,14 +72,26 @@ SC_TEST(SystemScheduler_ExecutesSystemsInPhaseOrder) { std::vector log; scheduler.registerSystem( std::make_unique(log, 1), - {.phase = safecrowd::engine::UpdatePhase::PostSimulation, .order = 0}); + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); scheduler.registerSystem( std::make_unique(log, 2), - {.phase = safecrowd::engine::UpdatePhase::PreSimulation, .order = 0}); + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); const safecrowd::engine::EngineStepContext ctx{}; - scheduler.executePhase(safecrowd::engine::UpdatePhase::PreSimulation, world, ctx); - scheduler.executePhase(safecrowd::engine::UpdatePhase::PostSimulation, world, ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PreSimulation, + safecrowd::engine::TriggerPolicy::EveryFrame, + world, + ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::PostSimulation, + safecrowd::engine::TriggerPolicy::EveryFrame, + world, + ctx); SC_EXPECT_EQ(log.size(), std::size_t{2}); SC_EXPECT_EQ(log[0], 2); @@ -76,7 +116,11 @@ SC_TEST(SystemScheduler_ExecutesSystemsInOrderWithinPhase) { {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); const safecrowd::engine::EngineStepContext ctx{}; - scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); SC_EXPECT_EQ(log.size(), std::size_t{2}); SC_EXPECT_EQ(log[0], 20); @@ -95,14 +139,36 @@ SC_TEST(SystemScheduler_PhaseIsolation_OtherPhaseSystemsNotExecuted) { std::vector log; scheduler.registerSystem( std::make_unique(log, 1), - {.phase = safecrowd::engine::UpdatePhase::PostSimulation, .order = 0}); + {.phase = safecrowd::engine::UpdatePhase::PostSimulation, + .order = 0, + .triggerPolicy = safecrowd::engine::TriggerPolicy::EveryFrame}); const safecrowd::engine::EngineStepContext ctx{}; - scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); SC_EXPECT_EQ(log.size(), std::size_t{0}); } +SC_TEST(SystemScheduler_ConfigureFlushesCommandsBetweenSystems) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + safecrowd::engine::EngineWorld world{core, buffer}; + + std::size_t configuredCount = 0; + scheduler.registerSystem(std::make_unique(), {}); + scheduler.registerSystem(std::make_unique(configuredCount), {}); + + scheduler.configure(world); + + SC_EXPECT_EQ(configuredCount, std::size_t{1}); + SC_EXPECT_EQ(world.query().view().size(), std::size_t{1}); +} + SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { safecrowd::engine::EcsCore core; safecrowd::engine::CommandBuffer buffer; @@ -115,8 +181,31 @@ SC_TEST(SystemScheduler_FlushesCommandBufferAfterPhase) { {.phase = safecrowd::engine::UpdatePhase::FixedSimulation, .order = 0}); const safecrowd::engine::EngineStepContext ctx{}; - scheduler.executePhase(safecrowd::engine::UpdatePhase::FixedSimulation, world, ctx); + scheduler.executePhase( + safecrowd::engine::UpdatePhase::FixedSimulation, + safecrowd::engine::TriggerPolicy::FixedStep, + world, + ctx); const auto entities = safecrowd::engine::WorldQuery{core}.view(); SC_EXPECT_EQ(entities.size(), std::size_t{1}); } + +SC_TEST(SystemScheduler_RegisterSystem_RejectsUnsupportedIntervalPolicy) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::SystemScheduler scheduler{core, buffer}; + + std::vector log; + bool threw = false; + try { + scheduler.registerSystem( + std::make_unique(log, 1), + {.phase = safecrowd::engine::UpdatePhase::PreSimulation, + .triggerPolicy = safecrowd::engine::TriggerPolicy::Interval}); + } catch (const std::exception&) { + threw = true; + } + + SC_EXPECT_TRUE(threw); +}