batch add is now available

This commit is contained in:
Michele Caini
2019-03-06 16:17:14 +01:00
parent cb93a3bee3
commit 9810da6982
7 changed files with 400 additions and 44 deletions

1
TODO
View File

@@ -17,7 +17,6 @@
* allow some features by component type (eg registry.assign(entity, component);
- it could be possible for eg default constructible types by storing aside (pool data) erased functions
- does it worth it?
* add and bulk add with components (sort of registry.create<A, B>(first, last) and registry.create<A, B>())
* events on replace, so that one can track updated components? indagate impact
- define basic reactive systems (track entities to which component is attached, track entities from which component is removed, and so on)
- define systems as composable mixins (initializazion, reactive, update, whatever) with flexible auto-detected arguments (registry, views, etc)

View File

@@ -6,9 +6,10 @@
# Table of Contents
* [Introduction](#introduction)
* [Design choices](#design-choices)
* [Design decisions](#design-decisions)
* [A bitset-free entity-component system](#a-bitset-free-entity-component-system)
* [Pay per use](#pay-per-use)
* [All or nothing](#all-or-nothing)
* [Vademecum](#vademecum)
* [The Registry, the Entity and the Component](#the-registry-the-entity-and-the-component)
* [Observe changes](#observe-changes)
@@ -49,7 +50,7 @@ more) written in modern C++.<br/>
The entity-component-system (also known as _ECS_) is an architectural pattern
used mostly in game development.
# Design choices
# Design decisions
## A bitset-free entity-component system
@@ -90,6 +91,22 @@ performance along critical paths is high.
So far, this choice has proven to be a good one and I really hope it can be for
many others besides me.
## All or nothing
`EnTT` is such that at every moment a pair `(T *, size)` is available to
directly access all the instances of a given component type `T`.<br/>
This was a guideline and a design decision that influenced many choices, for
better and for worse. I cannot say whether it will be useful or not to the
reader, but it's worth to mention it, because it's of the corner stones of this
library.
Many of the tools described below, from the registry to the views and up to the
groups give the possibility to get this information and have been designed
around this need, which was and remains one of my main requirements during the
development.<br/>
The rest is experimentation and the desire to invent something new, hoping to
have succeeded.
# Vademecum
The registry to store, the views and the groups to iterate. That's all.
@@ -129,7 +146,7 @@ Entities are represented by _entity identifiers_. An entity identifier is an
opaque type that users should not inspect or modify in any way. It carries
information about the entity itself and its version.
A registry can be used both to construct and destroy entities:
A registry can be used both to construct and to destroy entities:
```cpp
// constructs a naked entity with no components and returns its identifier
@@ -139,9 +156,9 @@ auto entity = registry.create();
registry.destroy(entity);
```
There exist also overloads of the `create` and `destroy` member functions that
accept two iterators, that is a range to assign or to destroy. It can be used to
create or destroy multiple entities at once:
There exists also an overload of the `create` and `destroy` member functions
that accepts two iterators, that is a range to assign or to destroy. It can be
used to create or destroy multiple entities at once:
```cpp
// destroys all the entities in a range
@@ -149,6 +166,11 @@ auto view = registry.view<a_component, another_component>();
registry.destroy(view.begin(), view.end());
```
In both cases, the `create` member function accepts also a list of default
constructible types of components to assign to the entities before to return.
It's a faster alternative to the creation and subsequent assignment of
components in separate steps.
When an entity is destroyed, the registry can freely reuse it internally with a
slightly different identifier. In particular, the version of an entity is
increased each and every time it's discarded.<br/>

View File

@@ -63,6 +63,9 @@ class registry {
using signal_type = sigh<void(registry &, const Entity)>;
using traits_type = entt_traits<Entity>;
template<typename Component>
using pool_type = sparse_set<Entity, std::decay_t<Component>>;
template<typename, typename>
struct non_owning_group;
@@ -136,6 +139,16 @@ class registry {
std::size_t extent;
};
void release(const Entity entity) {
// lengthens the implicit list of destroyed entities
const auto entt = entity & traits_type::entity_mask;
const auto version = ((entity >> traits_type::entity_shift) + 1) << traits_type::entity_shift;
const auto node = (available ? next : ((entt + 1) & traits_type::entity_mask)) | version;
entities[entt] = node;
next = entt;
++available;
}
template<typename Component>
inline auto pool() const ENTT_NOEXCEPT {
const auto ctype = type<Component>();
@@ -146,10 +159,10 @@ class registry {
});
assert(it != pools.cend() && it->pool);
return std::make_tuple(&*it, static_cast<sparse_set<Entity, std::decay_t<Component>> *>(it->pool.get()));
return std::make_tuple(&*it, static_cast<pool_type<Component> *>(it->pool.get()));
} else {
assert(ctype < pools.size() && pools[ctype].pool && pools[ctype].runtime_type == ctype);
return std::make_tuple(&pools[ctype], static_cast<sparse_set<Entity, std::decay_t<Component>> *>(pools[ctype].pool.get()));
return std::make_tuple(&pools[ctype], static_cast<pool_type<Component> *>(pools[ctype].pool.get()));
}
}
@@ -179,11 +192,11 @@ class registry {
}
if(!pdata->pool) {
pdata->pool = std::make_unique<sparse_set<Entity, std::decay_t<Component>>>();
pdata->pool = std::make_unique<pool_type<Component>>();
pdata->runtime_type = ctype;
}
return std::make_tuple(pdata, static_cast<sparse_set<Entity, std::decay_t<Component>> *>(pdata->pool.get()));
return std::make_tuple(pdata, static_cast<pool_type<Component> *>(pdata->pool.get()));
}
public:
@@ -328,7 +341,7 @@ public:
* There are no guarantees on the order of the components. Use a view if you
* want to iterate entities and components in the expected order.
*
* @warning
* @note
* Empty components aren't explicitly instantiated. Therefore, this function
* always returns `nullptr` for them.
*
@@ -452,11 +465,18 @@ public:
* function can be used to know if they are still valid or the entity has
* been destroyed and potentially recycled.
*
* The returned entity has no components assigned.
* The returned entity has assigned the given components, if any. The
* components must be at least default constructible. A compilation error
* will occur otherwhise.
*
* @return A valid entity identifier.
* @tparam Component Types of components to assign to the entity.
* @return A valid entity identifier if the component list is empty, a tuple
* containing the entity identifier and the references to the components
* just created otherwise.
*/
entity_type create() {
template<typename... Component>
std::conditional_t<sizeof...(Component) == 0, entity_type, std::tuple<entity_type, Component &...>>
create() {
entity_type entity;
if(available) {
@@ -472,7 +492,11 @@ public:
assert(entity < traits_type::entity_mask);
}
return entity;
if constexpr(sizeof...(Component) == 0) {
return entity;
} else {
return { entity, assign<Component>(entity)... };
}
}
/**
@@ -488,30 +512,64 @@ public:
* function can be used to know if they are still valid or the entity has
* been destroyed and potentially recycled.
*
* The generated entities have no components assigned.
* The entities so generated have assigned the given components, if any. The
* components must be at least default constructible. 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.
* @param last An iterator past the last element of the range to generate.
* @return No return value if the component list is empty, a tuple
* containing the pointers to the arrays of components just created and
* sorted the same of the entities otherwise.
*/
template<typename It>
void create(It first, It last) {
template<typename... Component, typename It>
std::conditional_t<sizeof...(Component) == 0, void, std::tuple<Component *...>>
create(It first, It last) {
static_assert(std::is_convertible_v<entity_type, typename std::iterator_traits<It>::value_type>);
const auto length = size_type(std::distance(first, last));
const auto sz = std::min(available, length);
[[maybe_unused]] entity_type candidate{};
available -= sz;
std::generate_n(first, sz, [this]() {
const auto tail = std::generate_n(first, sz, [&candidate, this]() mutable {
if constexpr(sizeof...(Component) > 0) {
candidate = std::max(candidate, next);
} else {
// suppress warnings
(void)candidate;
}
const auto entt = next;
const auto version = entities[entt] & (traits_type::version_mask << traits_type::entity_shift);
next = entities[entt] & traits_type::entity_mask;
return (entities[entt] = entt | version);
});
std::generate_n((first + sz), (length - sz), [this]() {
std::generate(tail, last, [this]() {
return entities.emplace_back(entity_type(entities.size()));
});
if constexpr(sizeof...(Component) > 0) {
const auto hint = size_type(std::max(candidate, *(last-1)))+1;
auto generator = [first, last, hint, this](auto &&adata) {
auto *comp = std::get<1>(adata)->construct(first, last, hint);
auto *pdata = std::get<0>(adata);
if(!pdata->construction.empty()) {
std::for_each(first, last, [pdata, this](const auto entity) {
pdata->construction.publish(*this, entity);
});
}
return comp;
};
return { generator(assure<Component>())... };
}
}
/**
@@ -550,14 +608,7 @@ public:
// just a way to protect users from listeners that attach components
assert(orphan(entity));
// lengthens the implicit list of destroyed entities
const auto entt = entity & traits_type::entity_mask;
const auto version = ((entity >> traits_type::entity_shift) + 1) << traits_type::entity_shift;
const auto node = (available ? next : ((entt + 1) & traits_type::entity_mask)) | version;
entities[entt] = node;
next = entt;
++available;
release(entity);
}
/**
@@ -568,8 +619,26 @@ public:
*/
template<typename It>
void destroy(It first, It last) {
assert(std::all_of(first, last, [this](const auto entity) { return valid(entity); }));
for(auto pos = pools.size(); pos; --pos) {
auto &pdata = pools[pos-1];
if(pdata.pool) {
std::for_each(first, last, [&pdata, this](const auto entity) {
if(pdata.pool->has(entity)) {
pdata.destruction.publish(*this, entity);
pdata.pool->destroy(entity);
}
});
}
};
// just a way to protect users from listeners that attach components
assert(std::all_of(first, last, [this](const auto entity) { return orphan(entity); }));
std::for_each(first, last, [this](const auto entity) {
destroy(entity);
release(entity);
});
}
@@ -1433,7 +1502,7 @@ public:
* more instances of this class in sync, as an example in a client-server
* architecture.
*
* @warning
* @note
* The loader returned by this function requires that the registry be empty.
* In case it isn't, all the data will be automatically deleted before to
* return.

View File

@@ -370,6 +370,41 @@ public:
direct.push_back(entity);
}
/**
* @brief Assigns one or more entities to a sparse set.
*
* This function requires to use a hint value for performance purposes.<br/>
* Its value indicates the size necessary to accommodate the largest entity
* if used as an index of a hypothetical array.
*
* @warning
* Attempting to assign an entity that already belongs to the sparse set
* results in undefined behavior.<br/>
* An assertion will abort the execution at runtime in debug mode if the
* sparse set already contains the given entity.
*
* @tparam It Type of forward iterator.
* @param first An iterator to the first element of the range of entities.
* @param last An iterator past the last element of the range of entities.
* @param hint Hint value to avoid searching for the largest entity.
*/
template<typename It>
void construct(It first, It last, size_type hint) {
if(hint > reverse.size()) {
// null is safe in all cases for our purposes
reverse.resize(hint, null);
}
std::for_each(first, last, [next = entity_type(direct.size()), this](const auto entity) mutable {
assert(!has(entity));
const auto pos = size_type(entity & traits_type::entity_mask);
assert(pos < reverse.size());
reverse[pos] = next++;
});
direct.insert(direct.end(), first, last);
}
/**
* @brief Removes an entity from a sparse set.
*
@@ -750,7 +785,7 @@ public:
* performance boost but less guarantees. Use `begin` and `end` if you want
* to iterate the sparse set in the expected order.
*
* @warning
* @note
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
@@ -873,7 +908,6 @@ public:
/**
* @brief Assigns an entity to a sparse set and constructs its object.
*
* @note
* This version accept both types that can be constructed in place directly
* and types like aggregates that do not work well with a placement new as
* performed usually under the hood during an _emplace back_.
@@ -907,6 +941,50 @@ public:
}
}
/**
* @brief Assigns one or more entities to a sparse set and constructs their
* objects.
*
* This function requires to use a hint value for performance purposes.<br/>
* Its value indicates the size necessary to accommodate the largest entity
* if used as an index of a hypothetical array.
*
* @note
* The object type must be at least default constructible.
*
* @note
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
*
* @warning
* Attempting to assign an entity that already belongs to the sparse set
* results in undefined behavior.<br/>
* An assertion will abort the execution at runtime in debug mode if the
* sparse set already contains the given entity.
*
* @tparam It Type of forward iterator.
* @param first An iterator to the first element of the range of entities.
* @param last An iterator past the last element of the range of entities.
* @param hint Hint value to avoid searching for the largest entity.
* @return A pointer to the array of instances just created and sorted the
* same of the entities.
*/
template<typename It>
object_type * construct(It first, It last, const size_type hint) {
if constexpr(std::is_empty_v<object_type>) {
underlying_type::construct(first, last, hint);
return &instances;
} else {
static_assert(std::is_default_constructible_v<object_type>);
const auto offset = instances.size();
instances.insert(instances.end(), last-first, {});
// entity goes after component in case constructor throws
underlying_type::construct(first, last, hint);
return instances.data() + offset;
}
}
/**
* @brief Removes an entity from a sparse set and destroies its object.
*
@@ -960,14 +1038,14 @@ public:
* this member function.
*
* @note
* Empty components aren't explicitly instantiated. Therefore, this function
* isn't available for them.
*
* @note
* Attempting to iterate elements using a raw pointer returned by a call to
* either `data` or `raw` gives no guarantees on the order, even though
* `sort` has been invoked.
*
* @warning
* Empty components aren't explicitly instantiated. Therefore, this function
* isn't available for them.
*
* @tparam Compare Type of comparison function object.
* @tparam Sort Type of sort function object.
* @tparam Args Types of arguments to forward to the sort function object.

View File

@@ -56,6 +56,35 @@ TEST(Benchmark, ConstructMany) {
timer.elapsed();
}
TEST(Benchmark, ConstructManyAndAssignComponents) {
entt::registry<> registry;
std::vector<entt::registry<>::entity_type> entities(1000000);
std::cout << "Constructing 1000000 entities at once and assign components" << std::endl;
timer timer;
registry.create(entities.begin(), entities.end());
for(const auto entity: entities) {
registry.assign<position>(entity);
registry.assign<velocity>(entity);
}
timer.elapsed();
}
TEST(Benchmark, ConstructManyWithComponents) {
entt::registry<> registry;
std::vector<entt::registry<>::entity_type> entities(1000000);
std::cout << "Constructing 1000000 entities at once with components" << std::endl;
timer timer;
registry.create<position, velocity>(entities.begin(), entities.end());
timer.elapsed();
}
TEST(Benchmark, Destroy) {
entt::registry<> registry;

View File

@@ -938,6 +938,81 @@ TEST(Registry, CreateManyEntitiesAtOnce) {
ASSERT_EQ(registry.version(entities[2]), entt::registry<>::version_type{0});
}
TEST(Registry, CreateAnEntityWithComponents) {
entt::registry<> registry;
const auto &[entity, ivalue, cvalue] = registry.create<int, char>();
ASSERT_FALSE(registry.empty<int>());
ASSERT_FALSE(registry.empty<char>());
ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{1});
ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{1});
ASSERT_TRUE((registry.has<int, char>(entity)));
ivalue = 42;
cvalue = 'c';
ASSERT_EQ(registry.get<int>(entity), 42);
ASSERT_EQ(registry.get<char>(entity), 'c');
}
TEST(Registry, CreateManyEntitiesWithComponentsAtOnce) {
entt::registry<> registry;
entt::registry<>::entity_type entities[3];
const auto entity = registry.create();
registry.destroy(registry.create());
registry.destroy(entity);
registry.destroy(registry.create());
const auto [iptr, cptr] = registry.create<int, char>(std::begin(entities), std::end(entities));
ASSERT_FALSE(registry.empty<int>());
ASSERT_FALSE(registry.empty<char>());
ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{3});
ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{3});
ASSERT_TRUE(registry.valid(entities[0]));
ASSERT_TRUE(registry.valid(entities[1]));
ASSERT_TRUE(registry.valid(entities[2]));
ASSERT_EQ(registry.entity(entities[0]), entt::registry<>::entity_type{0});
ASSERT_EQ(registry.version(entities[0]), entt::registry<>::version_type{2});
ASSERT_EQ(registry.entity(entities[1]), entt::registry<>::entity_type{1});
ASSERT_EQ(registry.version(entities[1]), entt::registry<>::version_type{1});
ASSERT_EQ(registry.entity(entities[2]), entt::registry<>::entity_type{2});
ASSERT_EQ(registry.version(entities[2]), entt::registry<>::version_type{0});
ASSERT_TRUE((registry.has<int, char>(entities[0])));
ASSERT_TRUE((registry.has<int, char>(entities[1])));
ASSERT_TRUE((registry.has<int, char>(entities[2])));
for(auto i = 0; i < 3; ++i) {
iptr[i] = i;
cptr[i] = char('a'+i);
}
for(auto i = 0; i < 3; ++i) {
ASSERT_EQ(registry.get<int>(entities[i]), i);
ASSERT_EQ(registry.get<char>(entities[i]), char('a'+i));
}
}
TEST(Registry, CreateManyEntitiesWithComponentsAtOnceWithListener) {
entt::registry<> registry;
entt::registry<>::entity_type entities[3];
listener listener;
registry.construction<int>().connect<&listener::incr<int>>(&listener);
registry.create<int, char>(std::begin(entities), std::end(entities));
ASSERT_EQ(listener.counter, 3);
}
TEST(Registry, NonOwningGroupInterleaved) {
entt::registry<> registry;
typename entt::registry<>::entity_type entity = entt::null;

View File

@@ -1,10 +1,13 @@
#include <memory>
#include <iterator>
#include <exception>
#include <algorithm>
#include <unordered_set>
#include <gtest/gtest.h>
#include <entt/entity/sparse_set.hpp>
struct empty_type {};
TEST(SparseSetNoType, Functionalities) {
entt::sparse_set<std::uint64_t> set;
@@ -58,6 +61,32 @@ TEST(SparseSetNoType, Functionalities) {
other = std::move(set);
}
TEST(SparseSetNoType, ConstructMany) {
entt::sparse_set<std::uint64_t> set;
entt::sparse_set<std::uint64_t>::entity_type entities[2];
entities[0] = 3;
entities[1] = 42;
set.construct(12);
set.construct(std::begin(entities), std::end(entities), 43);
set.construct(24);
ASSERT_TRUE(set.has(entities[0]));
ASSERT_TRUE(set.has(entities[1]));
ASSERT_FALSE(set.has(0));
ASSERT_FALSE(set.has(9));
ASSERT_TRUE(set.has(12));
ASSERT_TRUE(set.has(24));
ASSERT_FALSE(set.empty());
ASSERT_EQ(set.size(), 4u);
ASSERT_EQ(set.get(12), 0u);
ASSERT_EQ(set.get(entities[0]), 1u);
ASSERT_EQ(set.get(entities[1]), 2u);
ASSERT_EQ(set.get(24), 3u);
}
TEST(SparseSetNoType, Iterator) {
using iterator_type = typename entt::sparse_set<std::uint64_t>::iterator_type;
@@ -397,8 +426,7 @@ TEST(SparseSetWithType, Functionalities) {
other = std::move(set);
}
TEST(SparseSetWithType, FunctionalitiesEmptyType) {
struct empty_type {};
TEST(SparseSetWithType, EmptyType) {
entt::sparse_set<std::uint64_t, empty_type> set;
ASSERT_EQ(&set.construct(42), &set.construct(99));
@@ -407,6 +435,66 @@ TEST(SparseSetWithType, FunctionalitiesEmptyType) {
ASSERT_EQ(std::as_const(set).try_get(42), std::as_const(set).try_get(99));
}
TEST(SparseSetWithType, ConstructMany) {
entt::sparse_set<std::uint64_t, int> set;
entt::sparse_set<std::uint64_t>::entity_type entities[2];
entities[0] = 3;
entities[1] = 42;
set.reserve(4);
set.construct(12, 21);
auto *component = set.construct(std::begin(entities), std::end(entities), 43);
set.construct(24, 42);
ASSERT_TRUE(set.has(entities[0]));
ASSERT_TRUE(set.has(entities[1]));
ASSERT_FALSE(set.has(0));
ASSERT_FALSE(set.has(9));
ASSERT_TRUE(set.has(12));
ASSERT_TRUE(set.has(24));
ASSERT_FALSE(set.empty());
ASSERT_EQ(set.size(), 4u);
ASSERT_EQ(set.get(12), 21);
ASSERT_EQ(set.get(entities[0]), 0);
ASSERT_EQ(set.get(entities[1]), 0);
ASSERT_EQ(set.get(24), 42);
component[0] = 1;
component[1] = 2;
ASSERT_EQ(set.get(entities[0]), 1);
ASSERT_EQ(set.get(entities[1]), 2);
}
TEST(SparseSetWithType, ConstructManyEmptyType) {
entt::sparse_set<std::uint64_t, empty_type> set;
entt::sparse_set<std::uint64_t>::entity_type entities[2];
entities[0] = 3;
entities[1] = 42;
set.reserve(4);
set.construct(12);
auto *component = set.construct(std::begin(entities), std::end(entities), 43);
set.construct(24);
ASSERT_TRUE(set.has(entities[0]));
ASSERT_TRUE(set.has(entities[1]));
ASSERT_FALSE(set.has(0));
ASSERT_FALSE(set.has(9));
ASSERT_TRUE(set.has(12));
ASSERT_TRUE(set.has(24));
ASSERT_FALSE(set.empty());
ASSERT_EQ(set.size(), 4u);
ASSERT_EQ(&set.get(entities[0]), &set.get(entities[1]));
ASSERT_EQ(&set.get(entities[0]), &set.get(12));
ASSERT_EQ(&set.get(entities[0]), &set.get(24));
ASSERT_EQ(&set.get(entities[0]), component);
}
TEST(SparseSetWithType, AggregatesMustWork) {
struct aggregate_type { int value; };
// the goal of this test is to enforce the requirements for aggregate types
@@ -509,7 +597,6 @@ TEST(SparseSetWithType, ConstIterator) {
}
TEST(SparseSetWithType, IteratorEmptyType) {
struct empty_type {};
using iterator_type = typename entt::sparse_set<std::uint64_t, empty_type>::iterator_type;
entt::sparse_set<std::uint64_t, empty_type> set;
set.construct(3);
@@ -557,7 +644,6 @@ TEST(SparseSetWithType, IteratorEmptyType) {
}
TEST(SparseSetWithType, ConstIteratorEmptyType) {
struct empty_type {};
using iterator_type = typename entt::sparse_set<std::uint64_t, empty_type>::const_iterator_type;
entt::sparse_set<std::uint64_t, empty_type> set;
set.construct(3);
@@ -621,7 +707,6 @@ TEST(SparseSetWithType, Raw) {
}
TEST(SparseSetWithType, RawEmptyType) {
struct empty_type {};
entt::sparse_set<std::uint64_t, empty_type> set;
set.construct(3);
@@ -933,7 +1018,6 @@ TEST(SparseSetWithType, RespectUnordered) {
}
TEST(SparseSetWithType, RespectOverlapEmptyType) {
struct empty_type {};
entt::sparse_set<std::uint64_t, empty_type> lhs;
entt::sparse_set<std::uint64_t, empty_type> rhs;
@@ -1050,7 +1134,7 @@ TEST(SparseSetWithType, ConstructorExceptionDoesNotAddToSet) {
struct throwing_component {
struct constructor_exception: std::exception {};
throwing_component() { throw constructor_exception{}; }
[[noreturn]] throwing_component() { throw constructor_exception{}; }
// necessary to avoid the short-circuit construct() logic for empty objects
int data;