From 5273b8017e7b412c6b889e32cef168ab7f402996 Mon Sep 17 00:00:00 2001 From: Benjamin Palko Date: Wed, 9 Oct 2024 11:26:03 -0400 Subject: [PATCH] implement ECS library --- libs/component.hpp | 13 ++++++++ libs/componentarray.cpp | 47 +++++++++++++++++++++++++++++ libs/componentarray.hpp | 37 +++++++++++++++++++++++ libs/componentmanager.cpp | 63 +++++++++++++++++++++++++++++++++++++++ libs/componentmanager.hpp | 35 ++++++++++++++++++++++ libs/coordinator.cpp | 63 +++++++++++++++++++++++++++++++++++++++ libs/coordinator.hpp | 30 +++++++++++++++++++ libs/entity.cpp | 42 ++++++++++++++++++++++++++ libs/entity.hpp | 31 +++++++++++++++++++ libs/system.cpp | 54 +++++++++++++++++++++++++++++++++ libs/system.hpp | 29 ++++++++++++++++++ 11 files changed, 444 insertions(+) create mode 100644 libs/component.hpp create mode 100644 libs/componentarray.cpp create mode 100644 libs/componentarray.hpp create mode 100644 libs/componentmanager.cpp create mode 100644 libs/componentmanager.hpp create mode 100644 libs/coordinator.cpp create mode 100644 libs/coordinator.hpp create mode 100644 libs/entity.cpp create mode 100644 libs/entity.hpp create mode 100644 libs/system.cpp create mode 100644 libs/system.hpp diff --git a/libs/component.hpp b/libs/component.hpp new file mode 100644 index 0000000..323866e --- /dev/null +++ b/libs/component.hpp @@ -0,0 +1,13 @@ +#ifndef COMPONENT_HPP +#define COMPONENT_HPP + +#include +#include + +using ComponentType = std::uint8_t; + +const ComponentType MAX_COMPONENTS = 32; + +using Signature = std::bitset; + +#endif // COMPONENT_HPP diff --git a/libs/componentarray.cpp b/libs/componentarray.cpp new file mode 100644 index 0000000..3b3b45b --- /dev/null +++ b/libs/componentarray.cpp @@ -0,0 +1,47 @@ +#include +#include "componentarray.hpp" + +template +void ComponentArray::InsertData(Entity entity, T component) { + assert(mEntityToIndexMap.find(entity) == mEntityToIndexMap.end() && + "Component added to same entity more than once."); + + // Put new entry at end and update the maps + size_t newIndex = mSize; + mEntityToIndexMap[entity] = newIndex; + mIndexToEntityMap[newIndex] = entity; + mComponentArray[newIndex] = component; + ++mSize; +}; +template void ComponentArray::RemoveData(Entity entity) { + assert(mEntityToIndexMap.find(entity) != mEntityToIndexMap.end() && + "Removing non-existent component."); + + // Copy element at end into deleted element's place to maintain density + size_t indexOfRemovedEntity = mEntityToIndexMap[entity]; + size_t indexOfLastElement = mSize - 1; + mComponentArray[indexOfRemovedEntity] = mComponentArray[indexOfLastElement]; + + // Update map to point to moved spot + Entity entityOfLastElement = mIndexToEntityMap[indexOfLastElement]; + mEntityToIndexMap[entityOfLastElement] = indexOfRemovedEntity; + mIndexToEntityMap[indexOfRemovedEntity] = entityOfLastElement; + + mEntityToIndexMap.erase(entity); + mIndexToEntityMap.erase(indexOfLastElement); + + --mSize; +}; +template T &ComponentArray::GetData(Entity entity) { + assert(mEntityToIndexMap.find(entity) != mEntityToIndexMap.end() && + "Retrieving non-existent component."); + + // Return a reference to the entity's component + return mComponentArray[mEntityToIndexMap[entity]]; +}; +template void ComponentArray::EntityDestroyed(Entity entity) { + if (mEntityToIndexMap.find(entity) != mEntityToIndexMap.end()) { + // Remove the entity's component if it existed + RemoveData(entity); + } +}; diff --git a/libs/componentarray.hpp b/libs/componentarray.hpp new file mode 100644 index 0000000..f8b10c4 --- /dev/null +++ b/libs/componentarray.hpp @@ -0,0 +1,37 @@ +#ifndef COMPONENTARRAY_HPP +#define COMPONENTARRAY_HPP + +#include +#include +#include "entity.hpp" + +class IComponentArray { +public: + virtual ~IComponentArray() = default; + virtual void EntityDestroyed(Entity entity) = 0; +}; + +template class ComponentArray : public IComponentArray { + // The packed array of components (of generic type T), + // set to a specified maximum amount, matching the maximum number + // of entities allowed to exist simultaneously, so that each entity + // has a unique spot. + std::array mComponentArray; + + // Map from an entity ID to an array index. + std::unordered_map mEntityToIndexMap; + + // Map from an array index to an entity ID. + std::unordered_map mIndexToEntityMap; + + // Total size of valid entries in the array. + size_t mSize; + +public: + void InsertData(Entity entity, T component); + void RemoveData(Entity entity); + T &GetData(Entity entity); + void EntityDestroyed(Entity entity) override; +}; + +#endif // COMPONENTARRAY_HPP diff --git a/libs/componentmanager.cpp b/libs/componentmanager.cpp new file mode 100644 index 0000000..24584db --- /dev/null +++ b/libs/componentmanager.cpp @@ -0,0 +1,63 @@ +#include "componentmanager.hpp" +#include "componentarray.hpp" +#include + +// Private functions +template +std::shared_ptr> ComponentManager::GetComponentArray() { + const char *typeName = typeid(T).name(); + + assert(mComponentTypes.find(typeName) != mComponentTypes.end() && + "Component not registered before use."); + + return std::static_pointer_cast>( + mComponentArrays[typeName]); +} + +// Public functions +template void ComponentManager::RegisterComponent() { + const char *typeName = typeid(T).name(); + + assert(mComponentTypes.find(typeName) == mComponentTypes.end() && + "Registering component type more than once."); + + // Add this component type to the component type map + mComponentTypes.insert({typeName, mNextComponentType}); + + // Create a ComponentArray pointer and add it to the component arrays map + mComponentArrays.insert({typeName, std::make_shared>()}); + + // Increment the value so that the next component registered will be different + ++mNextComponentType; +}; +template ComponentType ComponentManager::GetComponentType() { + const char *typeName = typeid(T).name(); + + assert(mComponentTypes.find(typeName) != mComponentTypes.end() && + "Component not registered before use."); + + // Return this component's type - used for creating signatures + return mComponentTypes[typeName]; +} +template +void ComponentManager::AddComponent(Entity entity, T component) { + // Add a component to the array for an entity + GetComponentArray()->InsertData(entity, component); +} +template void ComponentManager::RemoveComponent(Entity entity) { + // Remove a component from the array for an entity + GetComponentArray()->RemoveData(entity); +} +template T &ComponentManager::GetComponent(Entity entity) { + // Get a reference to a component from the array for an entity + return GetComponentArray()->GetData(entity); +} +void ComponentManager::EntityDestroyed(Entity entity) { + // Notify each component array that an entity has been destroyed + // If it has a component for that entity, it will remove it + for (auto const &pair : mComponentArrays) { + auto const &component = pair.second; + + component->EntityDestroyed(entity); + } +}; diff --git a/libs/componentmanager.hpp b/libs/componentmanager.hpp new file mode 100644 index 0000000..16ab368 --- /dev/null +++ b/libs/componentmanager.hpp @@ -0,0 +1,35 @@ +#ifndef COMPONENTMANAGER_HPP +#define COMPONENTMANAGER_HPP + +#include +#include +#include +#include "component.hpp" +#include "componentarray.hpp" + +class ComponentManager { + // Map from type string pointer to a component type + std::unordered_map mComponentTypes{}; + + // Map from type string pointer to a component array + std::unordered_map> + mComponentArrays{}; + + // The component type to be assigned to the next registered component - + // starting at 0 + ComponentType mNextComponentType{}; + + // Convenience function to get the statically casted pointer to the + // ComponentArray of type T. + template std::shared_ptr> GetComponentArray(); + +public: + template void RegisterComponent(); + template ComponentType GetComponentType(); + template void AddComponent(Entity entity, T component); + template void RemoveComponent(Entity entity); + template T &GetComponent(Entity entity); + void EntityDestroyed(Entity entity); +}; + +#endif // COMPONENTMANAGER_HPP diff --git a/libs/coordinator.cpp b/libs/coordinator.cpp new file mode 100644 index 0000000..af6f545 --- /dev/null +++ b/libs/coordinator.cpp @@ -0,0 +1,63 @@ +#include "coordinator.hpp" + +void Coordinator::Init() { + // Create pointers to each manager + mComponentManager = std::make_unique(); + mEntityManager = std::make_unique(); + mSystemManager = std::make_unique(); +} + +// Entity methods +Entity Coordinator::CreateEntity() { return mEntityManager->CreateEntity(); } + +void Coordinator::DestroyEntity(Entity entity) { + mEntityManager->DestroyEntity(entity); + + mComponentManager->EntityDestroyed(entity); + + mSystemManager->EntityDestroyed(entity); +} + +// Component methods +template void Coordinator::RegisterComponent() { + mComponentManager->RegisterComponent(); +} + +template +void Coordinator::AddComponent(Entity entity, T component) { + mComponentManager->AddComponent(entity, component); + + auto signature = mEntityManager->GetSignature(entity); + signature.set(mComponentManager->GetComponentType(), true); + mEntityManager->SetSignature(entity, signature); + + mSystemManager->EntitySignatureChanged(entity, signature); +} + +template void Coordinator::RemoveComponent(Entity entity) { + mComponentManager->RemoveComponent(entity); + + auto signature = mEntityManager->GetSignature(entity); + signature.set(mComponentManager->GetComponentType(), false); + mEntityManager->SetSignature(entity, signature); + + mSystemManager->EntitySignatureChanged(entity, signature); +} + +template T &Coordinator::GetComponent(Entity entity) { + return mComponentManager->GetComponent(entity); +} + +template ComponentType Coordinator::GetComponentType() { + return mComponentManager->GetComponentType(); +} + +// System methods +template std::shared_ptr Coordinator::RegisterSystem() { + return mSystemManager->RegisterSystem(); +} + +template +void Coordinator::SetSystemSignature(Signature signature) { + mSystemManager->SetSignature(signature); +} diff --git a/libs/coordinator.hpp b/libs/coordinator.hpp new file mode 100644 index 0000000..73f5a36 --- /dev/null +++ b/libs/coordinator.hpp @@ -0,0 +1,30 @@ +#ifndef COORDINATOR_HPP +#define COORDINATOR_HPP + +#include +#include "componentmanager.hpp" +#include "entity.hpp" +#include "system.hpp" + +class Coordinator { + std::unique_ptr mComponentManager; + std::unique_ptr mEntityManager; + std::unique_ptr mSystemManager; + +public: + void Init(); + // Entity methods + Entity CreateEntity(); + void DestroyEntity(Entity entity); + // Component methods + template void RegisterComponent(); + template void AddComponent(Entity entity, T component); + template void RemoveComponent(Entity entity); + template T &GetComponent(Entity entity); + template ComponentType GetComponentType(); + // System methods + template std::shared_ptr RegisterSystem(); + template void SetSystemSignature(Signature signature); +}; + +#endif // COORDINATOR_HPP diff --git a/libs/entity.cpp b/libs/entity.cpp new file mode 100644 index 0000000..83f7262 --- /dev/null +++ b/libs/entity.cpp @@ -0,0 +1,42 @@ +#include "entity.hpp" +#include + +EntityManager::EntityManager() { + // Initialize the queue with all possible entity IDs + for (Entity entity = 0; entity < MAX_ENTITIES; ++entity) { + mAvailableEntities.push(entity); + } +} +Entity EntityManager::CreateEntity() { + assert(mLivingEntityCount < MAX_ENTITIES && + "Too many entities in existence."); + + // Take an ID from the front of the queue + Entity id = mAvailableEntities.front(); + mAvailableEntities.pop(); + ++mLivingEntityCount; + + return id; +} +void EntityManager::DestroyEntity(Entity entity) { + assert(entity < MAX_ENTITIES && "Entity out of range."); + + // Invalidate the destroyed entity's signature + mSignatures[entity].reset(); + + // Put the destroyed ID at the back of the queue + mAvailableEntities.push(entity); + --mLivingEntityCount; +} +void EntityManager::SetSignature(Entity entity, Signature signature) { + assert(entity < MAX_ENTITIES && "Entity out of range."); + + // Put this entity's signature into the array + mSignatures[entity] = signature; +} +Signature EntityManager::GetSignature(Entity entity) { + assert(entity < MAX_ENTITIES && "Entity out of range."); + + // Get this entity's signature from the array + return mSignatures[entity]; +} diff --git a/libs/entity.hpp b/libs/entity.hpp new file mode 100644 index 0000000..e2f7a15 --- /dev/null +++ b/libs/entity.hpp @@ -0,0 +1,31 @@ +#ifndef ENTITY_HPP +#define ENTITY_HPP + +#include +#include +#include +#include "component.hpp" + +using Entity = std::uint32_t; + +const Entity MAX_ENTITIES = 5000; + +class EntityManager { + // Queue of unused entity IDs + std::queue mAvailableEntities{}; + + // Array of signatures where the index corresponds to the entity ID + std::array mSignatures{}; + + // Total living entities - used to keep limits on how many exist + uint32_t mLivingEntityCount{}; + +public: + EntityManager(); + Entity CreateEntity(); + void DestroyEntity(Entity entity); + void SetSignature(Entity entity, Signature signature); + Signature GetSignature(Entity entity); +}; + +#endif // ENTITY_HPP diff --git a/libs/system.cpp b/libs/system.cpp new file mode 100644 index 0000000..a7e4ef4 --- /dev/null +++ b/libs/system.cpp @@ -0,0 +1,54 @@ +#include "system.hpp" + +#include + +template std::shared_ptr SystemManager::RegisterSystem() { + const char *typeName = typeid(T).name(); + + assert(mSystems.find(typeName) == mSystems.end() && + "Registering system more than once."); + + // Create a pointer to the system and return it so it can be used externally + auto system = std::make_shared(); + mSystems.insert({typeName, system}); + return system; +} + +template void SystemManager::SetSignature(Signature signature) { + const char *typeName = typeid(T).name(); + + assert(mSystems.find(typeName) != mSystems.end() && + "System used before registered."); + + // Set the signature for this system + mSignatures.insert({typeName, signature}); +} + +void SystemManager::EntityDestroyed(Entity entity) { + // Erase a destroyed entity from all system lists + // mEntities is a set so no check needed + for (auto const &pair : mSystems) { + auto const &system = pair.second; + + system->mEntities.erase(entity); + } +} + +void SystemManager::EntitySignatureChanged(Entity entity, + Signature entitySignature) { + // Notify each system that an entity's signature changed + for (auto const &pair : mSystems) { + auto const &type = pair.first; + auto const &system = pair.second; + auto const &systemSignature = mSignatures[type]; + + // Entity signature matches system signature - insert into set + if ((entitySignature & systemSignature) == systemSignature) { + system->mEntities.insert(entity); + } + // Entity signature does not match system signature - erase from set + else { + system->mEntities.erase(entity); + } + } +} diff --git a/libs/system.hpp b/libs/system.hpp new file mode 100644 index 0000000..af7e831 --- /dev/null +++ b/libs/system.hpp @@ -0,0 +1,29 @@ +#ifndef SYSTEM_HPP +#define SYSTEM_HPP + +#include +#include +#include +#include "component.hpp" +#include "entity.hpp" + +class System { +public: + std::set mEntities; +}; + +class SystemManager { + // Map from system type string pointer to a signature + std::unordered_map mSignatures{}; + + // Map from system type string pointer to a system pointer + std::unordered_map> mSystems{}; + +public: + template std::shared_ptr RegisterSystem(); + template void SetSignature(Signature signature); + void EntityDestroyed(Entity entity); + void EntitySignatureChanged(Entity entity, Signature entitySignature); +}; + +#endif // !SYSTEM_HPP