Skip to content

unrays/Pulse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pulse

A lightweight, ultra-modular C++ ECS framework for building flexible and efficient entity systems.

This is primarily as much an architectural challenge as it is a syntax challenge. This project served as a way for me to get a little more comfortable with ECS designs, C++ syntax, and generic templates and typing.

Small note, if you asked me to write this from memory without making any mistakes, I want to tell you right away that I would be incapable of it, the syntax is far too complex and I am only in my 2nd year of programming so I still have a long way to go before being able to do this professionally. However, I think I am clearly capable of creating minimal ecs systems that work very well. This is mainly an architectural experiment.

// Copyright (c) October 2025 Félix-Olivier Dumas. All rights reserved.
// Licensed under the terms described in the LICENSE file

#include <iostream>
#include <unordered_map>
#include <string>
#include <sstream>
#include <map>
#include <type_traits>
#include <cassert>
#include <iomanip>

#define VAR_NAME(x) #x
#define CLASS_NAME(x) typeid(x).name()

template<typename T> struct ID {
    T value;
    explicit ID(T v = 0) : value(v) {}
};

template <typename T> void printValue(const T& t) {
    //std::cout << t << std::endl;
    std::cout << "     Address: " << static_cast<const void*>(&t) << std::endl;
}

using EntityID = uint32_t;

class Entity {
private:
    static EntityID nuid;
           EntityID uid;
public:
    Entity() : uid(nuid++) { }
    EntityID getId() const { return uid; }

    bool operator==(const Entity& other) const { return uid == other.uid; }
    bool operator!=(const Entity& other) const { return uid != other.uid; }
    bool operator<(const Entity& other)  const { return uid <  other.uid; }

}; EntityID Entity::nuid = 0;

class Component {
private:

public:
    virtual ~Component() = default;
    void doStuff() { std::cout << "Mock Component check\n"; }

};

class Velocity : public Component { public: float vx, vy = 0; };

class Position : public Component { public: float x, y = 0; };

class Weight : public Component { public: float weight = 0; };

class Name : public Component { public: std::string name = "default"; };

template<typename E = Entity, typename C = Component> class Registry {
private:
    std::map<E, std::vector<std::unique_ptr<C>>> registry;


public:
    Registry() {}

    template <typename CC, typename E> void addComponent(const E& e) {
        static_assert(std::is_base_of_v<C, CC>, "Must derive from Component");
        static_assert(std::is_default_constructible_v<CC>, "Must be default constructible");

        auto& vec = registry[e];
        for (auto& comp : registry[e])
            if (typeid(*comp) == typeid(CC))
                throw std::runtime_error("Component already exists!");
        vec.push_back(std::make_unique<CC>());
    }
    
    template <typename CC, typename E> CC* getComponent(const E& e) {
        for (auto& comp : registry[e])
            if (auto derived = dynamic_cast<CC*>(comp.get()))
                return derived;
        return nullptr;
    }

    template<typename CC = Component> size_t countComponents(const E& e) {
        size_t count = 0;
        for (auto& comp : registry[e])
            if (auto derived = dynamic_cast<CC*>(comp.get())) ++count;
        return count;
    }

    template <typename E> bool hasComponents(const E& e) const { return !registry[e].empty(); }

    template <typename CC = Component> std::vector<std::string> getComponentNames(const E& e) {
        std::vector<std::string> cnvec;
        for (auto& comp : registry[e])
            if (auto derived = dynamic_cast<CC*>(comp.get()))
                cnvec.push_back(typeid(*derived).name());
        for (auto& name : cnvec) {
            const std::string prefix = "class ";
            if (name.rfind(prefix, 0) == 0)
                name = name.substr(prefix.size());
        } return cnvec;
    }
};


class System {
private:


public:
    virtual void update(Registry<>& registry, float dt) = 0;

};

class Event /* BASE */ {
    /* Exemples de dérivés :
         - OnDamageEvent(...)
         - OnCollisionEvent(...)
    */

    /* event = collisionTrigger.generateEvent(entity) */
};

class Trigger /* BASE */ {
private:
        /* Exemples de dérivés :
             - collisionTrigger(...)
             - damageTrigger(...)
        */

public:

    /* event = collisionTrigger.generateEvent(entity) */

};

template<typename E = Event, typename S = System> class EventDispatcher {
private:
    std::map<E, std::vector<std::unique_ptr<S>>> eventListeners;

public:
    template <typename SS, typename E> void registerEvent(const E& event) {
        static_assert(std::is_base_of_v<S, SS>, "Must derive from System");
        static_assert(std::is_default_constructible_v<SS>, "Must be default constructible");

        auto& vec = eventListeners[e];
        for (auto& s : vec)
            if (typeid(*s) == typeid(SS))
                throw std::runtime_error("Component already exists!");
        vec.push_back(std::make_unique<SS>());
    }

    /* Dans le fond, c'est un registry ecs mais qui sert aussi de proxy entre
       les triggers, events et les systèmes qui appliquent le comportement. */

    /* Remove Event() */

    /* dispatcher.queue(event) */



};

class ConsolePrintingSystem : public System {
private:


public:
    void update(Registry<>& registry, float dt) override {

    }
};

template <typename E> void printEntity(Registry<>& reg, const E& e) {
    std::cout << std::left;
    std::cout << "--------- Entity ---------\n";
    std::cout << std::setw(15) << "Name" << ": " << VAR_NAME(e) << "\n";
    std::cout << std::setw(15) << "Type" << ": " << CLASS_NAME(e) << "\n";
    std::cout << std::setw(15) << "ID" << ": " << e.getId() << "\n\n";

    std::cout << "---- Core Attributes ----\n";

    if (auto name = reg.getComponent<Name>(e))
        std::cout << std::setw(15) << "Name" << ": " << name->name << "\n";

    if (auto weight = reg.getComponent<Weight>(e))
        std::cout << std::setw(15) << "Weight" << ": " << weight->weight << " Kg\n";

    if (auto pos = reg.getComponent<Position>(e))
        std::cout << std::setw(15) << "Position" << ": (" << pos->x << ", " << pos->y << ")\n";

    if (auto vel = reg.getComponent<Velocity>(e))
        std::cout << std::setw(15) << "Velocity" << ": (" << vel->vx << ", " << vel->vy << ")\n";

    auto componentNames = reg.getComponentNames(e);
    std::cout << "\n------- Components -------\n";
    std::cout << std::setw(15) << "Count" << ": " << componentNames.size() << "\n";

    std::cout << std::setw(15) << "Names" << ": ";
    for (size_t i = 0; i < componentNames.size(); ++i) {
        if (i > 0) std::cout << ", ";
        std::cout << componentNames[i];
    }
    std::cout << "\n\n\n\n";
}

int main() {
    auto registry = Registry<Entity, Component>();

    auto cat     = Entity();
    auto dog     = Entity();
    auto hamster = Entity();

    registry.addComponent<Name>(cat);
    registry.addComponent<Weight>(cat);
    registry.addComponent<Velocity>(cat);
    registry.addComponent<Position>(cat);

    registry.addComponent<Name>(dog);
    registry.addComponent<Weight>(dog);

    registry.addComponent<Name>(hamster);

    auto name     = registry.getComponent  <Name>  (cat);
    auto weight   = registry.getComponent <Weight> (cat);
    auto velocity = registry.getComponent<Velocity>(cat);
    auto position = registry.getComponent<Position>(cat);

    auto componentCount = registry.countComponents(cat);
        
    name->name = "Garfield";
    weight->weight = 10.0f;
    velocity->vx = 5.0f; velocity->vy = 15.0f;
    position->x = 25; position->y = 75;



    printEntity(registry, cat);

    if (registry.getComponent<Name>(dog)->name == registry.getComponent<Name>(cat)->name) {
        std::cout << "Identiques" << std::endl;
    } else std::cout << "Non-identiques" << std::endl;
   

    /*=======================================================*/
    
    /* Les composants fournissent les données (data-only) */
    /* Les systèmes définissent le comportement */

    /*
        class LookAtSystem : SystemBase() {
           override fun execute() { ... }
    */

    /* [DEPRECATED] Faire une liste internet d'entités (registered_entities? ou whatever)
       puis ensuite à chaque "update" ou appel, on incrit les entités qui présentent
       un type spécifique donné ou requis au système (<T?>) afin de "sort" en tant que
       tel les entités qui présentent les composantes requises. Ensuite, à chaque "update"
       on vérifie si les entités sont déja inscrites dans le registre interne (std::vector?) 
       puis s'il ne le sont pas, on les ajoute tout simplement. La logique ici serait de faire
       quelque chose qui évite d'une facon ou d'une autre la copie des éléments qui peut être 
       très couteuse en condition du nombre d'entités dans notre système. Enfin, il ne suffira
       que d'affecter les entités de notre collection à notre comportement voulu afin qu'il se
       "propage" au système en entier. À noter que ce système est global et ne présente pas encore
       de notion de "trigger" ou bien "d'évènement" alors les comportement seront appliqués globalement
       sans différenciation ou variation apparente. Écrit le lundi 20 octobre 2025 */

    /* 
        [Game Loop]
           |
           v
        [CollisionSystem] --checks--> Entities (player, monsters)
           |
           v
        Trigger détecté? Oui --> [PlayerHitMonsterEvent(player, monster)]
           |
           v
        [Event Dispatcher] ---> notifie [HealthSystem]
           |
           v
        [HealthSystem] --> applique damage sur player uniquement
    */

    /* 
        Rôle principal (Dispatcher)

        Prendre un event généré par un trigger.

        Identifier tous les systèmes abonnés à ce type d’event.

        Notifier chaque système en appelant sa méthode type onEvent(event).

        Optionnel : vider ou gérer une queue pour traiter tous les events accumulés dans la frame.
    */

    /*
        [EventDispatcher.dispatch()]
           └─ prend event depuis queue ou directement passé
           └─ regarde map<EventType, list<System*>>
           └─ pour chaque system dans la liste
                 └─ system.onEvent(event)
    */

    /*
        Trigger

        ├─ condition(entity) : bool           # Vérifie si le trigger est activé pour une entité
        ├─ eventType : EventType             # Type d’event que ce trigger génère
        ├─ generateEvent(entity) : Event     # Crée l’événement associé à l’entité
        ├─ targetComponents : list<ComponentType>  # Optionnel : composants requis pour évaluer le trigger
    */

    /*
        [Trigger.condition(entity)] --true--> [Trigger.generateEvent(entity)]
                |
                v
        [EventDispatcher.queue(event)] --> [Systems abonnés traitent event]

    */

    /*
        System.update():
        for entity in managedEntities:
            if collisionTrigger.condition(entity):
                event = collisionTrigger.generateEvent(entity)
                dispatcher.queue(event)   # Trigger ne connaît pas les systèmes abonnés

    */

    /* Donc en conclusion, le plan est assez simple. Premièrement, il nous faudra créer les classes suivantes :
       System (déja prétent), Event, Trigger, EventDispatcher. Maintenant, nous avons un système qui dans notre
       architecture présente, doit avoir une fonction update() qui s'occupe de scanner les entités, les filtrer
       pour ensuite appliquer une condition (Trigger) qui si vrai, crée un évènement (Event) qui est ensuite envoyé
       au EventDispatcher, à noter que cet évènement contient les éléments du petit tableau ci-dessus. Ensuite,
       nous aurons aussi la fonction onEvent(event) qui, par l'intermédiaire du EventDispatcher, va recevoir des
       évnènements (Event envoyés par EventDispatcher) et appliquer le comportement voulu sur eux. Pour continuer, 
       nous avons ce fameux EventDispatcher qui, comme inscrit en haut, aura un dictionnaire (std::map) contenant
       un évènement (clé Event) associé à une liste de Systems auxquels sont abonnés à cet évent (Liste Systems, valeur)
       et représenté de tel -> std::vector<std::unique_ptr<Systems>> ou un équivalent. Lorsque ce EventDispatcher est notifié
       par un trigger, il vérifie premièrement si l'évènement est déja inscrit dans la liste, ni non, il l'ajoute, et puis 
       il parcours toute la liste de systems abonnés en appellant sys.onEvent(Event en question) afin d'appliquer le comportement
       voulu. De plus, il serait aussi possible d'implémenter un système de queue ou de buffer d'évènement afin de pouvoir
       traiter plusieurs évènements par frame sans surcharger le System ou bien le EventHandler, qui ne peut présentement
       que traiter un seul évent par appel. Écrit le lundi 20 Octobre 2025 par author.
       */

    /*=======================================================*/



    //newRegistry.addComponent<Component>(cat);
    //newRegistry.addComponent<Component>(cat);
    //newRegistry.addComponent<Component>(cat);

    //registry.addComponent<ComponentType pas instanciation>(cat); // possiblement faire ca, faut prendre le type de instancier dans la fonction addComponent();
    // Aussi, faire en sorte qu'on ne puisse qu'ajouter un seul composant de type par entité, par exemple uniquement un seul MockComponent...

    

    //auto comp = reg.getComponent<MockComponent>(cat); lui
}

Output

--------- Entity ---------
Name           : cat
Type           : class Entity
ID             : 0

---- Core Attributes ----
Name           : Garfield
Weight         : 10 Kg
Position       : (25, 75)
Velocity       : (5, 15)

------- Components -------
Count          : 4
Names          : Name, Weight, Velocity, Position

Releases

No releases published

Packages

No packages published

Languages