diff --git a/src/engine/EngineRuntime.cpp b/src/engine/EngineRuntime.cpp index 1dfc923..2cc7e0f 100644 --- a/src/engine/EngineRuntime.cpp +++ b/src/engine/EngineRuntime.cpp @@ -23,14 +23,26 @@ 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(); + core_ = EcsCore{}; + buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Ready; ++runIndex_; + + for (auto& system : systems_) { + system->configure(world_); + buffer_.flush(core_); + } } void EngineRuntime::play() { @@ -49,6 +61,8 @@ void EngineRuntime::pause() { void EngineRuntime::stop() { frameClock_.reset(); + core_ = EcsCore{}; + buffer_ = CommandBuffer{}; stats_ = {}; stats_.state = EngineState::Stopped; } @@ -58,6 +72,11 @@ void EngineRuntime::stepFrame(double deltaSeconds) { initialize(); } + if (stats_.state == EngineState::Paused) { + stats_.fixedStepsThisFrame = 0; + return; + } + frameClock_.beginFrame(deltaSeconds); ++stats_.frameIndex; @@ -67,6 +86,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..f3ac57f 100644 --- a/tests/EngineRuntimeTests.cpp +++ b/tests/EngineRuntimeTests.cpp @@ -1,7 +1,56 @@ #include "TestSupport.h" +#include +#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{}); + } +}; + +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 { + } +}; + +} // namespace + SC_TEST(EngineRuntimePlayAndStepUpdatesStats) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25, @@ -20,6 +69,103 @@ 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(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_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(EngineRuntimePauseAndStopResetLifecycleState) { safecrowd::engine::EngineRuntime runtime({ .fixedDeltaTime = 0.25,