1
0
Michele Caini 6 жил өмнө
parent
commit
16a11638a7

+ 46 - 14
docs/md/entity.md

@@ -432,10 +432,10 @@ from the documentation of the library that first introduced this tool,
 >only update the 10 changed units. So efficient.
 >only update the 10 changed units. So efficient.
 
 
 In `EnTT`, this means to iterating over a reduced set of entities and components
 In `EnTT`, this means to iterating over a reduced set of entities and components
-with respect to what would otherwise be returned from a view or group.<br/>
-On these words, however, the similarities with the proposal of Entitas also end.
-The rules of language and the design of the library obviously impose and allow
-different things.
+with respect to what would otherwise be returned from a view or a group.<br/>
+On these words, however, the similarities with the proposal of `Entitas` also
+end. The rules of language and the design of the library obviously impose and
+allow different things.
 
 
 An `observer` is initialized with an instance of a registry and a set of rules
 An `observer` is initialized with an instance of a registry and a set of rules
 that describe what are the entities to intercept. As an example:
 that describe what are the entities to intercept. As an example:
@@ -444,23 +444,24 @@ that describe what are the entities to intercept. As an example:
 entt::observer observer{registry, entt::collector.replace<sprite>()};
 entt::observer observer{registry, entt::collector.replace<sprite>()};
 ```
 ```
 
 
-The class also default constructible if required and it can be reconfigured at
-any time by means of the `connect` member function. Moreover, instances can be
+The class is default constructible if required and it can be reconfigured at any
+time by means of the `connect` member function. Moreover, instances can be
 disconnected from the underlying registries through the `disconnect` member
 disconnected from the underlying registries through the `disconnect` member
 function.<br/>
 function.<br/>
 The `observer` offers also some member functions to query its internal state and
 The `observer` offers also some member functions to query its internal state and
 to know if it's empty or how many entities it contains. Moreover, it can return
 to know if it's empty or how many entities it contains. Moreover, it can return
-a raw pointer to the list of entities it contains.<br/>
+a raw pointer to the list of entities it contains.
+
 However, the most important features of this class are that:
 However, the most important features of this class are that:
 
 
 * It's iterable and therefore users can easily walk through the list of entities
 * It's iterable and therefore users can easily walk through the list of entities
-  by means of a range-for loop.
+  by means of a range-for loop or the `each` member function.
 * It's clearable and therefore users can consume the entities and literally
 * It's clearable and therefore users can consume the entities and literally
   reset the observer after each iteration.
   reset the observer after each iteration.
 
 
 These aspects make the observer an incredibly powerful tool to know at any time
 These aspects make the observer an incredibly powerful tool to know at any time
-what are the entities that have started to respect the given rules since the
-last time one asked:
+what are the entities that matched the given rules since the last time one
+asked:
 
 
 ```cpp
 ```cpp
 for(const auto entity: observer) {
 for(const auto entity: observer) {
@@ -470,8 +471,20 @@ for(const auto entity: observer) {
 observer.clear();
 observer.clear();
 ```
 ```
 
 
-The `collector` is an utility to use to generate a list of `matcher`s (the
-actual rules) to use with an `observer`.<br/>
+Note that the snippet above is equivalent to the following:
+
+```cpp
+observer.each([](const auto entity) {
+    // ...
+});
+```
+
+At least as long as the `observer` isn't const. This means that the non-const
+overload of `each` does also reset the underlying data structure before to
+return to the caller, while the const overload does not for obvious reasons.
+
+The `collector` is an utility aimed to generate a list of `matcher`s (the actual
+rules) to use with an `observer` instead.<br/>
 There are two types of `matcher`s:
 There are two types of `matcher`s:
 
 
 * Observing matcher: an observer will return at least all the living entities
 * Observing matcher: an observer will return at least all the living entities
@@ -479,7 +492,7 @@ There are two types of `matcher`s:
   and not yet destroyed.
   and not yet destroyed.
 
 
   ```cpp
   ```cpp
-  collector.replace<sprite>();
+  entt::collector.replace<sprite>();
   ```
   ```
 
 
 * Grouping matcher: an observer will return at least all the living entities
 * Grouping matcher: an observer will return at least all the living entities
@@ -487,7 +500,7 @@ There are two types of `matcher`s:
   not yet left it.
   not yet left it.
 
 
   ```cpp
   ```cpp
-  collector.group<position, velocity>(entt::exclude<destroyed>);
+  entt::collector.group<position, velocity>(entt::exclude<destroyed>);
   ```
   ```
 
 
   A grouping matcher supports also exclusion lists as well as single components.
   A grouping matcher supports also exclusion lists as well as single components.
@@ -499,6 +512,25 @@ last time one asked.<br/>
 Note that, for a grouping matcher, if an entity already has all the components
 Note that, for a grouping matcher, if an entity already has all the components
 except one and the missing type is assigned to it, it is intercepted.
 except one and the missing type is assigned to it, it is intercepted.
 
 
+In addition, a matcher can be filtered with a `when` clause:
+
+```cpp
+entt::collector.replace<sprite>().when<position>(entt::exclude<velocity>);
+```
+
+This clause introduces a way to intercept entities if and only if they are
+already part of a hypothetical group. If they are not, they aren't returned by
+the observer, no matter if they matched the given rule.<br/>
+In the example above, whenever the component `sprite` of an entity is replaced,
+the observer probes the entity itself to verify that it has at least `position`
+and has not `velocity` before to store it aside. If one of the two conditions of
+the filter isn't respected, the entity is discared, no matter what.
+
+A `when` clause accepts a theoretically unlimited number of types as well as
+multiple elements in the exclusion list. Moreover, every matcher can have it's
+own clause and multiple clauses for the same matcher are combined in a single
+one.
+
 ## Runtime components
 ## Runtime components
 
 
 Defining components at runtime is useful to support plugin systems and mods in
 Defining components at runtime is useful to support plugin systems and mods in

+ 126 - 52
src/entt/entity/observer.hpp

@@ -6,6 +6,8 @@
 #include <cstddef>
 #include <cstddef>
 #include <cstdint>
 #include <cstdint>
 #include <utility>
 #include <utility>
+#include <algorithm>
+#include <type_traits>
 #include "../config/config.h"
 #include "../config/config.h"
 #include "../core/type_traits.hpp"
 #include "../core/type_traits.hpp"
 #include "registry.hpp"
 #include "registry.hpp"
@@ -17,46 +19,19 @@
 namespace entt {
 namespace entt {
 
 
 
 
-/**
- * @brief Matcher.
- *
- * Primary template isn't defined on purpose. All the specializations give a
- * compile-time error, but for a few reasonable cases.
- */
+/*! @brief Grouping matcher. */
 template<typename...>
 template<typename...>
-struct matcher;
-
-
-/**
- * @brief Observing matcher.
- *
- * An observing matcher contains a type for which changes should be
- * detected.<br/>
- * Because of the rules of the language, not all changes can be easily detected.
- * In order to avoid nasty solutions that could affect performance to an extent,
- * the matcher listens only to the `on_replace` signals emitted by a registry
- * and is therefore triggered whenever an instance of the given component is
- * explicitly replaced.
- *
- * @tparam AnyOf Type of component for which changes should be detected.
- */
-template<typename AnyOf>
-struct matcher<AnyOf> {};
+struct matcher {};
 
 
 
 
 /**
 /**
- * @brief Grouping matcher.
- *
- * A grouping matcher describes the group to track in terms of accepted and
- * excluded types.<br/>
- * This kind of matcher is triggered whenever an entity _enters_ the desired
- * group because of the components it is assigned.
+ * @brief Collector.
  *
  *
- * @tparam AllOf Types of components tracked by the matcher.
- * @tparam NoneOf Types of components used to filter out entities.
+ * Primary template isn't defined on purpose. All the specializations give a
+ * compile-time error, but for a few reasonable cases.
  */
  */
-template<typename... AllOf, typename... NoneOf>
-struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
+template<typename...>
+struct basic_collector;
 
 
 
 
 /**
 /**
@@ -66,12 +41,39 @@ struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
  * entities.<br/>
  * entities.<br/>
  * Its main purpose is to generate a descriptor that allows an observer to know
  * Its main purpose is to generate a descriptor that allows an observer to know
  * how to connect to a registry.
  * how to connect to a registry.
- *
+ */
+template<>
+struct basic_collector<> {
+    /**
+     * @brief Adds a grouping matcher to the collector.
+     * @tparam AllOf Types of components tracked by the matcher.
+     * @tparam NoneOf Types of components used to filter out entities.
+     * @return The updated collector.
+     */
+    template<typename... AllOf, typename... NoneOf>
+    static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
+        return basic_collector<matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
+    }
+
+    /**
+     * @brief Adds an observing matcher to the collector.
+     * @tparam AnyOf Type of component for which changes should be detected.
+     * @return The updated collector.
+     */
+    template<typename AnyOf>
+    static constexpr auto replace() ENTT_NOEXCEPT {
+        return basic_collector<matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
+    }
+};
+
+/**
+ * @brief Collector.
+ * @copydetails basic_collector<>
  * @tparam AnyOf Types of components for which changes should be detected.
  * @tparam AnyOf Types of components for which changes should be detected.
  * @tparam Matcher Types of grouping matchers.
  * @tparam Matcher Types of grouping matchers.
  */
  */
-template<typename... Matcher>
-struct basic_collector {
+template<typename... Reject, typename... Require, typename... Rule, typename... Other>
+struct basic_collector<matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>, Other...> {
     /**
     /**
      * @brief Adds a grouping matcher to the collector.
      * @brief Adds a grouping matcher to the collector.
      * @tparam AllOf Types of components tracked by the matcher.
      * @tparam AllOf Types of components tracked by the matcher.
@@ -80,17 +82,30 @@ struct basic_collector {
      */
      */
     template<typename... AllOf, typename... NoneOf>
     template<typename... AllOf, typename... NoneOf>
     static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
     static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
-        return basic_collector<Matcher..., matcher<type_list<AllOf...>, type_list<NoneOf...>>>{};
+        using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
+        return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
     }
     }
 
 
     /**
     /**
-     * @brief Adds one or more observing matchers to the collector.
-     * @tparam AnyOf Types of components for which changes should be detected.
+     * @brief Adds an observing matcher to the collector.
+     * @tparam AnyOf Type of component for which changes should be detected.
      * @return The updated collector.
      * @return The updated collector.
      */
      */
-    template<typename... AnyOf>
+    template<typename AnyOf>
     static constexpr auto replace() ENTT_NOEXCEPT {
     static constexpr auto replace() ENTT_NOEXCEPT {
-        return basic_collector<Matcher..., matcher<AnyOf>...>{};
+        using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
+        return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
+    }
+
+    /**
+     * @brief Updates the filter of the last added matcher.
+     * @tparam AllOf Types of components required by the matcher.
+     * @tparam NoneOf Types of components used to filter out entities.
+     * @return The updated collector.
+     */
+    template<typename... AllOf, typename... NoneOf>
+    static constexpr auto when(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
+        return basic_collector<matcher<matcher<type_list<Reject..., NoneOf...>, type_list<Require..., AllOf...>>, Rule...>, Other...>{};
     }
     }
 };
 };
 
 
@@ -120,6 +135,13 @@ constexpr basic_collector<> collector{};
  * If an entity respects the requirements of multiple matchers, it will be
  * If an entity respects the requirements of multiple matchers, it will be
  * returned once and only once by the observer in any case.
  * returned once and only once by the observer in any case.
  *
  *
+ * Matchers support also filtering by means of a _when_ clause that accepts both
+ * a list of types and an exclusion list.<br/>
+ * Whenever a matcher finds that an entity matches its requirements, the
+ * condition of the filter is verified before to register the entity itself.
+ * Moreover, a registered entity isn't returned by the observer if the condition
+ * set by the filter is broken in the meantime.
+ *
  * @b Important
  * @b Important
  *
  *
  * Iterators aren't invalidated if:
  * Iterators aren't invalidated if:
@@ -149,10 +171,12 @@ class basic_observer {
     template<std::size_t Index, typename>
     template<std::size_t Index, typename>
     struct matcher_handler;
     struct matcher_handler;
 
 
-    template<std::size_t Index, typename AnyOf>
-    struct matcher_handler<Index, matcher<AnyOf>> {
-        static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
-            (obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
+    template<std::size_t Index, typename... Reject, typename... Require, typename AnyOf>
+    struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, AnyOf>> {
+        static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &reg, const Entity entt) {
+            if(reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...)) {
+                (obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
+            }
         }
         }
 
 
         static void discard_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
         static void discard_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
@@ -162,20 +186,26 @@ class basic_observer {
         }
         }
 
 
         static void disconnect(basic_registry<Entity> &reg, const basic_observer &obs) {
         static void disconnect(basic_registry<Entity> &reg, const basic_observer &obs) {
-            (reg.template on_replace<AnyOf>().disconnect(&obs));
-            (reg.template on_destroy<AnyOf>().disconnect(&obs));
+            reg.template on_replace<AnyOf>().disconnect(&obs);
+            reg.template on_destroy<AnyOf>().disconnect(&obs);
+            (reg.template on_destroy<Require>().disconnect(&obs), ...);
+            (reg.template on_construct<Reject>().disconnect(&obs), ...);
         }
         }
 
 
         static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
         static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
             reg.template on_replace<AnyOf>().template connect<&maybe_valid_if>(&obs);
             reg.template on_replace<AnyOf>().template connect<&maybe_valid_if>(&obs);
             reg.template on_destroy<AnyOf>().template connect<&discard_if>(&obs);
             reg.template on_destroy<AnyOf>().template connect<&discard_if>(&obs);
+            (reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
+            (reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
         }
         }
     };
     };
 
 
-    template<std::size_t Index, typename... AllOf, typename... NoneOf>
-    struct matcher_handler<Index, matcher<type_list<AllOf...>, type_list<NoneOf...>>> {
+    template<std::size_t Index, typename... Reject, typename... Require, typename... NoneOf, typename... AllOf>
+    struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, type_list<NoneOf...>, type_list<AllOf...>>> {
         static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &reg, const Entity entt) {
         static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &reg, const Entity entt) {
-            if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)) {
+            if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)
+                    && reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...))
+            {
                 (obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
                 (obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
             }
             }
         }
         }
@@ -191,6 +221,8 @@ class basic_observer {
             ((reg.template on_destroy<AllOf>().disconnect(&obs)), ...);
             ((reg.template on_destroy<AllOf>().disconnect(&obs)), ...);
             ((reg.template on_construct<NoneOf>().disconnect(&obs)), ...);
             ((reg.template on_construct<NoneOf>().disconnect(&obs)), ...);
             ((reg.template on_destroy<NoneOf>().disconnect(&obs)), ...);
             ((reg.template on_destroy<NoneOf>().disconnect(&obs)), ...);
+            (reg.template on_destroy<Require>().disconnect(&obs), ...);
+            (reg.template on_construct<Reject>().disconnect(&obs), ...);
         }
         }
 
 
         static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
         static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
@@ -198,6 +230,8 @@ class basic_observer {
             (reg.template on_destroy<NoneOf>().template connect<&maybe_valid_if>(&obs), ...);
             (reg.template on_destroy<NoneOf>().template connect<&maybe_valid_if>(&obs), ...);
             (reg.template on_destroy<AllOf>().template connect<&discard_if>(&obs), ...);
             (reg.template on_destroy<AllOf>().template connect<&discard_if>(&obs), ...);
             (reg.template on_construct<NoneOf>().template connect<&discard_if>(&obs), ...);
             (reg.template on_construct<NoneOf>().template connect<&discard_if>(&obs), ...);
+            (reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
+            (reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
         }
         }
     };
     };
 
 
@@ -341,7 +375,47 @@ public:
 
 
     /*! @brief Resets the underlying container. */
     /*! @brief Resets the underlying container. */
     void clear() {
     void clear() {
-        return view.reset();
+        view.reset();
+    }
+
+    /**
+     * @brief Iterates entities and applies the given function object to them,
+     * then clears the observer.
+     *
+     * The function object is invoked for each entity.<br/>
+     * The signature of the function must be equivalent to the following form:
+     *
+     * @code{.cpp}
+     * void(const entity_type);
+     * @endcode
+     *
+     * @tparam Func Type of the function object to invoke.
+     * @param func A valid function object.
+     */
+    template<typename Func>
+    void each(Func func) const {
+        static_assert(std::is_invocable_v<Func, entity_type>);
+        std::for_each(begin(), end(), std::move(func));
+    }
+
+    /**
+     * @brief Iterates entities and applies the given function object to them,
+     * then clears the observer.
+     *
+     * The function object is invoked for each entity.<br/>
+     * The signature of the function must be equivalent to the following form:
+     *
+     * @code{.cpp}
+     * void(const entity_type);
+     * @endcode
+     *
+     * @tparam Func Type of the function object to invoke.
+     * @param func A valid function object.
+     */
+    template<typename Func>
+    void each(Func func) {
+        std::as_const(*this).each(std::move(func));
+        clear();
     }
     }
 
 
 private:
 private:

+ 105 - 2
test/entt/entity/observer.cpp

@@ -1,3 +1,5 @@
+#include <tuple>
+#include <type_traits>
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 #include <entt/entity/observer.hpp>
 #include <entt/entity/observer.hpp>
 #include <entt/entity/registry.hpp>
 #include <entt/entity/registry.hpp>
@@ -82,9 +84,49 @@ TEST(Observer, AllOf) {
     ASSERT_TRUE(observer.empty());
     ASSERT_TRUE(observer.empty());
 }
 }
 
 
+TEST(Observer, AllOfFiltered) {
+    constexpr auto collector =  entt::collector
+            .group<int>().when<char>(entt::exclude<double>);
+
+    entt::registry registry;
+    entt::observer observer{registry, collector};
+    const auto entity = registry.create();
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.assign<int>(entity);
+
+    ASSERT_EQ(observer.size(), entt::observer::size_type{});
+    ASSERT_TRUE(observer.empty());
+    ASSERT_EQ(observer.data(), nullptr);
+
+    registry.remove<int>(entity);
+    registry.assign<char>(entity);
+    registry.assign<double>(entity);
+    registry.assign<int>(entity);
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.remove<int>(entity);
+    registry.remove<double>(entity);
+    registry.assign<int>(entity);
+
+    ASSERT_EQ(observer.size(), entt::observer::size_type{1});
+    ASSERT_FALSE(observer.empty());
+    ASSERT_EQ(*observer.data(), entity);
+
+    registry.assign<double>(entity);
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.remove<double>(entity);
+
+    ASSERT_TRUE(observer.empty());
+}
+
 TEST(Observer, Observe) {
 TEST(Observer, Observe) {
     entt::registry registry;
     entt::registry registry;
-    entt::observer observer{registry, entt::collector.replace<int, char>()};
+    entt::observer observer{registry, entt::collector.replace<int>().replace<char>()};
     const auto entity = registry.create();
     const auto entity = registry.create();
 
 
     ASSERT_TRUE(observer.empty());
     ASSERT_TRUE(observer.empty());
@@ -116,6 +158,45 @@ TEST(Observer, Observe) {
     ASSERT_TRUE(observer.empty());
     ASSERT_TRUE(observer.empty());
 }
 }
 
 
+TEST(Observer, ObserveFiltered) {
+    constexpr auto collector =  entt::collector
+            .replace<int>().when<char>(entt::exclude<double>);
+
+    entt::registry registry;
+    entt::observer observer{registry, collector};
+    const auto entity = registry.create();
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.assign<int>(entity);
+    registry.replace<int>(entity);
+
+    ASSERT_EQ(observer.size(), entt::observer::size_type{});
+    ASSERT_TRUE(observer.empty());
+    ASSERT_EQ(observer.data(), nullptr);
+
+    registry.assign<char>(entity);
+    registry.assign<double>(entity);
+    registry.replace<int>(entity);
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.remove<double>(entity);
+    registry.replace<int>(entity);
+
+    ASSERT_EQ(observer.size(), entt::observer::size_type{1});
+    ASSERT_FALSE(observer.empty());
+    ASSERT_EQ(*observer.data(), entity);
+
+    registry.assign<double>(entity);
+
+    ASSERT_TRUE(observer.empty());
+
+    registry.remove<double>(entity);
+
+    ASSERT_TRUE(observer.empty());
+}
+
 TEST(Observer, AllOfObserve) {
 TEST(Observer, AllOfObserve) {
     entt::registry registry;
     entt::registry registry;
     entt::observer observer{};
     entt::observer observer{};
@@ -151,7 +232,6 @@ TEST(Observer, AllOfObserve) {
     ASSERT_TRUE(observer.empty());
     ASSERT_TRUE(observer.empty());
 }
 }
 
 
-
 TEST(Observer, CrossRulesCornerCase) {
 TEST(Observer, CrossRulesCornerCase) {
     entt::registry registry;
     entt::registry registry;
     entt::observer observer{registry, entt::collector.group<int>().group<char>()};
     entt::observer observer{registry, entt::collector.group<int>().group<char>()};
@@ -167,3 +247,26 @@ TEST(Observer, CrossRulesCornerCase) {
 
 
     ASSERT_FALSE(observer.empty());
     ASSERT_FALSE(observer.empty());
 }
 }
+
+TEST(Observer, Each) {
+    entt::registry registry;
+    entt::observer observer{registry, entt::collector.group<int>()};
+    const auto entity = std::get<0>(registry.create<int>());
+
+    ASSERT_FALSE(observer.empty());
+    ASSERT_EQ(observer.size(), entt::observer::size_type{1});
+
+    std::as_const(observer).each([entity](const auto entt) {
+        ASSERT_EQ(entity, entt);
+    });
+
+    ASSERT_FALSE(observer.empty());
+    ASSERT_EQ(observer.size(), entt::observer::size_type{1});
+
+    observer.each([entity](const auto entt) {
+        ASSERT_EQ(entity, entt);
+    });
+
+    ASSERT_TRUE(observer.empty());
+    ASSERT_EQ(observer.size(), entt::observer::size_type{});
+}