From d1660f168a6cd8b644b802122f0d6de861cd8695 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Sun, 5 Apr 2026 23:20:50 +0900 Subject: [PATCH] =?UTF-8?q?[Engine]=20CommandBuffer=20=EB=B0=8F=20WorldCom?= =?UTF-8?q?mands=20deferred=20mutation=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CommandBuffer: spawnEntity, destroyEntity, addComponent, removeComponent 명령 큐잉 및 flush(EcsCore&) 구현 - WorldCommands: 시스템용 CommandBuffer facade 추가 - CommandBufferTests: deferred 동작, flush 적용, flush 후 초기화, spawnEntity, WorldCommands 전달 검증 (7개) - CMakeLists.txt: CommandBuffer.h, CommandBufferTests.cpp 등록 --- CMakeLists.txt | 2 + src/engine/CommandBuffer.h | 85 +++++++++++++++++++++++ tests/CommandBufferTests.cpp | 129 +++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 src/engine/CommandBuffer.h create mode 100644 tests/CommandBufferTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d46af40..cebed69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_library(ecs_engine STATIC src/engine/EngineSystem.h src/engine/FrameClock.h src/engine/WorldQuery.h + src/engine/CommandBuffer.h src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp @@ -110,6 +111,7 @@ if (BUILD_TESTING) tests/DxfImportServiceTests.cpp tests/FacilityLayoutBuilderTests.cpp tests/WorldQueryTests.cpp + tests/CommandBufferTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/CommandBuffer.h b/src/engine/CommandBuffer.h new file mode 100644 index 0000000..c8afa59 --- /dev/null +++ b/src/engine/CommandBuffer.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "engine/EcsCore.h" + +namespace safecrowd::engine { + +class CommandBuffer { +public: + template + void spawnEntity(Ts... components) { + commands_.push_back( + [comps = std::make_tuple(std::move(components)...)](EcsCore& core) mutable { + Entity e = core.createEntity(); + std::apply([&](auto&... c) { (core.addComponent(e, std::move(c)), ...); }, comps); + }); + } + + void destroyEntity(Entity entity) { + commands_.emplace_back([entity](EcsCore& core) { + core.destroyEntity(entity); + }); + } + + template + void addComponent(Entity entity, T component) { + commands_.emplace_back([entity, comp = std::move(component)](EcsCore& core) mutable { + core.addComponent(entity, std::move(comp)); + }); + } + + template + void removeComponent(Entity entity) { + commands_.emplace_back([entity](EcsCore& core) { + core.removeComponent(entity); + }); + } + + void flush(EcsCore& core) { + for (auto& cmd : commands_) { + cmd(core); + } + commands_.clear(); + } + + [[nodiscard]] bool empty() const noexcept { + return commands_.empty(); + } + +private: + std::vector> commands_; +}; + +class WorldCommands { +public: + explicit WorldCommands(CommandBuffer& buffer) : buffer_(buffer) {} + + template + void spawnEntity(Ts... components) { + buffer_.spawnEntity(std::move(components)...); + } + + void destroyEntity(Entity entity) { + buffer_.destroyEntity(entity); + } + + template + void addComponent(Entity entity, T component) { + buffer_.addComponent(entity, std::move(component)); + } + + template + void removeComponent(Entity entity) { + buffer_.removeComponent(entity); + } + +private: + CommandBuffer& buffer_; +}; + +} // namespace safecrowd::engine diff --git a/tests/CommandBufferTests.cpp b/tests/CommandBufferTests.cpp new file mode 100644 index 0000000..5551423 --- /dev/null +++ b/tests/CommandBufferTests.cpp @@ -0,0 +1,129 @@ +#include "TestSupport.h" + +#include "engine/CommandBuffer.h" + +namespace { + +struct Position { + float x{0.0f}; + float y{0.0f}; +}; + +struct Velocity { + float vx{0.0f}; + float vy{0.0f}; +}; + +} // namespace + +SC_TEST(CommandBuffer_DestroyEntity_IsDeferred) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + const auto e = core.createEntity(); + buffer.destroyEntity(e); + + SC_EXPECT_TRUE(core.isAlive(e)); + + buffer.flush(core); + + SC_EXPECT_TRUE(!core.isAlive(e)); +} + +SC_TEST(CommandBuffer_AddComponent_IsDeferred) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + const auto e = core.createEntity(); + buffer.addComponent(e, Position{1.0f, 2.0f}); + + SC_EXPECT_TRUE(!core.hasComponent(e)); + + buffer.flush(core); + + SC_EXPECT_TRUE(core.hasComponent(e)); + SC_EXPECT_NEAR(core.getComponent(e).x, 1.0f, 1e-6); +} + +SC_TEST(CommandBuffer_RemoveComponent_IsDeferred) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + const auto e = core.createEntity(); + core.addComponent(e, Position{}); + buffer.removeComponent(e); + + SC_EXPECT_TRUE(core.hasComponent(e)); + + buffer.flush(core); + + SC_EXPECT_TRUE(!core.hasComponent(e)); +} + +SC_TEST(CommandBuffer_Flush_AppliesCommandsInOrder) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + const auto e = core.createEntity(); + buffer.addComponent(e, Position{}); + buffer.removeComponent(e); + + buffer.flush(core); + + SC_EXPECT_TRUE(!core.hasComponent(e)); +} + +SC_TEST(CommandBuffer_Flush_ClearsBuffer) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + const auto e = core.createEntity(); + buffer.destroyEntity(e); + + SC_EXPECT_TRUE(!buffer.empty()); + + buffer.flush(core); + + SC_EXPECT_TRUE(buffer.empty()); +} + +SC_TEST(CommandBuffer_SpawnEntity_CreatesEntityWithComponents) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + + buffer.spawnEntity(Position{3.0f, 4.0f}, Velocity{1.0f, 0.0f}); + + SC_EXPECT_TRUE(buffer.empty() == false); + + buffer.flush(core); + + const auto result = [&] { + std::vector alive; + core.entityRegistry().eachAlive([&](safecrowd::engine::Entity entity, const safecrowd::engine::Signature&) { + if (core.hasComponent(entity) && core.hasComponent(entity)) { + alive.push_back(entity); + } + }); + return alive; + }(); + + SC_EXPECT_EQ(result.size(), std::size_t{1}); + SC_EXPECT_NEAR(core.getComponent(result[0]).x, 3.0f, 1e-6); + SC_EXPECT_NEAR(core.getComponent(result[0]).vx, 1.0f, 1e-6); +} + +SC_TEST(WorldCommands_ForwardsToBuffer) { + safecrowd::engine::EcsCore core; + safecrowd::engine::CommandBuffer buffer; + safecrowd::engine::WorldCommands cmds{buffer}; + + const auto e = core.createEntity(); + cmds.addComponent(e, Position{5.0f, 6.0f}); + + SC_EXPECT_TRUE(!core.hasComponent(e)); + + buffer.flush(core); + + SC_EXPECT_TRUE(core.hasComponent(e)); + SC_EXPECT_NEAR(core.getComponent(e).y, 6.0f, 1e-6); +}