diff --git a/src/entt/entity/group.hpp b/src/entt/entity/group.hpp index 3e48e3dd6..ff7c4dbcf 100644 --- a/src/entt/entity/group.hpp +++ b/src/entt/entity/group.hpp @@ -17,6 +17,325 @@ namespace entt { +/** + * @brief Group range. + * + * Primary template isn't defined on purpose. All the specializations give a + * compile-time error, but for a few reasonable cases. + */ +template +class group_range; + + +/** + * @brief Non-owning group range. + * + * Iterable object to use to _visit_ a group. + * + * @sa group + * + * @tparam Entity A valid entity type (see entt_traits for more details). + * @tparam Exclude Types of components used to filter the group. + * @tparam Get Type of components observed by the group. + */ +template +class group_range, get_t> { + /*! @brief A group is allowed to create ranges. */ + friend class basic_group, get_t>; + + // I could have used std::conditional_t ... + template + struct pool { using type = storage; }; + + // ... if only MSVC didn't have a bug ... + template + struct pool { using type = const storage>; }; + + // ... that forces me to do the same in a worse way! :( + template + using pool_type = typename pool::type; + + class range_iterator { + friend class group_range, entt::get_t>; + + using it_type = typename sparse_set::iterator; + using pool_type = decltype(std::tuple_cat(std::declval, std::tuple *>>>()...)); + + range_iterator(it_type from, pool_type ref) ENTT_NOEXCEPT + : it{from}, + pools{ref} + {} + + public: + using difference_type = std::ptrdiff_t; + using value_type = decltype(std::tuple_cat( + std::declval>(), + std::declval, std::tuple>>()... + )); + using pointer = void; + using reference = value_type; + using iterator_category = std::input_iterator_tag; + + range_iterator() ENTT_NOEXCEPT = default; + + range_iterator & operator++() ENTT_NOEXCEPT { + return ++it, *this; + } + + range_iterator operator++(int) ENTT_NOEXCEPT { + range_iterator orig = *this; + return ++(*this), orig; + } + + [[nodiscard]] reference operator*() const ENTT_NOEXCEPT { + return std::apply([entt = *it](auto *... cpool) { return reference{entt, cpool->get(entt)...}; }, pools); + } + + [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT { + return other.it == it; + } + + [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT { + return !(*this == other); + } + + private: + it_type it; + pool_type pools; + }; + + group_range(const sparse_set &ref, std::tuple *...> gpools) + : handler{&ref}, + pools{gpools} + {} + +public: + /*! @brief Underlying entity identifier. */ + using entity_type = Entity; + /*! @brief Input iterator type. */ + using iterator = range_iterator; + + /** + * @brief Returns an iterator to the first element. + * + * The returned iterator points to the first element. If the range is empty, + * the returned iterator will be equal to `end()`. + * + * @note + * Iterators stay true to the order imposed to the underlying data + * structures. + * + * @return An iterator to the first element. + */ + [[nodiscard]] iterator begin() const ENTT_NOEXCEPT { + return iterator{handler->begin(), std::tuple_cat([](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool); + } + }(std::get *>(pools))...)}; + } + + /** + * @brief Returns an iterator that is past the last element. + * + * The returned iterator points to the element following the last element. + * Attempting to dereference the returned iterator results in undefined + * behavior. + * + * @note + * Iterators stay true to the order imposed to the underlying data + * structures. + * + * @return An iterator to the element following the last element that has + * the given components. + */ + [[nodiscard]] iterator end() const ENTT_NOEXCEPT { + return iterator{handler->end(), std::tuple_cat([](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool); + } + }(std::get *>(pools))...)}; + } + +private: + const sparse_set *handler; + std::tuple *...> pools; +}; + + +/** + * @brief Owning group range specialization. + * + * Iterable object to use to _visit_ a group. + * + * @sa group + * + * @tparam Entity A valid entity type (see entt_traits for more details). + * @tparam Exclude Types of components used to filter the group. + * @tparam Get Types of components observed by the group. + * @tparam Owned Types of components owned by the group. + */ +template +class group_range, get_t, Owned...> { + /*! @brief A group is allowed to create ranges. */ + friend class basic_group, get_t, Owned...>; + + // I could have used std::conditional_t ... + template + struct pool { using type = storage; }; + + // ... if only MSVC didn't have a bug ... + template + struct pool { using type = const storage>; }; + + // ... that forces me to do the same in a worse way! :( + template + using pool_type = typename pool::type; + + class range_iterator { + friend class group_range, entt::get_t, Owned...>; + + using it_type = typename sparse_set::iterator; + using owned_type = decltype(std::tuple_cat(std::declval, std::tuple>().begin())>>>()...)); + using get_type = decltype(std::tuple_cat(std::declval, std::tuple *>>>()...)); + + range_iterator(it_type from, owned_type oref, get_type gref) ENTT_NOEXCEPT + : it{from}, + owned{oref}, + get{gref} + {} + + public: + using difference_type = std::ptrdiff_t; + using value_type = decltype(std::tuple_cat( + std::declval>(), + std::declval, std::tuple>>()..., + std::declval, std::tuple>>()... + )); + using pointer = void; + using reference = value_type; + using iterator_category = std::input_iterator_tag; + + range_iterator() ENTT_NOEXCEPT = default; + + range_iterator & operator++() ENTT_NOEXCEPT { + return ++it, std::apply([](auto &&... curr) { (++curr, ...); }, owned), *this; + } + + range_iterator operator++(int) ENTT_NOEXCEPT { + range_iterator orig = *this; + return ++(*this), orig; + } + + [[nodiscard]] reference operator*() const ENTT_NOEXCEPT { + return std::tuple_cat( + std::make_tuple(*it), + std::apply([](auto &&... curr) { return std::forward_as_tuple(*curr...); }, owned), + std::apply([entt = *it](auto &&... curr) { return std::forward_as_tuple(curr->get(entt)...); }, get) + ); + } + + [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT { + return other.it == it; + } + + [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT { + return !(*this == other); + } + + private: + it_type it; + owned_type owned; + get_type get; + }; + + group_range(std::tuple *..., pool_type *...> cpools, const std::size_t &extent) + : pools{cpools}, + length{&extent} + {} + +public: + /*! @brief Underlying entity identifier. */ + using entity_type = Entity; + /*! @brief Input iterator type. */ + using iterator = range_iterator; + + /** + * @brief Returns an iterator to the first element. + * + * The returned iterator points to the first element. If the range is empty, + * the returned iterator will be equal to `end()`. + * + * @note + * Iterators stay true to the order imposed to the underlying data + * structures. + * + * @return An iterator to the first element. + */ + [[nodiscard]] iterator begin() const ENTT_NOEXCEPT { + return iterator{ + std::get<0>(pools)->sparse_set::end() - *length, + std::tuple_cat([this](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool->end() - *length); + } + }(std::get *>(pools))...), + std::tuple_cat([](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool); + } + }(std::get *>(pools))...) + }; + } + + /** + * @brief Returns an iterator that is past the last element. + * + * The returned iterator points to the element following the last element. + * Attempting to dereference the returned iterator results in undefined + * behavior. + * + * @note + * Iterators stay true to the order imposed to the underlying data + * structures. + * + * @return An iterator to the element following the last element that has + * the given components. + */ + [[nodiscard]] iterator end() const ENTT_NOEXCEPT { + return iterator{ + std::get<0>(pools)->sparse_set::end(), + std::tuple_cat([](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool->end()); + } + }(std::get *>(pools))...), + std::tuple_cat([](auto *cpool) { + if constexpr(ENTT_IS_EMPTY(typename std::decay_t::object_type)) { + return std::make_tuple(); + } else { + return std::make_tuple(cpool); + } + }(std::get *>(pools))...) + }; + } + +private: + const std::tuple *..., pool_type *...> pools; + const std::size_t *length; +}; + + /** * @brief Group. * @@ -346,6 +665,23 @@ public: traverse(std::move(func), get_type_list{}); } + /** + * @brief Returns an iterable object to use to _visit_ the group. + * + * The iterable object returns tuples that contain the current entity and a + * set of references to its non-empty components. The _constness_ of the + * components is as requested. + * + * @note + * Empty types aren't explicitly instantiated and therefore they are never + * returned during iterations. + * + * @return An iterable object to use to _visit_ the group. + */ + group_range, get_t> each() const { + return { *handler, pools }; + } + /** * @brief Sort a group according to the given comparison function. * @@ -759,6 +1095,23 @@ public: traverse(std::move(func), owned_type_list{}, get_type_list{}); } + /** + * @brief Returns an iterable object to use to _visit_ the group. + * + * The iterable object returns tuples that contain the current entity and a + * set of references to its non-empty components. The _constness_ of the + * components is as requested. + * + * @note + * Empty types aren't explicitly instantiated and therefore they are never + * returned during iterations. + * + * @return An iterable object to use to _visit_ the group. + */ + group_range, get_t, Owned...> each() const { + return { pools, *length }; + } + /** * @brief Sort a group according to the given comparison function. * diff --git a/test/entt/entity/group.cpp b/test/entt/entity/group.cpp index 88bf4fc88..85ec24705 100644 --- a/test/entt/entity/group.cpp +++ b/test/entt/entity/group.cpp @@ -158,11 +158,25 @@ TEST(NonOwningGroup, Each) { group.each([&cnt](auto, int &, char &) { ++cnt; }); group.each([&cnt](int &, char &) { ++cnt; }); - ASSERT_EQ(cnt, std::size_t{4}); + for(auto curr: group.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ++cnt; + } + + ASSERT_EQ(cnt, std::size_t{6}); cgroup.each([&cnt](auto, const int &, const char &) { --cnt; }); cgroup.each([&cnt](const int &, const char &) { --cnt; }); + for(auto curr: cgroup.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, const int &>)); + ASSERT_TRUE((std::is_same_v, const char &>)); + --cnt; + } + ASSERT_EQ(cnt, std::size_t{0}); } @@ -300,6 +314,12 @@ TEST(NonOwningGroup, IndexRebuiltOnDestroy) { ASSERT_EQ(ivalue, 1); ASSERT_EQ(uivalue, 1u); }); + + for(auto curr: group.each()) { + ASSERT_EQ(std::get<0>(curr), e1); + ASSERT_EQ(std::get<1>(curr), 1); + ASSERT_EQ(std::get<2>(curr), 1u); + } } TEST(NonOwningGroup, ConstNonConstAndAllInBetween) { @@ -324,6 +344,12 @@ TEST(NonOwningGroup, ConstNonConstAndAllInBetween) { ASSERT_TRUE((std::is_same_v)); ASSERT_TRUE((std::is_same_v)); }); + + for(auto curr: group.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, const char &>)); + } } TEST(NonOwningGroup, Find) { @@ -441,6 +467,13 @@ TEST(NonOwningGroup, EmptyAndNonEmptyTypes) { ASSERT_TRUE(entity == e0 || entity == e1); }); + for(auto curr: group.each()) { + ASSERT_EQ(std::tuple_size_v, 2); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE(std::get<0>(curr) == e0 || std::get<0>(curr) == e1); + } + ASSERT_EQ(group.size(), typename decltype(group)::size_type{2}); } @@ -462,7 +495,7 @@ TEST(NonOwningGroup, TrackEntitiesOnComponentDestruction) { ASSERT_FALSE(cgroup.empty()); } -TEST(NonOwningGroup, EachWithEmptyTypes) { +TEST(NonOwningGroup, EmptyTypes) { entt::registry registry; const auto entity = registry.create(); @@ -474,16 +507,49 @@ TEST(NonOwningGroup, EachWithEmptyTypes) { ASSERT_EQ(entity, entt); }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([check = true](int, char) mutable { ASSERT_TRUE(check); check = false; }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([entity](const auto entt, int, char) { ASSERT_EQ(entity, entt); }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([](const auto, int, char, double) { FAIL(); }); + + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 4); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_TRUE((std::is_same_v, double &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } } TEST(NonOwningGroup, FrontBack) { @@ -661,11 +727,25 @@ TEST(OwningGroup, Each) { group.each([&cnt](auto, int &, char &) { ++cnt; }); group.each([&cnt](int &, char &) { ++cnt; }); - ASSERT_EQ(cnt, std::size_t{4}); + for(auto curr: group.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ++cnt; + } + + ASSERT_EQ(cnt, std::size_t{6}); cgroup.each([&cnt](auto, const int &, const char &) { --cnt; }); cgroup.each([&cnt](const int &, const char &) { --cnt; }); + for(auto curr: cgroup.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, const int &>)); + ASSERT_TRUE((std::is_same_v, const char &>)); + --cnt; + } + ASSERT_EQ(cnt, std::size_t{0}); } @@ -887,6 +967,12 @@ TEST(OwningGroup, IndexRebuiltOnDestroy) { ASSERT_EQ(ivalue, 1); ASSERT_EQ(uivalue, 1u); }); + + for(auto curr: group.each()) { + ASSERT_EQ(std::get<0>(curr), e1); + ASSERT_EQ(std::get<1>(curr), 1); + ASSERT_EQ(std::get<2>(curr), 1u); + } } TEST(OwningGroup, ConstNonConstAndAllInBetween) { @@ -919,6 +1005,12 @@ TEST(OwningGroup, ConstNonConstAndAllInBetween) { ASSERT_TRUE((std::is_same_v)); ASSERT_TRUE((std::is_same_v)); }); + + for(auto curr: group.each()) { + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, const char &>)); + } } TEST(OwningGroup, Find) { @@ -1036,6 +1128,13 @@ TEST(OwningGroup, EmptyAndNonEmptyTypes) { ASSERT_TRUE(entity == e0 || entity == e1); }); + for(auto curr: group.each()) { + ASSERT_EQ(std::tuple_size_v, 2); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE(std::get<0>(curr) == e0 || std::get<0>(curr) == e1); + } + ASSERT_EQ(group.size(), typename decltype(group)::size_type{2}); } @@ -1057,7 +1156,7 @@ TEST(OwningGroup, TrackEntitiesOnComponentDestruction) { ASSERT_FALSE(cgroup.empty()); } -TEST(OwningGroup, EachWithEmptyTypes) { +TEST(OwningGroup, EmptyTypes) { entt::registry registry; const auto entity = registry.create(); @@ -1069,16 +1168,49 @@ TEST(OwningGroup, EachWithEmptyTypes) { ASSERT_EQ(entity, entt); }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([check = true](int, char) mutable { ASSERT_TRUE(check); check = false; }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([entity](const auto entt, int, char) { ASSERT_EQ(entity, entt); }); + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 3); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } + registry.group(entt::get).each([](const auto, double, int, char) { FAIL(); }); + + for(auto curr: registry.group(entt::get).each()) { + ASSERT_EQ(std::tuple_size_v, 4); + ASSERT_TRUE((std::is_same_v, entt::entity>)); + ASSERT_TRUE((std::is_same_v, double &>)); + ASSERT_TRUE((std::is_same_v, int &>)); + ASSERT_TRUE((std::is_same_v, char &>)); + ASSERT_EQ(entity, std::get<0>(curr)); + } } TEST(OwningGroup, FrontBack) {