646 lines
18 KiB
C++
646 lines
18 KiB
C++
#ifndef WI_ENTITY_COMPONENT_SYSTEM_H
|
|
#define WI_ENTITY_COMPONENT_SYSTEM_H
|
|
|
|
#include "wiArchive.h"
|
|
#include "wiJobSystem.h"
|
|
#include "wiUnorderedMap.h"
|
|
#include "wiUnorderedSet.h"
|
|
#include "wiVector.h"
|
|
|
|
#include <cstdint>
|
|
#include <cassert>
|
|
#include <atomic>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
// Entity-Component System
|
|
namespace wi::ecs
|
|
{
|
|
// The Entity is a global unique persistent identifier within the entity-component system
|
|
// It can be stored and used for the duration of the application
|
|
// The entity can be a different value on a different run of the application, if it was serialized
|
|
// It must be only serialized with the SerializeEntity() function. It will ensure that entities still match with their components correctly after serialization
|
|
using Entity = uint32_t;
|
|
inline constexpr Entity INVALID_ENTITY = 0;
|
|
// Runtime can create a new entity with this
|
|
inline Entity CreateEntity()
|
|
{
|
|
static std::atomic<Entity> next{ INVALID_ENTITY + 1 };
|
|
return next.fetch_add(1);
|
|
}
|
|
|
|
class ComponentLibrary;
|
|
struct EntitySerializer
|
|
{
|
|
wi::jobsystem::context ctx; // allow components to spawn serialization subtasks
|
|
wi::unordered_map<uint64_t, Entity> remap;
|
|
bool allow_remap = true;
|
|
uint64_t version = 0; // The ComponentLibrary serialization will modify this by the registered component's version number
|
|
wi::unordered_set<std::string> resource_registration; // register for resource manager serialization
|
|
ComponentLibrary* componentlibrary = nullptr;
|
|
wi::unordered_map<std::string, uint64_t> library_versions;
|
|
|
|
~EntitySerializer()
|
|
{
|
|
wi::jobsystem::Wait(ctx); // automatically wait for all subtasks after serialization
|
|
}
|
|
|
|
// Returns the library version of the currently serializing Component
|
|
// If not using ComponentLibrary, it returns version set by the user.
|
|
uint64_t GetVersion() const
|
|
{
|
|
return version;
|
|
}
|
|
uint64_t GetVersion(const std::string& name) const
|
|
{
|
|
auto it = library_versions.find(name);
|
|
if (it != library_versions.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void RegisterResource(const std::string& resource_name)
|
|
{
|
|
if (resource_name.empty())
|
|
return;
|
|
resource_registration.insert(resource_name);
|
|
}
|
|
};
|
|
// This is the safe way to serialize an entity
|
|
inline void SerializeEntity(wi::Archive& archive, Entity& entity, EntitySerializer& seri)
|
|
{
|
|
if (archive.IsReadMode())
|
|
{
|
|
// Entities are always serialized as uint64_t for back-compat
|
|
uint64_t mem;
|
|
archive >> mem;
|
|
|
|
if (mem != INVALID_ENTITY && seri.allow_remap)
|
|
{
|
|
auto it = seri.remap.find(mem);
|
|
if (it == seri.remap.end())
|
|
{
|
|
entity = CreateEntity();
|
|
seri.remap[mem] = entity;
|
|
}
|
|
else
|
|
{
|
|
entity = it->second;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
entity = (Entity)mem;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
#if defined(__GNUC__) && !defined(__SCE__)
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
|
|
#endif // __GNUC__ && !__SCE__
|
|
|
|
archive << entity;
|
|
|
|
#if defined(__GNUC__) && !defined(__SCE__)
|
|
#pragma GCC diagnostic pop
|
|
#endif // __GNUC__ && !__SCE__
|
|
}
|
|
}
|
|
|
|
// This is an interface class to implement a ComponentManager,
|
|
// inherit this class if you want to work with ComponentLibrary
|
|
class ComponentManager_Interface
|
|
{
|
|
public:
|
|
virtual ~ComponentManager_Interface() = default;
|
|
virtual void Copy(const ComponentManager_Interface& other) = 0;
|
|
virtual void Merge(ComponentManager_Interface& other) = 0;
|
|
virtual void Clear() = 0;
|
|
virtual void Serialize(wi::Archive& archive, EntitySerializer& seri) = 0;
|
|
virtual void Component_Serialize(Entity entity, wi::Archive& archive, EntitySerializer& seri) = 0;
|
|
virtual void Remove(Entity entity) = 0;
|
|
virtual void Remove_KeepSorted(Entity entity) = 0;
|
|
virtual void MoveItem(size_t index_from, size_t index_to) = 0;
|
|
virtual bool Contains(Entity entity) const = 0;
|
|
virtual size_t GetIndex(Entity entity) const = 0;
|
|
virtual size_t GetCount() const = 0;
|
|
virtual Entity GetEntity(size_t index) const = 0;
|
|
virtual const wi::vector<Entity>& GetEntityArray() const = 0;
|
|
};
|
|
|
|
// The ComponentManager is a container that stores components and matches them with entities
|
|
// Note: final keyword is used to indicate this is a final implementation.
|
|
// This allows function inlining and avoid calls, improves performance considerably
|
|
template<typename Component>
|
|
class ComponentManager final : public ComponentManager_Interface
|
|
{
|
|
public:
|
|
|
|
// reservedCount : how much components can be held initially before growing the container
|
|
ComponentManager(size_t reservedCount = 0)
|
|
{
|
|
components.reserve(reservedCount);
|
|
entities.reserve(reservedCount);
|
|
lookup.reserve(reservedCount);
|
|
}
|
|
|
|
// Clear the whole container
|
|
inline void Clear()
|
|
{
|
|
components.clear();
|
|
entities.clear();
|
|
lookup.clear();
|
|
}
|
|
|
|
// Perform deep copy of all the contents of "other" into this
|
|
inline void Copy(const ComponentManager<Component>& other)
|
|
{
|
|
components.reserve(GetCount() + other.GetCount());
|
|
entities.reserve(GetCount() + other.GetCount());
|
|
lookup.reserve(GetCount() + other.GetCount());
|
|
for (size_t i = 0; i < other.GetCount(); ++i)
|
|
{
|
|
Entity entity = other.entities[i];
|
|
assert(!Contains(entity));
|
|
entities.push_back(entity);
|
|
lookup[entity] = components.size();
|
|
components.push_back(other.components[i]);
|
|
}
|
|
}
|
|
|
|
// Merge in an other component manager of the same type to this.
|
|
// The other component manager MUST NOT contain any of the same entities!
|
|
// The other component manager is not retained after this operation!
|
|
inline void Merge(ComponentManager<Component>& other)
|
|
{
|
|
components.reserve(GetCount() + other.GetCount());
|
|
entities.reserve(GetCount() + other.GetCount());
|
|
lookup.reserve(GetCount() + other.GetCount());
|
|
|
|
for (size_t i = 0; i < other.GetCount(); ++i)
|
|
{
|
|
Entity entity = other.entities[i];
|
|
assert(!Contains(entity));
|
|
entities.push_back(entity);
|
|
lookup[entity] = components.size();
|
|
components.push_back(std::move(other.components[i]));
|
|
}
|
|
|
|
other.Clear();
|
|
}
|
|
|
|
inline void Copy(const ComponentManager_Interface& other)
|
|
{
|
|
Copy((ComponentManager<Component>&)other);
|
|
}
|
|
|
|
inline void Merge(ComponentManager_Interface& other)
|
|
{
|
|
Merge((ComponentManager<Component>&)other);
|
|
}
|
|
|
|
// Read/Write everything to an archive depending on the archive state
|
|
inline void Serialize(wi::Archive& archive, EntitySerializer& seri)
|
|
{
|
|
if (archive.IsReadMode())
|
|
{
|
|
const size_t prev_count = components.size();
|
|
|
|
size_t count;
|
|
archive >> count;
|
|
|
|
components.resize(prev_count + count);
|
|
for (size_t i = 0; i < count; ++i)
|
|
{
|
|
components[prev_count + i].Serialize(archive, seri);
|
|
}
|
|
|
|
entities.resize(prev_count + count);
|
|
for (size_t i = 0; i < count; ++i)
|
|
{
|
|
Entity entity;
|
|
SerializeEntity(archive, entity, seri);
|
|
entities[prev_count + i] = entity;
|
|
lookup[entity] = prev_count + i;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
archive << components.size();
|
|
for (Component& component : components)
|
|
{
|
|
component.Serialize(archive, seri);
|
|
}
|
|
for (Entity entity : entities)
|
|
{
|
|
SerializeEntity(archive, entity, seri);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Read one single component onto an archive, make sure entity are serialized first
|
|
inline void Component_Serialize(Entity entity, wi::Archive& archive, EntitySerializer& seri)
|
|
{
|
|
if(archive.IsReadMode())
|
|
{
|
|
bool component_exists;
|
|
archive >> component_exists;
|
|
if (component_exists)
|
|
{
|
|
auto& component = this->Create(entity);
|
|
component.Serialize(archive, seri);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto component = this->GetComponent(entity);
|
|
if (component != nullptr)
|
|
{
|
|
archive << true;
|
|
component->Serialize(archive, seri);
|
|
}
|
|
else
|
|
{
|
|
archive << false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a new component and retrieve a reference to it
|
|
inline Component& Create(Entity entity)
|
|
{
|
|
// INVALID_ENTITY is not allowed!
|
|
assert(entity != INVALID_ENTITY);
|
|
|
|
// Only one of this component type per entity is allowed!
|
|
assert(lookup.find(entity) == lookup.end());
|
|
|
|
// Entity count must always be the same as the number of coponents!
|
|
assert(entities.size() == components.size());
|
|
assert(lookup.size() == components.size());
|
|
|
|
// Update the entity lookup table:
|
|
lookup[entity] = components.size();
|
|
|
|
// New components are always pushed to the end:
|
|
components.emplace_back();
|
|
|
|
// Also push corresponding entity:
|
|
entities.push_back(entity);
|
|
|
|
return components.back();
|
|
}
|
|
|
|
// Remove a component of a certain entity if it exists
|
|
inline void Remove(Entity entity)
|
|
{
|
|
auto it = lookup.find(entity);
|
|
if (it != lookup.end())
|
|
{
|
|
// Directly index into components and entities array:
|
|
const size_t index = it->second;
|
|
const Entity entity = entities[index];
|
|
|
|
if (index < components.size() - 1)
|
|
{
|
|
// Swap out the dead element with the last one:
|
|
components[index] = std::move(components.back()); // try to use move instead of copy
|
|
entities[index] = entities.back();
|
|
|
|
// Update the lookup table:
|
|
lookup[entities[index]] = index;
|
|
}
|
|
|
|
// Shrink the container:
|
|
components.pop_back();
|
|
entities.pop_back();
|
|
lookup.erase(entity);
|
|
}
|
|
}
|
|
|
|
// Remove a component of a certain entity if it exists while keeping the current ordering
|
|
inline void Remove_KeepSorted(Entity entity)
|
|
{
|
|
auto it = lookup.find(entity);
|
|
if (it != lookup.end())
|
|
{
|
|
// Directly index into components and entities array:
|
|
const size_t index = it->second;
|
|
const Entity entity = entities[index];
|
|
|
|
if (index < components.size() - 1)
|
|
{
|
|
// Move every component left by one that is after this element:
|
|
for (size_t i = index + 1; i < components.size(); ++i)
|
|
{
|
|
components[i - 1] = std::move(components[i]);
|
|
}
|
|
// Move every entity left by one that is after this element and update lut:
|
|
for (size_t i = index + 1; i < entities.size(); ++i)
|
|
{
|
|
entities[i - 1] = entities[i];
|
|
lookup[entities[i - 1]] = i - 1;
|
|
}
|
|
}
|
|
|
|
// Shrink the container:
|
|
components.pop_back();
|
|
entities.pop_back();
|
|
lookup.erase(entity);
|
|
}
|
|
}
|
|
|
|
// Place an entity-component to the specified index position while keeping the ordering intact
|
|
inline void MoveItem(size_t index_from, size_t index_to)
|
|
{
|
|
assert(index_from < GetCount());
|
|
assert(index_to < GetCount());
|
|
if (index_from == index_to)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Save the moved component and entity:
|
|
Component component = std::move(components[index_from]);
|
|
Entity entity = entities[index_from];
|
|
|
|
// Every other entity-component that's in the way gets moved by one and lut is kept updated:
|
|
const int direction = index_from < index_to ? 1 : -1;
|
|
for (size_t i = index_from; i != index_to; i += direction)
|
|
{
|
|
const size_t next = i + direction;
|
|
components[i] = std::move(components[next]);
|
|
entities[i] = entities[next];
|
|
lookup[entities[i]] = i;
|
|
}
|
|
|
|
// Saved entity-component moved to the required position:
|
|
components[index_to] = std::move(component);
|
|
entities[index_to] = entity;
|
|
lookup[entity] = index_to;
|
|
}
|
|
|
|
// Check if a component exists for a given entity or not
|
|
inline bool Contains(Entity entity) const
|
|
{
|
|
if (lookup.empty())
|
|
return false;
|
|
return lookup.find(entity) != lookup.end();
|
|
}
|
|
|
|
// Retrieve a [read/write] component specified by an entity (if it exists, otherwise nullptr)
|
|
inline Component* GetComponent(Entity entity)
|
|
{
|
|
if (lookup.empty())
|
|
return nullptr;
|
|
auto it = lookup.find(entity);
|
|
if (it != lookup.end())
|
|
{
|
|
return &components[it->second];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Retrieve a [read only] component specified by an entity (if it exists, otherwise nullptr)
|
|
inline const Component* GetComponent(Entity entity) const
|
|
{
|
|
if (lookup.empty())
|
|
return nullptr;
|
|
const auto it = lookup.find(entity);
|
|
if (it != lookup.end())
|
|
{
|
|
return &components[it->second];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Retrieve component index by entity handle (if not exists, returns ~0ull value)
|
|
inline size_t GetIndex(Entity entity) const
|
|
{
|
|
if (lookup.empty())
|
|
return ~0ull;
|
|
const auto it = lookup.find(entity);
|
|
if (it != lookup.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
return ~0ull;
|
|
}
|
|
|
|
// Retrieve the number of existing entries
|
|
inline size_t GetCount() const { return components.size(); }
|
|
|
|
// Directly index a specific component without indirection
|
|
// 0 <= index < GetCount()
|
|
inline Entity GetEntity(size_t index) const { return entities[index]; }
|
|
|
|
// Directly index a specific [read/write] component without indirection
|
|
// 0 <= index < GetCount()
|
|
inline Component& operator[](size_t index) { return components[index]; }
|
|
|
|
// Directly index a specific [read only] component without indirection
|
|
// 0 <= index < GetCount()
|
|
inline const Component& operator[](size_t index) const { return components[index]; }
|
|
|
|
// Returns the tightly packed [read only] entity array
|
|
inline const wi::vector<Entity>& GetEntityArray() const { return entities; }
|
|
|
|
// Returns the tightly packed [read only] component array
|
|
inline const wi::vector<Component>& GetComponentArray() const { return components; }
|
|
|
|
private:
|
|
// This is a linear array of alive components
|
|
wi::vector<Component> components;
|
|
// This is a linear array of entities corresponding to each alive component
|
|
wi::vector<Entity> entities;
|
|
// This is a lookup table for entities
|
|
wi::unordered_map<Entity, size_t> lookup;
|
|
|
|
// Disallow this to be copied by mistake
|
|
ComponentManager(const ComponentManager&) = delete;
|
|
};
|
|
|
|
// This is the class to store all component managers,
|
|
// this is useful for bulk operation of all attached components within an entity
|
|
class ComponentLibrary
|
|
{
|
|
public:
|
|
struct LibraryEntry
|
|
{
|
|
std::unique_ptr<ComponentManager_Interface> component_manager;
|
|
uint64_t version = 0;
|
|
};
|
|
wi::unordered_map<std::string, LibraryEntry> entries;
|
|
|
|
// Create an instance of ComponentManager of a certain data type
|
|
// The name must be unique, it will be used in serialization
|
|
// version is optional, it will be propagated to ComponentManager::Serialize() inside the EntitySerializer parameter
|
|
template<typename T>
|
|
inline ComponentManager<T>& Register(const std::string& name, uint64_t version = 0)
|
|
{
|
|
entries[name].component_manager = std::make_unique<ComponentManager<T>>();
|
|
entries[name].version = version;
|
|
return static_cast<ComponentManager<T>&>(*entries[name].component_manager);
|
|
}
|
|
|
|
template<typename T>
|
|
inline ComponentManager<T>* Get(const std::string& name)
|
|
{
|
|
auto it = entries.find(name);
|
|
if (it == entries.end())
|
|
return nullptr;
|
|
return static_cast<ComponentManager<T>*>(it->second.component_manager.get());
|
|
}
|
|
|
|
template<typename T>
|
|
inline const ComponentManager<T>* Get(const std::string& name) const
|
|
{
|
|
auto it = entries.find(name);
|
|
if (it == entries.end())
|
|
return nullptr;
|
|
return static_cast<const ComponentManager<T>*>(it->second.component_manager.get());
|
|
}
|
|
|
|
inline uint64_t GetVersion(std::string name) const
|
|
{
|
|
auto it = entries.find(name);
|
|
if (it == entries.end())
|
|
return 0;
|
|
return it->second.version;
|
|
}
|
|
|
|
// Serialize all registered component managers
|
|
inline void Serialize(wi::Archive& archive, EntitySerializer& seri)
|
|
{
|
|
seri.componentlibrary = this;
|
|
if(archive.IsReadMode())
|
|
{
|
|
bool has_next = false;
|
|
size_t begin = archive.GetPos();
|
|
|
|
// First pass, gather component type versions and jump over all data:
|
|
// This is so that we can look up other component versions within component serialization if needed
|
|
do
|
|
{
|
|
archive >> has_next;
|
|
if (has_next)
|
|
{
|
|
std::string name;
|
|
archive >> name;
|
|
uint64_t jump_pos = 0;
|
|
archive >> jump_pos;
|
|
auto it = entries.find(name);
|
|
if (it != entries.end())
|
|
{
|
|
archive >> seri.version;
|
|
seri.library_versions[name] = seri.version;
|
|
}
|
|
archive.Jump(jump_pos);
|
|
}
|
|
} while (has_next);
|
|
|
|
// Jump back to beginning of component library data
|
|
archive.Jump(begin);
|
|
|
|
// Second pass, read all component data:
|
|
// At this point, all existing component type versions are available
|
|
do
|
|
{
|
|
archive >> has_next;
|
|
if(has_next)
|
|
{
|
|
std::string name;
|
|
archive >> name;
|
|
uint64_t jump_pos = 0;
|
|
archive >> jump_pos;
|
|
auto it = entries.find(name);
|
|
if(it != entries.end())
|
|
{
|
|
archive >> seri.version;
|
|
it->second.component_manager->Serialize(archive, seri);
|
|
}
|
|
else
|
|
{
|
|
// component manager of this name was not registered, skip serialization by jumping over the data
|
|
archive.Jump(jump_pos);
|
|
}
|
|
}
|
|
}
|
|
while(has_next);
|
|
}
|
|
else
|
|
{
|
|
// Save all component type versions:
|
|
for (auto& it : entries)
|
|
{
|
|
seri.library_versions[it.first] = it.second.version;
|
|
}
|
|
// Serialize all component data, at this point component type version lookup is also complete
|
|
for(auto& it : entries)
|
|
{
|
|
archive << true;
|
|
archive << it.first; // name
|
|
size_t offset = archive.WriteUnknownJumpPosition(); // we will be able to jump from here...
|
|
archive << it.second.version;
|
|
seri.version = it.second.version;
|
|
it.second.component_manager->Serialize(archive, seri);
|
|
archive.PatchUnknownJumpPosition(offset); // ...to here, if this component manager was not registered
|
|
}
|
|
archive << false;
|
|
}
|
|
}
|
|
|
|
// Serialize all components for one entity
|
|
inline void Entity_Serialize(Entity entity, wi::Archive& archive, EntitySerializer& seri)
|
|
{
|
|
seri.componentlibrary = this;
|
|
if(archive.IsReadMode())
|
|
{
|
|
bool has_next = false;
|
|
do
|
|
{
|
|
archive >> has_next;
|
|
if(has_next)
|
|
{
|
|
std::string name;
|
|
archive >> name;
|
|
uint64_t jump_size = 0;
|
|
archive >> jump_size;
|
|
auto it = entries.find(name);
|
|
if (it != entries.end())
|
|
{
|
|
archive >> seri.version;
|
|
it->second.component_manager->Component_Serialize(entity, archive, seri);
|
|
}
|
|
else
|
|
{
|
|
// component manager of this name was not registered, skip serialization by jumping over the data
|
|
archive.Jump(jump_size);
|
|
}
|
|
}
|
|
}
|
|
while(has_next);
|
|
}
|
|
else
|
|
{
|
|
for(auto& it : entries)
|
|
{
|
|
archive << true;
|
|
archive << it.first; // name
|
|
size_t offset = archive.WriteUnknownJumpPosition(); // we will be able to jump from here...
|
|
archive << it.second.version;
|
|
seri.version = it.second.version;
|
|
it.second.component_manager->Component_Serialize(entity, archive, seri);
|
|
archive.PatchUnknownJumpPosition(offset); // ...to here, if this component manager was not registered
|
|
}
|
|
archive << false;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
#endif // WI_ENTITY_COMPONENT_SYSTEM_H
|