Skip to content

Commit

Permalink
Added ECS storage Serialise and Deserialise functions. #70
Browse files Browse the repository at this point in the history
Component now stores function pointers to Serialise and Deserialise functions and writes/reads them to fil in binary.
Serialise and Deserialise are optional functions, if an Archetype has any non-serialisable components it will not be saved.
Added unit test for ECS Storage serialisation.
Added a Save Version counter to config.
  • Loading branch information
MStachowicz committed Apr 1, 2024
1 parent 45a7254 commit 86cc0a5
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 24 deletions.
55 changes: 37 additions & 18 deletions source/ECS/Component.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

namespace ECS
{
constexpr size_t Max_Component_Count = 32;
using ComponentID = size_t; // Unique identifier for any type passed into ECSStorage.
using ComponentBitset = std::bitset<Max_Component_Count>;
using ComponentID = uint8_t; // Unique identifier for any type passed into ECSStorage.
constexpr size_t Max_Component_Count = std::numeric_limits<ComponentID>::max() + 1;
using ComponentBitset = std::bitset<Max_Component_Count>; // Bitset to represent the presence of Components.

// Stores per ComponentType information ECS needs after type erasure.
class ComponentData
Expand All @@ -29,18 +29,21 @@ namespace ECS
ComponentID ID; // Unique ID/index of the Type. Corresponds to the index in the ComponentRegister::type_infos vector.
size_t size; // sizeof of the Type
size_t align; // alignof of the type
bool is_serialisable; // If the type is serialisable (has Serialise and Deserialise functions).
// Call the destructor of the object at p_address_to_destroy.
void (*Destruct)(void* p_address_to_destroy);
// move-assign the object pointed to by p_source_address into the memory pointed to by p_destination_address.
void (*MoveAssign)(void* p_destination_address, void* p_source_address);
// placement-new move-construct the object pointed to by p_source_address into the memory pointed to by p_destination_address.
void (*MoveConstruct)(void* p_destination_address, void* p_source_address);
// // Serialise the object into p_file.
// void (*Serialise)(void* p_address, std::ofstream& p_file);
// // Deserialise the object into p_file.
// void (*Deserialise)(void* p_address, std::ifstream& p_file);
// Serialise the object at p_address into p_out. (Optional function)
void (*Serialise)(void* p_address, std::ofstream& p_out, uint16_t p_version);
// Deserialise the object into p_destination_address from p_in. (Optional function)
void (*Deserialise)(void* p_destination_address, std::ifstream& p_in, uint16_t p_version);
};

// API for interfacing with Component's types/data after type erasure.
// Acts similar to a base class by storing static data required for an ECS::ComponentType to be valid.
class Component
{
static inline std::array<std::optional<ComponentData>, Max_Component_Count> type_infos = {};
Expand Down Expand Up @@ -90,12 +93,24 @@ namespace ECS
}
};

template <typename T>
concept Serializable = requires(T a, std::ofstream& out, uint16_t version)
{
{ T::Serialise(a, out, version) } -> std::same_as<void>;
};
template <typename T>
concept Deserializable = requires(std::ifstream& in, uint16_t version)
{
{ T::Deserialise(in, version) } -> std::same_as<T>;
};

// Construct the ComponentData for a ComponentType.
template <typename ComponentType>
ComponentData::ComponentData(Meta::PackArg<ComponentType>)
: ID{Component::get_ID<ComponentType>()}
, size{sizeof(std::decay_t<ComponentType>)}
, align{alignof(std::decay_t<ComponentType>)}
, is_serialisable{Serializable<std::decay_t<ComponentType>> && Deserializable<std::decay_t<ComponentType>>}
, Destruct{[](void* p_address)
{
using Type = std::decay_t<ComponentType>;
Expand All @@ -111,16 +126,20 @@ namespace ECS
using Type = std::decay_t<ComponentType>;
new (p_destination_address) Type(std::move(*static_cast<Type*>(p_source_address)));
}}
// , Serialise{[](void* p_address, std::ofstream& p_file)
// {
// using Type = std::decay_t<ComponentType>;
// Type::serialise(*static_cast<Type*>(p_address), p_file);
// }}
// , Deserialise{[](void* p_address, std::ifstream& p_file)
// {
// using Type = std::decay_t<ComponentType>;
// *static_cast<Type*>(p_address) = Type::deserialise(p_file);
// }}
{}
, Serialise{[](void* p_address, std::ofstream& p_out, uint16_t p_version)
{
using Type = std::decay_t<ComponentType>;
if constexpr (Serializable<Type>)
Type::Serialise(*static_cast<Type*>(p_address), p_out, p_version);
}}
, Deserialise{[](void* p_destination_address, std::ifstream& p_in, uint16_t p_version)
{
using Type = std::decay_t<ComponentType>;
if constexpr (Deserializable<Type>)
new (p_destination_address) Type(Type::Deserialise(p_in, p_version));
}}
{
static_assert((Serializable<std::decay_t<ComponentType>> == Deserializable<std::decay_t<ComponentType>>), "Component must have both Serialise and Deserialise functions or neither. Did you forget to implement one of the functions or use the wrong function signatures?");
}

} // namespace ECS
164 changes: 163 additions & 1 deletion source/ECS/Storage.cpp
Original file line number Diff line number Diff line change
@@ -1 +1,163 @@
#include "Storage.hpp"
#include "Storage.hpp"

#include "Utility/Serialise.hpp"

namespace ECS
{
// Type definitions for the saving and loading of the storage.
// If these types change or missmatch, saving/loading will break and needs to be handled using p_version.

using Archetype_Count_t = uint64_t;
using Entity_Count_t = uint64_t;
using Component_Count_t = uint64_t;
using ComponentID_t = uint8_t;

static_assert(std::is_same<std::vector<int>::size_type, Archetype_Count_t>::value, "Archetype_Count_t doesn't match Vector::size_type. Update save/load type used.");
static_assert(std::is_same<std::vector<int>::size_type, Entity_Count_t>::value, "Entity_Count_t doesn't match Vector::size_type. Update save/load type used.");
static_assert(std::is_same<std::vector<int>::size_type, Component_Count_t>::value, "Component_Count_t doesn't match Vector::size_type. Update save/load type used.");
static_assert(std::is_same<ComponentID_t, ComponentID_t>::value, "ComponentID_t doesn't match ComponentID. Update save/load type used.");

void Storage::Serialise(const Storage& p_storage, std::ofstream& p_out, uint16_t p_version)
{
//{ECS::Storage save format
//uint16_t : archetypes count (only serialisable ones with entity count > 0 are saved)
// {Start Archetype
// uint32_t : entity/element count (always non-zero)
// uint16_t : component count (always non-zero)
// uint16_t : componentIDs per entity (only serialisable components)
// {Start Entity
// // Serialise each serialisable component in the entity.
// }End Entity
// }End Archetype
//}

// When saving archetypes we only save ones that have entities all their components are serialisable.
// This means we can assume the archetypes are valid and avoid checking on deserialise.

// Lambda to check if an archetype should be saved, depending on if it has entities and any serialisable components.
auto should_save = [](const Archetype& p_archetype) { return !p_archetype.m_entities.empty() && p_archetype.m_is_serialisable; };

Archetype_Count_t archetype_count = std::count_if(p_storage.m_archetypes.begin(), p_storage.m_archetypes.end(), [&](const Archetype& p_archetype)
{ return should_save(p_archetype); });

Utility::write_binary(p_out, archetype_count); // Even if there are no archetypes to save, we still need to save the count to deserialise correctly.
if (archetype_count == 0) // No archetypes to deserialise, return early.
return;

for (const auto& archetype : p_storage.m_archetypes)
{
if (!should_save(archetype))
continue;

Entity_Count_t entity_count = archetype.m_entities.size();
Utility::write_binary(p_out, entity_count);

// Count the number of serialisable components in the archetype.
Component_Count_t component_count = archetype.m_components.size();
Utility::write_binary(p_out, component_count);

// Save the ComponentIDs of the serialisable components in the archetype.
for (const auto& component_layout : archetype.m_components)
{
ASSERT(component_layout.type_info.is_serialisable, "Only serialisable components should be saved.");

ComponentID_t component_ID = component_layout.type_info.ID;
Utility::write_binary(p_out, component_ID);
}

// Save the entities in the archetype.
for (Entity_Count_t i = 0; i < entity_count; ++i)
{
auto index_start_pos = archetype.m_instance_size * i; // Position of the start of the instance at p_instance_index.

for (const auto& component_layout : archetype.m_components)
{
BufferPosition component_start_pos = index_start_pos + component_layout.offset;
component_layout.type_info.Serialise(&archetype.m_data[component_start_pos], p_out, p_version);
}
}
}
}

Storage Storage::Deserialise(std::ifstream& p_in, uint16_t p_version)
{
//{ECS::Storage save format
//uint16_t : archetypes to load (always non-zero)
// {Start Archetype
// uint32_t : entity/element count (always non-zero)
// uint16_t : component count (always non-zero)
// uint16_t : componentIDs per entity
// {Start Entity
// // Deserialise each component in the entity.
// }End Entity
// }End Archetype
//}

// Because we only save archetypes with entities and serialisable components, we can assume they are valid and avoid checking.

Storage storage;

Archetype_Count_t archetype_count;
Utility::read_binary(p_in, archetype_count);

// No archetypes to deserialise, return early.
if (archetype_count == 0)
return storage;

storage.m_archetypes.reserve(archetype_count);

for (Archetype_Count_t i = 0; i < archetype_count; ++i)
{
Entity_Count_t entity_count; // Number of entities in the archetype.
Utility::read_binary(p_in, entity_count);

Component_Count_t component_count; // Number of components in the archetype.
Utility::read_binary(p_in, component_count);

ComponentBitset component_bitset; // Bitset of the components in the archetype.
// Used to keep the order of the components. We need to know the order of the components in the file to Deserialise them correctly.
std::vector<ComponentID_t> component_IDs;
component_IDs.reserve(component_count);

for (Component_Count_t j = 0; j < component_count; ++j)
{
ComponentID_t component_ID;
Utility::read_binary(p_in, component_ID);
component_bitset.set(component_ID);
component_IDs.push_back(component_ID);
}

ArchetypeID archetype_ID = storage.m_archetypes.size();
auto& archetype = storage.m_archetypes.emplace_back(component_bitset);
// Reserve enough size for entity_count entities.
archetype.reserve(next_greater_power_of_2(entity_count));

// If ECS::get_component_layout has changed, the order of the components in the Archetype may not match the order saved in the file.
// Create a vector of ComponentLayouts that matches the order of the components in the file to ensure they are deserialised correctly.
std::vector<ComponentLayout> components;
components.reserve(component_count);
for (const auto& component_ID : component_IDs)
components.push_back(archetype.get_component_layout(component_ID));

for (Entity_Count_t j = 0; j < entity_count; ++j)
{
const auto new_entity = Entity(storage.m_next_entity_ID++);
storage.m_entity_to_archetype_ID.push_back(std::make_optional(std::make_pair(archetype_ID, archetype.m_next_instance_ID)));

{// Add new_entity to the archetype. Similar to Archetype::push_back(Entity, ComponentTypes...)
const BufferPosition index_start_pos = archetype.m_instance_size * archetype.m_next_instance_ID; // Position of the start of the entity instance in the archetype.

for (const auto& component_layout : components)
{
const BufferPosition component_start_pos = index_start_pos + component_layout.offset;
component_layout.type_info.Deserialise(&archetype.m_data[component_start_pos], p_in, p_version);
}

archetype.m_entities.push_back(new_entity);
archetype.m_next_instance_ID++;
}
}
}
return storage;
}
}
24 changes: 23 additions & 1 deletion source/ECS/Storage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <algorithm>
#include <array>
#include <fstream>
#include <iostream>
#include <optional>
#include <utility>
Expand Down Expand Up @@ -118,7 +119,7 @@ namespace ECS
{
if (p_component_bitset[i])
{
const auto& info = Component::get_info(i);
const auto& info = Component::get_info(static_cast<ComponentID>(i));
max_allignof = std::max(max_allignof, info.align);
component_layouts.push_back({0, info});
}
Expand Down Expand Up @@ -184,6 +185,17 @@ namespace ECS
return component_layouts;
}

// Returns true if all of the ComponentTypes in the ComponentBitset are serialisable.
inline bool is_serialisable(const ComponentBitset& p_component_bitset)
{
for (size_t i = 0; i < p_component_bitset.size(); i++)
{
if (p_component_bitset[i] && !Component::get_info(static_cast<ComponentID>(i)).is_serialisable)
return false;
}
return true;
}

// A container of Entity objects and the components they own.
// Every unique combination of components makes an Archetype which is a contiguous store of all the ComponentTypes.
// Storage is interfaced using Entity as a key.
Expand All @@ -197,6 +209,7 @@ namespace ECS
{
ComponentBitset m_bitset; // The unique identifier for this archetype. Each bit corresponds to a ComponentType this archetype stores per ArchetypeInstanceID.
std::vector<ComponentLayout> m_components; // How the ComponentTypes are laid out in each instance of ArchetypeInstanceID.
bool m_is_serialisable; // If all of the ComponentTypes in this archetype are serialisable.
std::vector<Entity> m_entities; // Entity at every ArchetypeInstanceID. Should be indexed only using ArchetypeInstanceID.
size_t m_instance_size; // Size in Bytes of each archetype instance. In other words, the stride between every ArchetypeInstanceID.
ArchetypeInstanceID m_next_instance_ID; // The ArchetypeInstanceID past the end of the m_data. Equivalant to size() in a vector.
Expand All @@ -208,6 +221,7 @@ namespace ECS
Archetype(Meta::PackArgs<ComponentTypes...>) noexcept
: m_bitset{Component::get_component_bitset<ComponentTypes...>()}
, m_components{get_components_layout(m_bitset)}
, m_is_serialisable{is_serialisable(m_bitset)}
, m_entities{}
, m_instance_size{get_stride(m_components)}
, m_next_instance_ID{0}
Expand All @@ -222,6 +236,7 @@ namespace ECS
Archetype(const ComponentBitset& p_component_bitset) noexcept
: m_bitset{p_component_bitset}
, m_components{get_components_layout(m_bitset)}
, m_is_serialisable{is_serialisable(m_bitset)}
, m_entities{}
, m_instance_size{get_stride(m_components)}
, m_next_instance_ID{0}
Expand All @@ -243,6 +258,7 @@ namespace ECS
Archetype(Archetype&& p_other) noexcept
: m_bitset{std::move(p_other.m_bitset)}
, m_components{std::move(p_other.m_components)}
, m_is_serialisable{std::move(p_other.m_is_serialisable)}
, m_entities{std::move(p_other.m_entities)}
, m_instance_size{std::move(p_other.m_instance_size)}
, m_next_instance_ID{std::move(p_other.m_next_instance_ID)}
Expand All @@ -264,6 +280,7 @@ namespace ECS

m_bitset = std::move(p_other.m_bitset);
m_components = std::move(p_other.m_components);
m_is_serialisable = std::move(p_other.m_is_serialisable);
m_entities = std::move(p_other.m_entities);
m_instance_size = std::move(p_other.m_instance_size);
m_next_instance_ID = std::move(p_other.m_next_instance_ID);
Expand Down Expand Up @@ -824,5 +841,10 @@ namespace ECS

return count;
}

// Write the state of the storage to p_file stream.
static void Serialise(const Storage& p_storage, std::ofstream& p_out, uint16_t p_version);
// Construct a Storage from the state in p_file stream.
static Storage Deserialise(std::ifstream& p_in, uint16_t p_version);
};
} // namespace ECS
Loading

0 comments on commit 86cc0a5

Please sign in to comment.