Przeglądaj źródła

signals on tags

Michele Caini 8 lat temu
rodzic
commit
c0213e84f6
4 zmienionych plików z 345 dodań i 131 usunięć
  1. 81 49
      README.md
  2. 2 0
      TODO
  3. 172 73
      src/entt/entity/registry.hpp
  4. 90 9
      test/entt/entity/registry.cpp

+ 81 - 49
README.md

@@ -15,8 +15,9 @@
       * [Pay per use](#pay-per-use)
    * [Vademecum](#vademecum)
    * [The Registry, the Entity and the Component](#the-registry-the-entity-and-the-component)
-      * [Observe changes](#observe-changes)
       * [Single instance components](#single-instance-components)
+      * [Observe changes](#observe-changes)
+         * [Who let the tags out?](#who-let-the-tags-out?)
       * [Runtime components](#runtime-components)
          * [A journey through a plugin](#a-journey-through-a-plugin)
       * [Sorting: is it possible?](#sorting-is-it-possible)
@@ -529,54 +530,6 @@ std::tuple<Position &, Velocity &> tup = registry.get<Position, Velocity>(entity
 The `get` member function template gives direct access to the component of an
 entity stored in the underlying data structures of the registry.
 
-### Observe changes
-
-Because of how the registry works internally, it stores a couple of signal
-handlers for each pool in order to notify some of its data structures on the
-construction and destruction of components.<br/>
-These signal handlers are also exposed and made available to users. This is the
-basic brick to build fancy things like blueprints and reactive systems.
-
-To get a sink to be used to connect and disconnect listeners so as to be
-notified on the creation of a component, use the `construction` member function:
-
-```cpp
-// connects a free function
-registry.construction<Position>().connect<&MyFreeFunction>();
-
-// connects a member function
-registry.construction<Position>().connect<MyClass, &MyClass::member>(&instance);
-
-// disconnects a free function
-registry.construction<Position>().disconnect<&MyFreeFunction>();
-
-// disconnects a member function
-registry.construction<Position>().disconnect<MyClass, &MyClass::member>(&instance);
-```
-
-To be notified when components are destroyed, use the `destruction` member
-function instead.
-
-The function type of a listener is the same in both cases:
-
-```cpp
-void(Registry<Entity> &, Entity);
-```
-
-In other terms, a listener is provided with the registry that triggered the
-notification and the entity affected by the change. Note also that:
-
-* Listeners are invoked **after** components have been assigned to entities.
-* Listeners are invoked **before** components have been removed from entities.
-* The order of invocation of the listeners isn't guaranteed in any case.
-
-There are also some limitations on what a listener can and cannot do. In
-particular, connecting and disconnecting other functions from within the body of
-a listener should be avoided. It can result in undefined behavior.<br/>
-In general, events and therefore listeners must not be used as replacements for
-systems. They should not contain much logic and interactions with a registry
-should be kept to a minimum, if possible.
-
 ### Single instance components
 
 In those cases where all what is needed is a single instance component, tags are
@@ -652,6 +605,85 @@ auto player = registry.attachee<PlayingCharacter>();
 Note that iterating tags isn't possible for obvious reasons. Tags give direct
 access to single entities and nothing more.
 
+### Observe changes
+
+Because of how the registry works internally, it stores a couple of signal
+handlers for each pool in order to notify some of its data structures on the
+construction and destruction of components.<br/>
+These signal handlers are also exposed and made available to users. This is the
+basic brick to build fancy things like blueprints and reactive systems.
+
+To get a sink to be used to connect and disconnect listeners so as to be
+notified on the creation of a component, use the `construction` member function:
+
+```cpp
+// connects a free function
+registry.construction<Position>().connect<&MyFreeFunction>();
+
+// connects a member function
+registry.construction<Position>().connect<MyClass, &MyClass::member>(&instance);
+
+// disconnects a free function
+registry.construction<Position>().disconnect<&MyFreeFunction>();
+
+// disconnects a member function
+registry.construction<Position>().disconnect<MyClass, &MyClass::member>(&instance);
+```
+
+To be notified when components are destroyed, use the `destruction` member
+function instead.
+
+The function type of a listener is the same in both cases:
+
+```cpp
+void(Registry<Entity> &, Entity);
+```
+
+In other terms, a listener is provided with the registry that triggered the
+notification and the entity affected by the change. Note also that:
+
+* Listeners are invoked **after** components have been assigned to entities.
+* Listeners are invoked **before** components have been removed from entities.
+* The order of invocation of the listeners isn't guaranteed in any case.
+
+There are also some limitations on what a listener can and cannot do. In
+particular:
+
+* Connecting and disconnecting other functions from within the body of a
+  listener should be avoided. It can lead to undefined behavior.
+* Assigning and removing components and tags from within the body of a listener
+  that observes the destruction of instances of a given type should be avoided.
+  It can lead to undefined behavior. This type of listeners is intended to
+  provide users with an easy way to perform cleanup and nothing more.
+
+In general, events and therefore listeners must not be used as replacements for
+systems. They should not contain much logic and interactions with a registry
+should be kept to a minimum, if possible. Note also that the greater the number
+of listeners, the greater the performance hit when components are created or
+destroyed.
+
+#### Who let the tags out?
+
+As an extension, signals are also provided with tags. Although they are not
+strictly required internally, it makes sense that an user expects signal support
+even when it comes to tags actually.<br/>
+Signals for tags undergo exactly the same requirements of those introduced for
+components. Also the function type for a listener is the same and it's invoked
+with the same guarantees discussed above.
+
+To get the sinks for a tag just use `entt::tag_type_t` to disambiguate overloads
+of member functions as in the following example:
+
+```cpp
+registry.construction<MyTag>(entt::tag_type_t{}).connect<&MyFreeFunction>();
+registry.destruction<MyTag>(entt::tag_type_t{}).connect<MyClass, &MyClass::member>(&instance);
+```
+
+Listeners for tags and components are managed separately and do not influence
+each other in any case. Therefore, note that the greater the number of listeners
+for a type, the greater the performance hit when a tag of the given type is
+created or destroyed.
+
 ### Runtime components
 
 Defining components at runtime is useful to support plugins and mods in general.

+ 2 - 0
TODO

@@ -8,5 +8,7 @@
 * does it worth it to add an optional functor to the member functions of snapshot so as to filter out instances and entities?
 * ease the assignment of tags as string (use a template class with a non-type template parameter behind the scene)
 * dictionary based dependency class (templates copied over) + prefabs (shared state/copy-on-write)
+* update benchmarks, destroy is probably slower now because of signals on components/tags
 * "singleton mode" for tags (see #66)
+* introduce a fast destroy that doesn't check for components or tags (it does it only in debug) and discards the entity immediately (use it with snapshot orphans)
 * AOB

+ 172 - 73
src/entt/entity/registry.hpp

@@ -38,24 +38,19 @@ class Registry {
     using tag_family = Family<struct InternalRegistryTagFamily>;
     using component_family = Family<struct InternalRegistryComponentFamily>;
     using handler_family = Family<struct InternalRegistryHandlerFamily>;
+    using signal_type = SigH<void(Registry &, Entity)>;
     using traits_type = entt_traits<Entity>;
 
     template<typename... Component>
     static void creating(Registry &registry, Entity entity) {
-        if(registry.has<Component...>(entity)) {
-            const auto htype = handler_family::type<Component...>();
-            registry.handlers[htype]->construct(entity);
-        }
+        const auto ttype = handler_family::type<Component...>();
+        return registry.has<Component...>(entity) ? registry.handlers[ttype]->construct(entity) : void();
     }
 
     template<typename... Component>
     static void destroying(Registry &registry, Entity entity) {
-        const auto htype = handler_family::type<Component...>();
-        auto &handler = registry.handlers[htype];
-
-        if(handler->has(entity)) {
-            handler->destroy(entity);
-        }
+        auto &handler = registry.handlers[handler_family::type<Component...>()];
+        return handler->has(entity) ? handler->destroy(entity) : void();
     }
 
     struct Attachee {
@@ -76,7 +71,19 @@ class Registry {
     };
 
     template<typename Component>
-    SparseSet<Entity, Component> & assure() {
+    const SparseSet<Entity, Component> & pool() const noexcept {
+        const auto ctype = component_family::type<Component>();
+        assert(ctype < pools.size() && std::get<0>(pools[ctype]));
+        return static_cast<SparseSet<Entity, Component> &>(*std::get<0>(pools[ctype]));
+    }
+
+    template<typename Component>
+    SparseSet<Entity, Component> & pool() noexcept {
+        return const_cast<SparseSet<Entity, Component> &>(const_cast<const Registry *>(this)->pool<Component>());
+    }
+
+    template<typename Component>
+    void assure() {
         const auto ctype = component_family::type<Component>();
 
         if(!(ctype < pools.size())) {
@@ -88,8 +95,15 @@ class Registry {
         if(!cpool) {
             cpool = std::make_unique<SparseSet<Entity, Component>>();
         }
+    }
+
+    template<typename Tag>
+    void assure(tag_type_t) {
+        const auto ttype = tag_family::type<Tag>();
 
-        return static_cast<SparseSet<Entity, Component> &>(*cpool);
+        if(!(ttype < tags.size())) {
+            tags.resize(ttype + 1);
+        }
     }
 
 public:
@@ -104,7 +118,7 @@ public:
     /*! @brief Unsigned integer type. */
     using component_type = typename component_family::family_type;
     /*! @brief Type of sink for the given component. */
-    using sink_type = typename SigH<void(Registry &, Entity)>::sink_type;
+    using sink_type = typename signal_type::sink_type;
 
     /*! @brief Default constructor. */
     Registry() = default;
@@ -183,7 +197,8 @@ public:
      */
     template<typename Component>
     void reserve(size_type cap) {
-        assure<Component>().reserve(cap);
+        assure<Component>();
+        pool<Component>().reserve(cap);
     }
 
     /**
@@ -345,6 +360,25 @@ public:
      */
     void destroy(entity_type entity) {
         assert(valid(entity));
+
+        std::for_each(pools.begin(), pools.end(), [entity, this](auto &&tup) {
+            auto &cpool = std::get<0>(tup);
+
+            if(cpool && cpool->has(entity)) {
+                std::get<2>(tup).publish(*this, entity);
+                cpool->destroy(entity);
+            }
+        });
+
+        std::for_each(tags.begin(), tags.end(), [entity, this](auto &&tup) {
+            auto &tag = std::get<0>(tup);
+
+            if(tag && tag->entity == entity) {
+                std::get<2>(tup).publish(*this, entity);
+                tag.reset();
+            }
+        });
+
         const auto entt = entity & traits_type::entity_mask;
         const auto version = (((entity >> traits_type::entity_shift) + 1) & traits_type::version_mask) << traits_type::entity_shift;
         const auto node = (available ? next : ((entt + 1) & traits_type::entity_mask)) | version;
@@ -352,16 +386,6 @@ public:
         entities[entt] = node;
         next = entt;
         ++available;
-
-        // changing the number of pools during a destroy is supported now
-        for(std::size_t pos = pools.size(); pos; --pos) {
-            auto &cpool = std::get<0>(pools[pos-1]);
-
-            if(cpool && cpool->has(entity)) {
-                cpool->destroy(entity);
-                std::get<2>(pools[pos-1]).publish(*this, entity);
-            }
-        }
     }
 
     /**
@@ -389,15 +413,11 @@ public:
     Tag & assign(tag_type_t, entity_type entity, Args &&... args) {
         assert(valid(entity));
         assert(!has<Tag>());
-        const auto ttype = tag_family::type<Tag>();
-
-        if(!(ttype < tags.size())) {
-            tags.resize(ttype + 1);
-        }
-
-        tags[ttype].reset(new Attaching<Tag>{entity, Tag{std::forward<Args>(args)...}});
-
-        return static_cast<Attaching<Tag> *>(tags[ttype].get())->tag;
+        assure<Tag>(tag_type_t{});
+        auto &tup = tags[tag_family::type<Tag>()];
+        std::get<0>(tup).reset(new Attaching<Tag>{entity, Tag{std::forward<Args>(args)...}});
+        std::get<1>(tup).publish(*this, entity);
+        return get<Tag>();
     }
 
     /**
@@ -423,10 +443,10 @@ public:
     template<typename Component, typename... Args>
     Component & assign(entity_type entity, Args &&... args) {
         assert(valid(entity));
-        auto &pool = assure<Component>();
-        auto &component = pool.construct(entity, std::forward<Args>(args)...);
+        assure<Component>();
+        pool<Component>().construct(entity, std::forward<Args>(args)...);
         std::get<1>(pools[component_family::type<Component>()]).publish(*this, entity);
-        return component;
+        return pool<Component>().get(entity);
     }
 
     /**
@@ -436,7 +456,10 @@ public:
     template<typename Tag>
     void remove() {
         if(has<Tag>()) {
-            tags[tag_family::type<Tag>()].reset();
+            auto &tup = tags[tag_family::type<Tag>()];
+            auto &tag = std::get<0>(tup);
+            std::get<2>(tup).publish(*this, tag->entity);
+            tag.reset();
         }
     }
 
@@ -456,8 +479,10 @@ public:
     template<typename Component>
     void remove(entity_type entity) {
         assert(valid(entity));
-        assure<Component>().destroy(entity);
-        std::get<2>(pools[component_family::type<Component>()]).publish(*this, entity);
+        const auto ctype = component_family::type<Component>();
+        assert(ctype < pools.size() && std::get<0>(pools[ctype]));
+        std::get<2>(pools[ctype]).publish(*this, entity);
+        pool<Component>().destroy(entity);
     }
 
     /**
@@ -468,11 +493,15 @@ public:
     template<typename Tag>
     bool has() const noexcept {
         const auto ttype = tag_family::type<Tag>();
-        return (ttype < tags.size() &&
-                // it's a valid tag
-                tags[ttype] &&
-                // the associated entity hasn't been destroyed in the meantime
-                tags[ttype]->entity == (entities[tags[ttype]->entity & traits_type::entity_mask]));
+        bool found = false;
+
+        if(ttype < tags.size()) {
+            auto &tag = std::get<0>(tags[ttype]);
+            // it's a valid tag and the associated entity hasn't been destroyed in the meantime
+            found = tag && (tag->entity == (entities[tag->entity & traits_type::entity_mask]));
+        }
+
+        return found;
     }
 
     /**
@@ -513,7 +542,7 @@ public:
     template<typename Tag>
     const Tag & get() const noexcept {
         assert(has<Tag>());
-        return static_cast<Attaching<Tag> *>(tags[tag_family::type<Tag>()].get())->tag;
+        return static_cast<Attaching<Tag> *>(std::get<0>(tags[tag_family::type<Tag>()]).get())->tag;
     }
 
     /**
@@ -634,7 +663,7 @@ public:
      */
     template<typename Tag, typename... Args>
     Tag & replace(tag_type_t, Args &&... args) {
-        return get<Tag>() = Tag{std::forward<Args>(args)...};
+        return (get<Tag>() = Tag{std::forward<Args>(args)...});
     }
 
     /**
@@ -682,9 +711,9 @@ public:
     entity_type move(entity_type entity) {
         assert(valid(entity));
         assert(has<Tag>());
-        const auto ttype = tag_family::type<Tag>();
-        const auto owner = tags[ttype]->entity;
-        tags[ttype]->entity = entity;
+        auto &tag = std::get<0>(tags[tag_family::type<Tag>()]);
+        const auto owner = tag->entity;
+        tag->entity = entity;
         return owner;
     }
 
@@ -703,7 +732,7 @@ public:
     template<typename Tag>
     entity_type attachee() const noexcept {
         assert(has<Tag>());
-        return tags[tag_family::type<Tag>()]->entity;
+        return std::get<0>(tags[tag_family::type<Tag>()])->entity;
     }
 
     /**
@@ -737,6 +766,35 @@ public:
                 : assign<Component>(entity, std::forward<Args>(args)...));
     }
 
+    /**
+     * @brief Returns a sink object for the given tag.
+     *
+     * A sink is an opaque object used to connect listeners to tags.<br/>
+     * The sink returned by this function can be used to receive notifications
+     * whenever a new instance of the given tag is created and assigned to an
+     * entity.
+     *
+     * The function type for a listener is:
+     * @code{.cpp}
+     * void(Registry<Entity> &, Entity);
+     * @endcode
+     *
+     * Listeners are invoked **after** the tag has been assigned to the entity.
+     * The order of invocation of the listeners isn't guaranteed.<br/>
+     * Note also that the greater the number of listeners, the greater the
+     * performance hit when a new tag is created.
+     *
+     * @sa SigH::Sink
+     *
+     * @tparam Tag Type of tag of which to get the sink.
+     * @return A temporary sink object.
+     */
+    template<typename Tag>
+    sink_type construction(tag_type_t) noexcept {
+        assure<Tag>(tag_type_t{});
+        return std::get<1>(tags[tag_family::type<Tag>()]).sink();
+    }
+
     /**
      * @brief Returns a sink object for the given component.
      *
@@ -751,7 +809,9 @@ public:
      * @endcode
      *
      * Listeners are invoked **after** the component has been assigned to the
-     * entity. The order of invocation of the listeners isn't guaranteed.
+     * entity. The order of invocation of the listeners isn't guaranteed.<br/>
+     * Note also that the greater the number of listeners, the greater the
+     * performance hit when a new component is created.
      *
      * @sa SigH::Sink
      *
@@ -760,7 +820,37 @@ public:
      */
     template<typename Component>
     sink_type construction() noexcept {
-        return (assure<Component>(), std::get<1>(pools[component_family::type<Component>()]).sink());
+        assure<Component>();
+        return std::get<1>(pools[component_family::type<Component>()]).sink();
+    }
+
+    /**
+     * @brief Returns a sink object for the given tag.
+     *
+     * A sink is an opaque object used to connect listeners to tag.<br/>
+     * The sink returned by this function can be used to receive notifications
+     * whenever an instance of the given tag is removed from an entity and thus
+     * destroyed.
+     *
+     * The function type for a listener is:
+     * @code{.cpp}
+     * void(Registry<Entity> &, Entity);
+     * @endcode
+     *
+     * Listeners are invoked **before** the tag has been removed from the
+     * entity. The order of invocation of the listeners isn't guaranteed.<br/>
+     * Note also that the greater the number of listeners, the greater the
+     * performance hit when a tag is destroyed.
+     *
+     * @sa SigH::Sink
+     *
+     * @tparam Tag Type of tag of which to get the sink.
+     * @return A temporary sink object.
+     */
+    template<typename Tag>
+    sink_type destruction(tag_type_t) noexcept {
+        assure<Tag>(tag_type_t{});
+        return std::get<2>(tags[tag_family::type<Tag>()]).sink();
     }
 
     /**
@@ -777,7 +867,9 @@ public:
      * @endcode
      *
      * Listeners are invoked **before** the component has been removed from the
-     * entity. The order of invocation of the listeners isn't guaranteed.
+     * entity. The order of invocation of the listeners isn't guaranteed.<br/>
+     * Note also that the greater the number of listeners, the greater the
+     * performance hit when a component is destroyed.
      *
      * @sa SigH::Sink
      *
@@ -786,7 +878,8 @@ public:
      */
     template<typename Component>
     sink_type destruction() noexcept {
-        return (assure<Component>(), std::get<2>(pools[component_family::type<Component>()]).sink());
+        assure<Component>();
+        return std::get<2>(pools[component_family::type<Component>()]).sink();
     }
 
     /**
@@ -814,7 +907,8 @@ public:
      */
     template<typename Component, typename Compare>
     void sort(Compare compare) {
-        assure<Component>().sort(std::move(compare));
+        assure<Component>();
+        pool<Component>().sort(std::move(compare));
     }
 
     /**
@@ -849,7 +943,9 @@ public:
      */
     template<typename To, typename From>
     void sort() {
-        assure<To>().respect(assure<From>());
+        assure<To>();
+        assure<From>();
+        pool<To>().respect(pool<From>());
     }
 
     /**
@@ -869,11 +965,13 @@ public:
     template<typename Component>
     void reset(entity_type entity) {
         assert(valid(entity));
-        auto &cpool = assure<Component>();
+        assure<Component>();
+        const auto ctype = component_family::type<Component>();
+        auto &cpool = *std::get<0>(pools[ctype]);
 
         if(cpool.has(entity)) {
+            std::get<2>(pools[ctype]).publish(*this, entity);
             cpool.destroy(entity);
-            std::get<2>(pools[component_family::type<Component>()]).publish(*this, entity);
         }
     }
 
@@ -887,15 +985,15 @@ public:
      */
     template<typename Component>
     void reset() {
-        auto &cpool = assure<Component>();
-        auto &sig = std::get<2>(pools[component_family::type<Component>()]);
+        assure<Component>();
+        const auto ctype = component_family::type<Component>();
+        auto &cpool = *std::get<0>(pools[ctype]);
+        auto &sig = std::get<2>(pools[ctype]);
 
-        each([&cpool, &sig, this](auto entity) {
-            if(cpool.has(entity)) {
-                cpool.destroy(entity);
-                sig.publish(*this, entity);
-            }
-        });
+        for(const auto entity: cpool) {
+            sig.publish(*this, entity);
+            cpool.destroy(entity);
+        }
     }
 
     /**
@@ -970,7 +1068,7 @@ public:
         }
 
         for(std::size_t i = 0; i < tags.size() && orphan; ++i) {
-            const auto &tag = tags[i];
+            const auto &tag = std::get<0>(tags[i]);
             orphan = !(tag && (tag->entity == entity));
         }
 
@@ -1037,7 +1135,7 @@ public:
      */
     template<typename... Component>
     View<Entity, Component...> view() {
-        return View<Entity, Component...>{assure<Component>()...};
+        return View<Entity, Component...>{(assure<Component>(), pool<Component>())...};
     }
 
     /**
@@ -1090,7 +1188,7 @@ public:
      *
      * Persistent views occupy memory, no matter if they are in use or not.<br/>
      * This function can be used to discard all the internal data structures
-     * dedicated to a specific persisten view, with the goal of reducing the
+     * dedicated to a specific persistent view, with the goal of reducing the
      * memory pressure.
      *
      * @warning
@@ -1173,7 +1271,7 @@ public:
     PersistentView<Entity, Component...> persistent() {
         prepare<Component...>();
         const auto htype = handler_family::type<Component...>();
-        return PersistentView<Entity, Component...>{*handlers[htype], assure<Component>()...};
+        return PersistentView<Entity, Component...>{*handlers[htype], (assure<Component>(), pool<Component>())...};
     }
 
     /**
@@ -1200,7 +1298,8 @@ public:
      */
     template<typename Component>
     RawView<Entity, Component> raw() {
-        return RawView<Entity, Component>{assure<Component>()};
+        assure<Component>();
+        return RawView<Entity, Component>{pool<Component>()};
     }
 
     /**
@@ -1277,8 +1376,8 @@ public:
 
 private:
     std::vector<std::unique_ptr<SparseSet<Entity>>> handlers;
-    std::vector<std::tuple<std::unique_ptr<SparseSet<Entity>>, SigH<void(Registry &, Entity)>, SigH<void(Registry &, Entity)>>> pools;
-    std::vector<std::unique_ptr<Attachee>> tags;
+    std::vector<std::tuple<std::unique_ptr<SparseSet<Entity>>, signal_type, signal_type>> pools;
+    std::vector<std::tuple<std::unique_ptr<Attachee>, signal_type, signal_type>> tags;
     std::vector<entity_type> entities;
     size_type available{};
     entity_type next{};

+ 90 - 9
test/entt/entity/registry.cpp

@@ -6,13 +6,37 @@
 #include <entt/entity/entt_traits.hpp>
 #include <entt/entity/registry.hpp>
 
-struct ComponentListener {
-    void incr(entt::DefaultRegistry &, entt::DefaultRegistry::entity_type entity) {
+struct Listener {
+    template<typename Component>
+    void incrComponent(entt::DefaultRegistry &registry, entt::DefaultRegistry::entity_type entity) {
+        ASSERT_TRUE(registry.valid(entity));
+        ASSERT_TRUE(registry.has<Component>(entity));
         last = entity;
         ++counter;
     }
 
-    void decr(entt::DefaultRegistry &, entt::DefaultRegistry::entity_type entity) {
+    template<typename Tag>
+    void incrTag(entt::DefaultRegistry &registry, entt::DefaultRegistry::entity_type entity) {
+        ASSERT_TRUE(registry.valid(entity));
+        ASSERT_TRUE(registry.has<Tag>());
+        ASSERT_EQ(registry.attachee<Tag>(), entity);
+        last = entity;
+        ++counter;
+    }
+
+    template<typename Component>
+    void decrComponent(entt::DefaultRegistry &registry, entt::DefaultRegistry::entity_type entity) {
+        ASSERT_TRUE(registry.valid(entity));
+        ASSERT_TRUE(registry.has<Component>(entity));
+        last = entity;
+        --counter;
+    }
+
+    template<typename Tag>
+    void decrTag(entt::DefaultRegistry &registry, entt::DefaultRegistry::entity_type entity) {
+        ASSERT_TRUE(registry.valid(entity));
+        ASSERT_TRUE(registry.has<Tag>());
+        ASSERT_EQ(registry.attachee<Tag>(), entity);
         last = entity;
         --counter;
     }
@@ -568,12 +592,12 @@ TEST(DefaultRegistry, MergeTwoRegistries) {
     ne(dst.view<char, float, int>().begin(), dst.view<char, float, int>().end());
 }
 
-TEST(DefaultRegistry, Signals) {
+TEST(DefaultRegistry, ComponentSignals) {
     entt::DefaultRegistry registry;
-    ComponentListener listener;
+    Listener listener;
 
-    registry.construction<int>().connect<ComponentListener, &ComponentListener::incr>(&listener);
-    registry.destruction<int>().connect<ComponentListener, &ComponentListener::decr>(&listener);
+    registry.construction<int>().connect<Listener, &Listener::incrComponent<int>>(&listener);
+    registry.destruction<int>().connect<Listener, &Listener::decrComponent<int>>(&listener);
 
     auto e0 = registry.create();
     auto e1 = registry.create();
@@ -589,16 +613,73 @@ TEST(DefaultRegistry, Signals) {
     ASSERT_EQ(listener.counter, 1);
     ASSERT_EQ(listener.last, e0);
 
-    registry.destruction<int>().disconnect<ComponentListener, &ComponentListener::decr>(&listener);
+    registry.destruction<int>().disconnect<Listener, &Listener::decrComponent<int>>(&listener);
     registry.remove<int>(e1);
 
     ASSERT_EQ(listener.counter, 1);
     ASSERT_EQ(listener.last, e0);
 
-    registry.construction<int>().disconnect<ComponentListener, &ComponentListener::incr>(&listener);
+    registry.construction<int>().disconnect<Listener, &Listener::incrComponent<int>>(&listener);
+    registry.assign<int>(e1);
+
+    ASSERT_EQ(listener.counter, 1);
+    ASSERT_EQ(listener.last, e0);
+
+    registry.construction<int>().connect<Listener, &Listener::incrComponent<int>>(&listener);
+    registry.destruction<int>().connect<Listener, &Listener::decrComponent<int>>(&listener);
+    registry.assign<int>(e0);
+    registry.reset<int>(e1);
+
+    ASSERT_EQ(listener.counter, 1);
+    ASSERT_EQ(listener.last, e1);
+
+    registry.reset<int>();
+
+    ASSERT_EQ(listener.counter, 0);
+    ASSERT_EQ(listener.last, e0);
+
     registry.assign<int>(e0);
     registry.assign<int>(e1);
+    registry.destroy(e1);
 
     ASSERT_EQ(listener.counter, 1);
+    ASSERT_EQ(listener.last, e1);
+}
+
+TEST(DefaultRegistry, TagSignals) {
+    entt::DefaultRegistry registry;
+    Listener listener;
+
+    registry.construction<int>(entt::tag_type_t{}).connect<Listener, &Listener::incrTag<int>>(&listener);
+    registry.destruction<int>(entt::tag_type_t{}).connect<Listener, &Listener::decrTag<int>>(&listener);
+
+    auto e0 = registry.create();
+    registry.assign<int>(entt::tag_type_t{}, e0);
+
+    ASSERT_EQ(listener.counter, 1);
+    ASSERT_EQ(listener.last, e0);
+
+    auto e1 = registry.create();
+    registry.move<int>(e1);
+    registry.remove<int>();
+
+    ASSERT_EQ(listener.counter, 0);
+    ASSERT_EQ(listener.last, e1);
+
+    registry.construction<int>(entt::tag_type_t{}).disconnect<Listener, &Listener::incrTag<int>>(&listener);
+    registry.destruction<int>(entt::tag_type_t{}).disconnect<Listener, &Listener::decrTag<int>>(&listener);
+    registry.assign<int>(entt::tag_type_t{}, e0);
+    registry.remove<int>();
+
+    ASSERT_EQ(listener.counter, 0);
+    ASSERT_EQ(listener.last, e1);
+
+    registry.construction<int>(entt::tag_type_t{}).connect<Listener, &Listener::incrTag<int>>(&listener);
+    registry.destruction<int>(entt::tag_type_t{}).connect<Listener, &Listener::decrTag<int>>(&listener);
+
+    registry.assign<int>(entt::tag_type_t{}, e0);
+    registry.destroy(e0);
+
+    ASSERT_EQ(listener.counter, 0);
     ASSERT_EQ(listener.last, e0);
 }