Introduction
I'm a new to C++ so please take me easy :) I am currently working on a low-level game engine using C++, OpenGL, and GLFW; I've implemented the Event System and the Input Manager, which you can check out here: Improved Event System & InputManager Using C++.
Graph
For clarity, I've included a graph:
Components
- Entity: This is a simple type alias for size_t. Each entity in the system is represented by a unique Entity ID.
- Component: This is an abstract base class for all components. Components are the data that entities can possess.
- System: This is an abstract base class for all systems. Systems are where the logic of the game lives. Each system operates on entities that have a specific set of components. For example, a MovementSystem might operate on all entities that have both PositionComponent and VelocityComponent.
- BaseComponentPool and ComponentPool: These classes manage the storage of components. Each type of component has its own ComponentPool. The BaseComponentPool is an interface that allows the ComponentManager to store a collection of ComponentPool objects of different types.
- ComponentManager: This class manages all the ComponentPools. It provides methods to add and retrieve components to/from entities.
- EntityManager: This class manages the creation and destruction of entities.
- SystemContext: This class provides the context for systems to operate. It provides access to the EntityManager and ComponentManager, allowing systems to query entities and their components.
- SystemManager: This class manages all the systems. It provides methods to add, remove, enable, and disable systems. It also provides a method to update all systems, which is typically called once per game loop.
- ECSManager: This is the main interface to the ECS. It provides methods to create and destroy entities, add components to entities, and add systems. It also provides a method to update all systems, which is typically called once per game loop.
Control Flow
The flow of the ECS is as follows:
Entities are created using the ECSManager. Components are added to entities using the ECSManager, which delegates to the ComponentManager. Systems are added using the ECSManager, which delegates to the SystemManager. Each game loop, the ECSManager's updateSystems method is called, which delegates to the SystemManager's updateSystems method. Each system is updated, operating on the entities and their components as needed.
Concerns
- Question: Should I replace the simple Entity = size_t, with an Object Entity? This would improve type safety but would also reduce speed and increase complexity, since now every call to get the Entity's ID would be through the interface of the object.
- Problem: This design does not adhere to the dependecy inversion principle, because SystemContext, which is a high-level object, is exported to the Systems (which are low-level). Normally, this would be solved by using interfaces, but not here, because the SystemCotext object consits of templated methods, which cannot be virtual. However, I do not think this is a mark of bad design becuase the DIP is just a principle, not a hard-rule.
Any feedback/suggestions would be very much appreciated.
Source Code
Entity.h
#pragma once
using Entity = size_t;
Component.h
#pragma once
class Component
{
public:
virtual ~Component() = default;
};
ComponentPoolExceptions.h
#pragma once
#include <stdexcept>
#include "Entity.h"
class ComponentEntityNotFoundException : public std::runtime_error
{
public:
ComponentEntityNotFoundException(Entity entity, const std::string& componentType)
: std::runtime_error(entity + " not found in component pool: " + componentType) { }
};
ComponentPool.h
#pragma once
#include <stdexcept>
#include <vector>
#include <memory>
#include "Component.h"
#include "ComponentPoolExceptions.h"
class BaseComponentPool
{
public:
virtual ~BaseComponentPool() = default;
virtual void destroyEntityComponent(Entity entity) = 0;
virtual bool hasComponent(Entity entity) const = 0;
};
template <typename ComponentType>
class ComponentPool : public BaseComponentPool
{
public:
void addComponent(Entity entity, std::unique_ptr<ComponentType> component)
{
if (entity >= pool.size())
pool.resize(entity + 1);
pool[entity] = std::move(component);
}
void destroyEntityComponent(Entity entity) override
{
if (entity < pool.size())
pool[entity].reset();
}
const ComponentType& getComponent(Entity entity) const
{
if (entity >= pool.size() || pool[entity] == nullptr)
throw ComponentEntityNotFoundException(entity, typeid(ComponentType).name());
return *pool[entity];
}
bool hasComponent(Entity entity) const override
{
return entity < pool.size() && pool[entity] != nullptr;
}
private:
std::vector<std::unique_ptr<ComponentType>> pool;
};
ComponentManager.h
#pragma once
#include <typeindex>
#include "ComponentPool.h"
class ComponentManager
{
public:
template<typename ComponentType, typename... Args>
void addComponent(Entity entity, Args&&... args)
{
auto component = std::make_unique<ComponentType>(std::forward(args)...);
getComponentPool<ComponentType>().addComponent(entity, std::move(component));
}
template<typename ComponentType>
const ComponentType& getComponent(Entity entity)
{
return getComponentPool<ComponentType>().getComponent(entity);
}
template<typename ComponentType>
bool hasComponent(Entity entity)
{
return getComponentPool<ComponentType>().hasComponent(entity);
}
void destroyEntityComponents(Entity entity)
{
for (auto& [type, pool] : componentPools)
pool->destroyEntityComponent(entity);
}
private:
template<typename ComponentType>
ComponentPool<ComponentType>& getComponentPool()
{
std::type_index typeIndex(typeid(ComponentType));
auto it = componentPools.find(typeIndex);
if (it == componentPools.end()) {
auto newPool = std::make_unique<ComponentPool<ComponentType>>();
it = componentPools.emplace(typeIndex, std::move(newPool)).first;
}
return static_cast<ComponentPool<ComponentType>&>(*it->second);
}
private:
std::unordered_map<std::type_index, std::unique_ptr<BaseComponentPool>> componentPools;
};
EntityExceptions.h
#pragma once
#include <stdexcept>
#include <string>
#include "Entity.h"
class EntityOutOfBoundsException : std::runtime_error
{
public:
EntityOutOfBoundsException(Entity entity)
: runtime_error("Entity: " + std::to_string(entity) + " is out of bounds!") { }
};
EntityManager.h
#pragma once
#include <queue>
#include <unordered_set>
#include "Entity.h"
class EntityManager
{
public:
Entity createEntity();
void destroyEntity(Entity entity);
const std::unordered_set<Entity>& getActiveEntities() const { return activeEntities; }
private:
std::queue<Entity> freeEntities;
std::unordered_set<Entity> activeEntities;
Entity nextEntity = 0;
};
EntityManager.cpp
#include "EntityManager.h"
#include "EntityExceptions.h"
Entity EntityManager::createEntity()
{
Entity entity;
if (!freeEntities.empty()) {
entity = freeEntities.front();
freeEntities.pop();
}
else {
entity = nextEntity++;
}
activeEntities.insert(entity);
return entity;
}
void EntityManager::destroyEntity(Entity entity)
{
if (activeEntities.find(entity) == activeEntities.end())
throw EntityOutOfBoundsException(entity);
freeEntities.push(entity);
activeEntities.erase(entity);
}
SystemContext.h
#pragma once
#include "ComponentManager.h"
#include "EntityManager.h"
class SystemContext
{
public:
SystemContext(EntityManager& EntityManager, ComponentManager& componentManager)
: EntityManager(EntityManager), componentManager(componentManager) { }
template<typename ComponentType>
requires std::derived_from<ComponentType, Component>
const ComponentType& getComponent(Entity entity)
{
return componentManager.getComponent<ComponentType>(entity);
}
template<typename ComponentType>
requires std::derived_from<ComponentType, Component>
bool hasComponent(Entity entity)
{
return componentManager.hasComponent<ComponentType>(entity);
}
template<typename... ComponentTypes>
requires (std::derived_from<ComponentTypes, Component> && ...)
std::vector<Entity> getEntitiesWithComponents()
{
std::vector<Entity> entities;
auto& activeEntites = EntityManager.getActiveEntities();
for (const auto& entity : activeEntites)
if ((componentManager.hasComponent<ComponentTypes>(entity) && ...))
entities.push_back(entity);
return entities;
}
private:
EntityManager& EntityManager;
ComponentManager& componentManager;
};
System.h
#pragma once
#include "SystemContext.h"
class System
{
public:
virtual ~System() = default;
virtual void onAdded() = 0;
virtual void update(float deltaTime, SystemContext& context) = 0;
virtual void onRemoved() = 0;
void enable(bool enabled) { this->enabled = enabled; }
bool isEnabled() const { return enabled; }
private:
bool enabled = true;
};
SystemExceptions.h
#pragma once
#include <stdexcept>
class SystemNotFoundException : public std::runtime_error
{
public:
SystemNotFoundException(const std::string& systemType)
: std::runtime_error("System not found: " + systemType) {}
};
class SystemAlreadyAddedException : public std::runtime_error
{
public:
SystemAlreadyAddedException(const std::string& systemType)
: std::runtime_error("System is already added: " + systemType) {}
};
SystemManager.h
#pragma once
#include <list>
#include <memory>
#include <stdexcept>
#include <unordered_map>
#include "System.h"
#include "SystemExceptions.h"
class SystemManager
{
public:
template<typename SystemType, typename... Args>
void addSystem(Args&&... args)
{
std::type_index typeIndex(typeid(SystemType));
if (systemLookup.find(typeIndex) != systemLookup.end())
throw SystemAlreadyAddedException(typeid(SystemType).name());
std::unique_ptr<System> system = std::make_unique<SystemType>(std::forward<Args>(args)...);
system->onAdded();
systems.emplace_back(std::move(system));
systemLookup[typeIndex] = systems.size() - 1;
}
template<typename SystemType>
void removeSystem()
{
std::type_index typeIndex(typeid(SystemType));
auto it = systemLookup.find(typeIndex);
if (it == systemLookup.end())
throw SystemNotFoundException(typeid(SystemType).name());
systems[it->second]->onRemoved();
systems.erase(systems.begin() + it->second);
systemLookup.erase(it);
}
template<typename SystemType>
bool hasSystem() const
{
std::type_index typeIndex(typeid(SystemType));
return systemLookup.find(typeIndex) != systemLookup.end();
}
template<typename SystemType>
void enableSystem(bool enabled)
{
auto system = getSystem<SystemType>();
system->enabled(enabled);
}
void updateSystems(float deltaTime, SystemContext& context) const
{
for (auto& system : systems)
if(system->isEnabled())
system->update(deltaTime, context);
}
private:
std::vector<std::unique_ptr<System>> systems;
std::unordered_map<std::type_index, size_t> systemLookup;
};
ECSManager.h
#pragma once
#include "SystemManager.h"
class ECSManager
{
public:
ECSManager()
: context(entityManager, componentManager) { }
Entity createEntity()
{
return entityManager.createEntity();
}
void destroyEntity(Entity entity)
{
componentManager.destroyEntityComponents(entity);
entityManager.destroyEntity(entity);
}
template<typename ComponentType, typename... Args>
requires std::derived_from<ComponentType, Component>
void addComponent(Entity entity, Args&&... args)
{
componentManager.addComponent<ComponentType>(entity, std::forward(args)...);
}
template<typename SystemType, typename... Args>
requires std::derived_from<SystemType, System>
void addSystem(Args&&... args)
{
systemManager.addSystem<SystemType>(std::forward<Args>(args)...);
}
template<typename SystemType>
requires std::derived_from<SystemType, System>
void removeSystem()
{
systemManager.removeSystem<SystemType>();
}
template<typename SystemType>
requires std::derived_from<SystemType, System>
bool hasSystem() const
{
return systemManager.hasSystem<SystemType>();
}
template<typename SystemType>
requires std::derived_from<SystemType, System>
void enableSystem(bool enabled)
{
systemManager.enableSystem<SystemType>(enabled);
}
void updateSystems(float deltaTime)
{
systemManager.updateSystems(deltaTime, context);
}
private:
EntityManager entityManager;
ComponentManager componentManager;
SystemManager systemManager;
SystemContext context;
};