Partage
  • Partager sur Facebook
  • Partager sur Twitter

[C++] Simple Game Framework

Un framework C++ basé sur la SFML

8 août 2015 à 18:23:02

Bonjour à tous, je me présente, LeB0ucEtMistere, aka Arthur D. Je suis actuellement dans des études de mathématiques (prépa maths) et je développe en C++ majoritairement pendant mon temps libre.

Je viens vous présenter mon nouveau projet, en maturation depuis quelques années déjà : Le Simple Game Framework (SGF pour les intimes)

En savoir plus sur le projet

Genèse

Après plusieurs années de programmation et une flopée de programmes plus ou moins avortés, j'ai eu comme tout le monde envie de ma lancer dans le développement de mon jeu vidéo. Et là, comme beaucoup ça a été le drame, à plusieurs reprises ! Mais j'ai pris de la bouteille et j'ai surtout réalisé que faire un bon jeu, ça demande une team de passionnés, du temps (beaucoup) et des outils. J'ai donc (momentanément) laissé tombé l'idée de créer mon jeu pour me lancer corps et âme, ou presque, dans ce projet : créer des outils utiles à la plus grande majorité souhaitant développer un jeu vidéo en C++, en se basant sur la librairie SFML.

Il faut savoir que je ne souhaite pas créer un moteur de jeux à part entière. J'ai donc choisi de me baser sur la SFML. Cependant, après de nombreuses années passées à traîner sur le forum C++ d'OC, j'ai constaté que de nombreux débutants (moi y compris) doivent à chaque fois réinventer la roue : gestion des ressources externes (images, textures, musiques, ...), gestion de la boucle principale de jeu, import de maps, création d'un ECS, création d'algorithmes récurrents (pathfinding notamment). J'ai donc eu (comme beaucoup avant moi, certes) l'idée de regrouper ces fonctions au sein d'un framework.

Généralités et avancement

Le projet est développé en C++, autour de la librairie SFML. Il sera à terme composé d'un noyau solide comprenant un ECS et un "moteur" de jeu basique (boucle principale, events, statemachine, ...). Et autour de ce corps viendront s'adjoindre des modules complémentaires. On pourra imaginer une GUI, un passeur de Lua, un module de scripting, etc ...

Actuellement, le projet est en pré-alpha : entendez par là que seul le noyau est commencé et à peu près fonctionnel. Je précise dès maintenant que mes études me prennent beaucoup de temps et que le projet n'avance que pendant les vacances scolaires, et relativement lentement. Je n'ai pas d'échéancier et ne compte pas en avoir ! Certes le projet contient encore très peu de features et tout peut être amené à changer dans les mois qui suivent, mais je suis déjà dessus depuis maintenant 1 an et j'ai envie de partager mon expérience avec la communauté. De plus, j'ai actuellement besoin de retours sur ce que j'ai déjà fait, et j'aimerai avoir votre avis sur plusieurs questions que je me pose quant au développement de certaines features.

Features actuelles (rien n'est 100% fini of course mais ce qui est listé ici est fonctionnel) :

  • ECS
  • Machine à états
  • Boucle de jeu principale
  • Gestionnaire de ressources externes
  • Spécialisation du gestionnaire pour les classes de bases de la SFML
  • Logger paramètrable
  • Système d'exceptions
  • Tout début de GUI (pour faire des tests)

Je compte prochainement développer (par ordre de priorité) :

  • un système de message pour que les systèmes de l'ECS puissent interagir.
  • refonte du pipeline de rendu : je ne suis pas du tout heureux de ce qui est fait actuellement, pas assez flexible...
  • rafraichissement du code des ressource managers, trop verbeux pour l'utilisateur à mon goût
  • optimisation de l'ECS : pool d'entité et autres
  • documentation du code

Objectifs

L'objectif numéro un du framework est comme son nom l'indique la simplicité d'utilisation. Il s'adresse principalement aux codeurs débutants dans le monde du jeu vidéo et aux amateurs développant pour leur plaisir. Il n'a strictement aucune visée commerciale. Je veux juste fournir un outil performant et agréable à utiliser. Ce framework ne sera probablement pas un foudre de guerre (du moins surement pas tout de suite) mais je ne compte pas non plus faire de trop gros compromis sur la qualité. Il doit être capable de réaliser un jeu performant !

Je vise pour mon projet une optique d'expansion. A savoir que lorsque la première release stable sera sortie, j'aimerai encourager les développeurs intéressés par mon projet à prendre part à son développement en créant des *modules*. Nous reviendront à cette idée de modules par la suite. Ce projet devra rester à dimension humaines, pas de machine à gaz ! Ça irait à l'encontre de l'essence même du projet d'ailleurs.

Le projet et son originalité

Comme dit ci-dessus, ce projet vise les amateurs et non pas le commerce. Toutes les sources seront mis sous licence GPL, et seront accessibles.

De plus, j'ai l'intention de rendre le projet *modulaire*. Comme je le disais plus tôt, je souhaite mettre en place une interface pour que les users puissent facilement ajouter et proposer leurs propres modules, toujours dans une idée de collaboration. Dès que la première release stable sortira, je commencerai le développement de ces genres de modules et pourquoi pas, intégrerai vos propres modules si vous m'en proposez des pertinents. J'ai notamment sur ma to-do liste : les algos de pathfinding, les algos de bruits, un parseur de TMX files, un module de scripting, etc ...

Le mot de la fin

Voilà donc le repository GitHub contenant mon code. (pour l'instant, seul la branche develop contient du code) Je remercie tous ceux qui ont pris la peine de me lire jusqu'au bout et j'attends vos retours d'une part sur le concept, et d'autre part sur le code ! J'essaye de produire un code propre et moderne mais je reste un autodidacte et je sais qu'il y a ici des gens bien plus compétents à même de me conseiller. J'en profite d'ailleurs pour remercier tous les gens du forum C++ qui m'ont permis d'arriver jusqu'ici : entre autres Kssas`Peuk, Jo_Link_Noir, gbdivers, lmghs, bacelar, et j'en oublie surement mais j'ai une pensée pour vous tous :) Enfin, ce post sera tenu à jour au fur et à mesure de mes avancées, je me servirai aussi du fil comme dev-blog.

-
Edité par leboucetmistère 9 août 2015 à 10:47:31

  • Partager sur Facebook
  • Partager sur Twitter
8 août 2015 à 18:37:30

Salutation,

Vue que j'utilise la sfml ça pourrais être utile , fait nous part de ton avancé et bon courage :)

  • Partager sur Facebook
  • Partager sur Twitter
Ne jamais regarder plus haut que sois.
9 août 2015 à 10:57:36

Merci pour ton soutien.

Pour l'avancée, je suis actuellement dans une phase de cleaning du code, mais je ne la pusherai surement pas avant qu'une feature ait été ajoutée ... 
Sinon j'aimerai votre avis sur un souci de design que j'ai actuellement au niveau de la pipeline de rendu.

En effet la classe Game et les gamestate implémentent une méthode update et un méthode draw, ou l'on met respectivement les modifications sur les composants de scène et leur dessin à l'écran. Imaginons qu'on fasse un game state gérant un menu de pause : en fond on met une belle image et quelques boutons. on gère dans update les éventuelles animations graphiques survenant à l'écran et dans draw, on dessine nos boutons et notre fond. Et là est le souci : update est toujours appelé avant draw, donc si dans update j'appelle les systems de mon ECS et qu'il y a un éventuel renderSystem qui affiche les entités à l'écran (ce qui risque d'arriver si on a par exemple une animation à base de particules gérées par des systems), alors elle seront dessinées avant le fond, qui sera lui dessiné dans la fonction draw() ... donc le fond masquera tout ... D'où mon problème avec la pipeline actuelle ! 

J'ai pensé à deux solutions : 

1- la méthode barbare, on créé une méthode de pre-draw() appelée avant l'update() pour dessiner une première catégorie d'éléments : c'est moche et je veux vraiment pas faire ça ...
2- implémenter un système de layers comme le font déjà plusieurs moteurs : donner à chaque objet un niveau et tout dessiner dans une fonction qui ordonnera les layers et dessinera dans le bon ordre : ça va par contre m'obliger à rajouter un niveau d'indirection dans l'appel à une fonction de dessin  on ne pourra plus dessiner en utilisant directement la SFML, mais on dessinera en appelant une fonction custom de la classe game qui elle s'en chargera ! ça me semble plus propre mais est-ce la meilleur solution ? ^^j'y réfléchi encore !

En tout cas, si vous avez des idées, vous pouvez m'en faire part, je vous écoute !
Bonne journée à tous. 

EDIT : bien entendu le drawing sera dissocié de la classe sgf::Game pour ne pas faire mal à mon ami le SRP :p

-
Edité par leboucetmistère 9 août 2015 à 10:59:43

  • Partager sur Facebook
  • Partager sur Twitter
9 août 2015 à 14:55:17

Bonsoir ,

Le system de layer semble être la meilleur idée , surtout que si tu créer une fonctions systemLayer tu pourras réutilisé cette dernière  ( ce qui est fort probable pour ce type de projet ) , encore mieux tu pourras créer autant de layer que tu veux :)

Personnellement j'opterais pour la seconde méthode , ensuite tu peut aussi sauvegardé le layout d'avant puis réutilisé et draw dessus ce que tu veux (question performance on as fait mieux :p)

En espèrent t'avoir aidé :-°

En tous cas bon courage , et bonne soirée.

EDIT : En passant le livre d'or m'as éclater :lol:

-
Edité par Bloodyline 9 août 2015 à 15:03:19

  • Partager sur Facebook
  • Partager sur Twitter
Ne jamais regarder plus haut que sois.
10 août 2015 à 23:44:48

Hello :) 
Petit post pour vous annoncer que j'ai revu la rendre pipeline, en ajoutant une classe perso sgf::Window, qui fournit la majorité des fonctionnalité d'une sf::RenderWindow, et qui ajoute en plus la gestion des layers de dessin. Du coup, on peut maintenant dessiner de partout dans le code en choisissant ses layers :) ! plus de souci de background qui masque autre chose si dessiné avant ! Bon je ne push pas ces modifs tout de suite sur le github parce que je bosse sur autre chose, mais ça ne va pas tarder ! A plus.
  • Partager sur Facebook
  • Partager sur Twitter
11 août 2015 à 0:05:01

Salutation,

Heureux d'entendre ça ! du coup comment marche le système de layer ? par ordre numérique  ?

ça serais intéressent car du coup on donneras un ordre à chaque layer et on pourras plus " s'orienté " côté code.

Bon courage :).

  • Partager sur Facebook
  • Partager sur Twitter
Ne jamais regarder plus haut que sois.
11 août 2015 à 10:25:58

J'avais promis de vous pondre un bout de code donc voilà : 
Instanciation d'un MovementSystem

class MovementSystem : public sgf::System<PositionComponent> //nécessitera des entités possédant un PositionComponent
{
    
public:
    MovementSystem(sgf::World &world): sgf::System<PositionComponent>(world), speed(100)
    {}
    void run(sf::Time const& elapsed) override
    {
    auto time=elapsed.asSeconds();
        for(auto &it: _watchedEntity)
        {
            //On récupère les données du composant
            auto &data= it->second.getComponent<PositionComponent>("pos")._data;
            
            //on fait se déplacer l'entité
            data.x+=time*speed;
            data.y+=time*speed;
            
        }
    }
    
private:
    float speed;
    
};

La création d'une entité et d'un système : 

    std::unique_ptr<sgf::Entity> entity(sgf::make_unique<sgf::Entity>(12));
    entity->addComponent<PositionComponent>("pos", 200,200);
    
    world.registerEntity(entity);
    
    auto mov=std::make_unique<MovementSystem>(world);
    world.addSystem(*mov);
    _systems.push_back(std::move(mov));

Et le dessin d'images/élements de gui dans la fonction draw de la main loop :

void IntroState::Draw(sgf::Window& window)
{
    window.draw(_spriteLoader.getRessource("bckg_image" ),sgf::Layer::Background);
    window.draw(jouer.getCurrentSprite(),sgf::Layer::Foreground);
    window.draw(reglages.getCurrentSprite(),sgf::Layer::Foreground);
    window.draw(quitter.getCurrentSprite(),sgf::Layer::Foreground);
    window.draw(_spriteLoader.getRessource("gui_title" ),sgf::Layer::Foreground);
  
}

Où l'on voit l'utilisation des layers.
Il en existe actuellement 3 (ce qui changera par la suite, avec la possibilité pour l'user de créer ses layers, mais pas maintenant) Background, Middle, Foreground
Par défaut, un appel à sgf::Window::draw() dessine dans le Middle. 
Du coup, avec ce code j'ai pu dessiner dans un autre point du programme des entités (via un rendez system) en choisissant de les dessiner au dessus du background. Voilà ce que ça donne (petit test !) avec le rond rouge qui est une entité qui se déplace et qui passe sous les éléments de GUI  : 
Image(un peu grande pour mettre ici)

Voilà voilà, si vous avez des remarques sur la syntaxe c'est le moment de les partager ! ;) 
Bonne journée



-
Edité par leboucetmistère 11 août 2015 à 10:26:16

  • Partager sur Facebook
  • Partager sur Twitter
11 août 2015 à 10:46:44

Bonjour ,

Alors déjà beau boulots ça avance bien , rien à dire côté syntaxe et bien vue côté layer de mettre le middle par défaut.

Pour la documentation tu fait comment ? si tu ne la pas encore commencé je te conseille de le faire avant que ça devient énorme et là ça deviendras très difficile et tu ne sauras pas où commencé , du coup conseil pour toi ; plus tu avance , plus tu écrit ta docu'.

Très bonne continuation c'est bien fait , hâte de voir le résultat ^^.

Bonne journée.

  • Partager sur Facebook
  • Partager sur Twitter
Ne jamais regarder plus haut que sois.
11 août 2015 à 13:32:43

Comme je l'ai dit dans le post sur ZdS, je ne compte pas docu le code avant la première alpha release, car tout peut changer d'ici là ;) cependant si tu as des questions sur l'utilité de certaines fonctions, tu peux les oser ici et j'y répondrai ;)
  • Partager sur Facebook
  • Partager sur Twitter
11 août 2015 à 17:27:26

Intéressant ce projet, je le met dans un coin :p
  • Partager sur Facebook
  • Partager sur Twitter
M'en tape je suis une Miku sorcière et je n'accepte aucun projet, désolé !
18 août 2015 à 13:13:25

Bonjour à tous

Le projet a avancé durant les derniers jours et l'ECS devient peu à peu fonctionnel. Il y a maintenant un système de messaging fonctionnel basé sur un design de dispatching. Je vous remet le lien du github. Et comme je vous l'ai promis, un peu de code relatif à ce nouvel et non des moindres ajout ! 

Donc pour l'exemple on va créer un système de diffusion de messages de type PhysicEvent
 

enum Side
{
    top,bot,right,left
};

// PHYSIC EVENT //

//Structure de base du listener, qui va proposer des méthodes que l'on peut assimiler à des SLOTS.
//Tout ce qui veut recevoir des messages PhysicEvent devra en hériter et implémenter ces "SLOTS"
struct PhysicEventListener
{
    virtual void bounced(sgf::Entity& entity, Side where) = 0;
};

//Le contexte va contenir toutes les données que l'on souhaite joindre à l'émission du message :
//d'où il vient, ses spécificités, l'entité concernée, ...
struct PhysicEventContext
{
    enum pWhat {bounced} _whatHappenned;
    sgf::Entity* _entity;
    Side _where;
};

//Ici on définit le type qui sera source de ces messages :
//il prend le type de listeners, le type de context, et en facultatif un mutex et un conteneur différent pour le back end
//Tout ce qui veut envoyer des messages PhysicEvent devra en hériter

using PhysicEventSource = sgf::Messaging<PhysicEventListener,PhysicEventContext>;


Il va ensuite falloir faire hériter nos systèmes :

Le système Listener : 

class MovementSystem : public sgf::System<PositionComponent>, public PhysicEventListener
//on hérite de PhysicEventListener pour implémenter les "SLOTS"
{
    
public:
    MovementSystem(sgf::World &world): sgf::System<PositionComponent>(world)
    {}
    virtual void run(float elapsed) override
    {
    ...
    }
    virtual void bounced(sgf::Entity& entity, Side where) //appelé quand un PhysicEvent est lancé avec le type "bounced"
    {
        auto &data= entity.getComponent<PositionComponent>("pos")._data;

        switch (where) {
            case Side::top:
                data.speed.x*= -1;
                break;
            case Side::bot:
                data.speed.x*= -1;
                break;
            case Side::right:
                data.speed.y*= -1;
                break;
            case Side::left:
                data.speed.y*= -1;
                break;
            default:
                break;
        }
    }
};

Et le système source : 

class PhysicSystem : public sgf::System<PositionComponent>, public PhysicEventSource
//on hérite de la source poru implémenter le dispatching
{
public:
    PhysicSystem(sgf::World &world): sgf::System<PositionComponent>(world)
    {}
    virtual ~PhysicSystem() = default;
    virtual void run(float elapsed) override
    {
        
        for(auto it=_watchedEntity.begin(); it!=_watchedEntity.end(); it++)
        {
            PhysicEventContext ctx; //Création du contexte qui contiendra les données
            ctx._entity = &it->second;
            auto &data= it->second.getComponent<PositionComponent>("pos")._data;
            if (data.speed.x > 0 && data.position.x>1920-150) //moving down and close to the bottom wall
            {
                //remplissage du contexte
                ctx._whatHappenned = PhysicEventContext::pWhat::bounced;
                ctx._where = Side::top;
                //on lance le message en passant le contexte
                raiseEvent(ctx);
            }
            else if (data.speed.x < 0 && data.position.x<150) //moving up and close to the top wall
            {
                ctx._whatHappenned = PhysicEventContext::pWhat::bounced;
                ctx._where = Side::bot;
                raiseEvent(ctx);
            }
            else if (data.speed.y > 0 && data.position.y>1080-150) //moving right and close to the right wall
            {
                ctx._whatHappenned = PhysicEventContext::pWhat::bounced;
                ctx._where = Side::right;
                raiseEvent(ctx);
            }
            else if (data.speed.y < 0 && data.position.y<150) //moving left and close to the left wall
            {
                ctx._whatHappenned = PhysicEventContext::pWhat::bounced;
                ctx._where = Side::left;
                raiseEvent(ctx);
            }
            
        }
    }
    void dispatchEvent(PhysicEventListener &p,
                       const PhysicEventContext &context) override
    //fonction centrale devant être implémentée par toute source, héritée de sgf::Messaging
    //elle permet de définir comment dispatch l'event en fonction du contexte, appeler tel ou tel slot par exemple chez les listeners
    {
        switch (context._whatHappenned) {
            case PhysicEventContext::pWhat::bounced:
                p.bounced(*context._entity,context._where);
                break;
                
            default:
                break;
        }
    }
};

Enfin on a plus qu'à enregistrer les listeners chez la source.

auto physics=std::make_unique<PhysicSystem>(world);
physics->addListener(*mov);

Et voilà le tour est joué :) dès que run() sera appelée dans le system émetteur, si nécessaire des events seront levés et transmis à tous les listeners via les lots !

N'hésitez pas à me dire ce que vous pensez de tout ça ! ;) Cet ajout rend l'ECS totalement fonctionnel !

Sinon je suis actuellement entrain de bosser sur le dernier gros problème qui m'empêche de release la première alpha et qui risque surtout de m'amener à repenser tout le code des components ... ^^
Mais si tout ce passe bien vous pourrez bientôt définir des systems du type sgf::Systems<sf::Transformable> qui manageront tous les composants héritant de sf::Transformable ce qui simplifiera énormément l'utilisation des systems avec les libs telle que la SFML ;)


 

  • Partager sur Facebook
  • Partager sur Twitter

[C++] Simple Game Framework

× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
  • Editeur
  • Markdown