1
0
Michele Caini 8 жил өмнө
parent
commit
d295c88474

+ 149 - 14
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)).<br/>
 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.<br/>
 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<br/>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<br/>Half of the entities have all the components | 0.0009s | **3.8e-07s** |
 | Standard view, 1M entities, two components<br/>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<br/>Half of the entities have all the components | **0.0010s** | 0.0032s |
-| Standard view, 1M entities, ten components<br/>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<br/>Half of the entities have all the components | 0.0010s | **1.2e-06s** |
+| Standard view, 1M entities, ten components<br/>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<br/>Arrays are in reverse order | - | **0.0036s** |
 | Sort 150k entities, enforce permutation<br/>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.<br/>
+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?<br/>
+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.<br/>
+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`).<br/>
 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.<br/>
 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.<br/>
+list. It's also possible to ask a view if it contains a given entity.<br/>
 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.<br/>
 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.<br/>
+estimated number of entities it is going to return. It's also possible to ask a
+view if it contains a given entity.<br/>
 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.<br/>
+of the components for which it has been constructed. It's also possible to ask a
+view if it contains a given entity.<br/>
 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.<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

+ 1 - 2
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

+ 268 - 0
src/entt/entity/space.hpp

@@ -0,0 +1,268 @@
+#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

+ 1 - 0
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"

+ 1 - 0
test/CMakeLists.txt

@@ -55,6 +55,7 @@ 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
 )

+ 156 - 12
test/benchmark/benchmark.cpp

@@ -1,10 +1,10 @@
-#include <gtest/gtest.h>
 #include <iostream>
 #include <cstddef>
 #include <cstdint>
 #include <chrono>
-#include <vector>
+#include <gtest/gtest.h>
 #include <entt/entity/registry.hpp>
+#include <entt/entity/space.hpp>
 
 struct Position {
     std::uint64_t x;
@@ -17,7 +17,7 @@ struct Velocity {
 };
 
 template<std::size_t>
-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<entt::DefaultRegistry::entity_type> entities{};
 
     std::cout << "Destroying 1000000 entities" << std::endl;
 
     for(std::uint64_t i = 0; i < 1000000L; i++) {
-        entities.push_back(registry.create());
+        registry.create<int>();
     }
 
     Timer timer;
 
-    for(auto entity: entities) {
+    for(auto entity: registry.view<int>()) {
         registry.destroy(entity);
     }
 
@@ -281,13 +280,11 @@ TEST(Benchmark, IterateTenComponentsPersistent1M) {
 
 TEST(Benchmark, SortSingle) {
     entt::DefaultRegistry registry;
-    std::vector<entt::DefaultRegistry::entity_type> entities{};
 
     std::cout << "Sort 150000 entities, one component" << std::endl;
 
     for(std::uint64_t i = 0; i < 150000L; i++) {
-        auto entity = registry.create<Position>({ i, i });
-        entities.push_back(entity);
+        registry.create<Position>({ i, i });
     }
 
     Timer timer;
@@ -301,13 +298,11 @@ TEST(Benchmark, SortSingle) {
 
 TEST(Benchmark, SortMulti) {
     entt::DefaultRegistry registry;
-    std::vector<entt::DefaultRegistry::entity_type> entities{};
 
     std::cout << "Sort 150000 entities, two components" << std::endl;
 
     for(std::uint64_t i = 0; i < 150000L; i++) {
-        auto entity = registry.create<Position, Velocity>({ i, i }, { i, i });
-        entities.push_back(entity);
+        registry.create<Position, Velocity>({ i, i }, { i, i });
     }
 
     registry.sort<Position>([](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<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();
+}

+ 154 - 0
test/entt/entity/space.cpp

@@ -0,0 +1,154 @@
+#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());
+}