6
\$\begingroup\$

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 ECSManager a 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 ECSManager functions 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?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ ComponentArray is basically useless as it is just a worse std::unordered_map. Also introducing type-erared classes for templates without reason makes no sense either (Im talking about `ComponentArrayAbstract. \$\endgroup\$ Commented Jun 15, 2020 at 21:59
  • 1
    \$\begingroup\$ if you want to have a "default" container for your code base, e.g to be able to quickly switch implementations consider using using instead. Also any container class should be able to accept arbitrary allocators (have a look at how std containers are designed) \$\endgroup\$ Commented Jun 15, 2020 at 22:00
  • 1
    \$\begingroup\$ Oh and you might want to check out boost hana for heterogenous containers (containers which can store objects of more than one type). Especially hana::map which can be used to map types to arbitrary objects \$\endgroup\$ Commented Jun 15, 2020 at 22:05
  • 3
    \$\begingroup\$ Your ECSManager manager needs to implement the rule of five. As it is now horrible things will happen if you copy an instance of it and move semantics are disabled. \$\endgroup\$ Commented Jun 16, 2020 at 9:05

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.