From 85a36f252f9f1a91318ec756ed85fea52a81e046 Mon Sep 17 00:00:00 2001 From: SilverSupplier Date: Fri, 3 Apr 2026 23:48:44 +0900 Subject: [PATCH] =?UTF-8?q?[Engine]=20WorldQuery=20facade=20=EB=B0=8F=20En?= =?UTF-8?q?tityRegistry::eachAlive=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorldQuery: view(), contains(), get() read-only facade 추가 - EntityRegistry: eachAlive() 템플릿 순회 메서드 추가 - WorldQueryTests: view 필터링, 미등록 타입, 파괴된 entity, contains, get 검증 5개 테스트 추가 - CMakeLists.txt: WorldQuery.h, WorldQueryTests.cpp 등록 Closes #9 Co-Authored-By: Claude Sonnet 4.6 --- CMakeLists.txt | 2 + src/engine/EntityRegistry.h | 10 +++++ src/engine/WorldQuery.h | 56 +++++++++++++++++++++++++ tests/WorldQueryTests.cpp | 84 +++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/engine/WorldQuery.h create mode 100644 tests/WorldQueryTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f37dc4..d10b712 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ add_library(ecs_engine STATIC src/engine/EngineStepContext.h src/engine/EngineSystem.h src/engine/FrameClock.h + src/engine/WorldQuery.h src/engine/EntityRegistry.cpp src/engine/EngineRuntime.cpp src/engine/FrameClock.cpp @@ -100,6 +101,7 @@ if (BUILD_TESTING) tests/SafeCrowdDomainTests.cpp tests/EcsCoreTests.cpp tests/ImportContractsTests.cpp + tests/WorldQueryTests.cpp ) target_include_directories(safecrowd_tests diff --git a/src/engine/EntityRegistry.h b/src/engine/EntityRegistry.h index 392d487..9a3a952 100644 --- a/src/engine/EntityRegistry.h +++ b/src/engine/EntityRegistry.h @@ -22,6 +22,16 @@ class EntityRegistry { void setSignature(Entity entity, Signature signature); [[nodiscard]] Signature signatureOf(Entity entity) const; + template + void eachAlive(Fn&& fn) const { + for (std::size_t i = 0; i < entries_.size(); ++i) { + const Entry& entry = entries_[i]; + if (entry.alive) { + fn(Entity{static_cast(i), entry.generation}, entry.signature); + } + } + } + private: struct Entry { EntityGeneration generation{0}; diff --git a/src/engine/WorldQuery.h b/src/engine/WorldQuery.h new file mode 100644 index 0000000..cc3885a --- /dev/null +++ b/src/engine/WorldQuery.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include "engine/EcsCore.h" + +namespace safecrowd::engine { + +class WorldQuery { +public: + explicit WorldQuery(EcsCore& core) : core_(core) {} + + template + [[nodiscard]] std::vector view() const { + Signature required{}; + bool allRegistered = true; + ([&] { + const auto id = core_.componentRegistry().tryTypeOf(); + if (!id.has_value()) { + allRegistered = false; + } else { + required.set(id.value()); + } + }(), ...); + + if (!allRegistered) return {}; + + std::vector result; + core_.entityRegistry().eachAlive([&](Entity entity, const Signature& sig) { + if ((sig & required) == required) { + result.push_back(entity); + } + }); + return result; + } + + template + [[nodiscard]] bool contains(Entity entity) const { + return core_.hasComponent(entity); + } + + template + [[nodiscard]] T& get(Entity entity) { + return core_.getComponent(entity); + } + + template + [[nodiscard]] const T& get(Entity entity) const { + return core_.getComponent(entity); + } + +private: + EcsCore& core_; +}; + +} // namespace safecrowd::engine diff --git a/tests/WorldQueryTests.cpp b/tests/WorldQueryTests.cpp new file mode 100644 index 0000000..2bf469a --- /dev/null +++ b/tests/WorldQueryTests.cpp @@ -0,0 +1,84 @@ +#include "TestSupport.h" + +#include "engine/WorldQuery.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(WorldQuery_ViewFiltersEntitiesBySignature) { + safecrowd::engine::EcsCore core; + safecrowd::engine::WorldQuery query{core}; + + const auto e1 = core.createEntity(); + const auto e2 = core.createEntity(); + const auto e3 = core.createEntity(); + + core.addComponent(e1, Position{}); + core.addComponent(e1, Velocity{}); + core.addComponent(e2, Position{}); + core.addComponent(e3, Velocity{}); + + const auto result = 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}; + + static_cast(core.createEntity()); + + const auto result = query.view(); + SC_EXPECT_TRUE(result.empty()); +} + +SC_TEST(WorldQuery_ViewExcludesDestroyedEntities) { + safecrowd::engine::EcsCore core; + safecrowd::engine::WorldQuery query{core}; + + const auto e1 = core.createEntity(); + core.addComponent(e1, Position{}); + core.destroyEntity(e1); + + const auto e2 = core.createEntity(); + core.addComponent(e2, Position{}); + + const auto result = 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}; + + const auto e = core.createEntity(); + core.addComponent(e, Position{}); + + SC_EXPECT_TRUE(query.contains(e)); + SC_EXPECT_TRUE(!query.contains(e)); +} + +SC_TEST(WorldQuery_GetReturnsComponentRef) { + safecrowd::engine::EcsCore core; + safecrowd::engine::WorldQuery query{core}; + + const auto e = core.createEntity(); + core.addComponent(e, Position{3.0f, 4.0f}); + + const auto& pos = query.get(e); + SC_EXPECT_NEAR(pos.x, 3.0f, 1e-6); + SC_EXPECT_NEAR(pos.y, 4.0f, 1e-6); +}