diff --git a/include/libcatboy/ecs/component_view.hpp b/include/libcatboy/ecs/component_view.hpp index e69de29..c40bde9 100644 --- a/include/libcatboy/ecs/component_view.hpp +++ b/include/libcatboy/ecs/component_view.hpp @@ -0,0 +1,124 @@ +#ifndef LIBCATBOY_ECS_COMPONENT_VIEW_HPP +#define LIBCATBOY_ECS_COMPONENT_VIEW_HPP + +#include "sparseSet.hpp" + +#include +#include +#include +#include +#include +#include + +namespace libcatboy { +namespace ecs { + +template + requires(sizeof...(Sum) >= 1) +class component_view { + friend class iterator; +public: + class iterator { + friend class component_view; + private: + iterator(component_view* view, std::size_t index) + : m_view(view), m_denseMap(view->m_minimal->dense_map()), m_index(index) { + find_valid(); + } + public: + using value_type = std::tuple; + using difference_type = std::ptrdiff_t; + using reference = std::tuple; + using iterator_category = std::forward_iterator_tag; + public: + reference operator*() { return std::tuple_cat(std::make_tuple(index()), current_tuple()); } + + iterator& operator++() { + ++m_index; + find_valid(); + return *this; + } + + iterator operator++(int) { + iterator ret = *this; + ++(*this); + return ret; + } + public: + bool operator==(const iterator& rhs) const noexcept { return m_view == rhs.m_view && m_index == rhs.m_index; } + + bool operator!=(const iterator& rhs) const noexcept { return m_view != rhs.m_view || m_index != rhs.m_index; } + public: + void swap(iterator& other) noexcept { + std::swap(m_view, other.m_view); + std::swap(m_denseMap, other.m_denseMap); + std::swap(m_index, other.m_index); + } + private: + void find_valid() { + while (m_index < m_denseMap.size() && !m_view->valid(index())) + ++m_index; + } + private: + using component_types = std::tuple; + private: + auto current_tuple() { return current_tuple(std::make_index_sequence{}); } + + template + auto current_tuple(std::index_sequence) { + return std::forward_as_tuple( + (reinterpret_cast>&>(*m_view->m_sets[Is]) + .at(index()))...); + } + + auto current_tuple() const { return current_tuple(std::make_index_sequence{}); } + + template + auto current_tuple(std::index_sequence) const { + return std::forward_as_tuple( + (reinterpret_cast>&>(*m_view->m_sets[Is]) + .at(index()))...); + } + private: + std::size_t index() const noexcept { return m_denseMap[m_index]; } + private: + component_view* m_view; + std::span m_denseMap; + std::size_t m_index = 0; + }; +public: + component_view(std::array sets) + : m_sets(sets), m_minimal(sets.front()) { + for (std::size_t i = 1; i < sets.size(); ++i) { + if (m_minimal->size() > sets[i]->size()) + m_minimal = sets[i]; + } + } +public: + std::size_t size() const noexcept { + std::size_t count = 0; + for (auto idx : m_minimal->dense_map()) { + if (valid(idx)) ++count; + } + return count; + } +public: + iterator begin() { return { this, 0 }; } + iterator end() { return { this, m_minimal->size() }; } +private: + bool valid(Entity entity) const noexcept { + for (isparse_set* set : m_sets) { + if (!set->contains(entity)) return false; + } + return true; + } +private: + std::array m_sets; + + isparse_set* m_minimal = nullptr; +}; + +} // namespace ecs +} // namespace libcatboy + +#endif // LIBCATBOY_ECS_COMPONENT_VIEW_HPP diff --git a/include/libcatboy/ecs/ecs.hpp b/include/libcatboy/ecs/ecs.hpp index 56f34a4..f10afa6 100644 --- a/include/libcatboy/ecs/ecs.hpp +++ b/include/libcatboy/ecs/ecs.hpp @@ -1,6 +1,7 @@ #ifndef LIBCATBOY_ECS_ECS_HPP #define LIBCATBOY_ECS_ECS_HPP +#include "component_view.hpp" #include "fwd.hpp" #include "sparseSet.hpp" @@ -83,6 +84,12 @@ public: if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead"); get_set().erase(entity); } +public: + template + requires(sizeof...(Sum) >= 1) + component_view view() { + return { { (&get_set())... } }; + } private: template sparse_set& get_set() { diff --git a/include/libcatboy/ecs/sparseSet.hpp b/include/libcatboy/ecs/sparseSet.hpp index 80ce056..647c0f0 100644 --- a/include/libcatboy/ecs/sparseSet.hpp +++ b/include/libcatboy/ecs/sparseSet.hpp @@ -1,6 +1,7 @@ #ifndef LIBCATBOY_ECS_SPARSE_SET_HPP #define LIBCATBOY_ECS_SPARSE_SET_HPP +#include #include #include #include @@ -18,8 +19,11 @@ public: isparse_set(const isparse_set&) = default; isparse_set& operator=(const isparse_set&) = default; public: - virtual bool contains(std::size_t key) const noexcept = 0; - virtual void erase(std::size_t key) noexcept = 0; + virtual bool contains(std::size_t key) const noexcept = 0; + virtual void erase(std::size_t key) noexcept = 0; + virtual std::size_t size() const noexcept = 0; + virtual std::size_t sparse_size() const noexcept = 0; + virtual std::span dense_map() noexcept = 0; }; template @@ -70,6 +74,14 @@ public: m_reverseMap.pop_back(); set_index(key, TOMBSTONE); } +public: + std::size_t size() const noexcept override { return m_dense.size(); } + + std::size_t sparse_size() const noexcept override { return m_map.size(); } + + std::span dense_map() noexcept override { + return m_reverseMap; + } private: void set_index(std::size_t key, std::size_t dense) { if (key >= m_map.size()) m_map.resize(key + 1, TOMBSTONE); diff --git a/test/main.cpp b/test/main.cpp index 051066a..58181fa 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include 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(e1, 1, 2); + reg.emplace(e1, 10, 20); + + reg.emplace(e2, 3, 4); + + reg.emplace(e3, 5, 6); + reg.emplace(e3, 50, 60); + + std::vector result; + + for (auto [e, p, v] : reg.view()) { + 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(e1, 1, 2); + reg.emplace(e2, 3, 4); + + auto view = reg.view(); + + EXPECT_EQ(view.begin(), view.end()); +} + +TEST_F(RegistryTest, ViewReturnsMutableComponentReferences) { + const auto e = create(); + + reg.emplace(e, 1, 2); + reg.emplace(e, 3, 4); + + for (auto [entity, p, v] : reg.view()) { + EXPECT_EQ(entity, e); + + p.x = 100; + v.y = 200; + } + + EXPECT_EQ(reg.at(e), (position{ 100, 2 })); + EXPECT_EQ(reg.at(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(e0, 0, 0); + reg.emplace(e0, 10, 10); + + reg.emplace(e2, 2, 20); + reg.emplace(e2, 20, 20); + + reg.emplace(e3, 3, 30); + + std::vector result; + + for (auto [e, p, v] : reg.view()) { + 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(e, i, i); + } + + const auto e1 = create(); + const auto e2 = create(); + + reg.emplace(e1, 100, 100); + reg.emplace(e1, 1, 1); + + reg.emplace(e2, 200, 200); + reg.emplace(e2, 2, 2); + + std::vector result; + + for (auto [e, p, v] : reg.view()) + 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(e1, 1, 2); + reg.emplace(e1, 3, 4); + + reg.emplace(e2, 5, 6); + reg.emplace(e2, 7, 8); + + reg.erase(e1); + + std::vector result; + + for (auto [e, p, v] : reg.view()) + result.push_back(e); + + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result.front(), e2); +} + } // namespace libcatboy::ecs::test