From c0213e84f622653d069a106549e603d280bb680e Mon Sep 17 00:00:00 2001 From: Michele Caini Date: Sun, 15 Apr 2018 23:05:11 +0200 Subject: [PATCH] signals on tags --- README.md | 130 +++++++++++------- TODO | 2 + src/entt/entity/registry.hpp | 245 ++++++++++++++++++++++++---------- test/entt/entity/registry.cpp | 101 ++++++++++++-- 4 files changed, 346 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 5961cbe55..381c0de9c 100644 --- a/README.md +++ b/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 tup = registry.get(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.
-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().connect<&MyFreeFunction>(); - -// connects a member function -registry.construction().connect(&instance); - -// disconnects a free function -registry.construction().disconnect<&MyFreeFunction>(); - -// disconnects a member function -registry.construction().disconnect(&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); -``` - -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.
-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(); 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.
+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().connect<&MyFreeFunction>(); + +// connects a member function +registry.construction().connect(&instance); + +// disconnects a free function +registry.construction().disconnect<&MyFreeFunction>(); + +// disconnects a member function +registry.construction().disconnect(&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); +``` + +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.
+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(entt::tag_type_t{}).connect<&MyFreeFunction>(); +registry.destruction(entt::tag_type_t{}).connect(&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. diff --git a/TODO b/TODO index 42dcde4ba..c30195ef3 100644 --- a/TODO +++ b/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 diff --git a/src/entt/entity/registry.hpp b/src/entt/entity/registry.hpp index 4a6fb949e..1ea89cbc6 100644 --- a/src/entt/entity/registry.hpp +++ b/src/entt/entity/registry.hpp @@ -38,24 +38,19 @@ class Registry { using tag_family = Family; using component_family = Family; using handler_family = Family; + using signal_type = SigH; using traits_type = entt_traits; template static void creating(Registry ®istry, Entity entity) { - if(registry.has(entity)) { - const auto htype = handler_family::type(); - registry.handlers[htype]->construct(entity); - } + const auto ttype = handler_family::type(); + return registry.has(entity) ? registry.handlers[ttype]->construct(entity) : void(); } template static void destroying(Registry ®istry, Entity entity) { - const auto htype = handler_family::type(); - auto &handler = registry.handlers[htype]; - - if(handler->has(entity)) { - handler->destroy(entity); - } + auto &handler = registry.handlers[handler_family::type()]; + return handler->has(entity) ? handler->destroy(entity) : void(); } struct Attachee { @@ -76,7 +71,19 @@ class Registry { }; template - SparseSet & assure() { + const SparseSet & pool() const noexcept { + const auto ctype = component_family::type(); + assert(ctype < pools.size() && std::get<0>(pools[ctype])); + return static_cast &>(*std::get<0>(pools[ctype])); + } + + template + SparseSet & pool() noexcept { + return const_cast &>(const_cast(this)->pool()); + } + + template + void assure() { const auto ctype = component_family::type(); if(!(ctype < pools.size())) { @@ -88,8 +95,15 @@ class Registry { if(!cpool) { cpool = std::make_unique>(); } + } - return static_cast &>(*cpool); + template + void assure(tag_type_t) { + const auto ttype = tag_family::type(); + + 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::sink_type; + using sink_type = typename signal_type::sink_type; /*! @brief Default constructor. */ Registry() = default; @@ -183,7 +197,8 @@ public: */ template void reserve(size_type cap) { - assure().reserve(cap); + assure(); + pool().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()); - const auto ttype = tag_family::type(); - - if(!(ttype < tags.size())) { - tags.resize(ttype + 1); - } - - tags[ttype].reset(new Attaching{entity, Tag{std::forward(args)...}}); - - return static_cast *>(tags[ttype].get())->tag; + assure(tag_type_t{}); + auto &tup = tags[tag_family::type()]; + std::get<0>(tup).reset(new Attaching{entity, Tag{std::forward(args)...}}); + std::get<1>(tup).publish(*this, entity); + return get(); } /** @@ -423,10 +443,10 @@ public: template Component & assign(entity_type entity, Args &&... args) { assert(valid(entity)); - auto &pool = assure(); - auto &component = pool.construct(entity, std::forward(args)...); + assure(); + pool().construct(entity, std::forward(args)...); std::get<1>(pools[component_family::type()]).publish(*this, entity); - return component; + return pool().get(entity); } /** @@ -436,7 +456,10 @@ public: template void remove() { if(has()) { - tags[tag_family::type()].reset(); + auto &tup = tags[tag_family::type()]; + auto &tag = std::get<0>(tup); + std::get<2>(tup).publish(*this, tag->entity); + tag.reset(); } } @@ -456,8 +479,10 @@ public: template void remove(entity_type entity) { assert(valid(entity)); - assure().destroy(entity); - std::get<2>(pools[component_family::type()]).publish(*this, entity); + const auto ctype = component_family::type(); + assert(ctype < pools.size() && std::get<0>(pools[ctype])); + std::get<2>(pools[ctype]).publish(*this, entity); + pool().destroy(entity); } /** @@ -468,11 +493,15 @@ public: template bool has() const noexcept { const auto ttype = tag_family::type(); - 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 const Tag & get() const noexcept { assert(has()); - return static_cast *>(tags[tag_family::type()].get())->tag; + return static_cast *>(std::get<0>(tags[tag_family::type()]).get())->tag; } /** @@ -634,7 +663,7 @@ public: */ template Tag & replace(tag_type_t, Args &&... args) { - return get() = Tag{std::forward(args)...}; + return (get() = Tag{std::forward(args)...}); } /** @@ -682,9 +711,9 @@ public: entity_type move(entity_type entity) { assert(valid(entity)); assert(has()); - const auto ttype = tag_family::type(); - const auto owner = tags[ttype]->entity; - tags[ttype]->entity = entity; + auto &tag = std::get<0>(tags[tag_family::type()]); + const auto owner = tag->entity; + tag->entity = entity; return owner; } @@ -703,7 +732,7 @@ public: template entity_type attachee() const noexcept { assert(has()); - return tags[tag_family::type()]->entity; + return std::get<0>(tags[tag_family::type()])->entity; } /** @@ -737,6 +766,35 @@ public: : assign(entity, std::forward(args)...)); } + /** + * @brief Returns a sink object for the given tag. + * + * A sink is an opaque object used to connect listeners to tags.
+ * 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); + * @endcode + * + * Listeners are invoked **after** the tag has been assigned to the entity. + * The order of invocation of the listeners isn't guaranteed.
+ * 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 + sink_type construction(tag_type_t) noexcept { + assure(tag_type_t{}); + return std::get<1>(tags[tag_family::type()]).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.
+ * 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 sink_type construction() noexcept { - return (assure(), std::get<1>(pools[component_family::type()]).sink()); + assure(); + return std::get<1>(pools[component_family::type()]).sink(); + } + + /** + * @brief Returns a sink object for the given tag. + * + * A sink is an opaque object used to connect listeners to tag.
+ * 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); + * @endcode + * + * Listeners are invoked **before** the tag has been removed from the + * entity. The order of invocation of the listeners isn't guaranteed.
+ * 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 + sink_type destruction(tag_type_t) noexcept { + assure(tag_type_t{}); + return std::get<2>(tags[tag_family::type()]).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.
+ * 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 sink_type destruction() noexcept { - return (assure(), std::get<2>(pools[component_family::type()]).sink()); + assure(); + return std::get<2>(pools[component_family::type()]).sink(); } /** @@ -814,7 +907,8 @@ public: */ template void sort(Compare compare) { - assure().sort(std::move(compare)); + assure(); + pool().sort(std::move(compare)); } /** @@ -849,7 +943,9 @@ public: */ template void sort() { - assure().respect(assure()); + assure(); + assure(); + pool().respect(pool()); } /** @@ -869,11 +965,13 @@ public: template void reset(entity_type entity) { assert(valid(entity)); - auto &cpool = assure(); + assure(); + const auto ctype = component_family::type(); + 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()]).publish(*this, entity); } } @@ -887,15 +985,15 @@ public: */ template void reset() { - auto &cpool = assure(); - auto &sig = std::get<2>(pools[component_family::type()]); + assure(); + const auto ctype = component_family::type(); + 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 View view() { - return View{assure()...}; + return View{(assure(), pool())...}; } /** @@ -1090,7 +1188,7 @@ public: * * Persistent views occupy memory, no matter if they are in use or not.
* 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 persistent() { prepare(); const auto htype = handler_family::type(); - return PersistentView{*handlers[htype], assure()...}; + return PersistentView{*handlers[htype], (assure(), pool())...}; } /** @@ -1200,7 +1298,8 @@ public: */ template RawView raw() { - return RawView{assure()}; + assure(); + return RawView{pool()}; } /** @@ -1277,8 +1376,8 @@ public: private: std::vector>> handlers; - std::vector>, SigH, SigH>> pools; - std::vector> tags; + std::vector>, signal_type, signal_type>> pools; + std::vector, signal_type, signal_type>> tags; std::vector entities; size_type available{}; entity_type next{}; diff --git a/test/entt/entity/registry.cpp b/test/entt/entity/registry.cpp index 97102fff6..4c9b131c3 100644 --- a/test/entt/entity/registry.cpp +++ b/test/entt/entity/registry.cpp @@ -6,13 +6,37 @@ #include #include -struct ComponentListener { - void incr(entt::DefaultRegistry &, entt::DefaultRegistry::entity_type entity) { +struct Listener { + template + void incrComponent(entt::DefaultRegistry ®istry, entt::DefaultRegistry::entity_type entity) { + ASSERT_TRUE(registry.valid(entity)); + ASSERT_TRUE(registry.has(entity)); last = entity; ++counter; } - void decr(entt::DefaultRegistry &, entt::DefaultRegistry::entity_type entity) { + template + void incrTag(entt::DefaultRegistry ®istry, entt::DefaultRegistry::entity_type entity) { + ASSERT_TRUE(registry.valid(entity)); + ASSERT_TRUE(registry.has()); + ASSERT_EQ(registry.attachee(), entity); + last = entity; + ++counter; + } + + template + void decrComponent(entt::DefaultRegistry ®istry, entt::DefaultRegistry::entity_type entity) { + ASSERT_TRUE(registry.valid(entity)); + ASSERT_TRUE(registry.has(entity)); + last = entity; + --counter; + } + + template + void decrTag(entt::DefaultRegistry ®istry, entt::DefaultRegistry::entity_type entity) { + ASSERT_TRUE(registry.valid(entity)); + ASSERT_TRUE(registry.has()); + ASSERT_EQ(registry.attachee(), entity); last = entity; --counter; } @@ -568,12 +592,12 @@ TEST(DefaultRegistry, MergeTwoRegistries) { ne(dst.view().begin(), dst.view().end()); } -TEST(DefaultRegistry, Signals) { +TEST(DefaultRegistry, ComponentSignals) { entt::DefaultRegistry registry; - ComponentListener listener; + Listener listener; - registry.construction().connect(&listener); - registry.destruction().connect(&listener); + registry.construction().connect>(&listener); + registry.destruction().connect>(&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().disconnect(&listener); + registry.destruction().disconnect>(&listener); registry.remove(e1); ASSERT_EQ(listener.counter, 1); ASSERT_EQ(listener.last, e0); - registry.construction().disconnect(&listener); - registry.assign(e0); + registry.construction().disconnect>(&listener); registry.assign(e1); ASSERT_EQ(listener.counter, 1); ASSERT_EQ(listener.last, e0); + + registry.construction().connect>(&listener); + registry.destruction().connect>(&listener); + registry.assign(e0); + registry.reset(e1); + + ASSERT_EQ(listener.counter, 1); + ASSERT_EQ(listener.last, e1); + + registry.reset(); + + ASSERT_EQ(listener.counter, 0); + ASSERT_EQ(listener.last, e0); + + registry.assign(e0); + registry.assign(e1); + registry.destroy(e1); + + ASSERT_EQ(listener.counter, 1); + ASSERT_EQ(listener.last, e1); +} + +TEST(DefaultRegistry, TagSignals) { + entt::DefaultRegistry registry; + Listener listener; + + registry.construction(entt::tag_type_t{}).connect>(&listener); + registry.destruction(entt::tag_type_t{}).connect>(&listener); + + auto e0 = registry.create(); + registry.assign(entt::tag_type_t{}, e0); + + ASSERT_EQ(listener.counter, 1); + ASSERT_EQ(listener.last, e0); + + auto e1 = registry.create(); + registry.move(e1); + registry.remove(); + + ASSERT_EQ(listener.counter, 0); + ASSERT_EQ(listener.last, e1); + + registry.construction(entt::tag_type_t{}).disconnect>(&listener); + registry.destruction(entt::tag_type_t{}).disconnect>(&listener); + registry.assign(entt::tag_type_t{}, e0); + registry.remove(); + + ASSERT_EQ(listener.counter, 0); + ASSERT_EQ(listener.last, e1); + + registry.construction(entt::tag_type_t{}).connect>(&listener); + registry.destruction(entt::tag_type_t{}).connect>(&listener); + + registry.assign(entt::tag_type_t{}, e0); + registry.destroy(e0); + + ASSERT_EQ(listener.counter, 0); + ASSERT_EQ(listener.last, e0); }