registry: stomp and spawn

This commit is contained in:
Michele Caini
2019-08-22 18:00:14 +02:00
parent 94136531a5
commit 83f9d42d31
5 changed files with 261 additions and 99 deletions

5
TODO
View File

@@ -28,5 +28,6 @@ TODO
* make meta work across boundaries
- inline variables are fine here, only the head represents a problem
- we should always resolve by looking into the list of types when working across boundaries, no direct resolve
* review stomp signature: first, last, other, from <-- the registry "acquires" the other entity
* create from prototype (literally a stomp with batch creation)
* nested groups: AB/ABC/ABCD/... (hints: sort, check functions)
* use make_tuple and reference_wrapper instead of explicing tuple<T &>
* improve multi-stomp so as to reuse the known pool and the known element

View File

@@ -20,7 +20,7 @@
* [Sorting: is it possible?](#sorting-is-it-possible)
* [Helpers](#helpers)
* [Null entity](#null-entity)
* [Multiple registries](#multiple-registries)
* [Stomp and spawn](#stomp-and-spawn)
* [Dependencies](#dependencies)
* [Tags](#tags)
* [Actor](#actor)
@@ -44,6 +44,7 @@
* [Empty type optimization](#empty-type-optimization)
* [Multithreading](#multithreading)
* [Iterators](#iterators)
* [Beyond this document](#beyond-this-document)
<!--
@endcond TURN_OFF_DOXYGEN
-->
@@ -666,26 +667,29 @@ const auto entity = registry.create();
const bool null = (entity == entt::null);
```
### Multiple registries
### Stomp and spawn
The use of multiple registries is quite common. Examples of use are the
separation of the UI from the simulation or the loading of different scenes in
the background, possibly on a separate thread, without having to keep track of
which entity belongs to which scene.<br/>
In fact, with `EnTT` this is even a recommended practice, as the registry is
nothing more than a container and different optimizations can be applied to
different containers.
nothing more than a container and different optimizations and strategies can be
applied to different containers.
Once there are multiple registries available, however, a method is needed to
transfer information from one container to another and this results in the
`stomp` member function of the `registry` class.<br/>
This function allows to take one or more entities from a registry and use them
to _stomp_ other entities in another registry (or even the same, actually making
local copies).<br/>
It opens definitely the doors to a lot of interesting features like migrating
entities between registries, prototypes, shadow registry, prefabs, shared
components without an explicit owner and copy-on-write policies among the other
things.
Once there are multiple registries available, however, one or more methods are
needed to transfer information from one container to another and this results in
the `stomp` member function and a couple of overloads of the `create` member
function for the `registry` class .<br/>
The `stomp` function allows to take one entity from a registry and use it to
_stomp_ one or more entities in another registry (or even the same, actually
making local copies). On the other hand, the overloads of the `create` member
function can be used to spawn new entities from a prototype.
These features open definitely the doors to a lot of interesting features like
migrating entities between registries, prototypes, shadow registry, prefabs,
shared components without an explicit owner and copy-on-write policies among the
other things.
### Dependencies
@@ -1672,3 +1676,14 @@ This may change in the future and the iterators will almost certainly return
both the entities and a list of references to their components sooner or later.
Multi-pass guarantee won't break in any case and the performance should even
benefit from it further.
# Beyond this document
There are many other features and functions not listed in this document.<br/>
`EnTT` and in particular its ECS part is in continuous development and some
things could be forgotten, others could have been omitted on purpose to reduce
the size of this file. Unfortunately, some parts may even be outdated and still
to be updated.
For further information, it's recommended to refer to the documentation included
in the code itself or join the official channels to ask a question.

View File

@@ -85,9 +85,9 @@ class basic_registry {
}
}
template<typename It>
auto batch(basic_registry &registry, It first, It last) {
auto it = storage<Entity, Component>::batch(first, last);
template<typename It, typename... Comp>
auto batch(basic_registry &registry, It first, It last, const Comp &... value) {
auto it = storage<Entity, Component>::batch(first, last, value...);
if(!construction.empty()) {
std::for_each(first, last, [this, &registry, it](const auto entt) mutable {
@@ -543,11 +543,11 @@ public:
* Users should not care about the type of the returned entity identifier.
* In case entity identifers are stored around, the `valid` member
* function can be used to know if they are still valid or the entity has
* been destroyed and potentially recycled.
* been destroyed and potentially recycled.<br/>
* The returned entity has assigned the given components, if any.
*
* The returned entity has assigned the given components, if any. The
* components must be at least default constructible. A compilation error
* will occur otherwhise.
* The components must be at least default constructible. A compilation
* error will occur otherwhise.
*
* @tparam Component Types of components to assign to the entity.
* @return A valid entity identifier if the component list is empty, a tuple
@@ -556,24 +556,14 @@ public:
*/
template<typename... Component>
auto create() {
entity_type entity;
if(destroyed == null) {
entity = entities.emplace_back(entity_type(entities.size()));
// traits_type::entity_mask is reserved to allow for null identifiers
ENTT_ASSERT(to_integer(entity) < traits_type::entity_mask);
} else {
const auto entt = to_integer(destroyed);
const auto version = to_integer(entities[entt]) & (traits_type::version_mask << traits_type::entity_shift);
destroyed = entity_type{to_integer(entities[entt]) & traits_type::entity_mask};
entity = entity_type{entt | version};
entities[entt] = entity;
}
entity_type entities[1]{};
if constexpr(sizeof...(Component) == 0) {
return entity;
create<Component...>(std::begin(entities), std::end(entities));
return entities[0];
} else {
return std::tuple<entity_type, decltype(assign<Component>(entity))...>{entity, assign<Component>(entity)...};
auto it = create<Component...>(std::begin(entities), std::end(entities));
return std::tuple<entity_type, decltype(assign<Component>(entities[0]))...>{entities[0], *std::get<typename pool_type<Component>::iterator_type>(it)...};
}
}
@@ -582,6 +572,9 @@ public:
*
* @sa create
*
* The components must be at least move and default insertable. A
* compilation error will occur otherwhise.
*
* @tparam Component Types of components to assign to the entity.
* @tparam It Type of forward iterator.
* @param first An iterator to the first element of the range to generate.
@@ -597,13 +590,16 @@ public:
std::generate(first, last, [this]() {
entity_type curr;
if(destroyed != null) {
if(destroyed == null) {
curr = entities.emplace_back(entity_type(entities.size()));
// traits_type::entity_mask is reserved to allow for null identifiers
ENTT_ASSERT(to_integer(curr) < traits_type::entity_mask);
} else {
const auto entt = to_integer(destroyed);
const auto version = to_integer(entities[entt]) & (traits_type::version_mask << traits_type::entity_shift);
destroyed = entity_type{to_integer(entities[entt]) & traits_type::entity_mask};
curr = (entities[entt] = entity_type{entt | version});
} else {
curr = entities.emplace_back(entity_type(entities.size()));
curr = entity_type{entt | version};
entities[entt] = curr;
}
return curr;
@@ -614,6 +610,48 @@ public:
}
}
/**
* @brief Creates a new entity from a prototype entity.
*
* @sa create
*
* @tparam Component Types of components to copy.
* @tparam Exclude Types of components not to be copied.
* @param src A valid entity identifier to be copied.
* @param other The registry that owns the source entity.
* @return A valid entity identifier.
*/
template<typename... Component, typename... Exclude>
auto create(entity_type src, basic_registry &other, exclude_t<Exclude...> = {}) {
entity_type entities[1]{};
create<Component...>(std::begin(entities), std::end(entities), src, other, exclude<Exclude...>);
return entities[0];
}
/**
* @brief Assigns each element in a range an entity from a prototype entity.
*
* @sa create
*
* @tparam Component Types of components to copy.
* @tparam Exclude Types of components not to be copied.
* @param first An iterator to the first element of the range to generate.
* @param last An iterator past the last element of the range to generate.
* @param src A valid entity identifier to be copied.
* @param other The registry that owns the source entity.
*/
template<typename... Component, typename It, typename... Exclude>
void create(It first, It last, entity_type src, basic_registry &other, exclude_t<Exclude...> = {}) {
create(first, last);
if constexpr(sizeof...(Component) == 0) {
stomp<Component...>(first, last, src, other, exclude<Exclude...>);
} else {
static_assert(sizeof...(Component) == 0 || sizeof...(Exclude) == 0);
(assure<Component>()->batch(*this, first, last, other.get<Component>(src)), ...);
}
}
/**
* @brief Destroys an entity and lets the registry recycle the identifier.
*
@@ -1528,16 +1566,15 @@ public:
}
/**
* @brief Makes a full or partial copy of an entity.
* @brief Stomps an entity and its components.
*
* The components must be copyable for obvious reasons. The entities
* must be both valid.<br/>
* If no components are provided, the registry will try to copy all the
* existing types. The non-copyable ones will be ignored.
*
* This feature supports exclusion lists. The excluded types have higher
* priority than those indicated for copying. An excluded type will never be
* copied.
* This feature supports exclusion lists as an alternative to component
* lists. An excluded type will never be copied.
*
* @warning
* Attempting to copy components that aren't copyable results in unexpected
@@ -1554,25 +1591,45 @@ public:
*
* @tparam Component Types of components to copy.
* @tparam Exclude Types of components not to be copied.
* @param from A valid entity identifier to be copied.
* @param other The registry that owns the target entity.
* @param to A valid entity identifier to copy to.
* @param dst A valid entity identifier to copy to.
* @param src A valid entity identifier to be copied.
* @param other The registry that owns the source entity.
*/
template<typename... Component, typename... Exclude>
void stomp(const Entity from, basic_registry &other, const Entity to, exclude_t<Exclude...> = {}) {
static_assert(std::conjunction_v<std::is_copy_constructible<Component>...>);
ENTT_ASSERT(valid(from) && other.valid(to));
void stomp(const entity_type dst, const entity_type src, basic_registry &other, exclude_t<Exclude...> = {}) {
const entity_type entities[1]{dst};
stomp<Component...>(std::begin(entities), std::end(entities), src, other, exclude<Exclude...>);
}
for(auto pos = pools.size(); pos; --pos) {
const auto &pdata = pools[pos-1];
/**
* @brief Stomps the entities in a range and their components.
*
* @sa stomp
*
* @tparam Component Types of components to copy.
* @tparam Exclude Types of components not to be copied.
* @param first An iterator to the first element of the range to stomp.
* @param last An iterator past the last element of the range to stomp.
* @param src A valid entity identifier to be copied.
* @param other The registry that owns the source entity.
*/
template<typename... Component, typename It, typename... Exclude>
void stomp(It first, It last, const entity_type src, basic_registry &other, exclude_t<Exclude...> = {}) {
static_assert(sizeof...(Component) == 0 || sizeof...(Exclude) == 0);
static_assert(std::conjunction_v<std::is_copy_constructible<Component>...>);
for(auto pos = other.pools.size(); pos; --pos) {
const auto &pdata = other.pools[pos-1];
ENTT_ASSERT(!sizeof...(Component) || !pdata.pool || pdata.stomp);
if(pdata.pool && pdata.stomp
&& (!sizeof...(Component) || ... || (pdata.runtime_type == to_integer(type<Component>())))
&& ((pdata.runtime_type != to_integer(type<Exclude>())) && ...)
&& pdata.pool->has(from))
&& pdata.pool->has(src))
{
pdata.stomp(*pdata.pool, from, other, to);
std::for_each(first, last, [this, &pdata, src](const auto entity) {
pdata.stomp(*pdata.pool, src, *this, entity);
});
}
}
}

View File

@@ -351,8 +351,7 @@ public:
*/
template<typename It>
iterator_type batch(It first, It last) {
const auto length = last - first;
instances.resize(instances.size() + length);
instances.resize(instances.size() + std::distance(first, last));
// entity goes after component in case constructor throws
underlying_type::batch(first, last);
return begin();
@@ -375,8 +374,7 @@ public:
*/
template<typename It>
iterator_type batch(It first, It last, const object_type &value) {
const auto length = last - first;
instances.resize(instances.size() + length, value);
instances.resize(instances.size() + std::distance(first, last), value);
// entity goes after component in case constructor throws
underlying_type::batch(first, last);
return begin();

View File

@@ -1187,6 +1187,82 @@ TEST(Registry, CreateManyEntitiesWithComponentsAtOnceWithListener) {
ASSERT_EQ(listener.counter, 6);
}
TEST(Registry, CreateFromPrototype) {
entt::registry registry;
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
const auto full = registry.create(prototype, registry);
ASSERT_TRUE((registry.has<int, char>(full)));
ASSERT_EQ(registry.get<int>(full), 3);
ASSERT_EQ(registry.get<char>(full), 'c');
const auto partial = registry.create<int>(prototype, registry);
ASSERT_TRUE(registry.has<int>(partial));
ASSERT_FALSE(registry.has<char>(partial));
ASSERT_EQ(registry.get<int>(partial), 3);
const auto exclude = registry.create(prototype, registry, entt::exclude<int>);
ASSERT_FALSE(registry.has<int>(exclude));
ASSERT_TRUE(registry.has<char>(exclude));
ASSERT_EQ(registry.get<char>(exclude), 'c');
}
TEST(Registry, CreateManyFromPrototype) {
entt::registry registry;
entt::entity entities[2];
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
registry.create(std::begin(entities), std::end(entities), prototype, registry);
ASSERT_TRUE((registry.has<int, char>(entities[0])));
ASSERT_TRUE((registry.has<int, char>(entities[1])));
ASSERT_EQ(registry.get<int>(entities[0]), 3);
ASSERT_EQ(registry.get<char>(entities[1]), 'c');
registry.create<int>(std::begin(entities), std::end(entities), prototype, registry);
ASSERT_TRUE(registry.has<int>(entities[0]));
ASSERT_FALSE(registry.has<char>(entities[1]));
ASSERT_EQ(registry.get<int>(entities[0]), 3);
registry.create(std::begin(entities), std::end(entities), prototype, registry, entt::exclude<int>);
ASSERT_FALSE(registry.has<int>(entities[0]));
ASSERT_TRUE(registry.has<char>(entities[1]));
ASSERT_EQ(registry.get<char>(entities[0]), 'c');
}
TEST(Registry, CreateFromPrototypeWithListener) {
entt::registry registry;
entt::entity entities[3];
listener listener;
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
registry.assign<empty_type>(prototype);
registry.on_construct<int>().connect<&listener::incr<int>>(listener);
registry.create<int, char>(std::begin(entities), std::end(entities), prototype, registry);
ASSERT_EQ(listener.counter, 3);
registry.on_construct<int>().disconnect<&listener::incr<int>>(listener);
registry.on_construct<empty_type>().connect<&listener::incr<empty_type>>(listener);
registry.create<char, empty_type>(std::begin(entities), std::end(entities), prototype, registry);
ASSERT_EQ(listener.counter, 6);
}
TEST(Registry, NonOwningGroupInterleaved) {
entt::registry registry;
typename entt::entity entity = entt::null;
@@ -1416,67 +1492,82 @@ TEST(Registry, CloneMoveOnlyComponent) {
TEST(Registry, Stomp) {
entt::registry registry;
const auto entity = registry.create();
registry.assign<int>(entity, 3);
registry.assign<char>(entity, 'c');
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
auto other = registry.create();
registry.stomp<int, char, double>(entity, registry, other);
auto entity = registry.create();
registry.stomp<int, char, double>(entity, prototype, registry);
ASSERT_TRUE(registry.has<int>(other));
ASSERT_TRUE(registry.has<char>(other));
ASSERT_EQ(registry.get<int>(other), 3);
ASSERT_EQ(registry.get<char>(other), 'c');
ASSERT_TRUE((registry.has<int, char>(entity)));
ASSERT_EQ(registry.get<int>(entity), 3);
ASSERT_EQ(registry.get<char>(entity), 'c');
registry.replace<int>(entity, 42);
registry.replace<char>(entity, 'a');
registry.stomp<int>(entity, registry, other);
registry.replace<int>(prototype, 42);
registry.replace<char>(prototype, 'a');
registry.stomp<int>(entity, prototype, registry);
ASSERT_EQ(registry.get<int>(other), 42);
ASSERT_EQ(registry.get<char>(other), 'c');
ASSERT_EQ(registry.get<int>(entity), 42);
ASSERT_EQ(registry.get<char>(entity), 'c');
}
TEST(Registry, StompExclude) {
entt::registry registry;
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
const auto entity = registry.create();
registry.assign<int>(entity, 3);
registry.assign<char>(entity, 'c');
registry.stomp(entity, prototype, registry, entt::exclude<char>);
const auto other = registry.create();
registry.stomp<int, char>(entity, registry, other, entt::exclude<char>);
ASSERT_TRUE(registry.has<int>(entity));
ASSERT_FALSE(registry.has<char>(entity));
ASSERT_EQ(registry.get<int>(entity), 3);
ASSERT_TRUE(registry.has<int>(other));
ASSERT_FALSE(registry.has<char>(other));
ASSERT_EQ(registry.get<int>(other), 3);
registry.replace<int>(prototype, 42);
registry.stomp(entity, prototype, registry, entt::exclude<int>);
registry.replace<int>(entity, 42);
registry.stomp(entity, registry, other, entt::exclude<int>);
ASSERT_TRUE((registry.has<int, char>(entity)));
ASSERT_EQ(registry.get<int>(entity), 3);
ASSERT_EQ(registry.get<char>(entity), 'c');
ASSERT_TRUE(registry.has<int>(other));
ASSERT_TRUE(registry.has<char>(other));
ASSERT_EQ(registry.get<int>(other), 3);
ASSERT_EQ(registry.get<char>(other), 'c');
registry.remove<int>(entity);
registry.remove<char>(entity);
registry.stomp(entity, prototype, registry, entt::exclude<int, char>);
registry.remove<int>(other);
registry.remove<char>(other);
registry.stomp(entity, registry, other, entt::exclude<int, char>);
ASSERT_TRUE(registry.orphan(entity));
}
ASSERT_TRUE(registry.orphan(other));
TEST(Registry, StompMulti) {
entt::registry registry;
const auto prototype = registry.create();
registry.assign<int>(prototype, 3);
registry.assign<char>(prototype, 'c');
entt::entity entities[2];
registry.create(std::begin(entities), std::end(entities));
registry.stomp(std::begin(entities), std::end(entities), prototype, registry);
ASSERT_TRUE((registry.has<int, char>(entities[0])));
ASSERT_TRUE((registry.has<int, char>(entities[1])));
ASSERT_EQ(registry.get<int>(entities[0]), 3);
ASSERT_EQ(registry.get<char>(entities[1]), 'c');
}
TEST(Registry, StompMoveOnlyComponent) {
entt::registry registry;
const auto prototype = registry.create();
registry.assign<std::unique_ptr<int>>(prototype);
registry.assign<char>(prototype);
const auto entity = registry.create();
registry.stomp(entity, prototype, registry);
registry.assign<std::unique_ptr<int>>(entity);
registry.assign<char>(entity);
const auto other = registry.create();
registry.stomp(entity, registry, other);
ASSERT_TRUE(registry.has<char>(other));
ASSERT_FALSE(registry.has<std::unique_ptr<int>>(other));
ASSERT_TRUE(registry.has<char>(entity));
ASSERT_FALSE(registry.has<std::unique_ptr<int>>(entity));
}
TEST(Registry, GetOrAssign) {