removed spaces: not satisfied with the current implementation

This commit is contained in:
Michele Caini
2018-03-14 08:38:20 +01:00
parent 7a3e881099
commit 041e31ea78
6 changed files with 0 additions and 688 deletions

114
README.md
View File

@@ -25,7 +25,6 @@
* [Multi component standard view](#multi-component-standard-view)
* [Persistent View](#persistent-view)
* [Give me everything](#give-me-everything)
* [Spaces](#spaces)
* [Side notes](#side-notes)
* [Crash Course: core functionalities](#crash-course-core-functionalities)
* [Compile-time identifiers](#compile-time-identifiers)
@@ -947,119 +946,6 @@ In general, all these functions can result in poor performance.<br/>
entity. For similar reasons, `orphans` can be even slower. Both functions should
not be used frequently to avoid the risk of a performance hit.
## Spaces
Spaces are sort of partitions of a registry. They can be used to easily get a
subset of the entities of a view or a registry without recurring to multiple
registries to separate them explicitly.<br/>
To learn more about their intended use,
[here](https://gamedevelopment.tutsplus.com/tutorials/spaces-useful-game-object-containers--gamedev-14091)
is an interesting article that goes deep into the topic.
Spaces aren't for free. In most of the cases, the cost isn't relevant. However,
keep in mind that they add an extra check during iterations and it could slow
down a bit the whole thing.<br/>
Alternatives to spaces exist, but they have their own problems:
* Multiple registries: memory usage tends to grow up and some tasks are just
more difficult to accomplish (as an example, putting an entity logically in
more than one registry requires syncing them and it can quickly become a
problem).
* Dedicated components: memory usage tends to grow up and the number of spaces
is fixed and defined at compile-time (at least, it ought to be for performance
reasons), moreover the solution is much more error-prone.
Another benefit of spaces defined as an external class is that users of a space
do not have access to the whole registry, thus separation of responsibility is
automatically enforced. In both the alternatives described above, systems have
access to the whole set of entities instead and can easily break the contract
with the callers.
The `EnTT` framework offers support to spaces out of the box. Spaces are
constructed using a registry to which they refer:
```cpp
entt::DefaultRegistry registry;
entt::Space<typename entt::DefaultRegistry::entity_type> space{registry};
```
They offer the classical set of member functions to know the estimated number of
entities and to check if a space has a given entity.<br/>
Refer to the [official documentation](https://skypjack.github.io/entt/) for all
the details.
In addition, they expose two member functions to create an entity through a
space or to assign to a space an already existent entity, other than member
functions to remove entities from a space:
```cpp
// creates an entity using a space
auto entity = space.create();
// assigns an already existent entity to a space
space.assign(registry.create());
// removes an entity from the given space
space.remove(entity);
// removes all the entities from a space
space.reset();
```
Entities returned through the `create` member function are created directly into
the underlying registry and assigned immediately to the space.<br/>
Removing an entity from a space doesn't mean that it's destroyed within the
underlying registry in any case.
Spaces and thus the entities they contain can be easily iterated in a range-for
loop:
```cpp
for(auto entity: space) {
// ...
}
```
However, this isn't the best way to iterate entities in a space, mainly because
this member function returns all the entities it contains, no matter what are
their components. To iterate entities that have specific components, spaces
expose two dedicated member functions that differ for the view they use under
the hood:
```cpp
// uses a standard view internally
space.view<AComponent, AnotherComponent>([](auto entity, auto &aComponent, auto &anotherComponent) {
// ...
});
// uses a persistent view internally
space.persisten<AComponent, AnotherComponent>([](auto entity, auto &aComponent, auto &anotherComponent) {
// ...
});
```
Spaces get rid of entities that are no longer in use during iterations. They
aren't kept in sync with a registry each and every time an entity is destroyed
so as to avoid penalties in terms of performance. Instead, spaces remove invalid
entities as soon as they are detected during iterations.
Because of the _lazy clean_ policy, the size of a space could grow up if
destroyed entities are never detected for some reasons. To avoid it, spaces has
a member function named `shrink` that forces a clean up and reduce the size to a
minimum:
```cpp
// gets rid of all the invalid entities still tracked by a space
space.shrink();
```
Note that the size of a space isn't a problem in terms of performance. Views
rule during iterations, mainly because of the order which may have been imposed
by the user for some reasons and must be respected. Therefore unused entities
are never visited and thus they don't affect iterations. However, memory usage
can be reduced by shrinking spaces every so often.
## Side notes
* Entity identifiers are numbers and nothing more. They are not classes and they

View File

@@ -1,268 +0,0 @@
#ifndef ENTT_ENTITY_SPACE_HPP
#define ENTT_ENTITY_SPACE_HPP
#include <utility>
#include "sparse_set.hpp"
#include "registry.hpp"
namespace entt {
/**
* @brief A space is a sort of partition of a registry.
*
* Spaces can be used to create partitions of a registry. They can be useful for
* logically separating menus, world and any other type of scene, while still
* using only one registry.<br/>
* Similar results are obtained either using multiple registries or using
* dedicated components, even though in both cases the memory usage isn't the
* same. On the other side, spaces can introduce performance hits that are
* sometimes unacceptable (mainly if you are working on AAA games or kind of).
*
* For more details about spaces and their use, take a look at this article:
* https://gamedevelopment.tutsplus.com/tutorials/spaces-useful-game-object-containers--gamedev-14091
*
* @tparam Entity A valid entity type (see entt_traits for more details).
*/
template<typename Entity>
class Space: private SparseSet<Entity> {
using view_type = SparseSet<Entity>;
template<typename View, typename Func>
inline void each(View view, Func func) {
// use the view to iterate so as to respect order of components if any
view.each([func = std::move(func), this](auto entity, auto &&... components) {
if(this->has(entity)) {
if(this->data()[this->get(entity)] == entity) {
func(entity, std::forward<decltype(components)>(components)...);
} else {
// lazy destroy to avoid keeping a space in sync
this->destroy(entity);
}
}
});
}
public:
/*! @brief Type of registry to which the space refers. */
using registry_type = Registry<Entity>;
/*! @brief Underlying entity identifier. */
using entity_type = typename registry_type::entity_type;
/*! @brief Input iterator type. */
using iterator_type = typename view_type::iterator_type;
/*! @brief Unsigned integer type. */
using size_type = typename view_type::size_type;
/**
* @brief Constructs a space by using the given registry.
* @param registry An entity-component system properly initialized.
*/
Space(registry_type &registry)
: registry{registry}
{}
/**
* @brief Returns the number of entities tracked by a space.
* @return Number of entities tracked by the space.
*/
size_type size() const noexcept {
return SparseSet<Entity>::size();
}
/**
* @brief Checks if there exists at least an entity tracked by a space.
* @return True if the space tracks at least an entity, false otherwise.
*/
bool empty() const noexcept {
return SparseSet<Entity>::empty();
}
/**
* @brief Returns an iterator to the first entity tracked by a space.
*
* The returned iterator points to the first entity tracked by the space. If
* the space is empty, the returned iterator will be equal to `end()`.
*
* @return An iterator to the first entity tracked by a space.
*/
iterator_type begin() const noexcept {
return SparseSet<Entity>::begin();
}
/**
* @brief Returns an iterator that is past the last entity tracked by a
* space.
*
* The returned iterator points to the entity following the last entity
* tracked by the space. Attempting to dereference the returned iterator
* results in undefined behavior.
*
* @return An iterator to the entity following the last entity tracked by a
* space.
*/
iterator_type end() const noexcept {
return SparseSet<Entity>::end();
}
/**
* @brief Checks if a space contains an entity.
* @param entity A valid entity identifier.
* @return True if the space contains the given entity, false otherwise.
*/
bool contains(entity_type entity) const noexcept {
return this->has(entity) && this->data()[this->get(entity)] == entity;
}
/**
* @brief Creates a new entity and returns it.
*
* The space creates an entity from the underlying registry and registers it
* immediately before to return the identifier. Use the `assign` member
* function to register an already existent entity created at a different
* time.
*
* The returned entity has no components assigned.
*
* @return A valid entity identifier.
*/
entity_type create() {
const auto entity = registry.create();
assign(entity);
return entity;
}
/**
* @brief Assigns an entity to a space.
*
* The space starts tracking the given entity and will return it during
* iterations whenever required.<br/>
* Entities can be assigned to more than one space at the same time.
*
* @warning
* Attempting to use an invalid entity or to assign an entity that doesn't
* belong to the underlying registry results in undefined behavior.<br/>
* An assertion will abort the execution at runtime in debug mode in case of
* invalid entity or if the registry doesn't own the entity.
*
* @param entity A valid entity identifier.
*/
void assign(entity_type entity) {
assert(registry.valid(entity));
if(this->has(entity)) {
this->destroy(entity);
}
this->construct(entity);
}
/**
* @brief Removes an entity from a space.
*
* The space stops tracking the given entity and won't return it anymore
* during iterations.<br/>
* In case the entity belongs to more than one space, it won't be removed
* automatically from all the other ones as a consequence of invoking this
* function.
*
* @param entity A valid entity identifier.
*/
void remove(entity_type entity) {
if(this->has(entity)) {
this->destroy(entity);
}
}
/**
* @brief Iterates entities using a standard view under the hood.
*
* A space does not return directly views to iterate entities because it
* requires to apply a filter to those sets. Instead, it uses a view
* internally and returns only those entities that are tracked by the space
* itself.<br/>
* This member function can be used to iterate a space by means of a
* standard view. Naming the function the same as the type of view used to
* perform the task proved to be a good choice so as not to tricky users.
*
* @note
* Performance tend to degenerate when the number of components to iterate
* grows up and the most of the entities have all the given components.<br/>
* To get a performance boost, consider using the `persistent` member
* function instead.
*
* @tparam Component Type of components used to construct the view.
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename... Component, typename Func>
void view(Func func) {
each(registry.template view<Component...>(), std::move(func));
}
/**
* @brief Iterates entities using a persistent view under the hood.
*
* A space does not return directly views to iterate entities because it
* requires to apply a filter to those sets. Instead, it uses a view
* internally and returns only those entities that are tracked by the space
* itself.<br/>
* This member function can be used to iterate a space by means of a
* persistent view. Naming the function the same as the type of view used to
* perform the task proved to be a good choice so as not to tricky users.
*
* @tparam Component Type of components used to construct the view.
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename... Component, typename Func>
void persistent(Func func) {
each(registry.template persistent<Component...>(), std::move(func));
}
/**
* @brief Performs a clean up step.
*
* Spaces do a lazy cleanup during iterations to avoid introducing
* performance hits when entities are destroyed.<br/>
* This function can be used to force a clean up step and to get rid of all
* those entities that are still tracked by a space but have been destroyed
* in the underlying registry.
*/
void shrink() {
for(auto entity: *this) {
if(!registry.fast(entity)) {
this->destroy(entity);
}
}
}
/**
* @brief Resets a whole space.
*
* The space stops tracking all the entities assigned to it so far. After
* calling this function, iterations won't return any entity.
*/
void reset() {
SparseSet<Entity>::reset();
}
private:
Registry<Entity> &registry;
};
/**
* @brief Default space class.
*
* The default space is the best choice for almost all the applications.<br/>
* Users should have a really good reason to choose something different.
*/
using DefaultSpace = Space<DefaultRegistry::entity_type>;
}
#endif // ENTT_ENTITY_SPACE_HPP

View File

@@ -4,7 +4,6 @@
#include "entity/actor.hpp"
#include "entity/entt_traits.hpp"
#include "entity/registry.hpp"
#include "entity/space.hpp"
#include "entity/sparse_set.hpp"
#include "entity/view.hpp"
#include "locator/locator.hpp"

View File

@@ -55,7 +55,6 @@ add_executable(
$<TARGET_OBJECTS:odr>
entt/entity/actor.cpp
entt/entity/registry.cpp
entt/entity/space.cpp
entt/entity/sparse_set.cpp
entt/entity/view.cpp
)

View File

@@ -4,7 +4,6 @@
#include <chrono>
#include <gtest/gtest.h>
#include <entt/entity/registry.hpp>
#include <entt/entity/space.hpp>
struct Position {
std::uint64_t x;
@@ -315,152 +314,3 @@ TEST(Benchmark, SortMulti) {
timer.elapsed();
}
TEST(Benchmark, SpaceConstruct) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Constructing 1000000 entities" << std::endl;
Timer timer;
for(std::uint64_t i = 0; i < 1000000L; i++) {
space.create();
}
timer.elapsed();
}
TEST(Benchmark, SpaceAssign) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Assigning 1000000 entities" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
registry.create<int>();
}
Timer timer;
for(auto entity: registry.view<int>()) {
space.assign(entity);
}
timer.elapsed();
}
TEST(Benchmark, SpaceIterateSingleComponent1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Iterating over 1000000 entities, one component" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position>();
space.assign(entity);
}
Timer timer;
space.view<Position>([](auto, auto &) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateTwoComponents1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Iterating over 1000000 entities, two components" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity>();
space.assign(entity);
}
Timer timer;
space.view<Position, Velocity>([](auto, auto &...) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateTwoComponentsPersistent1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
registry.prepare<Position, Velocity>();
std::cout << "Iterating over 1000000 entities, two components, persistent view" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity>();
space.assign(entity);
}
Timer timer;
space.persistent<Position, Velocity>([](auto, auto &...) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateFiveComponents1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Iterating over 1000000 entities, five components" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity, Comp<1>, Comp<2>, Comp<3>>();
space.assign(entity);
}
Timer timer;
space.view<Position, Velocity, Comp<1>, Comp<2>, Comp<3>>([](auto, auto &...) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateFiveComponentsPersistent1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
registry.prepare<Position, Velocity, Comp<1>, Comp<2>, Comp<3>>();
std::cout << "Iterating over 1000000 entities, five components, persistent view" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity, Comp<1>, Comp<2>, Comp<3>>();
space.assign(entity);
}
Timer timer;
space.persistent<Position, Velocity, Comp<1>, Comp<2>, Comp<3>>([](auto, auto &...) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateTenComponents1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
std::cout << "Iterating over 1000000 entities, ten components" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity, Comp<1>, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>();
space.assign(entity);
}
Timer timer;
space.view<Position, Velocity, Comp<1>, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>([](auto, auto &...) {});
timer.elapsed();
}
TEST(Benchmark, SpaceIterateTenComponentsPersistent1M) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
registry.prepare<Position, Velocity, Comp<1>, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>();
std::cout << "Iterating over 1000000 entities, ten components, persistent view" << std::endl;
for(std::uint64_t i = 0; i < 1000000L; i++) {
const auto entity = registry.create<Position, Velocity, Comp<1>, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>();
space.assign(entity);
}
Timer timer;
space.persistent<Position, Velocity, Comp<1>, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>([](auto, auto &...) {});
timer.elapsed();
}

View File

@@ -1,154 +0,0 @@
#include <gtest/gtest.h>
#include <entt/entity/registry.hpp>
#include <entt/entity/space.hpp>
TEST(Space, SpaceContainsView) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
auto e0 = space.create();
auto e1 = registry.create();
space.assign(e1);
registry.assign<int>(e1);
ASSERT_TRUE(space.contains(e0));
ASSERT_TRUE(space.contains(e1));
space.view<int>([e0, e1](auto entity, auto&&...) {
ASSERT_NE(entity, e0);
ASSERT_EQ(entity, e1);
});
space.view<double>([e0, e1](auto, auto&&...) {
FAIL();
});
auto count = 0;
for(auto entity: space) {
(void)entity;
++count;
}
ASSERT_EQ(count, 2);
const auto view = registry.view<int>();
ASSERT_EQ(view.size(), typename decltype(view)::size_type{1});
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{2});
ASSERT_FALSE(space.empty());
registry.reset();
space.reset();
ASSERT_TRUE(space.empty());
for(auto i = 0; i < 5; ++i) {
registry.destroy(space.create());
registry.create<int>();
}
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{5});
ASSERT_FALSE(space.empty());
space.view<int>([](auto, auto&&...) {
FAIL();
});
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{0});
ASSERT_TRUE(space.empty());
}
TEST(Space, ViewContainsSpace) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
auto e0 = registry.create();
auto e1 = space.create();
registry.assign<int>(e0);
registry.assign<int>(e1);
ASSERT_FALSE(space.contains(e0));
ASSERT_TRUE(space.contains(e1));
space.view<int>([e0, e1](auto entity, auto&&...) {
ASSERT_NE(entity, e0);
ASSERT_EQ(entity, e1);
});
space.view<double>([e0, e1](auto, auto&&...) {
FAIL();
});
auto count = 0;
for(auto entity: space) {
(void)entity;
++count;
}
ASSERT_EQ(count, 1);
const auto view = registry.view<int>();
ASSERT_EQ(view.size(), typename decltype(view)::size_type{2});
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{1});
ASSERT_FALSE(space.empty());
registry.reset();
space.reset();
ASSERT_TRUE(space.empty());
for(auto i = 0; i < 5; ++i) {
registry.destroy(space.create());
registry.create<int>();
registry.create<int>();
}
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{5});
ASSERT_FALSE(space.empty());
space.view<int>([](auto, auto&&...) {
FAIL();
});
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{0});
ASSERT_TRUE(space.empty());
}
TEST(Space, AssignRemove) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
ASSERT_TRUE(space.empty());
space.remove(space.create());
ASSERT_TRUE(space.empty());
}
TEST(Space, Shrink) {
entt::DefaultRegistry registry;
entt::DefaultSpace space{registry};
for(auto i = 0; i < 5; ++i) {
space.create();
}
for(auto entity: space) {
registry.destroy(entity);
}
space.create();
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{5});
ASSERT_FALSE(space.empty());
space.shrink();
ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{1});
ASSERT_FALSE(space.empty());
}