diff --git a/README.md b/README.md index 4e434925f..0f6a57280 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ * [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) @@ -76,7 +77,7 @@ Requests for feature, PR, suggestions ad feedback are highly appreciated. If you find you can help me and want to contribute to the `EnTT` framework with your experience or you do want to get part of the project for some other -reason, feel free to contact me directly (you can find the mail in the +reasons, feel free to contact me directly (you can find the mail in the [profile](https://github.com/skypjack)).
I can't promise that each and every contribution will be accepted, but I can assure that I'll do my best to take them all seriously. @@ -91,6 +92,7 @@ compile-time or at runtime). * An incredibly fast entity-component system based on sparse sets, with its own views and a _pay for what you use_ policy to adjust performance and memory usage according to the users' requirements. +* Spaces, a nice and easy way to create partitions between entities. * Actor class for those who aren't confident with entity-component systems. * The smallest and most basic implementation of a service locator ever seen. * A cooperative scheduler for processes of any type. @@ -175,8 +177,7 @@ case.
In the end, I did it, but it wasn't much satisfying. Actually it wasn't satisfying at all. The fastest and nothing more, fairly little indeed. When I realized it, I tried hard to keep intact the great performance of `EnTT` and to -add all the features I wanted to see in *my* entity-component system at the same -time. +add all the features I wanted to see in *my own library* at the same time. Today `EnTT` is finally what I was looking for: still faster than its _competitors_, lower memory usage in the average case, a really good API and an @@ -194,21 +195,32 @@ Dell XPS 13 out of the mid 2014): | Create 1M entities | 0.0167s | **0.0046s** | | Destroy 1M entities | 0.0053s | **0.0022s** | | Standard view, 1M entities, one component | 0.0012s | **1.9e-07s** | -| Standard view, 1M entities, two components | 0.0012s | **0.0010s** | -| Standard view, 1M entities, two components
Half of the entities have all the components | 0.0009s | **0.0006s** | +| Standard view, 1M entities, two components | 0.0012s | **3.8e-07s** | +| Standard view, 1M entities, two components
Half of the entities have all the components | 0.0009s | **3.8e-07s** | | Standard view, 1M entities, two components
One of the entities has all the components | 0.0008s | **1.0e-06s** | | Persistent view, 1M entities, two components | 0.0012s | **2.8e-07s** | -| Standard view, 1M entities, five components | **0.0010s** | 0.0024s | +| Standard view, 1M entities, five components | 0.0010s | **7.0e-07s** | | Persistent view, 1M entities, five components | 0.0010s | **2.8e-07s** | -| Standard view, 1M entities, ten components | **0.0011s** | 0.0058s | -| Standard view, 1M entities, ten components
Half of the entities have all the components | **0.0010s** | 0.0032s | -| Standard view, 1M entities, ten components
One of the entities has all the components | 0.0008s | **1.7e-06s** | +| Standard view, 1M entities, ten components | 0.0011s | **1.2e-06s** | +| Standard view, 1M entities, ten components
Half of the entities have all the components | 0.0010s | **1.2e-06s** | +| Standard view, 1M entities, ten components
One of the entities has all the components | 0.0008s | **1.2e-06s** | | Persistent view, 1M entities, ten components | 0.0011s | **3.0e-07s** | | Sort 150k entities, one component
Arrays are in reverse order | - | **0.0036s** | | Sort 150k entities, enforce permutation
Arrays are in reverse order | - | **0.0005s** | Note: The default version of `EntityX` (`master` branch) wasn't added to the -comparison because it's already much slower than its compile-time counterpart. +comparison because it's already much slower than its compile-time +counterpart. + +Pretty interesting, aren't them? In fact, these benchmarks are the same used by +`EntityX` to show _how good it is_. To be honest, they aren't so good and these +results shouldn't be taken much seriously.
+The proposed entity-component system is incredibly fast to iterate entities and +the compiler can make a lot of optimizations as long as components aren't used. +Similarly, its extra level of indirection pulls in a lot of interesting features +(as an example, it's possible to create/destroy entities and components during +iterations) with the risk of slowing down everything if users do not use it +carefully and choose the right tool (namely the best _view_) in each case. `EnTT` includes its own tests and benchmarks. See [benchmark.cpp](https://github.com/skypjack/entt/blob/master/test/benchmark.cpp) @@ -333,7 +345,7 @@ The `Registry` to store, the `View` to iterate. That's all. An entity (the _E_ of an _ECS_) is an opaque identifier that users should just use as-is and store around if needed. Do not try to inspect an entity -identifier, its type can change in future and a registry offers all the +identifier, its format can change in future and a registry offers all the functionalities to query them out-of-the-box. The underlying type of an entity (either `std::uint16_t`, `std::uint32_t` or `std::uint64_t`) can be specified when defining a registry (actually the `DefaultRegistry` is nothing more than a @@ -652,6 +664,14 @@ In fact, there are two functions that respond to slightly different needs: ## View: to persist or not to persist? +First of all, it is worth answering an obvious question: why views?
+Roughly speaking, they are a good tool to enforce single responsibility. A +system that has access to a registry can create and destroy entities, as well as +assign and remove components. On the other side, a system that has access to a +view can only iterate entities and their components as well as modify their data +members.
+It is a subtle difference that can help designing a better software sometimes. + There are mainly two kinds of views: standard (also known as `View`) and persistent (also known as `PersistentView`).
Both of them have pros and cons to take in consideration. In particular: @@ -729,7 +749,7 @@ terms of performance in all the situation. This kind of views can access the underlying data structures directly and avoid superfluous checks.
They offer a bunch of functionalities to get the number of entities they are going to return and a raw access to the entity list as well as to the component -list.
+list. It's also possible to ask a view if it contains a given entity.
Refer to the [official documentation](https://skypjack.github.io/entt/) for all the details. @@ -774,7 +794,8 @@ set of candidates in order to speed up iterations.
They offer fewer functionalities than their companion views for single component. In particular, a multi component standard view exposes utility functions to reset its internal state (optimization purposes) and to get the -estimated number of entities it is going to return.
+estimated number of entities it is going to return. It's also possible to ask a +view if it contains a given entity.
Refer to the [official documentation](https://skypjack.github.io/entt/) for all the details. @@ -850,7 +871,8 @@ immediately and does nothing. A persistent view offers a bunch of functionalities to get the number of entities it's going to return, a raw access to the entity list and the possibility to sort the underlying data structures according to the order of one -of the components for which it has been constructed.
+of the components for which it has been constructed. It's also possible to ask a +view if it contains a given entity.
Refer to the [official documentation](https://skypjack.github.io/entt/) for all the details. @@ -928,6 +950,119 @@ In general, all these functions can result in poor performance.
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.
+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.
+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 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.
+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.
+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([](auto entity, auto &aComponent, auto &anotherComponent) { + // ... +}); + +// uses a persistent view internally +space.persisten([](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 diff --git a/TODO b/TODO index 5447d5a8f..6b8b55944 100644 --- a/TODO +++ b/TODO @@ -3,8 +3,7 @@ * to analyze, long term feature: systems organizer based on dependency graphs for implicit parallelism (I don't want to think anymore in future :-)) * save/restore functionalities - see #27 * parent-child relationships between entities directly managed by the registry. is it possible to do that in a clean and safe way? -* blueprint registry - external tool, kind of factory to create entitites template for initialization -* scene management (I prefer the concept of spaces, that is a kind of scene anyway) +* blueprint registry - kind of factory to create entitites template for initialization (get rid of the extra versions of Registry::create) * raw view (affects sparse sets - custom iterator in derived one - and introduces a new kind of view): single component view to iterate components only instead of entities (in the right order!!) it should speed up systems like rendering or whatever requires a single component and isn't interested in the entity, for it avoids the double check of the get * AOB diff --git a/src/entt/entity/space.hpp b/src/entt/entity/space.hpp new file mode 100644 index 000000000..909e17dae --- /dev/null +++ b/src/entt/entity/space.hpp @@ -0,0 +1,268 @@ +#ifndef ENTT_ENTITY_SPACE_HPP +#define ENTT_ENTITY_SPACE_HPP + + +#include +#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.
+ * 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 +class Space: private SparseSet { + using view_type = SparseSet; + + template + 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(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; + /*! @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 ®istry) + : 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::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::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::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::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.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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 + void view(Func func) { + each(registry.template view(), 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.
+ * 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 + void persistent(Func func) { + each(registry.template persistent(), 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.
+ * 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::reset(); + } + +private: + Registry ®istry; +}; + + +/** + * @brief Default space class. + * + * The default space is the best choice for almost all the applications.
+ * Users should have a really good reason to choose something different. + */ +using DefaultSpace = Space; + + +} + + +#endif // ENTT_ENTITY_SPACE_HPP diff --git a/src/entt/entt.hpp b/src/entt/entt.hpp index b107aa1c5..3d40d9c17 100644 --- a/src/entt/entt.hpp +++ b/src/entt/entt.hpp @@ -4,6 +4,7 @@ #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" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32bd6d39b..a56bd5b6d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -55,6 +55,7 @@ add_executable( $ entt/entity/actor.cpp entt/entity/registry.cpp + entt/entity/space.cpp entt/entity/sparse_set.cpp entt/entity/view.cpp ) diff --git a/test/benchmark/benchmark.cpp b/test/benchmark/benchmark.cpp index c075fa6ba..231e453ef 100644 --- a/test/benchmark/benchmark.cpp +++ b/test/benchmark/benchmark.cpp @@ -1,10 +1,10 @@ -#include #include #include #include #include -#include +#include #include +#include struct Position { std::uint64_t x; @@ -17,7 +17,7 @@ struct Velocity { }; template -struct Comp {}; +struct Comp { int x; }; struct Timer final { Timer(): start{std::chrono::system_clock::now()} {} @@ -47,17 +47,16 @@ TEST(Benchmark, Construct) { TEST(Benchmark, Destroy) { entt::DefaultRegistry registry; - std::vector entities{}; std::cout << "Destroying 1000000 entities" << std::endl; for(std::uint64_t i = 0; i < 1000000L; i++) { - entities.push_back(registry.create()); + registry.create(); } Timer timer; - for(auto entity: entities) { + for(auto entity: registry.view()) { registry.destroy(entity); } @@ -281,13 +280,11 @@ TEST(Benchmark, IterateTenComponentsPersistent1M) { TEST(Benchmark, SortSingle) { entt::DefaultRegistry registry; - std::vector entities{}; std::cout << "Sort 150000 entities, one component" << std::endl; for(std::uint64_t i = 0; i < 150000L; i++) { - auto entity = registry.create({ i, i }); - entities.push_back(entity); + registry.create({ i, i }); } Timer timer; @@ -301,13 +298,11 @@ TEST(Benchmark, SortSingle) { TEST(Benchmark, SortMulti) { entt::DefaultRegistry registry; - std::vector entities{}; std::cout << "Sort 150000 entities, two components" << std::endl; for(std::uint64_t i = 0; i < 150000L; i++) { - auto entity = registry.create({ i, i }, { i, i }); - entities.push_back(entity); + registry.create({ i, i }, { i, i }); } registry.sort([](const auto &lhs, const auto &rhs) { @@ -320,3 +315,152 @@ 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(); + } + + Timer timer; + + for(auto entity: registry.view()) { + 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(); + space.assign(entity); + } + + Timer timer; + space.view([](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(); + space.assign(entity); + } + + Timer timer; + space.view([](auto, auto &...) {}); + timer.elapsed(); +} + +TEST(Benchmark, SpaceIterateTwoComponentsPersistent1M) { + entt::DefaultRegistry registry; + entt::DefaultSpace space{registry}; + registry.prepare(); + + 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(); + space.assign(entity); + } + + Timer timer; + space.persistent([](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, Comp<2>, Comp<3>>(); + space.assign(entity); + } + + Timer timer; + space.view, Comp<2>, Comp<3>>([](auto, auto &...) {}); + timer.elapsed(); +} + +TEST(Benchmark, SpaceIterateFiveComponentsPersistent1M) { + entt::DefaultRegistry registry; + entt::DefaultSpace space{registry}; + registry.prepare, 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, Comp<2>, Comp<3>>(); + space.assign(entity); + } + + Timer timer; + space.persistent, 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, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>(); + space.assign(entity); + } + + Timer timer; + space.view, 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, 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, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>(); + space.assign(entity); + } + + Timer timer; + space.persistent, Comp<2>, Comp<3>, Comp<4>, Comp<5>, Comp<6>, Comp<7>, Comp<8>>([](auto, auto &...) {}); + timer.elapsed(); +} diff --git a/test/entt/entity/space.cpp b/test/entt/entity/space.cpp new file mode 100644 index 000000000..2a4a46a8a --- /dev/null +++ b/test/entt/entity/space.cpp @@ -0,0 +1,154 @@ +#include +#include +#include + +TEST(Space, SpaceContainsView) { + entt::DefaultRegistry registry; + entt::DefaultSpace space{registry}; + + auto e0 = space.create(); + auto e1 = registry.create(); + + space.assign(e1); + registry.assign(e1); + + ASSERT_TRUE(space.contains(e0)); + ASSERT_TRUE(space.contains(e1)); + + space.view([e0, e1](auto entity, auto&&...) { + ASSERT_NE(entity, e0); + ASSERT_EQ(entity, e1); + }); + + space.view([e0, e1](auto, auto&&...) { + FAIL(); + }); + + auto count = 0; + + for(auto entity: space) { + (void)entity; + ++count; + } + + ASSERT_EQ(count, 2); + + const auto view = registry.view(); + + 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(); + } + + ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{5}); + ASSERT_FALSE(space.empty()); + + space.view([](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(e0); + registry.assign(e1); + + ASSERT_FALSE(space.contains(e0)); + ASSERT_TRUE(space.contains(e1)); + + space.view([e0, e1](auto entity, auto&&...) { + ASSERT_NE(entity, e0); + ASSERT_EQ(entity, e1); + }); + + space.view([e0, e1](auto, auto&&...) { + FAIL(); + }); + + auto count = 0; + + for(auto entity: space) { + (void)entity; + ++count; + } + + ASSERT_EQ(count, 1); + + const auto view = registry.view(); + + 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(); + registry.create(); + } + + ASSERT_EQ(space.size(), entt::DefaultSpace::size_type{5}); + ASSERT_FALSE(space.empty()); + + space.view([](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()); +}