Questions sur la conception d'un jeu

Certaines choses ne sont pas encore claires dans ma tête...

a marqué ce sujet comme résolu.

Bonjours à tous,

Je suis en train d'apprendre le C++, et pour m'entraîner j'essaie de reproduire des petits jeux qui existent déjà. Étant encore plutôt débutant, je ne suis pas toujours sûr de concevoir ces jeux de la meilleure manière, et c'est pour ça que je sollicite votre aide. :)

Voici le contexte actuel : je suis en train de coder un Pac-Man (pas besoin de connaître le jeu pour comprendre le problème) en essayant d'être le plus proche possible de l'original. Mon premier problème est arrivé assez rapidement :

1.

J'ai tout d'abord créé une classe Pac-Man et une classe Map. J'ai donc les objets pacMan et map qui sont mis à jours et dessinés à chaque boucle (le code ne provient pas directement de mon projet, c'est juste pour donner une image) :

1
2
3
4
5
6
7
8
9
//boucle principale

    //mise à jour
    map.update();
    pacMan.update();

    // affichage des entités
    window.draw(map);
    window.draw(pacMan);

Map contient les tuiles, c'est-à-dire les murs solides et les dots (les points que Pac-Man doit manger) dans un tableau. Chaque fois que Pac-Man passe sur un dot, le dot disparaît et le score augmente. C'est là que je suis confus : où et comment faut-il gérer la disparition et l'augmentation du score ? Dans la classe Map ? Dans la classe PacMan ? Ou dans la boucle principale ?

J'ai d'abord pensé à faire passer par paramètre la position de PacMan et le score :

1
map.update(pacMan.position(), score);

Sauf que PacMan a besoin de connaître les murs lorsqu'il se déplace, donc il faudrait aussi faire :

1
pacMan.update(map.tiles());

Ou alors je pourrais aussi faire passer ces informations par le constructeurs. Mais cela m'a paru un peu absurde de passer toujours plein de paramètres dans les méthodes updates ou dans le constructeur. Est-ce une bonne idée ?

Pour finir, j'ai décidé de mettre ces objets (tuiles, score…) dans une autre classe en tant que membres statiques, afin qu'ils soit utilisables partout dans le projet. Mais j'ai souvent entendu qu'utiliser des variables globales était une mauvaise pratique. Pourtant, je trouve ça bien agréable à utiliser. Devrais-je éviter ? Et si non, comment devrait-je appeler cette classe ?

2.

Viennent en suite les quatre fantômes. Au début je me suis dit que je devrais créer une classe Ghost, puis quatre autres classes qui héritent de cette dernière (car les fantômes agissent tous différemment). Cependant, j'ai pu lire qu'il valait souvent mieux utiliser une seule classes avec plusieurs composants. Ainsi, je définie mes quatre fantômes en tant que Ghost, en faisant passer leur type par le constructeur (du genre Ghost blinky(RED), Ghost pinky(PINK)).

Maintenant, lors de la méthode update des Ghosts, on a besoin de connaître la position de Pac-Man. Certains fantômes ont même besoin de la direction de Pac-Man. Du coup ici, la question est la même qu'avant : comment ces Ghosts doivent-il accéder à la position/direction de Pac-Man ? En les faisant passer par paramètre (dans le constructeur ou update()) ? Ou en les mettant en tant que variables globales à nouveau ? Car ne serait-ce pas inutile d'obtenir la direction de Pac-Man pour un fantôme qui n'en a pas besoin ?

Cette fois-ci, mettre la position/direction de Pac-Man en tant que variables globales m'a paru plutôt une mauvaise idée. Donc je les fais passer en paramètres (au constructeur).

Un autre problème, c'est que le fantôme bleu doit connaître la position du fantôme rouge. Du coup je devrais aussi passer la position du fantôme rouge par paramètre pour tous les fantômes ? Cela aussi me semble absurde. Devrais-je faire plusieurs fonction updates avec à chaque fois des paramètres différents ? Ou finalement opter pour créer quatre classes héritant de Ghost (qui seront donc assez minuscules) ?

Voilà

Je suis donc confus dans la manière de modifier un élément provenant d'une autre entité. J'espère donc que vous serez capables de m'indiquer quelques astuces afin d'avoir un code en ordre, propre et "moderne". ^^

Je vous remercie d'avance. :)

Les questions que tu te pose sont des questions de conception et non de développement pur. Il existe des livres entier a ce sujet dont le célèbre "UML 2 et les design patterns : analyse et conception orientées objet et développement itératif"

L'utilisation des variable globales est a proscrire le plus possible, tu dois privilégier l'encapsulation via des variables private et des méthodes d'accès (getteur, setteur). Tu dois définir a qui appartient ces variables en te posant les bonnes questions. Ici par exemple j'aurais commencer a réfléchir de cette fâçon:

  • Une classe Partie qui initialise le jeu lors de sa création et dispose d'une méthode opérant un tour de jeu

  • Une classe Pacman représentant le héro, disposant de méthode de déplacement

  • Une classe Ghost et ses 4 classes fille permettant de jouer les fantomes de différentes manières si leurs algorithme de déplacement n'est pas identique

La classe ghost pourrait ainsi avoir une méthode

1
deplacement(positionPacman, DirectionPacman)

et pacman disposerait des deux méthodes

1
2
getPosition()
getDirection()

La classe partie irai alors faire:

1
ghostred.deplacement(pacman.getPosition, pacman.getDirection)

ou ghostred est une instance d'une classe fille de ghost

PS: Ce n'est qu'un exemple très rapide, si tu veut en discuter plus longuement pour en voir les détails et définir une bonne conception je suis ouvert

+0 -0

Merci pour la réponse. :)

Donc si j'ai bien compris on évite les variables globales, et on fait passer les informations nécessaires par les méthodes de mise à jour.

Mais qu'en est-il des valeurs constantes ? Tels que la taille d'une tuile ? La taille de la scène ? Dois-je les mettre dans la classe Partie ? Et donc, comme j'en ai besoin dès la construction des classes, dois-je les faire passer par le constructeur ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class PacMan
{
    public:
        PacMan(const int &tileSize, const int &sceneWidth, const int &sceneHeight) :
            m_tileSize {tileSize}, m_sceneWidth {sceneWidth}, m_sceneHeight {sceneHeight}
        {
            ...
        }

    private:
        const int &m_tileSize;
        const int &m_sceneWidth;
        const int &m_sceneHeight;
    ...

Et ainsi pour chaque classe qui en a besoin ?

Et pour les variables non-constantes, tels que le score, le nombre de dots restants ? Devrais-je les faire passer par les méthodes de mise à jour en tant que référence non-constantes, afin de les modifier dans ces méthodes ?

1
2
3
4
5
6
7
8
9
void PacMan::update(const Matrix &tiles, int &score, int &dotCount)
{
    if(tiles(m_position) == DOT)
    {
        score += 10;
        --dotCount;
    }
    ...
}

Où dois-je laisser la classe Partie s'occuper des modification du score et dotCount ?

Les variables constantes c'est des variables, alors on suit la même logique, on donne au propriétaire. La taille d'une tuiles appartient plutot a une classe 'Maps' de même que la taille de la map.

Si un jour tu venais a modifier cette constante dans ton code source, tu ne sera pas obliger de courir partout pour la modifier sur toutes la classes. De plus une bonne habitude consiste a offrir un fichier de config (un simple txt) ou tu mettra les variables constante amener a être modifier et tu lira ce fichier au moment de construire l'instance de ta classe.

Ta deuxième question est plus pertinente par contre. Moi je préfererai que ce soit partie qui s'en occupe, par habitude. Tu peut par exemple faire un return qui indique si il a manger ou non.

Après les détails dépendent de l'ensemble de ta conception

PS: De même ne rentre surtout pas le lien du fichier directement dans chaque classe. C'est une constante que tu ne donne que a une classe précise comme 'Partie' et qui ensuite le passe en argument. (Oui je sais ça revient au même que tout a l'heure, mais il faut savoir jauger entre bonne conception et bonne maintenance.)

+0 -0

D'accord je vois plus clair.

Mais à propos des constantes, ce que je veux dire, c'est comment faire passer la constante tileSize (appartenant à Map) à PacMan par exemple ? Est-ce une bonne idée de la faire passer par le constructeur et la garder dans un membre qui fait référence (comme je l'ai écrit plus haut) ou non ?

Pour au final avoir :

1
2
Map map; // map possède une constante tileSize
PacMan pacMan(map.getTileSize()); // pacMan connait maintenant tileSize pour toujours

Ou devrais-je faire en sorte que PacMan soit le plus indépendant possible de tileSize ?

Pacman n'est pas censé connaitre les tileSize. Il doit en être indépendant. C'est un problème graphique d'affichage, ça appartient donc a ta classe chargé de l'affichage.

De mon côté je vient de réfléchir un peu au problème. Je ne suis pas le meilleur du monde car je suis encore en apprentissage dessus mais voici mon idée.

La classe Partie:

  • Attributs:
    • int: score
    • Map: map
    • Pacman: pacman
    • Ghost[4]: ghosts
  • Méthodes:
    • effectuerTour()

La classe Map:

  • Attributs:
    • int: TileWidth
    • int: TileHeight
    • String: fichier //le fichier qui contient l'architecture du niveau
  • Méthodes:
    • getDirectionPossible()
    • afficherMap()
    • afficherPacman(int position)
    • afficherFantome(int position, Color couleur)
    • estDelicieux(int position) //Indique si pacman mange ou non et enleve le dot le cas echeant

La classe Pacman:

  • Attributs:
    • int: positionX
    • int: positionY
    • int: direction
  • Méthodes:
    • setDirection() //Fixer par partie directement car venant du clavier et donc d'un evenement
    • avancer()

La classe Ghost:

  • Attributs:
    • int: positionX
    • int: positionY
    • int: direction
    • Color: couleur
  • Méthodes:
    • choisirDirection()
    • avancer()

A chaque effectuerTour() la partie devras:

  • Pacman.avancer()
  • map.estDelicieux(pacman.position) //Mise a jour du score le cas echeant
  • Pour chaque fantomes:
    • Ghost.choisirDirection(this.pacman.getPosition, this.pacman.getDirection)
    • Ghost.avancer() //If ghost.position == pacman.position Then variableWhileDeLaPartie == 0
  • map.afficherMap()
  • map.afficherPacman(pacman.position)
  • Pour chaque fantomes:
    • map.afficherFantome(Ghost[x].position, Ghost[x].couleur)

PS: Bien entendu c'est un brouillon, je ne prend que le principe de fond et pas les problème technique des algorithme. Les fantomes devrais avoir un attribut sur leurs vitesse de deplacement, le if position == position devrais prendre un intervalle de tolérance du a la grandeur des icones et au fait qu'il ne tomberont jamais pile l'un sur l'autre… bref tu devrais comprendre…

+0 -0

Il ne faut surtout pas que pacMan soit en contact avec getTileSize car tu aurais un problème de cohésion. Sanoc te propose, je pense, une bonne structure de classe qui sera correct pour la cohésion des classes.

+1 -0

Ma conception a l'avantage de suivre certaines règles de base:

  • Un faible couplage, les classes sont assez autonome et ne sont pas trop interconnecté, elle présente plus de service qu'elle n'en demande
  • Une forte cohésion, chaque classe est très spécialisé dans son domaine. La classe Pacman ne s'occupe pas de faire du café et de modéliser un pacman
  • Expertise, les méthodes sont offertes au classe ayant le plus d'information pour les appliqué

Il existe bien d'autre règles de base. Si la conception t’intéresse et que tu a fini les bases en C++ tu peut te tourner vers les "Principes GRASP" qui sont enfaite des principes et des questions a te poser pour définir une bonne modélisation de ton logiciel

+0 -0

Ma conception a l'avantage de suivre certaines règles de base:

  • Un faible couplage, les classes sont assez autonome et ne sont pas trop interconnecté, elle présente plus de service qu'elle n'en demande
  • Une forte cohésion, chaque classe est très spécialisé dans son domaine. La classe Pacman ne s'occupe pas de faire du café et de modéliser un pacman
  • Expertise, les méthodes sont offertes au classe ayant le plus d'information pour les appliqué

Il existe bien d'autre règles de base. Si la conception t’intéresse et que tu a fini les bases en C++ tu peut te tourner vers les "Principes GRASP" qui sont enfaite des principes et des questions a te poser pour définir une bonne modélisation de ton logiciel

Sanoc

tu m'enlève les mots de la bouche ;)

+0 -0

D'accord merci pour ces explications. :)

Donc ça va changer (un peu) mon code. Actuellement, PacMan et Ghost héritent de Actor, qui gère les déplacement et les collision. Or cet Actor gère aussi l'affichage du sprite, ce qui nécessite de connaître tileSize.

Donc si j'ai bien compris, Actor ne devra rien dessiner, juste gérer la position relative aux tuiles. Puis ce sera à Map de dessiner les Actors en multipliant leur position par tileSize. C'est juste ?

Cependant, ce qui me perturbe un peu, c'est que c'est Map qui se charge de dessiner Map. Ne serait-ce pas encore mieux d'avoir une classe Scene qui gère l'affichage du jeu ? Qui se chargerait donc de dessiner Map et les Actors.

Ainsi Map et Actor seraient totalement indépendants de l'interface graphique.

Cela change un peu la conception de mon jeu, car jusqu'à maintenant, je faisait en sorte que chaque entité soit dessinable et transformable. C'est donc une mauvaise idée ?

Donc si j'ai bien compris, Actor ne devra rien dessiner, juste gérer la position relative aux tuiles. Puis ce sera à Map de dessiner les Actors en multipliant leur position par tileSize. C'est juste ?

Loris

La question est si tu etait un pacman, pourrait tu t'afficher? Je pense que cette responsabilité viendrait plutot a l'environnement autour de toi.

Ce que tu dit est donc juste, il ne tiendra que la position, c'est ensuite a la classe map de mettre en fonction de la taille d'une tuile. Tout comme toi tu te pose au sol, tu ne sait pas a quel position tu es d'une origine imaginaire…

Cependant, ce qui me perturbe un peu, c'est que c'est Map qui se charge de dessiner Map. Ne serait-ce pas encore mieux d'avoir une classe Scene qui gère l'affichage du jeu ? Qui se chargerait donc de dessiner Map et les Actors.

Ainsi Map et Actor seraient totalement indépendants de l'interface graphique.

Loris

En voila une superbe idée!! Cela complexifie un peu ta conception mais tu y gagne beaucoup au moment de faire des améliorations et au moment de faire de la maintenance.

Cela change un peu la conception de mon jeu, car jusqu'à maintenant, je faisait en sorte que chaque entité soit dessinable et transformable. C'est donc une mauvaise idée ?

Loris

Une très mauvaise idée, car ce n'est pas a pacman de ce soucier de la taille de ton écran, ni du nombre de couleur qu'il contient…

PS: Je viens de penser a un exemple de maintenance améliorative que tu pourra faire grâce a ton idée:

Si un jour tu décide de vouloir changer de map en allant sur les bord de celle en cours, le jeu ce met en pause, tu a un petit défilement de ta map vers l'autre map et pof le jeu reprend sauf que du coup tu es a l'opposé (bord gauche -> bord droit) (un peu a la zelda sur nes). Et ben ce jour la tu n'aura qu'a précharger toutes tes map et tu les affichera dans tes scènes, tu n'aura ainsi pas de lag en plein milieu du changement.

+0 -0

D'accord merci pour ces explications. :)

Donc ça va changer (un peu) mon code. Actuellement, PacMan et Ghost héritent de Actor, qui gère les déplacement et les collision. Or cet Actor gère aussi l'affichage du sprite, ce qui nécessite de connaître tileSize.

oui tu pourrais quand même garder ta classe actor mais tu devrais éviter que tes acteurs se dessinent

Donc si j'ai bien compris, Actor ne devra rien dessiner, juste gérer la position relative aux tuiles. Puis ce sera à Map de dessiner les Actors en multipliant leur position par tileSize. C'est juste ?

Totalement en accord avec toi!

Cependant, ce qui me perturbe un peu, c'est que c'est Map qui se charge de dessiner Map. Ne serait-ce pas encore mieux d'avoir une classe Scene qui gère l'affichage du jeu ? Qui se chargerait donc de dessiner Map et les Actors.

Oui ça pourrait être une idée sauf que rendu la ça vaut peut-être pas la peine de créer une classe pour ça

Ainsi Map et Actor seraient totalement indépendants de l'interface graphique.

Cela change un peu la conception de mon jeu, car jusqu'à maintenant, je faisait en sorte que chaque entité soit dessinable et transformable. C'est donc une mauvaise idée ?

Loris

+1 -0

oui tu pourrais quand même garder ta classe actor mais tu devrais éviter que tes acteurs se dessinent

Effectivement je ne l'ai pas développer. Tu a bien fait de le faire. Le principe de la classe actor peut être bon puisque certaine de tes méthodes et attributs sont communes a pacman et aux ghost. (positionX, positionY, avancer(), getteur et setteur).

Oui ça pourrait être une idée sauf que rendu la ça vaut peut-être pas la peine de créer une classe pour ça

MadaQC

Après tout est discutable sur une conception. Tout dépend de ce que tu pense pouvoir amener par la suite. Si tu ne pense pas changer de map très rapidement sans éviter les lag, alors oui c'est mince comme amélioration. MAIS! Le principe était bon et tu a bien compris le principe de fond!

+1 -0

Cependant, ce qui me perturbe un peu, c'est que c'est Map qui se charge de dessiner Map. Ne serait-ce pas encore mieux d'avoir une classe Scene qui gère l'affichage du jeu ? Qui se chargerait donc de dessiner Map et les Actors.

Loris

Oui ça pourrait être une idée sauf que rendu la ça vaut peut-être pas la peine de créer une classe pour ça

MadaQC

C'est vrai que cette classe Scene serait assez minuscule. Mais dans ce cas, qu'est-ce qui se chargera de tout dessiner ? La classe Partie ?

la classe map. Je mettrais une classe case aussi pour gérer si il reste des point ou pas sur chaque case le type de point(les grosses bulles) et les bonus ;)

+0 -0

Oui mais en quelque sorte, Map est indépendant de PacMan, elle doit juste gérer les tuiles. Ce serait donc une entité au même titre que les Actors, qui ne doivent pas gérer l'affichage.

Je ne pense pas faire de classe Case (ou Tile plutôt), la tilemap est actuellement stockée dans un vector de int (ou un nombre peut correspondre à vide/solide/dot/energizer/tunel), auquel j'accède par une classe Matrix. Ça me semble suffisant. ;) (Ah, et les fruits bonus ne font pas partie de la tilemap, car ils se trouvent entre deux tuiles.)

Je ne sais pas comment tu gère ça en C++ mais en java il faut faire une classe qui hérite d'une classe de fenêtre pour faire une fenetre, c'est donc ma fenêtre qui aurait gérer l'affichage de la map. Sinon ton idée de scène est tout à fait correct conceptuellement parlant.

De même il aurait était possible effectivement de faire une classe case tu aurais ainsi pu les gérer plus efficacement via un tableau de case.

En réalité tu peut toujours redécoupage, à un moment il faut juste regarder avec un peu de recul et ce dire "stop, ça suffit comme ça. Mes choix suffisent à répondre au besoin du projet et prennent en compte suffisamment les besoins futur."

+0 -0

Je ne connais pas beaucoup Java, mais avec la bibliothèque que j'utilise (SFML), une fenêtre est un objet définit par la classe RenderWindow. Donc la classe Partie aura un membre de type RenderWindow, sur lequel il appellera (plusieurs fois donc) la méthode draw(entité-dessinable) durant effectuerTour(). La classe Scene pourrait justement se charger de gérer cette RenderWindow, séparément du reste.

Mais bon je vais encore réfléchir un peu à comment gérer l'affichage sans trop me compliquer.

Bah moi j'hésiterai pas et je mettrais la classe Scène. En attendant si tu n'a plus de question, pense a mettre le sujet en résolu.

Et au plaisir de te revoir sur le forum :)

+0 -0

Lu'!

Je n'ai qu'un point majeur à redire sur la conception proposée : pourquoi la map est elle en charge de s'afficher ? Ou, pour être plus précis, et le MVC là dedans ? Quel est le symptôme dans la conception : Map se retrouve avec deux responsabilités : déterminer les directions possibles pour divers éléments et leur caractère bouffable ou non (ce n'est déjà peut être plus de son ressort pour ce dernier point d'ailleurs) et l'affichage d'elle même et des bestioles. Nous avons deux responsabilité : viol du SRP.

Ici, le MVC est parfaitement appliquable ;) . Le tout est d'augmenter encore la séparation entre les éléments. La map, les fantômes et le pacman sont des observables, la vue principale et les représentations graphiques des éléments, des observeurs.

Second point, Partie pourrait encore être découpé et/ou renommé. On a bien envie d'avoir un contrôleur qui se charge de : provoquer les appels nécessaires vers le modèle lorsque le joueur appuie sur un bouton, dire au modèle qu'il peut faire sa tâche idle. Concrètement du côté de cette classe, on aurait :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void run(){
  while(true){
    if(event.poll()){
      switch(event){
      case UP : model.playerMovement(UP);
      //...
      }
    }
    model.idle();
    view.refresh();
  }
}

Sachant que ce idle côté modèle serait juste les actions des fantômes, etc. Finalement, le dernier point est que la vue (qui aura été notifiée au fil du tour par les observables) doit mettre son affichage à jour.

Dernier point, mineur cette fois, la majorité des int proposés sont en fait des naturels (unsigned).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte