Partage
  • Partager sur Facebook
  • Partager sur Twitter

Jeu, Tour par Tour, Console, C++, Partie 2

Anonyme
    2 septembre 2021 à 21:17:46

    Bonjour,

    Nous sommes deux étudiants apprenant le C++ pendant notre temps libre.


    Nous avons déjà envoyé un message sur 3 forums différents et sur 2 discords orientés programmation en parlant du petit jeu tour par tour qu’on a créé dans le but d’apprendre le C++. Avec tous les retours que nous avons obtenus, nous avons pu progresser et apprendre de nos erreurs (merci beaucoup!). Nous avons suivi les conseils bien qu’il y ait encore des zones floues que nous allons aborder ici en espérant avoir des réponses précises et compréhensibles. 



    Il y a en tout, dans notre projet, 5 fichiers. Les voici :

    • main.cpp

    • game.hpp / game.cpp

    • entity.hpp / entity.cpp


    Et comme toujours, voici notre github où vous pouvez retrouver le code :

    https://github.com/Haulun/Console_Turn_Based_Game



    D’abord, ce que nous avons pu tirer de tous les conseils reçus :


    • Nous avons ajouté des fonctions pour éviter d’avoir un gros pavé de +150 lignes.

    • Nous avons rajouté le makefile pour ceux qui le voulaient.

    • Nous avons créé une classe dédiée au jeu, le main servant à exécuter le programme.

    • Nous avons appris ce qu'étaient les “unique_ptr” et nous les avons utilisés (voir ci-dessous).

    • Nous avons supprimé les classes filles inutiles au fonctionnement du jeu (c’est-à-dire, toutes ! Il ne nous reste plus que la classe Entity).

    • Nous avons fait en sorte que l’interface de jeu soit meilleure et plus lisible (les textes précédents inutiles s’effacent).

    • Nous avons fait en sorte que le joueur puisse quitter le jeu n’importe quand.

    • Nous avons initialisé par défaut nos variables membres lors de la déclaration.

    • INFO : Nous avons aucun warning, ni d’erreurs de compilation, ni d’erreurs à l’exécution !



    Et voilà, cependant, les zones encore floues pour nous :


    Nous utilisons peut-être mal les unique_ptr. Nous ne voyons en fait aucune différence avec les pointeurs qu’on utilisait précédemment (avec “ new ” et  “delete ”) car pour les unique_ptr, il nous semble qu’il faut utiliser “ reset() ” pour supprimer l’objet (que ce soit avant la fin du programme ou pour mettre un autre objet à la place du précédent) donc les questions qu’on se pose sont:

    • Pourquoi utiliser unique_ptr au lieu de pointeurs ‘‘normaux’’, quelles sont les différences ?

    • Les utilisons-nous mal ?

    • Dans la fonction terminateGame() dans la classe Game, sont-ils vraiment nécessaires ou les derniers objets sont directement détruits à la fin du programme avec les unique_ptr ?

    • Les fonctions make (makeTroll(), makeGoblin, etc...) et chooseHero() sont-elles correctes ?


    De plus, nous avons reçu un message concernant cela mais, nous avons du mal à savoir comment faire autrement que de mettre une dizaine de getters et/ou setters dans la classe Entity pour récupérer les informations des entités ou pour les modifier. 

    • Est-ce vraiment nécessaire ?

    • Si oui, avez-vous des choses à nous conseiller ?


    En outre, pensez-vous (on nous avait déjà fait la remarque) que nos classes Entity et Game sont trop surchargées (principe de responsabilité unique ?).


    Enfin, dans la fonction Game::enemyChooseAction() nous utilisons la fonction heal() à la fois comme booléen pour une condition if et comme action de se heal, pensez-vous que cette façon de faire est correcte ou faudrait-il rajouter une fonction “ bool healable() ” ? 



    Il y a cependant des remarques que nous n’avons pas encore implémentées et que nous comptons probablement ajouter: 

    • Passer toutes les variables (vie, shield, etc) dans un fichier XML indépendant.

    • Ajouter un Cmake

    • Utiliser ncurses pour l’interface



    Voilà l'avancée, j’espère que vous pourrez nous aider autant que la première fois car ça nous a vraiment permis de progresser et d’en apprendre davantage sur le C++.



    Cordialement.


    -
    Edité par Anonyme 2 septembre 2021 à 21:18:05

    • Partager sur Facebook
    • Partager sur Twitter
      2 septembre 2021 à 21:45:57

      Bonjour,

      En fait la classe std::unique_ptr< T > supprime la ressource qu' elle détient (si elle existe) lorsque qu'elle arrive à la fin de sa portée (renseigne toi sur la portée des variables pour en savoir plus). Tu n'as donc pas besoin de faire un 'reset()' pour libérer la ressource qu'elle détient (on parle d'ownership dans ce cas).

      La ressource est libérée par le destructeur de l'unique_ptr qui doit ressembler à quelque chose comme:

      std::unique_ptr::~unique_ptr()
      {
          if( ptr )
          {
              //Suppression de la ressource
          }
      }

      J' utilise notamment les unique_ptr pour faire un wrapper de la bibliothèque SDL2 écrite en C. La SDL2 fourni des fonctions pour libérer ses propres types comme 'SDL_DestroyRenderer(SDL_Renderer *renderer)' et tu peux donner en paramètre à un std::unique_ptr en plus du type , le prototype de cette fonction ainsi, dans le destructeur
       de std::unique_ptr la fonction fournie dans le constructeur sera lancée au moment de l'appel du destructeur.

      -
      Edité par Warren79 2 septembre 2021 à 21:52:19

      • Partager sur Facebook
      • Partager sur Twitter

      Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

      Anonyme
        2 septembre 2021 à 21:54:14

        Donc est-ce que dans le initGame(), le mainLoop() et dans le terminateGame(), les enemy.reset() et player.reset() sont utiles ou inutiles. Si il y en a qui sont utiles, lesquels sont-ils ?

        Cordialement

        • Partager sur Facebook
        • Partager sur Twitter
          2 septembre 2021 à 22:04:23

          En fait si les ressources détenues dans tes std::unique_ptr ne sont plus nécessaires mais que leurs présences dans la mémoire ne te dérange pas, alors laisse faire la libération automatique des ressources du C++ (Ressource Finalisation Is Destruction ou RFID ). Par contre si tu as un (par exemple) std::vector de std::unqiue_ptr< > d' énnemis et que tu parcours ce vector pour dessiner ces énnemis alors il peut être judicieux de déplacer ( std::move() ) ces unique_ptr vers un garbage collector pour des statistiques des cadavres fait par le joueur ( :) )  ou bien d'appeler la fonction reset de ces unique_ptr.
          • Partager sur Facebook
          • Partager sur Twitter

          Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

          Anonyme
            2 septembre 2021 à 22:16:04

            C'est assez compliqué à comprendre ^^' .

            Mais si on change d'objet ?

            Imaginons que l'ennemi (gobelin) est mort et qu'on veut recréer un ennemi (troll) avec le même unique_ptr, le fait de faire enemy = makeTroll() détruira le gobelin et recréera un objet troll ?

            Cordialement

            • Partager sur Facebook
            • Partager sur Twitter
              2 septembre 2021 à 22:27:33

              int trollHealth{100};
              
              std::unique_ptr< Troll > trollCharacter{ std::make_unique<Troll>(trollHealth) };
              
              //Ci-dessous le troll d'avant est supprimé et on crée un nouveau troll.
              trollCharacter.reset( new Troll{200} );

               :)

              -
              Edité par Warren79 2 septembre 2021 à 22:27:49

              • Partager sur Facebook
              • Partager sur Twitter

              Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

              Anonyme
                2 septembre 2021 à 23:26:21

                Merci,

                N'hésitez pas à faire d'autres retours les autres pros du C++ hehe :D

                Cordialement

                • Partager sur Facebook
                • Partager sur Twitter
                  3 septembre 2021 à 6:27:05

                  Quelques commentaires sur des détails syntaxiques :

                  - utilises les listes d'initialisation https://github.com/Haulun/Console_Turn_Based_Game/blob/main/entity.cpp#L5

                  - utilise =default https://github.com/Haulun/Console_Turn_Based_Game/blob/main/entity.cpp#L20

                  - utilise const (ou mieux ne déclare pas des variables qui servent a rien) https://github.com/Haulun/Console_Turn_Based_Game/blob/main/game.cpp#L164

                  - tu as des variables membres non initialisées a la declaration https://github.com/Haulun/Console_Turn_Based_Game/blob/main/game.hpp#L12

                  --------------------------------------------------------

                  Parlons un peu de programmation par contrat (cf wikipedia pour ce concept)

                  player et ennemi sont des pointeurs https://github.com/Haulun/Console_Turn_Based_Game/blob/main/game.cpp#L34 

                  Leur semantique (= ce que cela signifie "être un pointeur"), c'est "peut être null ou pointer vers un objet". Quand tu ecris par exemple "enemy->isDead()", cela veut dire implicitement que tu considère que le pointeur n'est pas nul. Dit autrement, ce code signifie "supposons que le pointeur n'est pas nul, appeler la fonction isDead sur l'objet pointé".

                  Dans la programmation par contrat, cela s'appelle une pré-condition. Cela veut dire : "si cette pré-condition (le pointeur n'est pas nul) est respectée, alors mon code est valide. Sinon..."

                  J'imagine que tu vois où je vais en venir : dans un code de qualité, c'est à dire un code qui est robuste aux bugs, on vérifie les pré-conditions, pour détecter le plus tôt possible s'il y a un bug.

                  Donc il faut vérifier TOUJOURS qu'un pointeur n'est pas nul, avant de l'utiliser.

                  2 cas possibles :

                  - soit c'est possible que le pointeur soit null a un endroit du code. Dans ce cas, on met par exemple un if(ptr==nullptr) et on gère le cas où le pointeur est nul.

                  - soit le pointeur n'est pas sensé être nul a un endroit du code et si cela arrive, c'est une erreur de programmation. Et donc la meilleure chose à faire dans ce cas, c'est crasher le programme, pour indiquer au développeur qu'il y a un problème a cet endroit du code. Pour cela, on utilise par exemple une assertion assert(ptr==nullptr).

                  Il y a pleins d'autres choses à améliorer au niveau des bonnes pratiques (en particulier faire des tests, je te conseille de regarder le TDD = test driven dev), mais c'est inutile de te surcharger au début de ton apprentissage.

                  -------------------------------------------------------------------

                  Concernant les getters et setters.

                  C'est une autre grosse partie de ton apprentissage :) Donc je passe rapidement, pour ne pas trop te surcharger.

                  En gros, l'idée, c'est qu'une bonne conception n'est pas de demander une info (getter) pour faire un calcul et modifier la valeur (setter), mais de donner directement les infos pour que chaque objet puisse faire ses propres calculs lui même. "Ne demandez pas les infos pour faire le boulot vous meme, mais donnez les infos pour que chacun puisse faire son boulot".

                  Un exemple pour illustrer. Ca, non :

                  const auto point_vie = ennemi.getPointVie();
                  const auto degats = arme.getDegats();
                  const auto nouveau_point_vie = point_vie - degats;
                  ennmi.setPointVie(nouveau_point_vie);

                  Ca, oui :

                  ennemi.prendCoupDansLesDents(arme);
                  
                  // et dans prendCoupDansLesDents
                  void Ennemi::prendCoupDansLesDents() {
                      m_point_vie -= arme.getDegats();
                  }

                  (il y a quand meme un getter ici)

                  En termes de conception, le premier code implique qu'un calcul qui concerne une classe (ici Ennemi) est fait en dehors de la classe. Ce qui veut dire que le code qui concerne une classe n'est pas que dans cette classe, mais dans plusieurs endroits du code. 

                  Sur un code de 100 lignes, c'est pas catastrophique. Dans un code de plusieurs centaines de milliers de lignes, ca complique beaucoup la compréhension du code. On parle de "localité spatiale" : mettre les infos ensemble, proche dans le code, pour faciliter la comprehension. Si des informations qui sont liées ensemble se trouvent éloigné dans le code, le code devient plus dur a comprendre.

                  Faire des getters et setters, cela pousse à penser son code en termes d'infos contenu dans une classe et pas en termes de comportements (services) qu'une classe doit proposer. Ici, les points de vie sont une info interne à la classe Ennemi. Donc le code qui modifie les points de vie de Ennemi doit être dans cette classe. Et si c'est le cas, alors il n'y a priori pas besoin de getter et setter sur point_de_vie.

                  Par du principe qu'avoir un getter et un setter est un problème de conception et qu'il faut toujours se poser la question de savoir si tu n'as pas fait une erreur de conception. (Ca ne sera pas toujours le cas)

                  -----------------------------------------------------------------

                  Concernant l'apprentissage.

                  Dans ce message, j'ai fait 3 grosses parties :

                  - les problématiques liées à la syntaxe du langage

                  - les problématiques liées à la qualité du code (= pas simplement dire quand un code est valide ou non, mais comment suivre des bonnes pratiques)

                  - les problématiques liées à la conception (= savoir comment le code est pensé et organisé)

                  Les débutants ont tendance à se focaliser sur l'apprentissage de la première partie, parce qu'ils pensent souvent que savoir programmer dans un langage, c'est connaître la syntaxe d'un langage. Mais programmer, c'est plus que connaître une syntaxe.

                  Je ne sais pas quel cours de C++ tu suis, mais si tu n'en suis pas et te base uniquement sur tes projets pour apprendre, je dois te donner mon avis : l'apprentissage par projet uniquement n'est pas une bonne chose. Parce que l'apprentissage suit les besoins du projet et pas un ordre pédagogique. Et cela crée des lacunes dans l'apprentissage.

                  A mon sens, il faut aussi suivre un cours et faire les exos de ce cours. Pas juste un projet.

                  Un exemple de lacune, c'est que tu n'utilises pas les listes d'initialisation alors que tu utilises les pointeurs. Dans l'ordre d'apprentissage, tu devrais deja connaitre le premier si tu utilises le second. (Et je ne vais même pas te demander si tu connais la sémantique d'entité avant d'utiliser les pointeurs).

                  Bon courage

                  -
                  Edité par gbdivers 3 septembre 2021 à 6:32:06

                  • Partager sur Facebook
                  • Partager sur Twitter
                    3 septembre 2021 à 8:14:50

                    gbdivers a écrit:

                    Ca, oui :

                    ennemi.prendCoupDansLesDents(arme);
                    
                    // et dans prendCoupDansLesDents
                    void Ennemi::prendCoupDansLesDents() {
                        m_point_vie -= arme.getDegats();
                    }

                    (il y a quand meme un getter ici)

                    Tu peux remplacer le getDegat par un computeDamage(Resistance targetResistance): une arme pourrait en effet avoir un degré d'efficacité variable en fonction des résistances de la cible, ou du type de celle ci, par exemple un slime va résister à une épée, mais pas à un marteau, un troll ne peut pas mourir sans une attaque de feu ou acide, une armure de métal recevra plus de dégâts via une arme enchantée par la foudre,... etc

                    Du coup, tu as plus de règles possibles, et plus de getter dans l'Entity vu que c'est lui qui crée un objet Resistance à un instant T.

                    L'objet Resistance lui en aura, mais c'est naturel, c'est son job d'être une classe de donnée temporaire.



                    • Partager sur Facebook
                    • Partager sur Twitter
                    Anonyme
                      3 septembre 2021 à 13:01:07

                      Oof ! Ca en fait des choses à changer mais on prend notes de tout !! 

                       ^^

                      Merci bien !

                      PS : gbdivers, on nous a dit sur un autre forum que  d'initialiser dans le hpp était une mauvaise idée car on les réinitialise dans le cpp dans la fonction Game::initGame() et ça pourrait porter à confusion

                      -
                      Edité par Anonyme 3 septembre 2021 à 14:38:30

                      • Partager sur Facebook
                      • Partager sur Twitter
                        3 septembre 2021 à 16:35:14

                        Haulun a écrit:

                        PS : gbdivers, on nous a dit sur un autre forum que  d'initialiser dans le hpp était une mauvaise idée car on les réinitialise dans le cpp dans la fonction Game::initGame() et ça pourrait porter à confusion

                        Ne les "reinitialise" pas dans le .cpp dans ce cas.

                        Pour les "bonnes" pratiques, il faut bien comprendre qu'elles sont pensées avec en tête des vrais projets pro. Donc des centaines de millier ou millions de lignes de code. Dans un mini projet comme le tien, il est parfois difficile de comprendre les bonnes pratiques, parce que mal faire les choses n'a pas beaucoup de conséquence dans ce type de mini projet. Il faut donc que tu imagines avoir un projet avec des milliers de classes, des milliers de fonctions, des milliers de variables, pour comprendre les bonnes pratiques.

                        Un chose a savoir, c'est qu'une variable non initialisée peut prendre une valeur aléatoire. Ça peut être un bug très compliquer a trouver, parce que n'est pas un code invalide, ton programme va fonctionner. Mais le comportement de ton programme ne sera pas correct. Donc le but est de minimiser le risque d'oublier une variable non initialisée. (Encore une fois, imagine que tu as des milliers de variables dans ton projet).

                        Si tu initialises une variable membre dans le .cpp, cela veut dire que tu dois initialiser cette variable dans chaque constructeur. Donc au lieu d'avoir 1 endroit pour initialiser une variable, tu vas devoir le faire a 10 endroits différents si tu as 10 constructeurs. Donc 10 fois plus de chance d'oublier une initialisation. C'est une erreur facile a faire, quand un code est maintenu pendant des années, par plusieurs personnes qui vont et viennent dans l’équipe.

                        Le second argument, c'est la cohérence. Si tu as une variable qui est initialise par défaut dans les constructeurs, il faut faire attention que la valeur d'initialisation soit la même dans tous les constructeurs (ça n'aurait pas de sens de faire des initialisations différentes). Avec une seule initialisation, tu es sur que la valeur d'initialisation est toujours la même. Et si tu modifies cette valeur d'initialisation, tu auras besoin de la faire a 1 seul endroit, pas dans chaque constructeur.

                        En soi, je suis d'accord avec les arguments de Godrick, c'est juste sa conclusion que je ne suis pas d'accord ("Je retirerais les initialization dans Game.hpp pour eviter la redondance"), pour les raisons que je t'ai donné.

                        (A noter, Godrick n'a pas dit que c’était une mauvaise idée d'initialiser dans le .h. Il a dit qu'initialiser 2 fois, dans le .h et le .cpp, est une mauvaise chose. Et que lui aurait supprimé l'initialisation dans le .h. Ce n'est pas pareil)

                        --------------------------------------------------------

                        C'est un autre probleme de l'apprentissage par projet, tu vas avoir beaucoup d'avis différents, avec des pratiques différentes (et pas forcement correctes).

                        Ces discussions sur les bonnes pratiques peuvent sembler intéressantes, mais ce n'est pas forcement pertinent pendant l'apprentissage. Parce que le type de projets sur lequel un débutant travaille ne permet pas de comprendre la justification de ces bonnes pratiques.

                        A mon sens, la "bonne" façon d'apprendre, c'est de suivre les bonnes pratiques d'un bon cours, sans chercher a vouloir comprendre tous les détails et justifications. Et quand on a pratique assez longtemps et que l'on a pris l'habitude de ces bonnes pratiques, on peut commencer a essayer de les comprendre.

                        C'est pour cela que tu as pleins de questions (sur les pointeurs, les getters/setters, les initialisations par défaut, etc). Et que j'ai volontairement survolé ces questions (les vraies discussions complètes sur les bonnes pratiques, c'est des discussions par des milliers d'experts pendant des années).

                        Il faut accepter de ne pas tout comprendre dans un premier temps, pendant ton apprentissage.

                        -
                        Edité par gbdivers 3 septembre 2021 à 16:40:48

                        • Partager sur Facebook
                        • Partager sur Twitter
                          4 septembre 2021 à 11:59:41

                          Bonjour Haulun, et bonjour à tous.

                          Haulun a écrit :

                          ... Pourquoi utiliser unique_ptr au lieu de pointeurs ‘‘normaux’’, quelles sont les différences ? ...

                          Warren79 a écrit :

                          En fait si les ressources détenues dans tes std::unique_ptr ne sont plus nécessaires mais que leurs présences dans la mémoire ne te dérange pas, alors laisse faire la libération automatique des ressources du C++ (Ressource Finalisation Is Destruction ou RFID ). ...

                          Pour compléter la réponse de Warren, j'ajouterai que l'utilisation des "smart pointeurs" est la garantie de ne pas avoir de fuite mémoire : Tu es sûr que les objets que tu crées serons détruits et que la mémoire sera rendu au système. Je ne connaissais pas le RFID (Radio Fréquence IDentification    ;-)   ), par contre je connaissais sous le terme RAII.

                          Bonne continuation.

                          PS: Zut, j'avais oublier in I, dans RAII !

                          -
                          Edité par Dedeun 4 septembre 2021 à 17:40:17

                          • Partager sur Facebook
                          • Partager sur Twitter
                            4 septembre 2021 à 16:48:35

                            Dedeun à écrit [...]

                            En fait on dit souvent RAII "le mal nommé" (repris du livre de Koala01 / Philippe Dunski). En fait RAII garantit que les ressources seront bien initialisées (c'est le sens de cet acronyme). Mais gardé tout seul comme ça , ça n' indique pas que les ressources seront dûment libérées, alors que c'est ce que l'on cherche au travers des classes de la STL.:)

                            • Partager sur Facebook
                            • Partager sur Twitter

                            Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

                            Jeu, Tour par Tour, Console, C++, Partie 2

                            × 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