I am (relatively) new to game programming and have been exploring creating an entity component system for my simple 2D game engine.
Here is what I have so far:
Entity.h
#pragma once
using Entity = int;
ComponentArray.h
#pragma once
#include "Entity.h"
#include <unordered_map>
class ComponentArrayAbstract {};
template<typename T>
class ComponentArray : public ComponentArrayAbstract {
public:
void AddComponent(Entity entity, T component);
void RemoveComponent(Entity enitity);
T& GetComponent(Entity entity);
private:
std::unordered_map<Entity, T> mComponents;
};
template<typename T>
void ComponentArray<T>::AddComponent(Entity entity, T component) {
mComponents.emplace(entity, component);
}
template<typename T>
void ComponentArray<T>::RemoveComponent(Entity entity) {
mComponents.erase(entity);
}
template<typename T>
T& ComponentArray<T>::GetComponent(Entity enitity) {
return mComponents[enitity];
}
ECSManager.h
#pragma once
#include "Entity.h"
#include "ComponentArray.h"
#include <bitset>
#include <array>
#include <vector>
#include <functional>
const int MAX_COMPONENT_TYPES = 32;
using ComponentType = int;
using ComponentSignature = std::bitset<MAX_COMPONENT_TYPES>;
class ECSManager;
using System = std::function<void(ECSManager*, double)>;
class ECSManager {
public:
ECSManager() = default;
~ECSManager();
ECSManager(const ECSManager& other) = delete;
ECSManager(ECSManager&& other) = delete;
ECSManager& operator=(const ECSManager& other) = delete;
ECSManager& operator=(ECSManager&& other) = delete;
// Entity Functions
Entity CreateEntity(int id); // TODO: use hashed string ids instead.
void DestroyEntity(Entity entity);
template<typename... Args>
std::vector<Entity> GetEntitiesWith();
template<typename T>
T& GetComponent(Entity entity) const;
// Component Functions
template<typename T>
void RegisterComponentType();
template<typename T>
void AddComponent(Entity entity, T component);
template<typename T>
void RemoveComponent(Entity entity);
// System Functions
void AddSystem(System system);
void UpdateSystems(double deltaTime);
private:
ComponentSignature GetComponentSignature(Entity entity) const;
void SetComponentSignature(Entity entity, ComponentSignature signature);
template<typename T>
ComponentArray<T>* GetComponentArray() const;
template<typename T>
ComponentType GetComponentType() const;
int* GetComponentCount() const;
template<typename T>
ComponentSignature CreateComponentSignature() const;
template<typename T, typename Second, typename... Args>
ComponentSignature CreateComponentSignature() const;
std::unordered_map<Entity, ComponentSignature> mComponentSignatures;
std::array<ComponentArrayAbstract*, MAX_COMPONENT_TYPES> mComponentArrays;
std::vector<System> mSystems;
};
template<typename... Args>
std::vector<Entity> ECSManager::GetEntitiesWith() {
ComponentSignature systemSignature = CreateComponentSignature<Args...>();
std::vector<Entity> entities;
for (auto i : mComponentSignatures) {
if ((i.second & systemSignature) == i.second) entities.push_back(i.first);
}
return entities;
}
template<typename T>
T& ECSManager::GetComponent(Entity entity) const {
return GetComponentArray<T>()->GetComponent(entity);
}
template<typename T>
void ECSManager::RegisterComponentType() {
mComponentArrays[GetComponentType<T>()] = new ComponentArray<T>();
}
template<typename T>
void ECSManager::AddComponent(Entity entity, T component) {
GetComponentArray<T>()->AddComponent(entity, component);
SetComponentSignature(entity, GetComponentSignature(entity).set(GetComponentType<T>(), true));
}
template<typename T>
void ECSManager::RemoveComponent(Entity entity) {
GetComponentArray<T>()->RemoveComponent(entity);
SetComponentSignature(entity, GetComponentSignature(entity).set(GetComponentType<T>(), false));
}
template<typename T>
ComponentArray<T>* ECSManager::GetComponentArray() const {
return ((ComponentArray<T>*) (mComponentArrays[GetComponentType<T>()]));
}
template<typename T>
ComponentType ECSManager::GetComponentType() const {
static int index = (*GetComponentCount())++; return index;
}
template<typename T>
ComponentSignature ECSManager::CreateComponentSignature() const {
ComponentSignature signature;
signature.set(GetComponentType<T>(), true);
return signature;
}
template<typename T, typename Second, typename... Args>
ComponentSignature ECSManager::CreateComponentSignature() const {
ComponentSignature signature;
signature.set(GetComponentType<T>(), true);
signature |= CreateComponentSignature<Second, Args...>();
return signature;
}
ECSManager.cpp
#include "ECSManager.h"
#include "ComponentArray.h"
#include <unordered_map>
#include <array>
#include <vector>
#include <functional>
ECSManager::~ECSManager() {
for (ComponentArrayAbstract* componentArray : mComponentArrays) {
if (componentArray != nullptr) {
delete componentArray;
}
}
}
Entity ECSManager::CreateEntity(int id) {
mComponentSignatures.emplace(id, ComponentSignature());
return id;
}
void ECSManager::DestroyEntity(Entity entity) {
mComponentSignatures.erase(entity);
}
ComponentSignature ECSManager::GetComponentSignature(Entity entity) const {
return mComponentSignatures.at(entity);
}
void ECSManager::SetComponentSignature(Entity entity, ComponentSignature signature) {
mComponentSignatures[entity] = signature;
}
void ECSManager::AddSystem(System system) {
mSystems.push_back(system);
}
void ECSManager::UpdateSystems(double deltaTime) {
for (System system : mSystems) {
system(this, deltaTime);
}
}
int* ECSManager::GetComponentCount() const {
static int componentCount = 0;
return &componentCount;
}
main.cpp
#include "ECSManager.h"
#include <stdio.h>
struct Component1 {
int num1;
};
struct Component2 {
int num2;
};
void TestSystem(ECSManager* world, double deltaTime) {
for (Entity entity : world->GetEntitiesWith< Component1, Component2>()) {
Component1 component1 = world->GetComponent<Component1>(entity);
Component2 component2 = world->GetComponent<Component2>(entity);
printf("%i\n", component1.num1 + component2.num2);
}
}
int main() {
ECSManager world;
world.RegisterComponentType<Component1>();
world.RegisterComponentType<Component2>();
Entity entity1 = world.CreateEntity(1);
world.AddComponent<Component1>(entity1, {1}); // I will use hashed string ids in the future.
world.AddComponent<Component2>(entity1, {2});
Entity entity2 = world.CreateEntity(2);
world.AddComponent<Component1>(entity2, {3});
world.AddComponent<Component2>(entity2, {4});
world.AddSystem(TestSystem);
world.UpdateSystems(0.16);
return 0;
}
I have a few questions:
Implementation:
Is the implementation flawed in any way, eg. are my uses of std containers appropriate? Is it relatively cache friendly? (I'm looking at the unordered map of entities). I initially wanted to make
ECSManagera namespace with functions, like my other subsystems (Renderer,Input, etc. I don't know if this is a good idea either!). Is this feasible, or a good way to do it? I decided not to, since there are lots of template functions, so accessing data would be a pain.Style:
Are there any issues with the way I have written my code? Should I implement the
ECSManagerfunctions in a cpp file, since most are template functions anyway?API:
Is the API realistic and scalable? Is it holding me back in any way?
Have I made any silly mistakes?
ComponentArrayis basically useless as it is just a worsestd::unordered_map. Also introducing type-erared classes for templates without reason makes no sense either (Im talking about `ComponentArrayAbstract. \$\endgroup\$usinginstead. Also any container class should be able to accept arbitrary allocators (have a look at how std containers are designed) \$\endgroup\$hana::mapwhich can be used to map types to arbitrary objects \$\endgroup\$