3
\$\begingroup\$

I plan to create a 3D space simulator (realistic (orbital) physics, procedural planets) based on this ECS library.

I am very new to game development so feedback is very welcome. I am especially concerned about performance/bottlenecks.


#pragma once
#include <iostream>
#include <vector>
#include <chrono>

namespace ecs
{
  typedef size_t ID;
  const ID NULL_ID = (0-1);//(size_t)(0-1) == SIZE_MAX;
  typedef std::chrono::nanoseconds Duration;
  typedef std::chrono::time_point<std::chrono::high_resolution_clock> TimePoint;
  typedef std::chrono::high_resolution_clock Clock;

  /********************ENTITIES and COMPONENTS********************/

  class Entity
  {
    template<typename T, typename... Targs>
    friend class Iterator;
    public:

    static Entity createEntity();
    void removeEntity();
    template<typename T, typename... Args>
    void createComponent(const Args&... args);
    template<typename T>
    void removeComponent();
    template<typename T>
    T & getComponent();
    template<typename T>
    bool hasComponents();
    template<typename T1, typename T2, typename... Targs>
    bool hasComponents();

    ID getID()
    {
      return entityID;
    }

    private:

    Entity(ID entityID) : entityID(entityID) {}
    ID entityID;

    struct EntityEntry
    {
      std::vector<void (*)(ID)> removeComponentFunctions = std::vector<void (*)(ID)>();
      bool exists = true;
    };

    static std::vector<EntityEntry> entityEntryArray;
    static std::vector<ID> freeEntityIDs;

    template<typename T>
    static std::vector<T> componentArray;
    template<typename T>
    static std::vector<ID> entityToComponentIDs;

    static void removeEntity(ID entityID);
    template<typename T, typename... Args>
    static void createComponent(ID entityID, const Args&... args);
    template<typename T>
    static void removeComponent(ID entityID);
    template<typename T>
    static T & getComponent(ID entityID);
    template<typename T>
    static bool hasComponents(ID entityID);
    template<typename T1, typename T2, typename... Targs>
    static bool hasComponents(ID entityID);
  };
  template<typename T>
  std::vector<T> Entity::componentArray = std::vector<T>();
  template<typename T>
  std::vector<ID> Entity::entityToComponentIDs = std::vector<ID>();

  std::vector<ID> Entity::freeEntityIDs = std::vector<ID>();
  std::vector<Entity::EntityEntry> Entity::entityEntryArray = std::vector<EntityEntry>();

  Entity Entity::createEntity()
  {
    ID tempID;
    if(freeEntityIDs.empty())
    {
      tempID = entityEntryArray.size();
      entityEntryArray.emplace_back();
    }
    else
    {
      tempID = freeEntityIDs.back();
      freeEntityIDs.pop_back();
      entityEntryArray[tempID].exists = true;
    }
    return Entity(tempID);
  }
  void Entity::removeEntity()
  {
    removeEntity(this->entityID);
  }
  template<typename T, typename... Args>
  void Entity::createComponent(const Args&... args)
  {
    createComponent<T>(this->entityID, args...);
  }
  template<typename T>
  void Entity::removeComponent()
  {
    removeComponent<T>(this->entityID);
  }
  template<typename T>
  T & Entity::getComponent()
  {
    return getComponent<T>(this->entityID);
  }
  template<typename T>
  bool Entity::hasComponents()
  {
    return hasComponents<T>(this->entityID);
  }
  template<typename T1, typename T2, typename... Targs>
  bool Entity::hasComponents()
  {
    return hasComponents<T1, T2, Targs...>(this->entityID);
  }

  void Entity::removeEntity(ID entityID)
  {
    freeEntityIDs.push_back(entityID);
    for(void (*f)(ID) : entityEntryArray[entityID].removeComponentFunctions)
    {
      f(entityID);
    }
    entityEntryArray[entityID].removeComponentFunctions = std::vector<void (*)(ID)>();
    entityEntryArray[entityID].exists = false;
  }
  template<typename T, typename... Args>
  void Entity::createComponent(ID entityID, const Args&... args)
  {
    if(hasComponents<T>(entityID))
    {
      return;
    }
    if(entityID >= entityToComponentIDs<T>.size())
    {
      entityToComponentIDs<T>.resize(entityID+1, NULL_ID);
      entityToComponentIDs<T>[entityID] = componentArray<T>.size();
      componentArray<T>.emplace_back(args...);
    }
    else if(entityID < entityToComponentIDs<T>.size())
    {
      bool foundBiggerID = false;
      for(ID i = entityID + 1; i < entityToComponentIDs<T>.size(); i++)
      {
        if(entityToComponentIDs<T>[i] != NULL_ID)
        {
          if(foundBiggerID == false)
          {
            componentArray<T>.emplace(componentArray<T>.begin() + entityToComponentIDs<T>[i], args...);
            entityToComponentIDs<T>[entityID] = entityToComponentIDs<T>[i];
            foundBiggerID = true;
          }
          entityToComponentIDs<T>[i] += 1;
        }
      }
    }
    entityEntryArray[entityID].removeComponentFunctions.push_back(&removeComponent<T>);
  }
  template<typename T>
  void Entity::removeComponent(ID entityID)
  {
    if(!hasComponents<T>(entityID))
    {
      return;
    }
    componentArray<T>.erase(componentArray<T>.begin() + entityToComponentIDs<T>[entityID]);
    for(ID i = entityID; i<entityToComponentIDs<T>.size(); i++)
    {
      if(entityToComponentIDs<T>[i] != NULL_ID)
      {
        entityToComponentIDs<T>[i] -= 1;
      }
    }
    entityToComponentIDs<T>[entityID] = NULL_ID;
  }
  template<typename T>
  T & Entity::getComponent(ID entityID)
  {
    return componentArray<T>[entityToComponentIDs<T>[entityID]];
  }
  template<typename T>
  bool Entity::hasComponents(ID entityID)
  {
    return entityToComponentIDs<T>.size() > entityID && entityToComponentIDs<T>[entityID] != NULL_ID;
  }
  template<typename T1, typename T2, typename... Targs>
  bool Entity::hasComponents(ID entityID)
  {
    return hasComponents<T1>(entityID) && hasComponents<T2, Targs...>(entityID);
  }
  template<>
  bool Entity::hasComponents<void>(ID entityID)
  {
    return entityEntryArray.size() > entityID && entityEntryArray[entityID].exists;
  }

  template<typename T, typename... Targs>
  class Iterator
  {
    private:

    ID entityID;

    public:

    Iterator() : entityID(NULL_ID) {}
    explicit Iterator(ID entityID) : entityID(entityID) {}
    explicit Iterator(Entity entity) : entityID(entity.entityID) {}

    Iterator<T, Targs...> begin();
    Iterator<T, Targs...> end();

    void operator= (const Iterator<T, Targs...>& a);
    bool operator== (const Iterator<T, Targs...>& a);
    bool operator!= (const Iterator<T, Targs...>& a);
    Entity operator*();
    Entity operator->();
    Iterator<T, Targs...> operator++();
  };

  template<typename T, typename... Targs>
  Iterator<T, Targs...> Iterator<T, Targs...>::begin()
  {
    ID i = 0;
    while(end().entityID > i)
    {
      if(!Entity::hasComponents<T, Targs...>(i))
      {
        i+=1;
        continue;
      }
      return Iterator<T, Targs...>(i);
    }
    return end();
  }
  template<typename T, typename... Targs>
  Iterator<T, Targs...> Iterator<T, Targs...>::end()
  {
    return Iterator<T, Targs...>(Entity::entityEntryArray.size());
  }
  template<typename T, typename... Targs>
  void Iterator<T, Targs...>::operator= (const Iterator<T, Targs...>& a)
  {
    entityID = a.entityID;
  }
  template<typename T, typename... Targs>
  bool Iterator<T, Targs...>::operator== (const Iterator<T, Targs...>& a)
  {
    return entityID == a.entityID;
  }
  template<typename T, typename... Targs>
  bool Iterator<T, Targs...>::operator!= (const Iterator<T, Targs...>& a)
  {
    return entityID != a.entityID;
  }
  template<typename T, typename... Targs>
  Entity Iterator<T, Targs...>::operator*()
  {
    return Entity(entityID);
  }
  template<typename T, typename... Targs>
  Entity Iterator<T, Targs...>::operator->()
  {
    return Entity(entityID);
  }
  template<typename T, typename... Targs>
  Iterator<T, Targs...> Iterator<T, Targs...>::operator++() // Prefix Increment
  {
    entityID+=1;
    while(end().entityID > entityID)
    {
      if(Entity::hasComponents<T, Targs...>(entityID))
      {
        return Iterator<T, Targs...>(entityID);
      }
      entityID+=1;
    }
    entityID = end().entityID;
    return end();
  }

  /********************SYSTEMS********************/

  class SystemManager
  {
    public:

    static void addSystem(void (*update)(), const Duration& deltaTime);
    template<typename T>
    static void addSystem(void (*update)(const T&));
    static void removeSystem(void (*update)());
    template<typename T>
    static void removeSystem(void (*update)(const T&));
    static void runSystems();
    template<typename T>
    static void throwEvent(const T& event);

    private:

    struct TimeBasedSystem
    {
      TimeBasedSystem(void (*update)(), Duration deltaTime) :
        update(update),
        deltaTime(deltaTime),
        lastUpdateCallTime(TimePoint())
      {
      }
      void (*update)();
      Duration deltaTime;
      TimePoint lastUpdateCallTime;
    };
    struct EventRegisterHelper
    {
      explicit EventRegisterHelper(void (*f)())
      {
        runEventBasedSystemsList.push_back(f);
      }
    };
    template<typename T>
    static void runEventBasedSystems();

    static std::vector<TimeBasedSystem> timeBasedSystems;
    static std::vector<void(*)()> runEventBasedSystemsList;
    template<typename T>
    static std::vector<void(*)(const T&)> eventBasedSystem;
    template<typename T>
    static std::vector<T> eventQueue;
  };
  std::vector<SystemManager::TimeBasedSystem> SystemManager::timeBasedSystems = std::vector<SystemManager::TimeBasedSystem>();
  std::vector<void(*)()> SystemManager::runEventBasedSystemsList = std::vector<void(*)()>();
  template<typename T>
  std::vector<void(*)(const T&)> SystemManager::eventBasedSystem = std::vector<void(*)(const T&)>();
  template<typename T>
  std::vector<T> SystemManager::eventQueue = std::vector<T>();

  void SystemManager::addSystem(void (*update)(), const Duration& deltaTime)
  {
    timeBasedSystems.emplace_back(update, deltaTime);
  }
  template<typename T>
  void SystemManager::addSystem(void (*update)(const T&))
  {
    eventBasedSystem<T>.push_back(update);
  }
  void SystemManager::removeSystem(void (*update)())
  {
    for(size_t i = 0; i<timeBasedSystems.size(); ++i)
    {
      if(timeBasedSystems[i].update == update)
      {
        timeBasedSystems.erase(timeBasedSystems.begin() + i);
        --i;
      }
    }
  }
  template<typename T>
  void SystemManager::removeSystem(void (*update)(const T&))
  {
    for(size_t i = 0; i<eventBasedSystem<T>.size(); ++i)
    {
      if(eventBasedSystem<T>[i] == update)
      {
        eventBasedSystem<T>.erase(eventBasedSystem<T>.begin() + i);
        --i;
      }
    }
  }
  void SystemManager::runSystems()
  {
    for(TimeBasedSystem & system : timeBasedSystems)
    {
      TimePoint now = Clock::now();
      Duration currentDeltaTime = now - system.lastUpdateCallTime;
      if(system.deltaTime <= currentDeltaTime)
      {
        system.update();
        system.lastUpdateCallTime = now;
      }
    }
    for(auto f : runEventBasedSystemsList)
    {
      f();
    }
  }
  template<typename T>
  void SystemManager::throwEvent(const T& event)
  {
    //register event type
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wunused-variable"
    const static EventRegisterHelper _(&runEventBasedSystems<T>);
    #pragma GCC diagnostic pop

    eventQueue<T>.push_back(event);
  }
  template<typename T>
  void SystemManager::runEventBasedSystems()
  {
    for(T & event : eventQueue<T>)
    {
      for(auto f : eventBasedSystem<T>)
      {
        f(event);
      }
    }
    eventQueue<T>.clear();
  }
}

Create an entity:

Entity a = Entity::createEntity();

Destroy an entity:

a.removeEntity();

This will also automatically destroy all components that have been assigned to this entity.

Assign a component to an entity:

struct Position
{
  double x;
  double y;
};
a.createComponent<Position>(Position{ 0.2, 0.3 });

You can replace the Position struct with any data structure that you wish to use.

Remove a component from an entity:

a.removeComponent<Position>();

Access a component that is assigned to an entity:

a.getComponent<Position>().x = 1337.42; // is undefined when a has no Position component assigned

This function returns a reference so you do not need a setComponent<T>(...function, but this reference is not stable when you assign or remove components so it might get invalid over time. If you want to store the return value of this function in a variable be sure that it stays valid as long as you need it.

Check if an entity has a component:

bool a_has_Position = a.hasComponents<Position>();
bool a_has_Position_and_Mass_and_Velocity = a.hasComponents<Position, Mass, Velocity>();
bool a_is_a_valid_entity = a.hasComponents<void>();

Looping over entities:

for(auto a : Iterator<Mass, Position>()) // loops over all entities that have the components Mass and Position
{
    for(
        auto b_iter = ++Iterator<Mass, Position>(a); // this sets b_iter to an Iterator, that is one entity further than a is
        b_iter != Iterator<Mass, Position>().end();
        ++b_iter
    )
    {
        Entity b = *b_iter;
        // do gravity calculations or whatever
    }
}

for(auto a : Iterator<void>()) // loops over all valid entities
{ /*...*/ }

Run all added systems and resolve all thrown events:

SystemManager::runSystems();

Add a system:

void gravitySystem()
{
    // loop over entities or whatever
}
SystemManager::addSystem(&gravitySystem, std::chrono::milliseconds(10));

This system will only be called by SystemManager::runSystems() if the last call of this system was 10 milliseconds or more ago.

Throw an event:

struct SomeEvent
{
    double value;
}
SystemManager::throwEvent(SomeEvent{0.001});

Again, you can replace SomeEvent with any structure you want to use.

Catch an event:

void catchEvent(const SomeEvent& event)
{
    std::cout << "Catched event: " << event.value << std::endl;
}
SystemManager::addSystem(&catchEvent);

You can replace catchEvent() with any function you want. If you want to catch all events T you need to add a function that looks like this:

void customEventCatcher(const T& event)
{
    // ...
}

Remove a system:

SystemManager::removeSystem(&catchEvent);
SystemManager::removeSystem(&gravitySystem);

If you have added the same system several times, all of them will be removed, even if you have assigned different durations to them.


GitHub: https://tsoj.github.io/ecs_prototype/

\$\endgroup\$
3
\$\begingroup\$

A few tips (not a full review)

Constructing components

For the client to add a component to an entity, they do this:

struct Position
{
  double x;
  double y;
};
a.createComponent<Position>(Position{ 0.2, 0.3 });

I think this syntax can be cleaned up a little. In the createComponent function, you're constructing components like this:

componentArray<T>.emplace_back(args...);

If you change that line to this:

componentArray<T>.push_back(T{args...});

Then creating a component becomes:

a.createComponent<Position>(0.2, 0.3);

I find this syntax a bit cleaner.

Static data structures

You're storing all your entities and components statically on the Entity class. This is OK because it is unlikely that anyone would need to have two separate systems. But static is global and global is bad. You should store all of the components and entities in a Registry object. Don't make this a singleton. Never uses singletons. This makes it obvious what depends on the game objects and allows the client to have multiple systems.

If you do this, then you can't have an Entity class because such a class would need to store a pointer to the Registry. With a thousand game objects, you have a thousand pointers which are just unnecessary. You should call functions on the Registry with the entity IDs to manipulate the game.

Systems

Your SystemManager is almost useless. I like the idea of event systems (I've never used them in any of my games though). Systems are functions and functions have parameters. Systems are heavily dependent on order. Take this example.

struct DataStructure {};
struct Registry {
   // all of the entities and components
};

void writeData(Registry &reg, DataStructure &data) {
  // iterate some components and write to the data structure
}
void readAndWriteData(Registry &reg, DataStructure &data, int someOtherThing) {
  // iterate some components, reading and writing to the data structure.
}
void readData(Registry &reg, const DataStructure &data) {
  // modify some of the components based on the data structure
}

struct Game {
  DataStructure data;
  Registry reg;

  void runSystems(int thing) {
    writeData(reg, data);
    readAndWriteData(reg, data, thing);
    readData(reg, data);

    // handle timing
  }
};

All of the systems take a different set of parameters and they must all be executed in the right order.

\$\endgroup\$
  • \$\begingroup\$ I'd probably need to use RTTI to get something like a Registry working since template member variables are not supported. I'm not sure yet if it's worth it. \$\endgroup\$ – Darius Duesentrieb Mar 23 '18 at 14:33
  • \$\begingroup\$ If you've created a non-trivial game with your system, my suggestions probably aren't worth it. If you haven't, maybe you should consider my suggestions. I've used a system like I describe to create a few non-trivial games. If your goal is just to make games, you could use an existing library. github.com/skypjack/entt/blob/master/README.md I love this library. It's simple, well documented and stays out of your way. \$\endgroup\$ – Kerndog73 Mar 23 '18 at 21:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.