Browse Source

save/restore - see #27

Michele Caini 8 years ago
parent
commit
2cc1850212

+ 2 - 1
CMakeLists.txt

@@ -16,7 +16,7 @@ endif()
 # Project configuration
 #
 
-project(entt VERSION 2.4.2)
+project(entt VERSION 2.5.0)
 
 if(NOT CMAKE_BUILD_TYPE)
     set(CMAKE_BUILD_TYPE Debug)
@@ -86,6 +86,7 @@ if(BUILD_TESTING)
 
     option(BUILD_BENCHMARK "Build benchmark." OFF)
     option(BUILD_MOD "Build mod example." OFF)
+    option(BUILD_SNAPSHOT "Build snapshot example." OFF)
 
     # gtest, gtest_main, gmock and gmock_main targets are available from now on
     set(GOOGLETEST_DEPS_DIR ${entt_SOURCE_DIR}/deps/googletest)

+ 246 - 2
README.md

@@ -19,6 +19,10 @@
       * [Runtime components](#runtime-components)
          * [A journey through a plugin](#a-journey-through-a-plugin)
       * [Sorting: is it possible?](#sorting-is-it-possible)
+      * [Snapshot: complete vs continuous](#snapshot-complete-vs-continuous)
+         * [Snapshot loader](#snapshot-loader)
+         * [Continuous loader](#continuous-loader)
+         * [Archives](#archives)
    * [View: to persist or not to persist?](#view-to-persist-or-not-to-persist)
       * [Standard View](#standard-view)
          * [Single component standard view](#single-component-standard-view)
@@ -45,6 +49,8 @@
    * [Event emitter](#event-emitter)
 * [License](#license)
 * [Support](#support)
+   * [Donation](#donation)
+   * [Hire me](#hire-me)
 
 # Introduction
 
@@ -657,6 +663,244 @@ In fact, there are two functions that respond to slightly different needs:
   In this case, instances of `Movement` are arranged in memory so that cache
   misses are minimized when the two components are iterated together.
 
+### Snapshot: complete vs continuous
+
+The `Registry` class offers basic support to serialization.<br/>
+It doesn't convert components and tags to bytes directly, there wasn't the need
+of another tool for serialization out there. Instead, it accepts an opaque
+object with a suitable interface (namely an _archive_) to serialize its internal
+data structures and restore them later. The way types and instances are
+converted to a bunch of bytes is completely in charge to the archive and thus to
+the users.
+
+The goal of the serialization part is to allow users to make both a dump of the
+entire registry or a narrower snapshot, that is to select only the components
+and the tags in which they are interested.<br/>
+Intuitively, the use cases are different. As an example, the first approach is
+suitable for local save/restore functionalities while the latter is suitable for
+creating client-server applications and for transferring somehow parts of the
+representation side to side.
+
+To take a snapshot of the registry, use the `snapshot` member function. It
+returns a temporary object properly initialized to _save_ the whole registry or
+parts of it.
+
+Example of use:
+
+```cpp
+OutputArchive output;
+
+registry.snapshot()
+    .entities(output)
+    .destroyed(output)
+    .component<AComponent, AnotherComponent>(output)
+    .tag<MyTag>(output);
+```
+
+It isn't necessary to invoke all these functions each and every time. What
+functions to use in which case mostly depends on the goal and there is not a
+golden rule to do that.
+
+The `entities` member function asks to the registry to serialize all the
+entities that are still in use along with their versions. On the other side, the
+`destroyed` member function tells to the registry to serialize the entities that
+have been destroyed and are no longer in use.<br/>
+These two functions can be used to save and restore the whole set of entities
+with the versions they had during serialization.
+
+The `component` member function is a function template the aim of which is to
+store aside components. The presence of a template parameter list is a
+consequence of a couple of design choices from the past and in the present:
+
+* First of all, there is no reason to force an user to serialize all the
+  components at once and most of the times it isn't desiderable. As an example,
+  in case the stuff for the HUD in a game is put into the registry for some
+  reasons, its components can be freely discarded during a serialization step
+  because probably the software already knows how to reconstruct the HUD
+  correctly from scratch.
+
+* Furthermore, the registry makes heavy use of _type-erasure_ techniques
+  internally and doesn't know at any time what types of components it contains.
+  Therefore being explicit at the call point is mandatory.
+
+The `tag` member function is similar to the previous one, apart from the fact
+that it works with tags and not with components.<br/>
+Note also that both `component` and `tag` store items along with entities. It
+means that they work properly without a call to the `entities` member function.
+
+Once a snapshot is created, there exist mainly two _ways_ to load it: as a whole
+and in a kind of _continuous mode_.<br/>
+The following sections describe both loaders and archives in details.
+
+#### Snapshot loader
+
+A snapshot loader requires that the destination registry be empty and loads all
+the data at once while keeping intact the identifiers that the entities
+originally had.<br/>
+To do that, the registry offers a member function named `restore` that returns a
+temporary object properly initialized to _restore_ a snapshot.
+
+Example of use:
+
+```cpp
+InputArchive input;
+
+registry.restore()
+    .entities()
+    .destroyed()
+    .component<AComponent, AnotherComponent>(output)
+    .tag<MyTag>(output)
+    .orphans();
+```
+
+It isn't necessary to invoke all these functions each and every time. What
+functions to use in which case mostly depends on the goal and there is not a
+golden rule to do that. For obvious reasons, what is important is that the data
+are restored in exactly the same order in which they were serialized.
+
+The `entities` and `destroyed` member functions restore the sets of entities and
+the versions that the entities originally had at the source.
+
+The `component` member function restores all and only the components specified
+and assigns them to the right entities. Note that the template parameter list
+must be exactly the same used during the serialization. The same applies to the
+`tag` member function.
+
+The `orphans` member function literally destroys those entities that have
+neither components nor tags. It's usually useless if the snapshot is a full dump
+of the source. However, in case all the entities are serialized but only few
+components and tags are saved, it could happen that some of the entities have
+neither components nor tags once restored. The best users can do to deal with
+them is to destroy those entities and thus update their versions.
+
+#### Continuous loader
+
+A continuous loader is designed to load data from a source registry to a
+(possibly) non-empty destination. The loader can accomodate in a registry more
+than one snapshot in a sort of _continuous loading_ that updates the
+destination one step at a time.<br/>
+Identifiers that entities originally had are not transferred to the target.
+Instead, the loader maps remote identifiers to local ones while restoring a
+snapshot. Because of that, this kind of loader offers a way to update
+automatically identifiers that are part of components or tags (as an example, as
+data members or gathered in a container).<br/>
+Another difference with the snapshot loader is that the continuous loader does
+not need to work with the private data structures of a registry. Furthermore, it
+has an internal state that must persist over time. Therefore, there is no reason
+to create it by means of a registry, or to limit its lifetime to that of a
+temporary object.
+
+Example of use:
+
+```cpp
+entt::ContinuousLoader<entity_type> loader{registry};
+InputArchive input;
+
+loader.entities(input)
+    .destroyed(input)
+    .component<AComponent, AnotherComponent>(input)
+    .component<DirtyComponent>(input, &DirtyComponent::parent, &DirtyComponent::child)
+    .tag<MyTag>(input)
+    .tag<DirtyTag>(input, &DirtyTag::container)
+    .orphans()
+    .shrink();
+```
+
+It isn't necessary to invoke all these functions each and every time. What
+functions to use in which case mostly depends on the goal and there is not a
+golden rule to do that. For obvious reasons, what is important is that the data
+are restored in exactly the same order in which they were serialized.
+
+The `entities` and `destroyed` member functions restore groups of entities and
+map each entity to a local counterpart when required. In other terms, for each
+remote entity identifier not yet registered by the loader, the latter creates a
+local identifier so that it can keep the local entity in sync with the remote
+one.
+
+The `component` and `tag` member functions restore all and only the components
+and the tags specified and assign them to the right entities.<br/>
+In case the component or the tag contains entities itself (either as data
+members of type `entity_type` or as containers of entities), the loader can
+update them automatically. To do that, it's enough to specify the data members
+to update as shown in the example. If the component or the tag was in the middle
+of the template parameter list during serialization, multiple commands are
+required during a restore:
+
+```cpp
+registry.snapshot().component<ASimpleComponent, AnotherSimpleComponent, AMoreComplexComponent, TheLastComponent>();
+
+// ...
+
+loader
+    .component<ASimpleComponent, AnotherSimpleComponent>(input)
+    .component<AMoreComplexComponent>(input, &AMoreComplexComponent::entity);
+    .component<TheLastComponent>(input);
+```
+
+The `orphans` member function literally destroys those entities that have
+neither components nor tags after a restore. It has exactly the same purpose
+described in the previous section and works the same way.
+
+Finally, `shrink` helps to purge local entities that no longer have a remote
+conterpart. Users should invoke this member function after restoring each
+snapshot, unless they know exactly what they are doing.
+
+#### Archives
+
+Archives must publicly expose a predefined set of member functions. The API is
+straightforward and consists only of a group of function call operators that
+are invoked by the registry.
+
+In particular:
+
+* An output archive, the one used when creating a snapshot, must expose a
+  function call operator with the following signature to store entities:
+
+  ```cpp
+  void operator()(Entity);
+  ```
+
+  Where `Entity` is the type of the entities used by the registry.<br/>
+  In addition, it must accept the types of both the components and the tags to
+  serialize. Therefore, given a type `T` (either a component or a tag), it must
+  contain a function call operator with the following signature:
+
+  ```cpp
+  void operator()(const T &);
+  ```
+
+  The output archive can freely decide how to serialize the data. The register
+  is not affected at all by the decision.
+
+* An input archive, the one used when restoring a snapshot, must expose a
+  function call operator with the following signature to load entities:
+
+  ```cpp
+  void operator()(Entity &);
+  ```
+
+  Where `Entity` is the type of the entities used by the registry. Each time the
+  function is invoked, the archive must read the next element from the
+  underlying storage and copy it in the given variable.<br/>
+  In addition, it must accept the types of both the components and the tags to
+  restore. Therefore, given a type `T` (either a component or a tag), it must
+  contain a function call operator with the following signature:
+
+  ```cpp
+  void operator()(T &);
+  ```
+
+  Every time such an operator is invoked, the archive must read the next element
+  from the underlying storage and copy it in the given variable.
+
+`EnTT` comes with some examples (actually some tests) that show how to integrate
+a well known library for serialization as an archive. It uses
+[`Cereal C++`](https://uscilab.github.io/cereal/) under the hood, mainly
+because I wanted to learn how it works at the time I was writing the code.
+
+The code is not production-ready and it isn't neither the only nor (probably)
+the best way to do it. However, feel free to use it at your own risk.
+
 ## View: to persist or not to persist?
 
 First of all, it is worth answering an obvious question: why views?<br/>
@@ -2223,7 +2467,7 @@ just click [here](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=
 ## Hire me
 
 If you start using `EnTT` and need help, if you want a new feature and want me
-to give it the highest priority, or for any other reason, I'm available for
-hiring.<br/>
+to give it the highest priority, if you have any other reason to contact me:
+do not hesitate. I'm available for hiring.<br/>
 Feel free to take a look at my [profile](https://github.com/skypjack) and
 contact me by mail.

+ 19 - 0
cmake/in/cereal.in

@@ -0,0 +1,19 @@
+project(cereal-download NONE)
+cmake_minimum_required(VERSION 3.2)
+
+include(ExternalProject)
+
+ExternalProject_Add(
+    cereal
+    GIT_REPOSITORY https://github.com/USCiLab/cereal.git
+    GIT_TAG v1.2.2
+    DOWNLOAD_DIR ${CEREAL_DEPS_DIR}
+    TMP_DIR ${CEREAL_DEPS_DIR}/tmp
+    STAMP_DIR ${CEREAL_DEPS_DIR}/stamp
+    SOURCE_DIR ${CEREAL_DEPS_DIR}/src
+    BINARY_DIR ${CEREAL_DEPS_DIR}/build
+    CONFIGURE_COMMAND ""
+    BUILD_COMMAND ""
+    INSTALL_COMMAND ""
+    TEST_COMMAND ""
+)

+ 75 - 2
src/entt/entity/registry.hpp

@@ -13,6 +13,7 @@
 #include <type_traits>
 #include "../core/family.hpp"
 #include "entt_traits.hpp"
+#include "snapshot.hpp"
 #include "sparse_set.hpp"
 #include "view.hpp"
 
@@ -949,10 +950,11 @@ public:
         if(available) {
             for(auto pos = entities.size(); pos; --pos) {
                 const entity_type curr = pos - 1;
-                const auto entt = entities[curr] & traits_type::entity_mask;
+                const auto entity = entities[curr];
+                const auto entt = entity & traits_type::entity_mask;
 
                 if(curr == entt) {
-                    func(entities[curr]);
+                    func(entity);
                 }
             }
         } else {
@@ -1183,6 +1185,77 @@ public:
         return RawView<Entity, Component>{ensure<Component>()};
     }
 
+    /**
+     * @brief Returns a temporary object to use to create snapshots.
+     *
+     * A snapshot is either a full or a partial dump of a registry.<br/>
+     * It can be used to save and restore its internal state or to keep two or
+     * more instances of this class in sync, as an example in a client-server
+     * architecture.
+     *
+     * @return A not movable and not copyable object to use to take snasphosts.
+     */
+    Snapshot<Entity> snapshot() const {
+        using follow_fn_type = entity_type(*)(const Registry &, entity_type);
+        using raw_fn_type = const entity_type *(*)(const Registry &, component_type);
+        const entity_type seed = available ? (next | (entities[next] & ~traits_type::entity_mask)) : next;
+
+        follow_fn_type follow = [](const Registry &registry, entity_type entity) -> entity_type {
+            const auto &entities = registry.entities;
+            const auto entt = entity & traits_type::entity_mask;
+            const auto next = entities[entt] & traits_type::entity_mask;
+            return (next | (entities[next] & ~traits_type::entity_mask));
+        };
+
+        raw_fn_type raw = [](const Registry &registry, component_type component) -> const entity_type * {
+            const auto &pools = registry.pools;
+            return (component < pools.size() && pools[component]) ? pools[component]->data() : nullptr;
+        };
+
+        return { *this, seed, available, follow, raw };
+    }
+
+    /**
+     * @brief Returns a temporary object to use to load snapshots.
+     *
+     * A snapshot is either a full or a partial dump of a registry.<br/>
+     * It can be used to save and restore its internal state or to keep two or
+     * more instances of this class in sync, as an example in a client-server
+     * architecture.
+     *
+     * @warning
+     * 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.
+     *
+     * @return A not movable and not copyable object to use to load snasphosts.
+     */
+    SnapshotLoader<Entity> restore() {
+        using ensure_fn_type = void(*)(Registry &, entity_type, bool);
+
+        ensure_fn_type ensure = [](Registry &registry, entity_type entity, bool destroyed) {
+            using promotion_type = std::conditional_t<sizeof(size_type) >= sizeof(entity_type), size_type, entity_type>;
+            // explicit promotion to avoid warnings with std::uint16_t
+            const auto entt = promotion_type{entity} & traits_type::entity_mask;
+            auto &entities = registry.entities;
+
+            if(!(entt < entities.size())) {
+                auto curr = entities.size();
+                entities.resize(entt + 1);
+                std::iota(entities.data() + curr, entities.data() + entt, entity_type(curr));
+            }
+
+            entities[entt] = entity;
+
+            if(destroyed) {
+                registry.destroy(entity);
+                const auto version = (entity & (~traits_type::entity_mask));
+                entities[entt] = ((entities[entt] & traits_type::entity_mask) | version);
+            }
+        };
+
+        return { (*this = {}), ensure };
+    }
 
 private:
     std::vector<std::unique_ptr<SparseSet<Entity>>> handlers;

+ 713 - 0
src/entt/entity/snapshot.hpp

@@ -0,0 +1,713 @@
+#ifndef ENTT_ENTITY_SNAPSHOT_HPP
+#define ENTT_ENTITY_SNAPSHOT_HPP
+
+
+#include <unordered_map>
+#include <algorithm>
+#include <cstddef>
+#include <utility>
+#include <cassert>
+#include <iterator>
+#include <type_traits>
+#include "entt_traits.hpp"
+
+
+namespace entt {
+
+
+/**
+ * @brief Forward declaration of the registry class.
+ */
+template<typename>
+class Registry;
+
+
+/**
+ * @brief Utility class to create snapshots from a registry.
+ *
+ * A _snapshot_ can be either a dump of the entire registry or a narrower
+ * selection of components and tags of interest.<br/>
+ * This type can be used in both cases if provided with a correctly configured
+ * output archive.
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ */
+template<typename Entity>
+class Snapshot final {
+    /*! @brief A registry is allowed to create snapshots. */
+    friend class Registry<Entity>;
+
+    using follow_fn_type = Entity(*)(const Registry<Entity> &, Entity);
+    using raw_fn_type = const Entity *(*)(const Registry<Entity> &, typename Registry<Entity>::component_type);
+
+    Snapshot(const Registry<Entity> &registry, Entity seed, std::size_t size, follow_fn_type follow, raw_fn_type raw) noexcept
+        : registry{registry},
+          seed{seed},
+          size{size},
+          follow{follow},
+          raw{raw}
+    {}
+
+    Snapshot(const Snapshot &) = default;
+    Snapshot(Snapshot &&) = default;
+
+    Snapshot & operator=(const Snapshot &) = default;
+    Snapshot & operator=(Snapshot &&) = default;
+
+    template<typename Component, typename Archive>
+    void get(Archive &archive, const Registry<Entity> &registry) {
+        const auto component = registry.template component<Component>();
+        const auto sz = registry.template size<Component>();
+        const auto *entities = raw(registry, component);
+
+        archive(static_cast<Entity>(sz));
+
+        for(std::remove_const_t<decltype(sz)> i{}; i < sz; ++i) {
+            const auto entity = entities[i];
+            archive(entity);
+            archive(registry.template get<Component>(entity));
+        };
+    }
+
+    template<typename Tag, typename Archive>
+    void get(Archive &archive) {
+        const bool has = registry.template has<Tag>();
+
+        // numerical length is forced for tags to facilitate loading
+        archive(has ? Entity(1): Entity{});
+
+        if(has) {
+            archive(registry.template attachee<Tag>());
+            archive(registry.template get<Tag>());
+        }
+    }
+
+public:
+    /**
+     * @brief Puts aside all the entities that are still in use.
+     *
+     * Entities are serialized along with their versions. Destroyed entities are
+     * not taken in consideration by this function.
+     *
+     * @tparam Archive Type of output archive.
+     * @param archive A valid reference to an output archive.
+     * @return An object of this type to continue creating the snapshot.
+     */
+    template<typename Archive>
+    Snapshot entities(Archive &archive) && {
+        archive(static_cast<Entity>(registry.size()));
+        registry.each([&archive, this](auto entity) { archive(entity); });
+        return *this;
+    }
+
+    /**
+     * @brief Puts aside destroyed entities.
+     *
+     * Entities are serialized along with their versions. Entities that are
+     * still in use are not taken in consideration by this function.
+     *
+     * @tparam Archive Type of output archive.
+     * @param archive A valid reference to an output archive.
+     * @return An object of this type to continue creating the snapshot.
+     */
+    template<typename Archive>
+    Snapshot destroyed(Archive &archive) && {
+        archive(static_cast<Entity>(size));
+
+        if(size) {
+            auto curr = seed;
+            archive(curr);
+
+            for(auto i = size - 1; i; --i) {
+                curr = follow(registry, curr);
+                archive(curr);
+            }
+        }
+
+        return *this;
+    }
+
+    /**
+     * @brief Puts aside the given components.
+     *
+     * Each component is serialized together with the entity to which it
+     * belongs. Entities are serialized along with their versions.
+     *
+     * @tparam Component Types of components to serialize.
+     * @tparam Archive Type of output archive.
+     * @param archive A valid reference to an output archive.
+     * @return An object of this type to continue creating the snapshot.
+     */
+    template<typename... Component, typename Archive>
+    Snapshot component(Archive &archive) && {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (get<Component>(archive, registry), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+    /**
+     * @brief Puts aside the given tags.
+     *
+     * Each tag is serialized together with the entity to which it belongs.
+     * Entities are serialized along with their versions.
+     *
+     * @tparam Tag Types of tags to serialize.
+     * @tparam Archive Type of output archive.
+     * @param archive A valid reference to an output archive.
+     * @return An object of this type to continue creating the snapshot.
+     */
+    template<typename... Tag, typename Archive>
+    Snapshot tag(Archive &archive) && {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (get<Tag>(archive), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+private:
+    const Registry<Entity> &registry;
+    const Entity seed;
+    const std::size_t size;
+    follow_fn_type follow;
+    raw_fn_type raw;
+};
+
+
+/**
+ * @brief Utility class to restore a snapshot as a whole.
+ *
+ * A snapshot loader requires that the destination registry be empty and loads
+ * all the data at once while keeping intact the identifiers that the entities
+ * originally had.<br/>
+ * An example of use is the implementation of a save/restore utility.
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ */
+template<typename Entity>
+class SnapshotLoader final {
+    /*! @brief A registry is allowed to create snapshot loaders. */
+    friend class Registry<Entity>;
+
+    using ensure_fn_type = void(*)(Registry<Entity> &, Entity, bool);
+
+    SnapshotLoader(Registry<Entity> &registry, ensure_fn_type ensure_fn) noexcept
+        : registry{registry},
+          ensure_fn{ensure_fn}
+    {
+        // restore a snapshot as a whole requires a clean registry
+        assert(!registry.capacity());
+    }
+
+    SnapshotLoader(const SnapshotLoader &) = default;
+    SnapshotLoader(SnapshotLoader &&) = default;
+
+    SnapshotLoader & operator=(const SnapshotLoader &) = default;
+    SnapshotLoader & operator=(SnapshotLoader &&) = default;
+
+    template<typename Archive, typename Func>
+    void each(Archive &archive, Func func) {
+        Entity length{};
+        archive(length);
+
+        while(length) {
+            Entity entity{};
+            archive(entity);
+            func(entity);
+            --length;
+        }
+    }
+
+    template<typename Component, typename Archive>
+    void assign(Archive &archive) {
+        each(archive, [&archive, this](auto entity) {
+            static constexpr auto destroyed = false;
+            ensure_fn(registry, entity, destroyed);
+            archive(registry.template assign<Component>(entity));
+        });
+    }
+
+    template<typename Tag, typename Archive>
+    void attach(Archive &archive) {
+        each(archive, [&archive, this](auto entity) {
+            static constexpr auto destroyed = false;
+            ensure_fn(registry, entity, destroyed);
+            archive(registry.template attach<Tag>(entity));
+        });
+    }
+
+public:
+    /**
+     * @brief Restores entities that were in use during serialization.
+     *
+     * This function restores the entities that were in use during serialization
+     * and gives them the versions they originally had.
+     *
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A valid loader to continue restoring data.
+     */
+    template<typename Archive>
+    SnapshotLoader entities(Archive &archive) && {
+        each(archive, [this](auto entity) {
+            static constexpr auto destroyed = false;
+            ensure_fn(registry, entity, destroyed);
+        });
+
+        return *this;
+    }
+
+    /**
+     * @brief Restores entities that were destroyed during serialization.
+     *
+     * This function restores the entities that were destroyed during
+     * serialization and gives them the versions they originally had.
+     *
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A valid loader to continue restoring data.
+     */
+    template<typename Archive>
+    SnapshotLoader destroyed(Archive &archive) && {
+        each(archive, [this](auto entity) {
+            static constexpr auto destroyed = true;
+            ensure_fn(registry, entity, destroyed);
+        });
+
+        return *this;
+    }
+
+    /**
+     * @brief Restores components and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the component is
+     * assigned doesn't exist yet, the loader will take care to create it with
+     * the version it originally had.
+     *
+     * @tparam Component Types of components to restore.
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A valid loader to continue restoring data.
+     */
+    template<typename... Component, typename Archive>
+    SnapshotLoader component(Archive &archive) && {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (assign<Component>(archive), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+    /**
+     * @brief Restores tags and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the tag is assigned
+     * doesn't exist yet, the loader will take care to create it with the
+     * version it originally had.
+     *
+     * @tparam Tag Types of tags to restore.
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A valid loader to continue restoring data.
+     */
+    template<typename... Tag, typename Archive>
+    SnapshotLoader tag(Archive &archive) && {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (attach<Tag>(archive), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+
+    /**
+     * @brief Destroys those entities that have neither components nor tags.
+     *
+     * In case all the entities were serialized but only part of the components
+     * and tags was saved, it could happen that some of the entities have
+     * neither components nor tags once restored.<br/>
+     * This functions helps to identify and destroy those entities.
+     *
+     * @return A valid loader to continue restoring data.
+     */
+    SnapshotLoader orphans() && {
+        registry.orphans([this](auto entity) {
+            registry.destroy(entity);
+        });
+
+        return *this;
+    }
+
+private:
+    Registry<Entity> &registry;
+    ensure_fn_type ensure_fn;
+};
+
+
+/**
+ * @brief Utility class for _continuous loading_.
+ *
+ * A _continuous loader_ is designed to load data from a source registry to a
+ * (possibly) non-empty destination. The loader can accomodate in a registry
+ * more than one snapshot in a sort of _continuous loading_ that updates the
+ * destination one step at a time.<br/>
+ * Identifiers that entities originally had are not transferred to the target.
+ * Instead, the loader maps remote identifiers to local ones while restoring a
+ * snapshot.<br/>
+ * An example of use is the implementation of a client-server applications with
+ * the requirement of transferring somehow parts of the representation side to
+ * side.
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ */
+template<typename Entity>
+class ContinuousLoader final {
+    using traits_type = entt_traits<Entity>;
+
+    Entity destroy(Entity entity) {
+        const auto it = remloc.find(entity);
+
+        if(it == remloc.cend()) {
+            const auto local = registry.create();
+            remloc.emplace(entity, std::make_pair(local, true));
+            registry.destroy(local);
+        }
+
+        return remloc[entity].first;
+    }
+
+    Entity restore(Entity entity) {
+        const auto it = remloc.find(entity);
+
+        if(it == remloc.cend()) {
+            const auto local = registry.create();
+            remloc.emplace(entity, std::make_pair(local, true));
+        } else {
+            remloc[entity].first =
+                    registry.valid(remloc[entity].first)
+                    ? remloc[entity].first
+                    : registry.create();
+
+            // set the dirty flag
+            remloc[entity].second = true;
+        }
+
+        return remloc[entity].first;
+    }
+
+    template<typename Instance, typename Type>
+    std::enable_if_t<std::is_same<Type, Entity>::value>
+    update(Instance &instance, Type Instance::*member) {
+        instance.*member = map(instance.*member);
+    }
+
+    template<typename Instance, typename Type>
+    std::enable_if_t<std::is_same<typename std::iterator_traits<typename Type::iterator>::value_type, Entity>::value>
+    update(Instance &instance, Type Instance::*member) {
+        for(auto &entity: (instance.*member)) {
+            entity = map(entity);
+        }
+    }
+
+    template<typename Archive, typename Func>
+    void each(Archive &archive, Func func) {
+        Entity length{};
+        archive(length);
+
+        while(length) {
+            Entity entity{};
+            archive(entity);
+            func(entity);
+            --length;
+        }
+    }
+
+    template<typename Component>
+    void reset() {
+        for(auto &&ref: remloc) {
+            const auto local = ref.second.first;
+
+            if(registry.valid(local)) {
+                registry.template reset<Component>(local);
+            }
+        }
+    }
+
+    template<typename Component, typename Archive>
+    void assign(Archive &archive) {
+        reset<Component>();
+
+        each(archive, [&archive, this](auto entity) {
+            entity = restore(entity);
+            archive(registry.template accommodate<Component>(entity));
+        });
+    }
+
+    template<typename Component, typename Archive, typename... Type>
+    void assign(Archive &archive, Type Component::*... member) {
+        reset<Component>();
+
+        each(archive, [&archive, member..., this](auto entity) {
+            entity = restore(entity);
+            auto &component = registry.template accommodate<Component>(entity);
+            archive(component);
+
+            using accumulator_type = int[];
+            accumulator_type accumulator = { 0, (update(component, member), 0)... };
+            (void)accumulator;
+        });
+    }
+
+    template<typename Tag, typename Archive>
+    void attach(Archive &archive) {
+        registry.template remove<Tag>();
+
+        each(archive, [&archive, this](auto entity) {
+            entity = restore(entity);
+            archive(registry.template attach<Tag>(entity));
+        });
+    }
+
+    template<typename Tag, typename Archive, typename... Type>
+    void attach(Archive &archive, Type Tag::*... member) {
+        registry.template remove<Tag>();
+
+        each(archive, [&archive, member..., this](auto entity) {
+            entity = restore(entity);
+            auto &tag = registry.template attach<Tag>(entity);
+            archive(tag);
+
+            using accumulator_type = int[];
+            accumulator_type accumulator = { 0, (update(tag, member), 0)... };
+            (void)accumulator;
+        });
+    }
+
+public:
+    /*! @brief Underlying entity identifier. */
+    using entity_type = Entity;
+
+    /**
+     * @brief Constructs a loader that is bound to a given registry.
+     * @param registry A valid reference to a registry.
+     */
+    ContinuousLoader(Registry<entity_type> &registry) noexcept
+        : registry{registry}
+    {}
+
+    /*! @brief Default copy constructor. */
+    ContinuousLoader(const ContinuousLoader &) = default;
+    /*! @brief Default move constructor. */
+    ContinuousLoader(ContinuousLoader &&) = default;
+
+    /*! @brief Default copy assignment operator. @return This loader. */
+    ContinuousLoader & operator=(const ContinuousLoader &) = default;
+    /*! @brief Default move assignment operator. @return This loader. */
+    ContinuousLoader & operator=(ContinuousLoader &&) = default;
+
+    /**
+     * @brief Restores entities that were in use during serialization.
+     *
+     * This function restores the entities that were in use during serialization
+     * and creates local counterparts for them if required.
+     *
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A non-const reference to this loader.
+     */
+    template<typename Archive>
+    ContinuousLoader & entities(Archive &archive) {
+        each(archive, [this](auto entity) { restore(entity); });
+        return *this;
+    }
+
+    /**
+     * @brief Restores entities that were destroyed during serialization.
+     *
+     * This function restores the entities that were destroyed during
+     * serialization and creates local counterparts for them if required.
+     *
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A non-const reference to this loader.
+     */
+    template<typename Archive>
+    ContinuousLoader & destroyed(Archive &archive) {
+        each(archive, [this](auto entity) { destroy(entity); });
+        return *this;
+    }
+
+    /**
+     * @brief Restores components and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the component is
+     * assigned doesn't exist yet, the loader will take care to create a local
+     * counterpart for it.
+     *
+     * @tparam Component Types of components to restore.
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A non-const reference to this loader.
+     */
+    template<typename... Component, typename Archive>
+    ContinuousLoader & component(Archive &archive) {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (assign<Component>(archive), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+    /**
+     * @brief Restores components and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the component is
+     * assigned doesn't exist yet, the loader will take care to create a local
+     * counterpart for it.<br/>
+     * Members can be either data members of type entity_type or containers of
+     * entities. In both cases, the loader will visit them and update the
+     * entities by replacing each one with its local counterpart.
+     *
+     * @tparam Component Type of component to restore.
+     * @tparam Archive Type of input archive.
+     * @tparam Type Types of members to update with their local counterparts.
+     * @param archive A valid reference to an input archive.
+     * @param member Members to update with their local counterparts.
+     * @return A non-const reference to this loader.
+     */
+    template<typename Component, typename Archive, typename... Type>
+    ContinuousLoader & component(Archive &archive, Type Component::*... member) {
+        assign(archive, member...);
+        return *this;
+    }
+
+    /**
+     * @brief Restores tags and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the tag is assigned
+     * doesn't exist yet, the loader will take care to create a local
+     * counterpart for it.
+     *
+     * @tparam Tag Types of tags to restore.
+     * @tparam Archive Type of input archive.
+     * @param archive A valid reference to an input archive.
+     * @return A non-const reference to this loader.
+     */
+    template<typename... Tag, typename Archive>
+    ContinuousLoader & tag(Archive &archive) {
+        using accumulator_type = int[];
+        accumulator_type accumulator = { 0, (attach<Tag>(archive), 0)... };
+        (void)accumulator;
+        return *this;
+    }
+
+    /**
+     * @brief Restores tags and assigns them to the right entities.
+     *
+     * The template parameter list must be exactly the same used during
+     * serialization. In the event that the entity to which the tag is assigned
+     * doesn't exist yet, the loader will take care to create a local
+     * counterpart for it.<br/>
+     * Members can be either data members of type entity_type or containers of
+     * entities. In both cases, the loader will visit them and update the
+     * entities by replacing each one with its local counterpart.
+     *
+     * @tparam Tag Type of tag to restore.
+     * @tparam Archive Type of input archive.
+     * @tparam Type Types of members to update with their local counterparts.
+     * @param archive A valid reference to an input archive.
+     * @param member Members to update with their local counterparts.
+     * @return A non-const reference to this loader.
+     */
+    template<typename Tag, typename Archive, typename... Type>
+    ContinuousLoader & tag(Archive &archive, Type Tag::*... member) {
+        attach<Tag>(archive, member...);
+        return *this;
+    }
+
+    /**
+     * @brief Helps to purge entities that no longer have a conterpart.
+     *
+     * Users should invoke this member function after restoring each snapshot,
+     * unless they know exactly what they are doing.
+     *
+     * @return A non-const reference to this loader.
+     */
+    ContinuousLoader & shrink() {
+        auto it = remloc.begin();
+
+        while(it != remloc.cend()) {
+            const auto local = it->second.first;
+            bool &dirty = it->second.second;
+
+            if(dirty) {
+                dirty = false;
+                ++it;
+            } else {
+                if(registry.valid(local)) {
+                    registry.destroy(local);
+                }
+
+                it = remloc.erase(it);
+            }
+        }
+
+        return *this;
+    }
+
+    /**
+     * @brief Destroys those entities that have neither components nor tags.
+     *
+     * In case all the entities were serialized but only part of the components
+     * and tags was saved, it could happen that some of the entities have
+     * neither components nor tags once restored.<br/>
+     * This functions helps to identify and destroy those entities.
+     *
+     * @return A non-const reference to this loader.
+     */
+    ContinuousLoader & orphans() {
+        registry.orphans([this](auto entity) {
+            registry.destroy(entity);
+        });
+
+        return *this;
+    }
+
+    /**
+     * @brief Tests if a loader knows about a given entity.
+     * @param entity An entity identifier.
+     * @return True if `entity` is managed by the loader, false otherwise.
+     */
+    bool has(entity_type entity) {
+        return !(remloc.find(entity) == remloc.cend());
+    }
+
+    /**
+     * @brief Returns the identifier to which an entity refers.
+     *
+     * @warning
+     * Attempting to use an entity that isn't managed by the loader results in
+     * undefined behavior.<br/>
+     * An assertion will abort the execution at runtime in debug mode if the
+     * loader doesn't knows about the entity.
+     *
+     * @param entity An entity identifier.
+     * @return The identifier to which `entity` refers in the target registry.
+     */
+    entity_type map(entity_type entity) {
+        assert(has(entity));
+        return remloc[entity].first;
+    }
+
+private:
+    std::unordered_map<Entity, std::pair<Entity, bool>> remloc;
+    Registry<Entity> &registry;
+};
+
+
+}
+
+
+#endif // ENTT_ENTITY_SNAPSHOT_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/snapshot.hpp"
 #include "entity/sparse_set.hpp"
 #include "entity/view.hpp"
 #include "locator/locator.hpp"

+ 1 - 1
src/entt/signal/sigh.hpp

@@ -100,7 +100,7 @@ class SigH;
  *
  * * `Param` is a type to which `Ret` can be converted.
  * * The return type is true if the handler must stop collecting data, false
- *   otherwise.
+ * otherwise.
  *
  * @tparam Ret Return type of a function type.
  * @tparam Args Types of arguments of a function type.

+ 20 - 0
test/CMakeLists.txt

@@ -36,6 +36,25 @@ if(BUILD_MOD)
     add_test(NAME mod COMMAND mod)
 endif()
 
+# Test snapshot
+
+if(BUILD_SNAPSHOT)
+    set(CEREAL_DEPS_DIR ${entt_SOURCE_DIR}/deps/cereal)
+    configure_file(${entt_SOURCE_DIR}/cmake/in/cereal.in ${CEREAL_DEPS_DIR}/CMakeLists.txt)
+    execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . WORKING_DIRECTORY ${CEREAL_DEPS_DIR})
+    execute_process(COMMAND ${CMAKE_COMMAND} --build . WORKING_DIRECTORY ${CEREAL_DEPS_DIR})
+    set(CEREAL_SRC_DIR ${CEREAL_DEPS_DIR}/src/include)
+
+    add_executable(
+        snapshot
+        $<TARGET_OBJECTS:odr>
+        snapshot/snapshot.cpp
+    )
+    target_include_directories(snapshot PRIVATE ${CEREAL_SRC_DIR})
+    target_link_libraries(snapshot PRIVATE gtest_main Threads::Threads)
+    add_test(NAME snapshot COMMAND snapshot)
+endif()
+
 # Test core
 
 add_executable(
@@ -55,6 +74,7 @@ add_executable(
     $<TARGET_OBJECTS:odr>
     entt/entity/actor.cpp
     entt/entity/registry.cpp
+    entt/entity/snapshot.cpp
     entt/entity/sparse_set.cpp
     entt/entity/view.cpp
 )

+ 489 - 0
test/entt/entity/snapshot.cpp

@@ -0,0 +1,489 @@
+#include <tuple>
+#include <queue>
+#include <vector>
+#include <gtest/gtest.h>
+#include <entt/entity/registry.hpp>
+
+template<typename Storage>
+struct OutputArchive {
+    OutputArchive(Storage &storage)
+        : storage{storage}
+    {}
+
+    template<typename Value>
+    void operator()(const Value &value) {
+        std::get<std::queue<Value>>(storage).push(value);
+    }
+
+private:
+    Storage &storage;
+};
+
+template<typename Storage>
+struct InputArchive {
+    InputArchive(Storage &storage)
+        : storage{storage}
+    {}
+
+    template<typename Value>
+    void operator()(Value &value) {
+        auto &queue = std::get<std::queue<Value>>(storage);
+        value = queue.front();
+        queue.pop();
+    }
+
+private:
+    Storage &storage;
+};
+
+struct AComponent {};
+
+struct AnotherComponent {
+    int key;
+    int value;
+};
+
+struct Foo {
+    entt::DefaultRegistry::entity_type bar;
+    std::vector<entt::DefaultRegistry::entity_type> quux;
+};
+
+TEST(Snapshot, Dump) {
+    entt::DefaultRegistry registry;
+
+    auto e0 = registry.create();
+    registry.assign<int>(e0, 42);
+    registry.assign<char>(e0, 'c');
+    registry.assign<double>(e0, .1);
+
+    auto e1 = registry.create();
+
+    auto e2 = registry.create();
+    registry.assign<int>(e2, 3);
+
+    auto e3 = registry.create();
+    registry.assign<char>(e3, '0');
+    registry.attach<float>(e3, .3f);
+
+    auto e4 = registry.create();
+    registry.attach<AComponent>(e4);
+
+    registry.destroy(e1);
+    auto v1 = registry.current(e1);
+
+    using storage_type = std::tuple<
+        std::queue<entt::DefaultRegistry::entity_type>,
+        std::queue<int>,
+        std::queue<char>,
+        std::queue<double>,
+        std::queue<float>,
+        std::queue<bool>,
+        std::queue<AComponent>,
+        std::queue<AnotherComponent>,
+        std::queue<Foo>
+    >;
+
+    storage_type storage;
+    OutputArchive<storage_type> output{storage};
+    InputArchive<storage_type> input{storage};
+
+    registry.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<int, char, AnotherComponent, double>(output)
+            .tag<float, bool, AComponent>(output);
+
+    registry.reset();
+
+    ASSERT_FALSE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_FALSE(registry.valid(e2));
+    ASSERT_FALSE(registry.valid(e3));
+    ASSERT_FALSE(registry.valid(e4));
+
+    registry.restore()
+            .entities(input)
+            .destroyed(input)
+            .component<int, char, AnotherComponent, double>(input)
+            .tag<float, bool, AComponent>(input)
+            .orphans();
+
+    ASSERT_TRUE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_TRUE(registry.valid(e2));
+    ASSERT_TRUE(registry.valid(e3));
+    ASSERT_TRUE(registry.valid(e4));
+
+    ASSERT_FALSE(registry.orphan(e0));
+    ASSERT_FALSE(registry.orphan(e2));
+    ASSERT_FALSE(registry.orphan(e3));
+    ASSERT_FALSE(registry.orphan(e4));
+
+    ASSERT_EQ(registry.get<int>(e0), 42);
+    ASSERT_EQ(registry.get<char>(e0), 'c');
+    ASSERT_EQ(registry.get<double>(e0), .1);
+    ASSERT_EQ(registry.current(e1), v1);
+    ASSERT_EQ(registry.get<int>(e2), 3);
+    ASSERT_EQ(registry.get<char>(e3), '0');
+
+    ASSERT_TRUE(registry.has<float>());
+    ASSERT_EQ(registry.attachee<float>(), e3);
+    ASSERT_EQ(registry.get<float>(), .3f);
+
+    ASSERT_TRUE(registry.has<AComponent>());
+    ASSERT_EQ(registry.attachee<AComponent>(), e4);
+
+    ASSERT_TRUE(registry.empty<AnotherComponent>());
+    ASSERT_FALSE(registry.has<long int>());
+}
+
+TEST(Snapshot, Partial) {
+    entt::DefaultRegistry registry;
+
+    auto e0 = registry.create();
+    registry.assign<int>(e0, 42);
+    registry.assign<char>(e0, 'c');
+    registry.assign<double>(e0, .1);
+
+    auto e1 = registry.create();
+
+    auto e2 = registry.create();
+    registry.assign<int>(e2, 3);
+
+    auto e3 = registry.create();
+    registry.assign<char>(e3, '0');
+    registry.attach<float>(e3, .3f);
+
+    auto e4 = registry.create();
+    registry.attach<AComponent>(e4);
+
+    registry.destroy(e1);
+    auto v1 = registry.current(e1);
+
+    using storage_type = std::tuple<
+        std::queue<entt::DefaultRegistry::entity_type>,
+        std::queue<int>,
+        std::queue<char>,
+        std::queue<double>,
+        std::queue<float>,
+        std::queue<bool>,
+        std::queue<AComponent>,
+        std::queue<Foo>
+    >;
+
+    storage_type storage;
+    OutputArchive<storage_type> output{storage};
+    InputArchive<storage_type> input{storage};
+
+    registry.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<char, int>(output)
+            .tag<bool, float>(output);
+
+    registry.reset();
+
+    ASSERT_FALSE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_FALSE(registry.valid(e2));
+    ASSERT_FALSE(registry.valid(e3));
+    ASSERT_FALSE(registry.valid(e4));
+
+    registry.restore()
+            .entities(input)
+            .destroyed(input)
+            .component<char, int>(input)
+            .tag<bool, float>(input);
+
+    ASSERT_TRUE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_TRUE(registry.valid(e2));
+    ASSERT_TRUE(registry.valid(e3));
+    ASSERT_TRUE(registry.valid(e4));
+
+    ASSERT_EQ(registry.get<int>(e0), 42);
+    ASSERT_EQ(registry.get<char>(e0), 'c');
+    ASSERT_FALSE(registry.has<double>(e0));
+    ASSERT_EQ(registry.current(e1), v1);
+    ASSERT_EQ(registry.get<int>(e2), 3);
+    ASSERT_EQ(registry.get<char>(e3), '0');
+    ASSERT_TRUE(registry.orphan(e4));
+
+    ASSERT_TRUE(registry.has<float>());
+    ASSERT_EQ(registry.attachee<float>(), e3);
+    ASSERT_EQ(registry.get<float>(), .3f);
+    ASSERT_FALSE(registry.has<long int>());
+
+    registry.snapshot()
+            .tag<float>(output)
+            .destroyed(output)
+            .entities(output);
+
+    registry.reset();
+
+    ASSERT_FALSE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_FALSE(registry.valid(e2));
+    ASSERT_FALSE(registry.valid(e3));
+    ASSERT_FALSE(registry.valid(e4));
+
+    registry.restore()
+            .tag<float>(input)
+            .destroyed(input)
+            .entities(input)
+            .orphans();
+
+    ASSERT_FALSE(registry.valid(e0));
+    ASSERT_FALSE(registry.valid(e1));
+    ASSERT_FALSE(registry.valid(e2));
+    ASSERT_TRUE(registry.valid(e3));
+    ASSERT_FALSE(registry.valid(e4));
+}
+
+TEST(Snapshot, Continuous) {
+    using entity_type = entt::DefaultRegistry::entity_type;
+
+    entt::DefaultRegistry src;
+    entt::DefaultRegistry dst;
+
+    entt::ContinuousLoader<entity_type> loader{dst};
+
+    std::vector<entity_type> entities;
+    entity_type entity;
+
+    using storage_type = std::tuple<
+        std::queue<entity_type>,
+        std::queue<AComponent>,
+        std::queue<AnotherComponent>,
+        std::queue<Foo>,
+        std::queue<double>
+    >;
+
+    storage_type storage;
+    OutputArchive<storage_type> output{storage};
+    InputArchive<storage_type> input{storage};
+
+    for(int i = 0; i < 10; ++i) {
+        src.create();
+    }
+
+    src.each([&src](auto entity) {
+        src.destroy(entity);
+    });
+
+    for(int i = 0; i < 5; ++i) {
+        entity = src.create();
+        entities.push_back(entity);
+
+        src.assign<AComponent>(entity);
+        src.assign<AnotherComponent>(entity, i, i);
+
+        if(i % 2) {
+            src.assign<Foo>(entity, entity);
+        } else if(i == 2) {
+            src.attach<double>(entity, .3);
+        }
+    }
+
+    src.view<Foo>().each([&entities](auto, auto &foo) {
+        foo.quux.insert(foo.quux.begin(), entities.begin(), entities.end());
+    });
+
+    entity = dst.create();
+    dst.assign<AComponent>(entity);
+    dst.assign<AnotherComponent>(entity, -1, -1);
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans();
+
+    decltype(dst.size()) aComponentCnt{};
+    decltype(dst.size()) anotherComponentCnt{};
+    decltype(dst.size()) fooCnt{};
+
+    dst.each([&dst, &aComponentCnt](auto entity) {
+        ASSERT_TRUE(dst.has<AComponent>(entity));
+        ++aComponentCnt;
+    });
+
+    dst.view<AnotherComponent>().each([&anotherComponentCnt](auto, const auto &component) {
+        ASSERT_EQ(component.value, component.key < 0 ? -1 : component.key);
+        ++anotherComponentCnt;
+    });
+
+    dst.view<Foo>().each([&dst, &fooCnt](auto entity, const auto &component) {
+        ASSERT_EQ(entity, component.bar);
+
+        for(auto entity: component.quux) {
+            ASSERT_TRUE(dst.valid(entity));
+        }
+
+        ++fooCnt;
+    });
+
+    ASSERT_TRUE(dst.has<double>());
+    ASSERT_EQ(dst.get<double>(), .3);
+
+    src.view<AnotherComponent>().each([](auto, auto &component) {
+        component.value = 2 * component.key;
+    });
+
+    auto size = dst.size();
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans();
+
+    ASSERT_EQ(size, dst.size());
+
+    ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
+    ASSERT_EQ(dst.size<AnotherComponent>(), anotherComponentCnt);
+    ASSERT_EQ(dst.size<Foo>(), fooCnt);
+    ASSERT_TRUE(dst.has<double>());
+
+    dst.view<AnotherComponent>().each([](auto, auto &component) {
+        ASSERT_EQ(component.value, component.key < 0 ? -1 : (2 * component.key));
+    });
+
+    entity = src.create();
+
+    src.view<Foo>().each([entity](auto, auto &component) {
+        component.bar = entity;
+    });
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans();
+
+    dst.view<Foo>().each([&loader, entity](auto, auto &component) {
+        ASSERT_EQ(component.bar, loader.map(entity));
+    });
+
+    entities.clear();
+    for(auto entity: src.view<AComponent>()) {
+        entities.push_back(entity);
+    }
+
+    src.destroy(entity);
+    loader.shrink();
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans()
+            .shrink();
+
+    dst.view<Foo>().each([&dst, &loader, entity](auto, auto &component) {
+        ASSERT_FALSE(dst.valid(component.bar));
+    });
+
+    ASSERT_FALSE(loader.has(entity));
+
+    entity = src.create();
+
+    src.view<Foo>().each([entity](auto, auto &component) {
+        component.bar = entity;
+    });
+
+    dst.reset<AComponent>();
+    aComponentCnt = src.size<AComponent>();
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans();
+
+    ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
+    ASSERT_TRUE(dst.has<double>());
+
+    src.reset<AComponent>();
+    src.remove<double>();
+    aComponentCnt = {};
+
+    src.snapshot()
+            .entities(output)
+            .destroyed(output)
+            .component<AComponent, AnotherComponent, Foo>(output)
+            .tag<double>(output);
+
+    loader.entities(input)
+            .destroyed(input)
+            .component<AComponent, AnotherComponent>(input)
+            .component<Foo>(input, &Foo::bar, &Foo::quux)
+            .tag<double>(input)
+            .orphans();
+
+    ASSERT_EQ(dst.size<AComponent>(), aComponentCnt);
+    ASSERT_FALSE(dst.has<double>());
+}
+
+TEST(Snapshot, ContinuousMoreOnShrink) {
+    using entity_type = entt::DefaultRegistry::entity_type;
+
+    entt::DefaultRegistry src;
+    entt::DefaultRegistry dst;
+
+    entt::ContinuousLoader<entity_type> loader{dst};
+
+    using storage_type = std::tuple<
+        std::queue<entity_type>,
+        std::queue<AComponent>
+    >;
+
+    storage_type storage;
+    OutputArchive<storage_type> output{storage};
+    InputArchive<storage_type> input{storage};
+
+    auto entity = src.create();
+    src.snapshot().entities(output);
+    loader.entities(input).shrink();
+
+    ASSERT_TRUE(dst.valid(entity));
+
+    loader.shrink();
+
+    ASSERT_FALSE(dst.valid(entity));
+}

+ 182 - 0
test/snapshot/snapshot.cpp

@@ -0,0 +1,182 @@
+#include <gtest/gtest.h>
+#include <sstream>
+#include <vector>
+#include <cereal/archives/json.hpp>
+#include <entt/entity/registry.hpp>
+
+struct Position {
+    float x;
+    float y;
+};
+
+struct Timer {
+    int duration;
+    int elapsed{0};
+};
+
+struct Relationship {
+    entt::DefaultRegistry::entity_type parent;
+};
+
+template<class Archive>
+void serialize(Archive &archive, Position &position) {
+  archive(position.x, position.y);
+}
+
+template<class Archive>
+void serialize(Archive &archive, Timer &timer) {
+  archive(timer.duration);
+}
+
+template<class Archive>
+void serialize(Archive &archive, Relationship &relationship) {
+  archive(relationship.parent);
+}
+
+TEST(Snapshot, Full) {
+    std::stringstream storage;
+
+    entt::DefaultRegistry source;
+    entt::DefaultRegistry destination;
+
+    auto e0 = source.create();
+    source.assign<Position>(e0, 16.f, 16.f);
+
+    source.destroy(source.create());
+
+    auto e1 = source.create();
+    source.assign<Position>(e1, .8f, .0f);
+    source.assign<Relationship>(e1, e0);
+
+    auto e2 = source.create();
+
+    auto e3 = source.create();
+    source.assign<Timer>(e3, 1000, 100);
+
+    source.destroy(e2);
+    auto v2 = source.current(e2);
+
+    {
+        // output finishes flushing its contents when it goes out of scope
+        cereal::JSONOutputArchive output{storage};
+        source.snapshot().entities(output).destroyed(output)
+                .component<Position, Timer, Relationship>(output);
+    }
+
+    cereal::JSONInputArchive input{storage};
+    destination.restore().entities(input).destroyed(input)
+            .component<Position, Timer, Relationship>(input);
+
+    ASSERT_TRUE(destination.valid(e0));
+    ASSERT_TRUE(destination.has<Position>(e0));
+    ASSERT_EQ(destination.get<Position>(e0).x, 16.f);
+    ASSERT_EQ(destination.get<Position>(e0).y, 16.f);
+
+    ASSERT_TRUE(destination.valid(e1));
+    ASSERT_TRUE(destination.has<Position>(e1));
+    ASSERT_EQ(destination.get<Position>(e1).x, .8f);
+    ASSERT_EQ(destination.get<Position>(e1).y, .0f);
+    ASSERT_TRUE(destination.has<Relationship>(e1));
+    ASSERT_EQ(destination.get<Relationship>(e1).parent, e0);
+
+    ASSERT_FALSE(destination.valid(e2));
+    ASSERT_EQ(destination.current(e2), v2);
+
+    ASSERT_TRUE(destination.valid(e3));
+    ASSERT_TRUE(destination.has<Timer>(e3));
+    ASSERT_EQ(destination.get<Timer>(e3).duration, 1000);
+    ASSERT_EQ(destination.get<Timer>(e3).elapsed, 0);
+}
+
+TEST(Snapshot, Continuous) {
+    std::stringstream storage;
+
+    entt::DefaultRegistry source;
+    entt::DefaultRegistry destination;
+
+    std::vector<entt::DefaultRegistry::entity_type> entities;
+    for(auto i = 0; i < 10; ++i) {
+        entities.push_back(source.create());
+    }
+
+    for(auto entity: entities) {
+        source.destroy(entity);
+    }
+
+    auto e0 = source.create();
+    source.assign<Position>(e0, 0.f, 0.f);
+    source.assign<Relationship>(e0, e0);
+
+    auto e1 = source.create();
+    source.assign<Position>(e1, 1.f, 1.f);
+    source.assign<Relationship>(e1, e0);
+
+    auto e2 = source.create();
+    source.assign<Position>(e2, .2f, .2f);
+    source.assign<Relationship>(e2, e0);
+
+    auto e3 = source.create();
+    source.assign<Timer>(e3, 1000, 1000);
+    source.assign<Relationship>(e3, e2);
+
+    {
+        // output finishes flushing its contents when it goes out of scope
+        cereal::JSONOutputArchive output{storage};
+        source.snapshot().entities(output).component<Position, Relationship, Timer>(output);
+    }
+
+    cereal::JSONInputArchive input{storage};
+    entt::ContinuousLoader<entt::DefaultRegistry::entity_type> loader{destination};
+    loader.entities(input)
+            .component<Position>(input)
+            .component<Relationship>(input, &Relationship::parent)
+            .component<Timer>(input);
+
+    ASSERT_FALSE(destination.valid(e0));
+    ASSERT_TRUE(loader.has(e0));
+
+    auto l0 = loader.map(e0);
+
+    ASSERT_TRUE(destination.valid(l0));
+    ASSERT_TRUE(destination.has<Position>(l0));
+    ASSERT_EQ(destination.get<Position>(l0).x, 0.f);
+    ASSERT_EQ(destination.get<Position>(l0).y, 0.f);
+    ASSERT_TRUE(destination.has<Relationship>(l0));
+    ASSERT_EQ(destination.get<Relationship>(l0).parent, l0);
+
+    ASSERT_FALSE(destination.valid(e1));
+    ASSERT_TRUE(loader.has(e1));
+
+    auto l1 = loader.map(e1);
+
+    ASSERT_TRUE(destination.valid(l1));
+    ASSERT_TRUE(destination.has<Position>(l1));
+    ASSERT_EQ(destination.get<Position>(l1).x, 1.f);
+    ASSERT_EQ(destination.get<Position>(l1).y, 1.f);
+    ASSERT_TRUE(destination.has<Relationship>(l1));
+    ASSERT_EQ(destination.get<Relationship>(l1).parent, l0);
+
+    ASSERT_FALSE(destination.valid(e2));
+    ASSERT_TRUE(loader.has(e2));
+
+    auto l2 = loader.map(e2);
+
+    ASSERT_TRUE(destination.valid(l2));
+    ASSERT_TRUE(destination.has<Position>(l2));
+    ASSERT_EQ(destination.get<Position>(l2).x, .2f);
+    ASSERT_EQ(destination.get<Position>(l2).y, .2f);
+    ASSERT_TRUE(destination.has<Relationship>(l2));
+    ASSERT_EQ(destination.get<Relationship>(l2).parent, l0);
+
+    ASSERT_FALSE(destination.valid(e3));
+    ASSERT_TRUE(loader.has(e3));
+
+    auto l3 = loader.map(e3);
+
+    ASSERT_TRUE(destination.valid(l3));
+    ASSERT_TRUE(destination.has<Timer>(l3));
+    ASSERT_EQ(destination.get<Timer>(l3).duration, 1000);
+    ASSERT_EQ(destination.get<Timer>(l3).elapsed, 0);
+    ASSERT_TRUE(destination.has<Relationship>(l3));
+    ASSERT_EQ(destination.get<Relationship>(l3).parent, l2);
+}