signals on tags

This commit is contained in:
Michele Caini
2018-04-15 23:05:11 +02:00
parent 29de6d89d4
commit c0213e84f6
4 changed files with 346 additions and 132 deletions

130
README.md
View File

@@ -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
TODO
View File

@@ -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

View File

@@ -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>>();
}
}
return static_cast<SparseSet<Entity, Component> &>(*cpool);
template<typename Tag>
void assure(tag_type_t) {
const auto ttype = tag_family::type<Tag>();
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{};

View File

@@ -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.assign<int>(e0);
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);
}