ECSTASY
All in the name
Loading...
Searching...
No Matches
Tutorial

Note
The tutorials on this page will help you to use ecstasy, not to understands the concepts and terms. I strongly advise you to look at the glossary before following any tutorials (except the getting started).

Getting Started

Here is a basic example of how to use the ECS. The usual order when using ecstasy is the following:

  • Define the components and systems (here Position, Velocity and Movement)
  • Create the Registry
  • Register the Systems
  • Populate the register with Entities
  • Run the registry systems with runSystems

In this example we create 10 entities. Every entities have a position component and only even entities have a velocity component. The Movement system will iterate on every entities having a position and a velocity component and add the velocity vector to the position. (Know this is not a perfect approach because the velocity should depend on the elapsed time).

Finally we run the systems over and over.

struct Position {
float x;
float y;
};
struct Velocity {
float x;
float y;
};
struct Movement : public ecstasy::ISystem {
void run(ecstasy::Registry &registry) override final
{
for (auto [position, velocity] : registry.query<Position, const Velocity>()) {
position.x += velocity.x;
position.y += velocity.y;
}
}
};
int main() {
// Declare your registry
// Register your systems
registry.addSystem<Movement>();
// Populate the registry with some entities
for (int i = 0; i < 10; i++) {
auto builder = registry.entityBuilder();
builder.with<Position>(i * 2, i * 10);
if (i % 2 == 0)
builder.with<Velocity>(i * 10, i * 2);
ecstasy::Entity entity = builder.build();
// If needed, use the entity
}
while (true) {
registry.runSystems();
}
}
int main(int argc, char **argv)
Definition main.cpp:22
System interface, base class of all systems.
Associative Map to store entity components.
Registry class definition.
Encapsulate an index to an entity.
Definition Entity.hpp:35
System interface, base class of all systems.
Definition ISystem.hpp:28
virtual void run(Registry &registry)=0
Run the system.
EntityBuilder & with(Args &&...args)
Add a component to the builder target entity.
Definition Registry.hpp:566
Base of an ECS architecture.
Definition Registry.hpp:82
EntityBuilder entityBuilder() noexcept
Create a new entity builder.
Definition Registry.cpp:33
void runSystems()
Run all systems present in the registry.
Definition Registry.cpp:92
S & addSystem(Args &&...args)
Add a new system in the registry.
Definition Registry.hpp:839

Using entities

Creating entities

There are 3 ways to create new entities in the registry:

The builders allows to create entities with components easily whereas the create() method is for internal purposes. They have the same syntax except one difference: the registry builder takes components as template parameters, and fetch the associated storages whereas the entities builder takes storages directly.

The builders have two methods:

  • with(): Adds a new component to the entities (supports constructor parameter or explicit std::initializer_list)
  • build(): Finalize the entity creation
Note
The with() method returns the builder, allowing to chain the calls.
Since the entities builder with() method takes the storage in parameters, it can deduce the storage/component type. You don't have to use the template parameters.
Warning
You should never try to use persistent pointers/reference to components. A component memory address is unstable and can change due to its storage. For example adding a new element to the storage may make the storage resize and move its components. Therefore you should be very carefull when using this inside a component constructor for example. In case you really need an identifier, use the entity ID.

Here is an example using the two builders:

ecstasy::Entities &entities = registry.getEntities();
// Registry builder
ecstasy::Entity e1 = registry.entityBuilder().with<Position>(2, 10)
.with<Velocity>(10, 2)
.build();
// Entities builder
ecstasy::Entity e2 = entities.builder().with(registry.getStorage<Position>(), 2, 10)
.with(registry.getStorage<Velocity>(), 10, 2)
.build();
Builder & with(S &storage, Args &&...args)
Add a component to the builder target entity.
Definition Entities.hpp:68
Resource holding all the Registry entities.
Definition Entities.hpp:30
Builder builder()
Create a new entity builder.
Definition Entities.cpp:65
Entity build()
Finalize the entity, making it alive.
Definition Registry.cpp:22
const getStorageType< C > & getStorage() const
Get the Storage for the component type C.
Definition Registry.hpp:973
ResourceReference< const Entities, Locked > getEntities() const
Get the Entities resource.

Using type resolution

You can add Component storages, Resources, Systems, or even the Registry instance to the with template parameters. This will fetch them from the registry and forward them to the component constructor.

struct NeedStorage {
NeedStorage(const ecstasy::MapStorage<Position> &aStorage) : storage(aStorage)
{
}
};
.with<NeedStorage, Position>() // No need to send registry.getStorage<Position>() as parameters
.build();
Associative Map to store entity components Recommended for sparse components.

Manage entity component

When you have an instance of an Entity you can check the presence of a component, get it or add one. Always by sending the component storage. However, you can also use the RegistryEntity class which, as its name says, is linked to a registry. It is only syntactic sugar to avoid fetching yourself storages from the registry.

Examples:

// Populate the registry...
// Entity class
ecstasy::Entity entity = registry.getEntity(0); // Get your entity
auto &positionStorage = registry.getStorage<Position>();
entity.has(storage); // check if the entity has the component Position
entity.add(storage, 5, 12) // add a new component Position(5, 12)
auto &pos = entity[storage]; // Get the position component
pos = entity.get(storage); // Same as above
// RegistryEntity class
ecstasy::RegistryEntity entity = ecstasy::RegistryEntity(entity, registry); // Link your entity to the registry
entity.has<Position>(); // check if the entity has the component Position
entity.add<Position>(5, 12) // add a new component Position(5, 12)
pos = entity.get<Position>(); // Same as above
bool has(S &storage) const
Test if the entity has an associated component in the storage S.
Definition Entity.hpp:216
S::Component & add(S &storage, Args &&...args)
Add a component to the entity.
Definition Entity.hpp:111
const S::Component & get(S &storage) const
Try to fetch the instance of component C associated to the current entity.
Definition Entity.hpp:178
Entity containing a reference to the registry.
const C & get() const
Try to fetch the instance of component C associated to the current entity.
C & add(Args &&...args)
Add a component to the entity.
bool has() const
Test if the entity has an associated component in the storage S.
Entity getEntity(Entity::Index index)
Get the Entity at the index index.
Definition Registry.cpp:38

Delete entities

First I need to define two words used in the entities context

  • kill: mark an entity for deletion without deleting it, until a call to Entities.maintain()
  • erase: delete the entity instantly, and its components if any (and accessible)
Note
You should never erase entities when iterating on them. Kill them instead.

If you don't want to wory about killing inside the loop and calling the maintain after you can use the simple DeletionStack helper. You can push entities to delete in it and they will be erased when the deletion stack is destroyed.

Examples:

auto &entities = registry.getEntities();
// Using kill/maintain
for (auto [entity] : registry.query<ecstasy::Entities>()) {
entities.kill(entity);
}
entities.maintain();
// Deletion stack
{
ecstasy::DeletionStack delStack(registry);
for (auto [entity] : registry.query<ecstasy::Entities>()) {
delStack.push(entity);
}
} // delStack is out of scope -> destroyed, and entities are erased
Helper to manage entity deletion safely within an iteration.
bool kill(Entity entity)
Mark en entity for deletion.
Definition Entities.cpp:96
std::vector< Entity > maintain()
Effectively erase the entities marked for deletion (using kill()).
Definition Entities.cpp:118
Namespace containing all symbols specific to ecstasy.
Definition ecstasy.hpp:30

Implement a custom storage

You can specify which storage you want a component to use with the define SET_COMPONENT_STORAGE. By default the MapStorage is used for all components but you may implement a more suitable storage based on your component.

To create a custom storage, you need to validate the IsStorage concept. In addition you also need to implement the contains and emplace methods (see MapStorage).

Making registry queries

When you populated your registry with entities and resources you can query them with the... query() method. It will iterate on every entities having the requested components and return a reference of them.

The easiest syntax is to use tuple unpacking in a for loop:

Note
You should get used to querying your types as const if you don't modify them because of thread safety
for (auto [position, velocity] : registry.query<Position, const Velocity>()) {
position.x += velocity.x;
position.y += velocity.y;
}

You can also parallelized your query in multiple threads using the splitThreads query method:

// Will make one thread for every 50 matching entities
registry.query<Position, const Velocity>().splitThreads(50, [](auto components) {
auto [position, velocity] = components;
position.x += velocity.x;
position.y += velocity.y;
});
RegistrySelectStackQuery< thread::AUTO_LOCK_DEFAULT, queryable_type_t< C >, queryable_type_t< Cs >... > query()
Construct a query for the given components.
Note
Batch queriess (multi threaded) are not always better than single threaded queries. The more complex the task is for each entity, the more you may improve performances with batch queries.

You can request as many component as you want in the template parameters of the query method.

If you need more complex queries, check the following parts.

Select ... Where ...

Sometimes you need to check whether an entity has a given component but you don't need to access it. Let's say you have a Dynamic component required to move your entities but it is only a marker, it stores no data.

You can use the Select ... Where ... syntax:

for (auto [position, velocity] : registry.select<Position, const Velocity>().where<const Dynamic>()) {
position.x += velocity.x;
position.y += velocity.y;
}

This syntax retrieve only the components in the Select clause but it ensures all components in the Select and Where clauses are present.

Note
You can have the same component in the select and the where clauses. By default all components in Select not present in Where clause are implicitly appended to the where clause.

Using modifiers

Even more complex queries, not sure everything here will be usefull but anyway it is implemented ! By default, all components in the query are joined together with a and operator: component A and component B and component C must be present. To modify this behavior you can use modifiers, at the moment the modifiers implemented are mostly boolean operators.

Note
Modifiers can be nested and used in simple queries as well as in Select ... Where ...
  1. ecstasy::Maybe

    The first and important modifier is the Maybe operator. It match on any entity and instead of returning a reference to the component it will returns a std::optional containing (or not) the component. Here comes an example:

    for (auto [position, velocity, mDensity] : registry.query<Position, const Velocity, Maybe<const Density>>()) {
    float multiplier = 1;
    // Density is a std::optional<std::reference_wrappper<const Density&>>
    if (mDensity)
    multiplier = mDensity->value;
    position.x += velocity.x * multiplier;
    position.y += velocity.y * multiplier;
    }
  2. ecstasy::Not

    This one is kinda explicit. We will use a reverse example of the previous Dynamic marker component:

    Warning
    Don't use in Select clause because it doesn't make sense to select non existing data.
    for (auto [position, velocity] : registry.select<Position, const Velocity>().where<Not<const Static>>()) {
    position.x += velocity.x;
    position.y += velocity.y;
    }
  3. Or / Xor

    Performs a Or/XOr between the given components. Returned values are of type std::tuple<std::optional<Q1>, std::optional<Q2>...>. It works for at least two components but can take more than 2. I really don't know real use case of this but well that was fun to code. For an easier access on the tuple you could leave the Or in the where clause and add Maybe selects in the Select:

    // Assuming positions is a std::vector<Position>
    for (auto [position, positions, velocity] : registry.select<Maybe<Position>, Maybe<Positions>, const Velocity>().where<Or<Position, Positions>>()) {
    if (position) {
    position.x += velocity.x;
    position.y += velocity.y;
    } else {
    for (auto pos in positions.value()) {
    pos.x += velocity.x;
    pos.y += velocity.y;
    }
    }
    }
  4. ecstasy::And

    Same as Or but performs an And. Meaning the returned values are not optional. It looks useless alone because it is the default behavior. But If you are creative you can do something fun with nested modifier... or just ignore that functionnality until you need it (if this times comes)

    // Assuming working with 2D or 3D (yes this is dumb you should do 2 systems)
    for (auto [position2, position3, velocity2, velocity3] : registry
    .select<Maybe<Position2D>, Maybe<Position3D>, Maybe<const Velocity2D>, Maybe<const Velocity3>>()
    .where<Or<And<Position2D, const Velocity2D>, And<Position3D, const Velocity3D>>>()) {
    if (position2) {
    position2.x += velocity2.x;
    position2.y += velocity2.y;
    } else {
    position3.x += velocity3.x;
    position3.y += velocity3.y;
    position3.z += velocity3.z;
    }
    }

Using conditions

In addition to the different modifiers, you can add runtime conditions to your query. Basic comparisons are implemented but you can add custom ones.

Conditions supports comparisons with:

  • Constant values: 0
  • Class member: &Life::value
  • Class getter: &Life::getValue
  1. ecstasy::EqualTo

    Ensure the two operands are equal for each matching entities.

    for (auto [life] : registry.select<const Life>().where<ecstasy::EqualTo<&Life::value, 0>>()) {
    // Condition equals: if (!(life.value == 0)) continue;
    }
  2. ecstasy::NotEqualTo

    Ensure the two operands are not equal for each matching entities.

    for (auto [life] : registry.select<const Life>().where<ecstasy::NotEqualTo<&Life::value, 0>>()) {
    // Condition equals: if (!(life.value != 0)) continue;
    }
  3. ecstasy::Less

    Ensure the first operand is strictly lower than the second operand.

    for (auto [life] : registry.select<const Life>().where<Shield, ecstasy::Less<&Life::value, &Shield::getValue>>()) {
    // Condition equals: if (!(life.value < shield.getValue())) continue;
    }
  4. ecstasy::LessEqual

    Ensure the first operand is lower than or equal to the second operand.

    for (auto [life] : registry.select<const Life>().where<Shield, ecstasy::LessEqual<&Life::value, &Shield::getValue>>()) {
    // Condition equals: if (!(life.value <= shield.getValue())) continue;
    }
  5. ecstasy::Greater

    Ensure the first operand is strictly greater than the second operand.

    for (auto [life] : registry.select<const Life>().where<Shield, ecstasy::Greater<&Life::value, &Shield::getValue>>()) {
    // Condition equals: if (!(life.value > shield.getValue())) continue;
    }
  6. ecstasy::GreaterEqual

    Ensure the first operand is greater than or equal to the second operand.

    for (auto [life] : registry.select<const Life>().where<Shield, ecstasy::GreaterEqual<&Life::value, &Shield::getValue>>()) {
    // Condition equals: if (!(life.value >= shield.getValue())) continue;
    }

Using systems

To create systems, you only have to create a new class inheriting the ISystem class and implement the run() method.

For example here is how you would create a Movement system:

struct Movement : public ecstasy::ISystem {
void run(ecstasy::Registry &registry) override final
{
for (auto [position, velocity] : registry.query<Position, const Velocity>()) {
position.x += velocity.x;
position.y += velocity.y;
}
}
};
// Add it to the registry
registry.addSystem<Movement>();
registry.runSystem<Movement>();
// or
registry.runSystems();
void runSystem()
Run a specific system from the registry.

Pipeline

By default systems added with Registry.addSystem() run in the registration order. But you can use pipelines to have a better configuration of your systems run order.

You can group your systems into phases which will have an associated priority, being also its identifier. Some predefined phases already exists and may be enough for you: PredefinedPhases.

Note
If you don't specify a phase, the OnUpdate is used.
Warning
I strongly recommend you to use enum types, or const values/macros.
// Default to OnUpdate
registry.addSystem<A>();
// Explicit OnUpdate, require to be casted as size_t if template parameter
registry.addSystem<B, static_cast<size_t>(Pipeline::PedefinedPhases::OnUpdate)>();
// Explicit OnLoad but using enum value directly
registry.addSystemInPhase<C>(Pipeline::PedefinedPhases::OnLoad);
registry.addSystem<D, 250>();
registry.addSystem<E, 251>();
registry.addSystem<F, Pipeline::PedefinedPhases::OnUpdate>();
// Will run in order:
// - OnLoad(100): C
// - Custom 250: D
// - Custom 251: E
// - OnUpdate(400): BCF
registry.runSystems();
// If you want you can still call systems one by one
registry.runSystem<A>(); // A
// Or even phase by phase
registry.runSystemsPhase<Pipeline::PredefinedPhases::OnUpdate>();
void runSystemsPhase(Pipeline::PhaseId phase)
Run all systems present in the registry for the given phase.
Definition Registry.cpp:97
S & addSystemInPhase(T phaseId, Args &&...args)
Add a new system in the registry in a specific phase.
Definition Registry.hpp:860

Timers

By default, phases and systems are runned at each frame (ie each registry.run() call). However you may want to limit them to run every two frames, or every 5 seconds. This is possible through the Timer class.

Every ISystem and Phase have a timer instance accessible through getTimer() getter.

Interval

Want to limit your system to run at a specific time ? Use setInterval() function:

Warning
Setting an interval means it will always wait at least the required interval between two systems calls but it can be longer.
registry.addSystem<MySystem>().getTimer().setInterval(std::chrono::milliseconds(500));
// Thanks to std::chrono, you can easily use other time units
registry.addSystem<MySystem>().getTimer().setInterval(std::chrono::seconds(5));
// Set render to every 16ms -> ~60Hz
registry.getPipeline().getPhase(Pipeline::PredefinedPhases::OnStore).getTimer().setInterval(std::chrono::milliseconds(16));
constexpr Timer & getTimer() noexcept
Get the phase timer.
Definition Pipeline.hpp:150
Phase & getPhase(PhaseId id, bool autoRegister=true)
Get a Phase instance from its identifier.
Definition Pipeline.cpp:76
constexpr Pipeline & getPipeline() noexcept
Get a reference to the registry pipeline.
void setInterval(Interval interval) noexcept
Set the Interval of the timer.
Definition Timer.cpp:51

Rate

Want to limit your system to run at a frame frequency instead ? Use setRate() function:

// Will run every two frames (ie run one, skip one)
registry.addSystem<MySystem>().getTimer().setRate(2);
// Will render every 5 frames
registry.getPipeline().getPhase(Pipeline::PredefinedPhases::OnStore).getTimer().setRate(5);
void setRate(std::uint32_t rate) noexcept
Set the Rate of the timer.
Definition Timer.cpp:33

Tips when using timers on Systems and Phases

The one sentence to remember about combining system and phases timers is this one: The system timer is only evaluated if the phase timer succeed.

Below are some resulting behaviors.

  1. System and Phase rates

    Combined rates are multiplied. Inverting the values will have the same behaviors. Ex: Rate limit of 5 on the system and 2 on the phase will result in a system rate of 10 frames.

  2. Phase interval and system rate

    The final interval is the phase interval multiplied by the system rate. Ex: Interval of 5s with rate of 3 will result in a system interval of 15s.

  3. Phase rate (R) and system interval (I)

    The system will be called at frames when the frame id is a multiple of the R and at least I seconds elapsed since last system call.

  4. Phase and system intervals

    The longest interval need to be satisfied. They are not added.

Using resources

Creating a resource is even simpler than creating a system: you only have to inherit IResource. And in case you cannot modify the inheritance tree of your object, you can just declare a using with ObjectWrapper (like PendingActions ).

Note
getResource() will return a ResourceReference (optional thread safe proxy) and you will therefore need to use the get() method.

For example here is how you would create and use an absolutely must have Counter system:

class Counter : public ecstasy::IResource {
int value;
Counter(int initial = 0)
{
this->value = initial;
}
void count()
{
++this->value;
}
};
// Add it to the registry
registry.addResource<Counter>(5);
// Get and use
registry.getResource<Counter>().get().count();
registry.getResource<Counter>().get().value;
Base class of all registry resources.
Base class of all registry resources.
Definition IResource.hpp:33
ResourceReference< const R, Locked > getResource() const
Get the Resource of type R.
Definition Registry.hpp:937
R & addResource(Args &&...args)
Add a new resource in the registry.
Definition Registry.hpp:884

Ensuring Thread Safety

By default ecstasy is not thread safe. By not thread safe I mean there is absolutely nothing enabled to prevent your threads to do whatever they want at the same time.

But no worries it is just some compilation options to set:

  • ECSTASY_LOCKABLE_RESOURCES will make IResource class validate the Lockable concept.
  • ECSTASY_LOCKABLE_STORAGES will make IStorage class validate the Lockable concept.
  • ECSTASY_AUTO_LOCK will lock any Lockable queryables in any registry queries or registry modifiers.
  • ECSTASY_THREAD_SAFE will enable the three options above.

This being done every lockables will be locked (if ECSTASY_AUTO_LOCK is set) when performing queries from the registry:

  • If they are queried const qualified they will be shared lock, meaning multiple threads can access the queryables at the same time in a read only way.
  • However if they are not const qualified the lock will be exclusive, waiting any thread reading (or writing) the queryable and then blocking any other thread to access it until unlocked.

This allows a generic thread safety while reducing thread contention as much as possible.

If you want to handle thread safety yourself you still have multiple options:

  1. Get it your own way

    Just disable the above options and do what you want, but outside the queries because of encapsulation you will not have the same generic access.

  2. Lock explicitly

    Keep the resources/storages as lockable and use the queryEx "Registry::queryEx" (and Select.whereEx) methods or AndEx, OrEx modifiers where you can explicitly specify the AutoLock value.

  3. AutoLock by default but disable when you know what you're doing

    I know sometimes ecstasy can lock multiple times the same mutex because it is hard to detect. But if you know it and it is sensitive systems (running lot of times per frame) you can use the *Ex methods to UnLock explicitly the same way.

Using cross platform RTTI

Why use custom RTTI implementation

C++ already has its own RTTI integration with std::type_info (as returned by typeid). With this you can get a unique hash for each type using std::type_info::hash_code(). However, this hash is not cross platform since it is implementation specific. And most important, it is not guaranteed to stay the same between 2 runs.

As explained in the std::type_info::hash_code() documentation:

Returns an unspecified value (here denoted by hash code) such that for all std::type_info objects referring to the same type,
their hash code is the same.

No other guarantees are given: std::type_info objects referring to different types may have the same hash code
(although the standard recommends that implementations avoid this as much as possible),
and hash code for the same type can change between invocations of the same program.

It also returns a std::type_info::name() function, but its results are mangled in g++ (maybe clang too) but not in MSVC.

For all these reasons, it make cross-platform serialization of custom types impossible using only std::type_info.

How to use it

How is it stored

This custom rtti use mainly two classes: TypeRegistry and AType.

The TypeRegistry is a singleton storing all AType instances.

The AType is the replacement of std::type_info. It is mainly a wrapper over the std::type_info with a user defined name (which is supposed to be the same for every program run) and therefore a cross platform hash which is the hash of the name.

And it also contains the IEntityComponentSerializer instances for the serialization part (see Serializing your entities section)

Registering types

First of all you need to register the types for which you want cross-platform RTTI using the macro REGISTER_TYPES.

// The macro is defined in this included
struct Position {
float x;
float y;
};
struct Size {
float width;
float height;
};
// You can register as much types as you want (variadic macro)
REGISTER_TYPES(Position, Size);
#define REGISTER_TYPES(TYPE,...)
Register multiple component types.
Note
I tried to do automatic type registration (ie not requiring to use a macro), but for all the above reasons, I couldn't find a way to do it.
The macro REGISTER_SERIALIZABLES will also register the type.

You can also do it without the macro like this:

// The macro is defined in this included
struct Position {
float x;
float y;
};
// This returns a reference to the new @ref AType created.
static TypeRegistry & getInstance() noexcept
Get the Instance object.
AType & registerType(std::string_view name)
Register a type in the registry.

Querying types

The type registry can be queried from multiple data types:

Serializing your entities

Ecstasy has some built in serialization helpers in the ecstasy::serialization namespace.

The current available serializers are the following:

  • RawSerializer: Custom binary serialization of components fields. Compact form but not related to any RFC.
  • JsonSerializer: Classic json serialization of components. Not compact but readable.

If you need a missing serializer, you can write it yourself by inheriting the Serializer class. In case of a commonly used serializer type (json/yaml/NDR...) feel free to open an issue about it.

Warning
The RawSerializer uses a std::stringstream. Therefore there are cursor positions, if you need to read twice the data, you need to reset the read cursor position.

Using a serializer

Following examples below will be using the RawSerializer type and the example component Position:

struct Position {
public:
float x;
float y;
};

Common types

Serializer should support fundamental and container types by default but it depends on the serializer implementation.

Serializer Fundamental (integers, floats, doubles...) strings containers
RawSerializer Ok OK Ok
JsonSerializer Ok OK Ok

1. Saving

To save an object in a serializer, you can either call the method save or use the insertion operator **<<**

RawSerializer serializer();
// Either with method save
serializer.save(42);
// Or with insertion operator
serializer << 42;

2. Exporting/Importing

You can export your serialized data using the export* methods. And import them back with import* methods.

RawSerializer serializer();
// Save all your variables...
// Export to a string
std::string serialized = serializer.exportBytes();
// Or to a file
serializer.exportFile("mydata.bin");
// Or to a stream
serializer.exportStream(std::cout);
RawSerializer importSerializer(serialized);
// Or import from bytes on existing serializer
importSerializer.importBytes(serialized);
// Or from a file
importSerializer.importFile("mydata.bin");
// Or from a stream
importSerializer.importStream(std::cin);

3. Updating

To update an object from a serializer, you can either call the method update or use the extraction operator **>>**

RawSerializer serializer(std::filesystem::path("mydata.bin"));
int my_number = 0;
// Either with method update
serializer.update(my_number);
// Or with extraction operator
serializer >> my_number;

4. Loading

To load an object from a serializer, you need to call the load template method.

Note
If there is no load method implemented for the type but the type is default constructible, it will be default constructed then updated.
RawSerializer serializer(std::filesystem::path("mydata.bin"));
// Fill your serializer with previously saved data using import methods
int my_number = serializer.load<int>();

Custom types

If you are working with custom type (ie components) you can implement the extraction (**>>**) and insertion operator (**<<**) with the said serializer type for the save/_update_. And a constructor taking the serializer as parameter for the load.

Note
The operators doesn't have to be inside the class, you can define it outside the class if you don't have access to the class definition. However it cannot work for the constructor.
// Custom constructor for load method
Position(RawSerializer &serializer) : x(serializer.load<float>()), y(serializer.load<float>())
{
}
// Extraction operator for save
RawSerializer &operator>>(RawSerializer &serializer) const
{
return serializer << x << y
}
// Insertion operator for update
Position &operator<<(RawSerializer &serializer)
{
serializer >> x >> y;
return *this;
}
std::basic_ostream< Char > & operator<<(std::basic_ostream< Char > &lhs, node_type rhs)

If you need to (de)serialize the type from the save/update/loadEntity methods, you first need to register the type as serializable by the expected Serializer using the variadic macro REGISTER_SERIALIZABLES

For example, if you want you type Position to be serializable by the RawSerializer and the (maybe to come) JsonSerializer:

REGISTER_SERIALIZABLES(Position, RawSerializer, JsonSerializer)
#define REGISTER_SERIALIZABLES(COMPONENT, a1,...)
Register a component to multiple serializers.

Working with Entities

Since you can serialize any type, you can serialize entity components manually using the functions above.

You can save entity components explicitly using the templated saveEntity method, or every registered components with the classic saveEntity method.

Warning
To use the non templated saveEntity method, you need to register them using the REGISTER_SERIALIZABLES macro (see below).
RawSerializer serializer();
registry.entityBuilder().with<Position>(1.0f, -8456.0f).with<NPC>(Position(42.f, 0.f), "Steve").build(),
registry);
// Save an entity explicit components
rawSerializer.saveEntity<NPC, Position>(entity);
// Or save the whole entity
rawSerializer.saveEntity(entity);
// Update an existing entity
rawSerializer.updateEntity(entity);
// Or create it entirely
ecstasy::RegistryEntity newEntity = rawSerializer.loadEntity(registry);