Partage
  • Partager sur Facebook
  • Partager sur Twitter

Séparer les classes et l'affichage

Anonyme
    16 avril 2018 à 19:55:49

    Bonjour, je suis en train de créer un Snake en C++, je sépare mon programme ainsi : J'ai une classe App qui contient : la map(qui contient les objets statiques), le snake(qui contient un vector qui stock les parties du serpent). Seul problème j'aimerais pouvoir afficher le programme de telle façon à ce que j'ai une classe externe aux autres qui aurait comme seul but d'afficher le jeu, tout simplement. Seulement, les données de mes classes (comme les cases de ma classe Map) sont privées, par conséquent je n'ai pas moyen de les afficher.

    Ma classe App :

    #include "Snake.h"
    #include "Map.h"
    
    class App
    {
    private:
    	int points;
    	bool dead = false;
    	Map map;
    	Snake snake;
    	AppRenderer renderer;
    public:
    	void run();
    
    
    
    	App(int l, int h) { map = Map(l,h); snake = Snake(Direction::UP); renderer = AppRenderer(this); };
    
    
    
    }
    
    class AppRenderer
    {
    private:
    	App &app;
    public:
    	AppRenderer(App &app){ this->app = app; };
    	void display(); 
    }

    Merci :)

    • Partager sur Facebook
    • Partager sur Twitter
      16 avril 2018 à 23:44:50

      Salut,

      Je salue ton initiative, car elle est excellente ;)

      En gros, tu a plusieurs solutions:

      Au lieu de fournir une référence vers ton application à ce que l'on peut appeler "ton contexte d'affichage", tu peux appliquer le principe appelé le DIP (pour Dépendency Inversion Principle) et ... transmettre ce contexte à tout ce qui a besoin d'être affiché.

      Si bien que tu prévoirais l'interface de ton AppRenderer (ce que j'appelle moi ton "contexte d'affichage"), de telle manière à ce qu'elle soit en mesure d'afficher les différents éléments qui doivent être affichés, par exemple:

      • la tête du serpent
      • les parties du "corps" de ton serpent
      • les vitamines
      • tout ce que je peux avoir oublié.

      Et, d'un autre coté, tu veillerais à fournir une fonction draw(AppRenderer &) à ... tous les éléments qui sont susceptibles d'être tracés.

      Cela te permettrait, par exemple, d'avoir une fonction d'affichage de ton serpent qui serait proche de

      void Snake::draw(AppRenderer & render) const{
          /* j'ai considéré le fait que la tête du serpent
           * était gérée séparément du reste du corps 
          head_.draw(render);
          for(auto const & p : parts_){
              p.draw(render);
          }
      };

      avec une classe particulière pour la tête qui disposerait également de cette fonction draw sous une forme proche de

      void Head::draw(AppRenderer & render) const{
          /* si on sait que l'on trace la tete, on a
           * besoin:
           * - du coté dans lequel elle regarde et
           * - de la position où elle se trouve
           */
          render.drawHead(position_, orientation_);
      
      }

      Et, d'un autre coté, tu aurais donc une notion propre aux ... autre partie du corps du serpent, qui disposerait également de cette fonction draw et qui ressemblerait à quelque chose comme

      void SnakePart::draw(AppRenderer & render) const{
          /* Si on sait qu'on trace une partie du serpent,
           * tout ce qu'il nous faut c'est... la position
           * à laquelle elle se trouve
           */
          render.drawPart(position_);
      }

      Sans oublier la notion de vitamine, qui apparait également dans ton jeu, qui disposerait également de cette fameuse fonction, sous une forme proche de

      void Vitamine::draw(AppRenderer & render) const{
          /* si on sait que l'on affiche une vitamie,
           * tout ce qu'il nous faut c'est... la position
           * où elle se trouve
           */
          render.drawVitamine(position_);
      }

      Et tu auras donc deviné que ton AppRenderer ressemblera à quelque chose comme

      AppRenderer{
      public:
         void drawHead(Position const &, Orientation);
         void drawPart(Position const &);
         void drawVitamine(Position const &);
      };

      (je vais te laisser implémenter ces fonctions particulières ;) )

      L'énorme avantage de cette technique, c'est que, pour peu que tu déclares ces trois fonctions comme étant virtuelles (et que tu penne les mesures adéquates pour assurer la sémantique d'entité à ta notion de AppRenderer), tu peux décider de créer à tout moment une classe qui en dérive pour obtenir un contexte d'affichage différent:

      • l'un qui utiliserait la SFML
      • l'autre qui utiliserait la console
      • un troisième qui utilise Qt
      • voir, en tirant un peu sur la corde (car le terme de "draw" est alors peut-être mal choisi) pour sauvegarder l'état de ton jeu à un moment donné (quoi que, s'il s'agit de sauvegarder un "screenshot", le terme draw en vaut un autre ;) )

      Bien sur, tu devras choisir entre utiliser un affichage à base de SFML ou à base de Qt, mais cela peut t'ouvrir énormément de possibilités ;)

      Une autre solution pourrait être d'utiliser un système de signaux et de slots ( !!! je ne pense pas forcément à celui de Qt: il est possible d'en mettre un en place en moins de 200 lignes de code ;) !!!) grâce auquel chaque partie serait en mesure d'émettre un signal particulier pour chaque étape importante de sa vie.

      Tu pourrais alors te contenter de connecter ton contexte d'affichage à ces différents signaux, et faire en sorte qu'il maintienne -- d'une façon ou d'une autre -- sa "propre représentation" de l'ensemble du plateau de jeu à jour.

      Il ne manquerait alors qu'un signal "update", émis sans doute par ton plateau de jeu, auquel ton contexte d'affichage serait évidemment connecté et qui provoquerait, au niveau du contexte d'affichage la... mise à jour de l'affichage.

      L'idée derrière tout cela serait de rendre les deux parties (la partie métier, représentée par ta classe App et tout ce qu'elle contient et la vue, représentée par ta AppRenderer (*) ) totalement indépendante l'une de l'autre, tout en leur permettant malgré tout de communiquer au travers du système de signaux et de slot ;)

      (*) qui pourrait carrément sortir de la classe App pour l'occasion ;) )

      Pour un jeu "aussi simple" qu'un snake, la première solution est très largement suffisante.

      Mais, dés le moment où tu commenceras à multiplier les éléments affichable à l'envi, la deuxième solution sera sans doute préférable ;)

      -
      Edité par koala01 16 avril 2018 à 23:45:33

      • Partager sur Facebook
      • Partager sur Twitter
      Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
      Anonyme
        17 avril 2018 à 11:20:36

        @koala01 Bonjour, merci de m'avoir répondu. Le problème est que la classe Vitamine, dans mon cas, ce n'est pas une classe, j'ai une classe Map qui contient des cases, et ces cases contiennent un std::string indiquant ce que contient la case, "v" pour du vide, "a" pour une pomme etc... Par conséquent je ne sais pas comment utiliser ma "méthode" drawApple() si ma vitamine dans ce contexte, n'est pas une classe. Dois-je créer une classe case dont-je ferais hériter toutes les autres ?

        Merci :)

        -
        Edité par Anonyme 17 avril 2018 à 11:20:50

        • Partager sur Facebook
        • Partager sur Twitter
          17 avril 2018 à 11:35:44

          Salut, 

          La je ne peux que te conseillé que d'avoir une classe Pomme (ou vitamine) et donc de changer ton système de tableau.

          T'a de la chance, cela ne devrais pas trop modifier ta classe Map ! :) 

          Tu peux lire cet article qui t'aidera à comprendre un peu mieux http://www.arolla.fr/blog/2017/02/principes-solid-vie-de-jours/ 

          petit detail: la lib standard de C++ utilise du snake_case pour ses classes, c'est une bonne pratique (peu importe le langage) de suivre le format utilisé par le langage, ainsi on n'a pas à se poser des questions inutile tel que 

          class foo_bar
          class FooBar
          class Foo_Bar
          class FOO_BAR
          

          Si toutes les libs et tous les developpeurs utilisaient la même façon de faire, on aurais déjà une galère en moins :) 

          • Partager sur Facebook
          • Partager sur Twitter

          Architecte logiciel - Software craftsmanship convaincu.

          Anonyme
            17 avril 2018 à 12:05:12

            Salut, @necros211,

            je dois donc renommer ma classe AppRenderer, en : app_renderer ?

            • Partager sur Facebook
            • Partager sur Twitter
              17 avril 2018 à 12:12:45

              Pas besoin de le faire sur ce projet car tu est bien avancé et que ce n'est pas vital.

              Mais c'est une bonne pratique oui :) 

              Et pourquoi pas avoir des namespaces, et donc quelquechose plus proche de app::renderer 

              -
              Edité par necros211 17 avril 2018 à 12:13:35

              • Partager sur Facebook
              • Partager sur Twitter

              Architecte logiciel - Software craftsmanship convaincu.

              Anonyme
                17 avril 2018 à 16:52:27

                Bon, j'ai programmer pratiquement tout le snake, sauf qu'une erreur(non pas de compilation) intervient :( Et ça doit faire 2-3 H que je cherche à déboguer mon programme, et donc pour éviter de créer 3000 sujets pour chaque erreurs je préfère poster mon problème ici . L'erreur intervient dans la "méthode" add_snake_part dans le fichier Snake.cc, voici le github : https://github.com/Asstryfi/Snake (il y a surement des erreurs de syntaxes quelque part, du à mes essaies de corrections) .

                Merci :)

                • Partager sur Facebook
                • Partager sur Twitter
                  17 avril 2018 à 17:47:33

                  Deux petites choses concernant ton code:

                  tu devrais transmettre ton contexte par référence aux différentes fonctions draw, car c'est typiquement une classe qui aura sémantique d'entité, vu qu'il est possible de l'intégrer dans une hiérarchie de classe

                  Au lieu d'utiliser un tableau pour maintenir la liste des parties du serpent, tu devrais envisager d'utiliser une liste, car on peut envisager le déplacement comme étant:

                  • l'ajout d'une partie du serpent à l'emplacement où se trouvait la tête juste avant et
                  • le retrait de la dernière partie du serpent

                  Avec une petite astuce, lorsque le serpent mange la vitamine (ou la pomme, ou quel que soit le nom que tu lui donne): il grandit "d'une partie", et on ne doit donc pas retirer la dernière, on peut très facilement arriver à un résultat correct :D

                  l'énorme avantage de cette technique sera que tu n'es pas obligé de calculer, pour chaque partie du serpent, la nouvelle position de celle-ci à chaque déplacement (d'autant plus que la nouvelle position ne dépend pas de la direction dans laquelle le serpent regarde mais de la partie de serpent précédente).

                  On ne le répétera jamais assez: si les accesseurs (getXXX) peuvent, sous certaines conditions, être tout à fait justifiés, les mutateurs n"ont absolument aucun intérêt et ouvrent la porte à une mauvaise manipulation de la classe.  J'ai écrit quelques interventions sur le sujet ici même, je vais te laisser ;)

                  Par contre, on peut considérer le fait de "tourner" comme étant un service que tu es effectivement en droit d'attendre de la part du serpent (comme on peut considérer le fait de "bouger" de la même manière)

                  La différence entre une fonction "turn" et une fonction "set_direction" pourrait ne pas être évidente, mais le fait est que "tourner" peut tout à fait faire "un peu plus" que de changer la direction dans laquelle le serpent regarde.

                  Elle pourrait -- par exemple -- éviter que le serpent n'essaye de faire un demi tour complet pour regarder, finalement, dans la direction de son corps.  Et ca, ce serait vachement utile, car aucun serpent n'est capable de le faire ;)

                  PS: n'oublie pas d'ajouter la fonction draw au serpent, pour lui permettre de la "transmettre" à l'ensemble de ses parties ;)

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                  Anonyme
                    17 avril 2018 à 18:32:38

                    Merci @koala01 d'avoir pris le temps de lire mon code et de m'en donner un avis :)

                    koala01 a écrit:

                    Deux petites choses concernant ton code:

                    tu devrais transmettre ton contexte par référence aux différentes fonctions draw, car c'est typiquement une classe qui aura sémantique d'entité, vu qu'il est possible de l'intégrer dans une hiérarchie de classe

                    Je ne vois pas vraiment ce que tu veux dire par "passer ton contexte par référence", il faut que je fournisse les classes aux différentes fonctions draw() ?

                    koala01 a écrit:

                    On ne le répétera jamais assez: si les accesseurs (getXXX) peuvent, sous certaines conditions, être tout à fait justifiés, les mutateurs n"ont absolument aucun intérêt et ouvrent la porte à une mauvaise manipulation de la classe.  J'ai écrit quelques interventions sur le sujet ici même, je vais te laisser ;)

                    Donc je dois laisser ma variable "Position" publique ?

                    koala01 a écrit:

                    PS: n'oublie pas d'ajouter la fonction draw au serpent, pour lui permettre de la "transmettre" à l'ensemble de ses parties ;)

                    Je ne sais pas si c'est ce que tu voulais dire mais j'ai fais cette méthode qui permet de dessiner toutes les parties du serpent :

                    void Snake::draw(AppRenderer renderer) const
                    {
                    	for(auto &it : snakes)
                    	{
                    		it.draw(renderer);
                    	}
                    }

                    Merci encore :)

                    -
                    Edité par Anonyme 17 avril 2018 à 18:32:59

                    • Partager sur Facebook
                    • Partager sur Twitter
                      17 avril 2018 à 19:40:55

                      Asstryfi a écrit:

                      Je ne vois pas vraiment ce que tu veux dire par "passer ton contexte par référence", il faut que je fournisse les classes aux différentes fonctions draw() ?

                      koala01 a écrit:

                      Il y a trois moyen de transmettre une donnée à une fonction qui en a besoin pour travailler

                        1. par valeur: la fonction appelée utilise une copie de la donnée, ce qui laisse la donnée d'origine "en l'état"
                        2. par référence(éventuellement constante): la fonction appelée utilise un "alias" de la donnée d'origine,si bien que les modifications apportées à cet alias seront prises en compte (s'il y en a) au niveau de la fonction appelante
                        3. par pointeur:on transmet l'adresse mémoire à laquelle se trouve la donnée. Hormis quelques cas bien particulier, on évitera autant que faire se peut d'avoir recours à cette méthode

                      Le code que j'ai vu sur github utilise la transmission par valeur. En plus de provoquer un tas de copies inutiles qui nuiront largement aux performances, cela va occasionne une série de problèmes (nommons les pour ce qu'ils sont :des bugs) parce que, bien qu'elles portent le même nom, la variable renderer que l'on utilise dans Snake::draw est une donnée différente de celle que l'on utilise dans Part::draw (*).

                      Si tu observes attentivement le code que je t'ai donné en exemple, tu constatera qu'il y a régulièrement une esperluette '&' qui se glisse entre l'identifiant de type et le nom d'une donnée (il y a souvent le mot clé const qui se glisse entre les deux, mais c'est juste pour indiquer que je n'envisage pas de les modifier ;) ).

                      Quelques exemples:

                      • void Snake::draw(AppRenderer & render) const{
                      • void Head::draw(AppRenderer & render) const{
                      • for(auto const & p : parts){
                      • (y en a surement d'autres)

                      Cette esperluette indique au compilateur qu'il s'agit d'une référence, et que c'est donc un alias de la donnée qui a servi à définir la valeur

                      Si bien que tous les ordres que je pourrai donner à -- mettons -- renderer seront effectivement transmis à la "donnée originelle", et non à "une pale copie de la donnée originelle".

                      (*) j'ai d'ailleurs beaucoup insisté sur la possibilité qui nous était donnée de faire dériver différentes classes de AppRenderer en fonction du contexte dans lequel l'affichage devrait avoir lieu.  Je ne l'ai pas fait par hasard.

                      J'aurais sans doute du être plus explicite et dire clairement que, du fait de cette possibilité, ta classe AppRenderer a ce que l'on appelle une sémantique d'entité, dont l'une des caractéristique est d'interdire purement et simplement la copie et l'assignation.

                      En donnant cette sémantique d'entité à ta classe AppRenderer, le compilateur aurait été en mesure de se rendre compte le passage se faisait par valeur et occasionnait une copie, et il aurait purement et simplement refusé d'aller plus loin, parce que tu lui demande de faire quelque chose qu'on lui a explicitement interdit de faire ;)

                      Donc je dois laisser ma variable "Position" publique ?

                      C'est une des possibilités, en effet... Mais c'est très certainement la pire des possibilités que tu puisse envisager

                      Car il faut bien te dire que, si tu place une donnée dans l'accessibilité publique, "tout le monde" peut y accéder sans la moindre restriction et en faire strictement "ce qu'il veut"

                      D'ici à ce qu'un imbécile distrait décide de définir cette positon à 419,812 alors que tu n'a qu'une grille de 20 / 20, il n'y a qu'un pas.

                      Peu importe comment il sera arrivé à ces nombres: comme le - et le * sont juste l'un à coté de l'autre au niveau d'un pavé numérique, il aura sans doute multiplié au lieu de soustraire, ou bien il aura fait une erreur dans sa logique...

                      Le "pourquoi" et le "comment" n'ont pas vraiment d'intérêt ici. Ce qui importe, c'est que ca peut arriver, et que si ca arrive, tu sera dans une merde noire.

                      C'est la raison pour laquelle il faut radicalement changer la manière dont tu réfléchis aux différents types de données que tu crée:

                      Au lieu de réfléchir en termes des données qui les composent (ex: "la tête est représentée par sa position et la direction dans laquelle elle regarde"), on va se poser la question des questions auxquelles on veut qu'elle puisse répondre et des ordres auxquelles on veut qu'elle soit en mesure d'obéir.

                      Pour la tête, il n'y a que trois ordres auxquelles elle doit obéir:

                      • tourne toi (dans la direction indiquée)
                      • déplaces-toi  (d'une case, dans la direction où tu regarde) et
                      • demande au renderer de t'afficher

                      Alors, bien sur, il faudra bien que la classe dispose de certaines données pour pouvoir obéir à ces trois ordres. Mais, on se rend tout de suite compte que la manière dont ces données sont représentées n'a absolument aucun intérêt pour quelqu'un qui manipule l'instance de cette classe

                      On pourrait -- aussi (si cela s'avère utile)-- envisager de se donner l'occasion de poser plusieurs questions à la tête de notre serpent, à savoir:

                      • Dans quelle direction regardes-tu? et
                      • quelle est ta position actuelle?
                      • (voire) Quelle sera ta prochaine position?

                      Et c'est là, surtout avec la dernière question, que l'on prendra conscience du fait que les réponses ne correspondent pas forcément aux données qui permettent à la classe de fournir les services que l'on attend de sa part.

                      En effet, la prochaine position de la tête devra forcément ... être calculée. Comment? sans doute en prenant en compte la position actuelle et l'orientation de la tête.

                      Mais ca, c'est la "popote interne" de la classe ;)

                      Je ne sais pas si c'est ce que tu voulais dire mais j'ai fais cette méthode qui permet de dessiner toutes les parties du serpent :

                      void Snake::draw(AppRenderer renderer) const
                      {
                      	for(auto &it : snakes)
                      	{
                      		it.draw(renderer);
                      	}
                      }
                      Oui, c'est bien ce que je voulais dire ;)

                      • Partager sur Facebook
                      • Partager sur Twitter
                      Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                      Anonyme
                        17 avril 2018 à 20:36:58

                        Merci beaucoup d'avoir pris du temps pour me répondre, je appliquer les conseils que tu m'as données :)
                        • Partager sur Facebook
                        • Partager sur Twitter
                          18 avril 2018 à 11:18:13

                          On peut aller plus loin et définir une fonction membre qui fait ce qu'on veut avec les parties du serpent, le ce-qu-on-veut étant indiqué par un paramètre fonctionnel.

                          Exemple

                          void afficher(int x, int y) {
                              std::cout << x << " " << y << std::endl;
                          }
                          
                          int main()
                          {
                              Snake s;
                              s.grow(1,10).grow(2,11).grow(3,12);  
                              s.foreach_part(afficher);                 // That's it
                              return 0;
                          }

                          avec

                          class Snake {
                          private:
                            std::vector<std::pair<int,int>> parts;
                          public:
                            Snake & grow(int x, int y) {
                                parts.emplace_back(x,y);
                                return *this;
                            }
                            void foreach_part(std::function<void(int, int)> f) {    // définition
                              for (auto & part : parts) {
                                  f(part.first, part.second);
                              }
                            }
                          };


                          [I like chaining :magicien:: petite chanson] qui marcherait aussi avec un "Renderer", simulé ici :

                          class Renderer {
                              std::string name;
                          public:
                              Renderer(const std::string &n) : name{n} {};
                          
                              void render_part(int x, int y) {               // une fonction membre
                                  std::cout << "drawing " << x << " " << y
                                            << " on " << name << std::endl;
                              }
                          };
                          

                          Code d'utilisation

                             Renderer r{ "big window" };
                             s.foreach_part([&r](int x, int y){     
                                                   r.render_part(x,y); 
                                                 });
                          

                          Résultat

                          drawing 1 10 on big window
                          drawing 2 11 on big window
                          drawing 3 12 on big window

                          PS: Et si vous n'aimez pas les lambdas

                           s.foreach_part(std::bind( &Renderer::render_part, r, 
                                                        std::placeholders::_1,
                                                        std::placeholders::_2
                                                        );




                          -
                          Edité par michelbillaud 18 avril 2018 à 16:04:26

                          • Partager sur Facebook
                          • Partager sur Twitter

                          Séparer les classes et l'affichage

                          × 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