feat(ecs): introduce component view

Closes: #1
This commit is contained in:
2026-09-07 23:05:39 +02:00
parent 320eb29957
commit 6639127bcf
4 changed files with 285 additions and 2 deletions
+140
View File
@@ -8,6 +8,7 @@
#include <type_traits>
#include <utility>
#include <vector>
#include <algorithm>
namespace libcatboy::ecs::test {
@@ -939,4 +940,143 @@ TEST_F(RegistryTest, RegistryIsDefaultConstructible) {
SUCCEED();
}
// =============================================================================
// COMPONENT VIEWS
// =============================================================================
TEST_F(RegistryTest, ViewReturnsEntitiesWithAllRequestedComponents) {
const auto e1 = create();
const auto e2 = create();
const auto e3 = create();
reg.emplace<position>(e1, 1, 2);
reg.emplace<velocity>(e1, 10, 20);
reg.emplace<position>(e2, 3, 4);
reg.emplace<position>(e3, 5, 6);
reg.emplace<velocity>(e3, 50, 60);
std::vector<entity> result;
for (auto [e, p, v] : reg.view<position, velocity>()) {
result.push_back(e);
EXPECT_EQ(p, (position{ e == e1 ? 1 : 5, e == e1 ? 2 : 6 }));
EXPECT_EQ(v, (velocity{ e == e1 ? 10 : 50, e == e1 ? 20 : 60 }));
}
ASSERT_EQ(result.size(), 2u);
EXPECT_NE(std::find(result.begin(), result.end(), e1), result.end());
EXPECT_NE(std::find(result.begin(), result.end(), e3), result.end());
EXPECT_EQ(std::find(result.begin(), result.end(), e2), result.end());
}
TEST_F(RegistryTest, ViewIsEmptyWhenNoEntityHasAllComponents) {
const auto e1 = create();
const auto e2 = create();
reg.emplace<position>(e1, 1, 2);
reg.emplace<velocity>(e2, 3, 4);
auto view = reg.view<position, velocity>();
EXPECT_EQ(view.begin(), view.end());
}
TEST_F(RegistryTest, ViewReturnsMutableComponentReferences) {
const auto e = create();
reg.emplace<position>(e, 1, 2);
reg.emplace<velocity>(e, 3, 4);
for (auto [entity, p, v] : reg.view<position, velocity>()) {
EXPECT_EQ(entity, e);
p.x = 100;
v.y = 200;
}
EXPECT_EQ(reg.at<position>(e), (position{ 100, 2 }));
EXPECT_EQ(reg.at<velocity>(e), (velocity{ 3, 200 }));
}
TEST_F(RegistryTest, ViewHandlesSparseEntityIDs) {
const auto e0 = create();
const auto e1 = create();
const auto e2 = create();
const auto e3 = create();
reg.emplace<position>(e0, 0, 0);
reg.emplace<velocity>(e0, 10, 10);
reg.emplace<position>(e2, 2, 20);
reg.emplace<velocity>(e2, 20, 20);
reg.emplace<position>(e3, 3, 30);
std::vector<entity> result;
for (auto [e, p, v] : reg.view<position, velocity>()) {
result.push_back(e);
}
ASSERT_EQ(result.size(), 2u);
EXPECT_NE(std::find(result.begin(), result.end(), e0), result.end());
EXPECT_NE(std::find(result.begin(), result.end(), e2), result.end());
EXPECT_EQ(std::find(result.begin(), result.end(), e1), result.end());
EXPECT_EQ(std::find(result.begin(), result.end(), e3), result.end());
}
TEST_F(RegistryTest, ViewUsesSmallestComponentSetAsDriver) {
// Position exists on many entities while velocity exists on only two.
// The result should still contain exactly the intersection.
//
// This primarily exercises the optimization in component_view that picks
// the smallest sparse set as the iteration source.
for (int i = 0; i < 100; ++i) {
const auto e = create();
reg.emplace<position>(e, i, i);
}
const auto e1 = create();
const auto e2 = create();
reg.emplace<position>(e1, 100, 100);
reg.emplace<velocity>(e1, 1, 1);
reg.emplace<position>(e2, 200, 200);
reg.emplace<velocity>(e2, 2, 2);
std::vector<entity> result;
for (auto [e, p, v] : reg.view<position, velocity>())
result.push_back(e);
ASSERT_EQ(result.size(), 2u);
EXPECT_NE(std::find(result.begin(), result.end(), e1), result.end());
EXPECT_NE(std::find(result.begin(), result.end(), e2), result.end());
}
TEST_F(RegistryTest, ViewStopsReturningEntityAfterComponentIsErased) {
const auto e1 = create();
const auto e2 = create();
reg.emplace<position>(e1, 1, 2);
reg.emplace<velocity>(e1, 3, 4);
reg.emplace<position>(e2, 5, 6);
reg.emplace<velocity>(e2, 7, 8);
reg.erase<velocity>(e1);
std::vector<entity> result;
for (auto [e, p, v] : reg.view<position, velocity>())
result.push_back(e);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.front(), e2);
}
} // namespace libcatboy::ecs::test