Cette étape vous permettra d'appréhender les bases de la Progammation Orientée Objet et du modèle d'architecture Modèle Vue Contrôleur.
Non... POO n'est pas un ennemi !
Vous voici dans un workshop pour réaliser le jeu SOKOBAN en Programmation Orientée Objet c'est-à-dire POO. Dans ce workshop il paraît même que nous parlerons du modèle d'architecture Modèle Vue Contrôleur c'est-à-dire MVC. Le programme est ambitieux mais ne vous faites aucune crainte, nous avancerons pas à pas et je vais essayer d'être clair.
Voici pour la présentation du workshop.
Nous allons, avant de passer à la pratique, nous écarter un peu du jeu SOKOBAN dans ce passage théorique. En effet, je vais vous présenter les concepts :
Imaginons un petit jeu où l'on doit déplacer un personnage (ceci ressemble un peu à SOKOBAN...). Mais il se déplacera sur une petite carte (20 colonnes et 10 lignes) ainsi pas besoins de gérer de caméra puisqu'il s'agit de la taille de l'écran.
Ce personnage doit :
Le personnage sera représenté par un "x".
Voici le code de la version procédurale de ce petit programme (consulter le code ici).
#include <Gamebuino-Meta.h>
// position du personnage
int xCharacter;
int yCharacter;
void setup() {
// initialiser la gambuino
gb.begin();
// initialisation de la position du personnage
xCharacter = 10;
yCharacter = 5;
}
void loop() {
// boucle d'attente
while(!gb.update());
// effacer l'écran
gb.display.clear();
manageMove();
paint();
}
// Gérer les déplacement
void manageMove() {
// Note :
// * Le test : (yCharacter > 0) permet de ne pas sortir du haut de l'écran.
// * Le test : (xCharacter < 19) permet de ne pas sortir de la droite de l'écran.
// * Le test : (yCharacter < 9) permet de ne pas sortir du bas de l'écran.
// * Le test : (xCharacter > 0) permet de ne pas sortir de la gauche de l'écran.
// Ainsi on définit les limites du personnage.
if(gb.buttons.pressed(BUTTON_UP)) {
if(yCharacter > 0) {
yCharacter--;
}
} else if(gb.buttons.pressed(BUTTON_RIGHT)) {
if(xCharacter < 19) {
xCharacter++;
}
} else if(gb.buttons.pressed(BUTTON_DOWN)) {
if(yCharacter < 9) {
yCharacter++;
}
} else if(gb.buttons.pressed(BUTTON_LEFT)) {
if(xCharacter > 0) {
xCharacter--;
}
}
}
// Gérer l'affichage
void paint() {
for(int y=0 ; y<=yCharacter ; y++) {
for(int x=0; x<20 || x<xCharacter ; x++) {
if(x == xCharacter && y == yCharacter) {
gb.display.print("x"); // afficher le personnage
} else {
gb.display.print(" "); // afficher une colonne vide
}
}
gb.display.println(); // passage à la ligne suivante
}
}
Le MVC est un modèle d'architecture, cela sert à décrire comment le code doit être découpé. Le MVC implique qu'il y ai 3 couches :
Dans votre programme principal vous devez avoir uniquement des interactions avec le/les contrôleur(s).
De plus, imaginez que vous souhaitez faire un portage de votre programme vers un autre terminal (sur ordinateur, par exemple) et bien dans ce cas vous n'avez que la couche vue à modifier, voilà un intérêt du MVC.
Encore un avantage du MVC, la résolution de bugs :
Pour un petit programme le découpage peut-être lourd mais voyez les avantages que cela apporte.
Un concept important dans la POO est la classe. Une classe est une structure de donnée qui a :
Faisons un point sur les classes.
Voici un exemple de classe C++ :
// 1) Déclaration
class Plan {
private:
int nbPieces;
public:
Plan(int unNbPieces);
void dessiner();
};
// 2) Définition
Plan::Plan(int unNbPieces) : nbPieces(unNbPieces) {
}
void Plan::dessiner() {
for(int i=1 ; i<=nbPieces ; i++) {
// dessiner la pièce...
}
}
// 3) Instanciation et utilisation
// Création d'un objet de type *Plan*
Plan *maMaison = new Plan(2);
// Utilisation de l'objet
maMaison->dessiner();
On remarque plusieurs parties dans le code ci-dessus.
Elle définit notre classes : ses attribut et le prototype de ses méthodes. Le mot "class" est important c'est lui qui dit au compilateur que ce que l'on écrit est une classe.
Ici nous avons un attribut : nbPieces c'est un entier qui contient le nombre de pièces du plan (c'est pas une surprise, enfin j'espère !).
Ensuite, nous avons deux méthodes :
Remarque : Faites attention au point-virgule à la fin des classes, sans ce point-virgule l'IDE Arduino donnera l'erreur new types may not be defined in a return type à la compilation.
De plus, une chose très importante : les attributs doivent être privés. Il s'agit de l'encapsulation, un concept de la POO, qui veut que l'on interdit l'accès aux attributs de classes depuis l'extérieur. Les méthodes sont en revanche publiques ce qui permet de les utiliser dans le programme principal par exemple.
Enfin, la déclaration se place dans un fichier .h (pour header, Plan.h).
Le constructeur
Plusieurs choses doivent être dites à son sujet :
La définition du constructeur est particulière, en effet dans beaucoup de langage (ce qui reste possible en C++) il aurait fallu écrire ceci :
Plan::Plan(int unNbPieces) {
nbPieces = unNbPieces;
}
Le C++ a un petit sucre syntaxique qui permet de faire la même chose de la manière suivante :
Plan::Plan(int unNbPieces) : nbPieces(unNbPieces) {
}
Remarque : toutes méthodes, dont le constructeur, doivent être préfixées par le nom de la classe, ici : Plan::.
La méthode dessiner est composée d'une boucle qui itère sur le nombre de pièces et le reste du code je le laisse pour votre imagination.
La définition se place dans un fichier .cpp (Plan.cpp pour notre exemple).
Lorsque qu'on crée un objet à partir d'une classe on dit qu'on instancie un objet. Cet objet est une instance, autrement dit, c'est une maison alors que la classe est un plan. On crée ici un pointeur de Plan, en effet le type est "Plan *". Ainsi pour instancier l'objet il faut faire appel au mot-clé C++ "new". Le "2" passé au constructeur c'est le nombre de pièces de notre maison.
Enfin pour appeler une méthode (sur un pointeur) il faut utiliser le symbole suivant : "->" (ici : maMaison->dessiner()).
Une représentation UML via un diagramme de classes est possible est le voici :
Remarques :
Revenons à notre jeu.
Maintenant que vous savez créer des classes voyons de quoi nous avons besoins pour que notre petit programme respecte le MVC. Il faut :
Comme vous le voyez par convention :
Comme vous savez créer des classes, avant de passer à la réalisation du SOKOBAN, vous pouvez essayer de réaliser une version orientée objet qui respecte le MVC de notre exemple, ne réinventez pas tout vous pouvez en effet vous inspirer de la version procédurale.
Pour vous guider voici un diagramme de classes (fait en UML) qui représente notre petit programme avec toutes les classes et méthodes nécessaires :
Si vous n'y arrivez pas, n'ayez crainte vous pouvez consulter le code de la version orientée objet que j'ai fait.
Remarques :
Enfin des améliorations sont possibles, effectivement il devrait être interdit d'instancier plusieurs personnages (imaginez le désordre que ça engendrerait !). Ne lisez la suite que si vous voulez avoir mal de tête ;). Pour faire cela nous pourrions utiliser le design pattern Singleton, mais ce n'est pas l'objectif de ce cours.
La pratique vous permettra de créer l'ensemble des classes et méthodes utiles au jeu SOKOBAN. Pour les méthodes nous ferrons juste la description, en effet, nous verrons la définition (le contenu) de chacune d'elle au fur et à mesure des étapes qui composent ce workshop.
Si vous ne vous sentez pas encore prêt à franchir le cap de la POO, il existe une version procédurale du jeu SOKOBAN (cf. partie 1 par jicehel).
Je vous invite à télécharger le code source qui vous servira de base au programme, vous le trouverez ici.
Voici le diagramme de classes du code que vous devez écrire :
Remarques :
Membre statique (ou membre de classe)
On définit par membre statique un membre appartenant à la classe. Par exemple, un attribut statique aura la même valeur pour chacune des instances de la classes. Si pour une classe donnée vous avez du code commun à toutes les instances vous pouvez alors l'écrire dans une méthode statique.
Remarques :
Astuces
On telle méthode s'écrit suivi par le mot-clé "const", comme dans l'exemple suivant :
class Voiture {
public:
void reviser() const;
};
void Voiture::reviser() const {
}
Effectivement, il paraît absurde de modifier la voiture lorsqu'on la révise, en effet on n'a pas à modifier l'état de la voiture.
Voici un exemple d'une fonction qui prend en paramètre un pointeur de notre classe Voiture :
void detruire(Voiture * unPointeur) {
}
On dit que notre type est un pointeur grâce au mot-clé "*". Ainsi unPointeur est un pointeur de Voiture.
Voici un exemple de fonction qui retourne une référence de Voiture :
Voiture& construire() {
}
Le "&" signifie qu'on a une référence.
Voici un exemple d'attribut statique et de méthode statique :
class Voiture {
private:
static int nbInstances;
public:
static int getNbInstances();
};
int Voiture::getNbInstances() {
return Voiture::nbInstances;
}
Plusieurs remarques :
Vous avez désormais toutes les cartes en main pour écrire les bases du Sokoban en orienté objet.
Si vous avez terminé ou si vous rencontrez des problèmes vous pouvez télécharger la solution ici.
Dans la prochaine étape, c'est-à-dire la deuxième, nous réaliserons la gestion de la caméra.
N'hésitez pas à me faire un retour : les améliorations que vous apporteriez (un regard extérieur est toujours bienvenu), les fautes, etc.