il y a 6 ans
Commençons le chapitre 2 avec un jeu qui nous apprendra certains concepts fondamentaux des jeux vidéo.
Durée 45 minutes
Niveau Débutant
Prérequis
TapTap est un jeu simple à seulement deux boutons. Le but du jeu est d'exécuter une séquence de mouvements le plus rapidement possible. Cette séquence est générée aléatoirement et est sans fin. A chaque étape de la séquence validée, le joueur gagne des points (s'il se trompe, il retombe à zéro). Mais le score du joueur diminue en permanence. Il faut donc frapper les touches le plus vite possible en faisant le moins de fautes !
En programmant TapTap pour la Gamebuino, nous verrons trois notions très importantes dans le développement de jeux vidéo. Tout d'abord, nous allons apprendre à structurer notre code dans la boucle de jeu. Après nous verrons les tableaux et les constantes en C++. Ce sont trois concepts très importants et puissants qui réapparaitrons tout au long du chapitre 2, et dans quasiment tous les jeux de manière générale :)
Avant de plonger dans les lignes de code, il faut toujours se poser un moment et se dire "Comment vais-je organiser mon code ?" Heureusement pour nous, dans le monde des jeux vidéo, l'architecture de la boucle principale est toujours la même :
En réalité, en fonction de la plateforme sur laquelle le jeu est exécuté, certaines étapes doivent être ajoutées. Comme par exemple la régulation de la fréquence de la boucle principale (pour que le jeu tourne à la même vitesse pour tout le monde), mais cet aspect est déjà géré avec gb.update
, nos jeux serons toujours à 25 images par secondes. Donc en réalité, il y a une zérotième étape dans notre boucle : while(!gb.update());
.
D'ailleurs, vous avez utilisé cette méthode dans l'atelier du Pong sans le savoir ! L'étape 1 consiste à voir ce que le joueur fait. Pour Pong c'était le mouvement des raquettes avec les boutons. L'étape 2 est responsable pour toute la logique du jeu. Dans Pong cela correspondait aux détections de collisions et au déplacement de la balle et on y a ajouté l'IA plus tard. Enfin, pour l'étape 3, nous avons tout affiché (raquettes, balle et score).
Cette technique d' "Entrées, Mise à jour, Affichage" est très pratique. Elle nous permet tout d'abord de nous y retrouver dans notre code, et puis elle nous force à découpler les entrées, la logique de jeu et l'affichage. Nous reviendrons plus tard sur les avantages de cela. Commençons par placer des commentaires pour mieux s'y retrouver :
#include <Gamebuino-Meta.h> void setup() { gb.begin(); } void loop() { while(!gb.update()); // ENTREES // // MISE A JOUR // // AFFICHAGE // gb.display.clear(); }
J'en ai aussi profité pour placer tout ce dont on aura forcément besoin, mais rien de nouveau ici ;)
Pour le jeu de Pong, nous avions d'abord créer un jeu avec une raquette et une balle. Une fois que c'était fini, vous avez ajouté une deuxième raquette et un score. Comme pour le Pong, il vaut mieux attaquer le jeu en créant une première version qui contient le strict minimum, puis se baser dessus pour faire le reste.
Ici, nous allons donc commencer par créer une variante plus simple. La séquence ne s'affichera pas, c'est-à-dire que nous avons juste à gérer un seul bloc, comme ceci :
Si le rectangle est sur la gauche, et que le joueur appuie sur la flèche de gauche, il gagne des points et un nouveau rectangle apparait. La logique est analogue si le rectangle est à droite. Si jamais il se trompe, son score est remis à zéro. Et enfin, pour mettre un peu de pression, le score diminue progressivement
Le jeu doit donc avoir une manière de savoir où est le rectangle (que l'on appellera brique dorénavant), et voir si le joueur a appuyé sur la bonne flèche. Il faut d'abord déclarer toutes ces variables.
#include <Gamebuino-Meta.h> int brique; // Position de la brique. Si elle vaut 1, la brique est à gauche. Si elle vaut 2, la brique est à droite int fleche; // 1 flèche de gauche, 2 flèche de droite, 0 pas de flèches (en attente du joueur) int score = 0; void setup() { gb.begin(); brique = random(1, 3); // 50% gauche, 50% droite fleche = 0; } void loop() { while(!gb.update()); // ENTREES // // MISE A JOUR // // AFFICHAGE // gb.display.clear(); }
Ici, nous avons juste déclaré les trois variables qu'il nous faut. brique
est un entier qui vaut soit 1, soit 2. S'il vaut 1, alors on considère que la brique est à droite. Autrement, s'il vaut 2, alors on considère que la brique est à gauche. fleche
est aussi un entier et, similairement à brique
, s'il vaut 1, on dira que la flèche gauche est appuyée par le joueur. Si fleche
vaut 2, cela correspond à la flèche de droite. Et enfin si aucune flèche n'est appuyée, notre variable vaudra 0. Pouah ! Ce n'est pas très facile à ingérer tout ça... Et là, nous avons que 2 directions à prendre en compte !
Comment faire pour que ce soit lisible sans trop se creuser la tête ? Avec des constantes évidement :D
Une constante est comme une variable, sauf qu'elle ne peut pas changer de valeur au cours du programme (d'où le nom). Une constante est simplement un nom qu'on donne à une valeur. Et c'est très utile dans les situations comme la nôtre : quand on utilise des valeurs arbitraires. Ici on utilise 1 pour designer la gauche, mais on aurait aussi pu utiliser -382 à la place. Sans constantes on écrit brique = 1;
pour dire "la briques est à gauche". Mais avec une constante on peut écrire brique = GAUCHE;
! Bien mieux non ? Même plus besoin de commentaires pour expliquer ce que fait cette instruction !
Alors, comment créons-nous des constantes ? Pour déclarer une constante, on fait comme pour une variable en ajoutant le mot clé const
avant de déclarer le type:
const int GAUCHE = 1;
Contrairement aux variables, il est obligatoire d'affecter une valeur aux constantes lors de leurs déclarations. Simplement parce-qu'après la déclaration, la valeur ne peut pas changer.
#include <Gamebuino-Meta.h> // Constantes const int GAUCHE = 1; const int DROITE = 2; const int SANS_DIRECTION = 3; int brique; // Position de la brique. Soit GAUCHE, soit DROITE int fleche; // Correspond à la flèche appuyé par l'utilisateur. Soit GAUCHE, soit DROITE, soit SANS_DIRECTION int score = 0; void setup() { gb.begin(); brique = random(GAUCHE, DROITE + 1); // +1 parce-que le deuxième paramètre n'est pas dans l'intervalle : random(1, 3) => 1 ou 2 (pas 3) fleche = SANS_DIRECTION; } void loop() { // loop... }
Et voilà, nous avons déclaré quelques constantes très utiles :D La brique est soit à gauche soit à droite. La variable fleche
est, elle, à gauche, à droite, ou sans direction. Bien plus compréhensible n'est-ce pas ?
Si vous lisez le code d'un jeu crée par quelqu'un d'autre, il est possible qu'il n'utilise pas la structure que l'on vient de voir pour déclarer ses constantes. Certains utilisent #define GAUCHE 1
. Ceci revient quasiment à faire const int GAUCHE = 1;
. Les différences ne sont pas importantes à notre niveau, et nous vous conseillons d'utiliser const
car c'est bien plus facile pour localiser les bugs éventuels lors d'une erreur de compilation. Nous utiliserons exclusivement des const
dans nos ateliers.
On peut ensuite facilement implémenter la gestion des entrées et faire l'affichage comme ceci :
#include <Gamebuino-Meta.h> // Constantes const int GAUCHE = 1; const int DROITE = 2; const int SANS_DIRECTION = 3; int brique; // Position de la brique. Soit GAUCHE, soit DROITE int fleche = SANS_DIRECTION; // Correspond à la flèche appuyé par l'utilisateur. Soit GAUCHE, soit DROITE, soit SANS_DIRECTION int score = 0; void setup() { gb.begin(); brique = random(GAUCHE, DROITE + 1); // +1 parce-que le deuxième paramètre n'est pas dans l'intervalle : random(1, 3) => 1 ou 2 (pas 3) fleche = SANS_DIRECTION; } void loop() { while(!gb.update()); // ENTREES // if (gb.buttons.released(BUTTON_LEFT)) { fleche = GAUCHE; } else if (gb.buttons.released(BUTTON_RIGHT)) { fleche = DROITE; } // MISE A JOUR // // AFFICHAGE // gb.display.clear(); if (brique == GAUCHE) { gb.display.fillRect(20, 40, 20, 10); } else { // DROITE gb.display.fillRect(40, 40, 20, 10); } // Score gb.display.print(score); }
Notre version simplifiée du jeu est presque finie, il nous reste juste à ajouter la logique de jeu.
Petit rappel de ce que nous voulons faire :
Ici nous avons donc trois possibilités à chaque image, mais la première condition nous dit de ne rien faire si elle est vraie. On peut la modifier un peu pour obtenir :
Ce qui nous donne le code suivant:
// Constantes // // Déclarations de variables // void setup() { // setup // } void loop() { // ENTREES // // ... // MISE A JOUR // // Doucement baisser le score if (score > 0) { score -= 1; } // Est-ce que le joueur a appuyer sur une flèche ? if (fleche != SANS_DIRECTION) { if (brique == fleche) { // Bonne flèche score += 15; brique = random(GAUCHE, DROITE + 1); } else { // Perdu :( score = 0; } fleche = SANS_DIRECTION; // L'entrée utilisateur a été prise en compte } // AFFICHAGE // // ... }
J'ai aussi ajouté un système qui diminue progressivement le score : score -= 1;
. L'opérateur x -= y
est un raccourci de x = x - y
. Ce raccourci existe pour presque tous les opérateurs et est très pratique :
+=
-=
*=
/=
Je l'utilise aussi pour augmenter le score lorsque le joueur appuie sur la bonne flèche : score += 15;
. A la fin du premier bloc if
, il ne faut pas oublier de réinitialiser fleche
, sinon la Gamebuino pensera que le joueur est constamment en train d'appuyer sur une flèche !
Notre petit jeu est fini ! Vous pouvez le téléverser (si vous ne l'avez toujours pas fait :P) et y jouer.
Notre petite démo du jeu n'est pas très amusante pour l'instant. On ne voit qu'une seule brique à la fois. Impossible de jouer rapidement. Nous allons donc devoir ajouter quelques briques. Le joueur devra frapper du côté de la brique qui est tout en bas comme dans notre démo, mais il pourra mieux réagir car il voit les briques suivantes :D
Intuitivement, vous vous dites sûrement qu'il suffit de rajouter les variables brique1
, brique2
, brique3
, brique4
, et brique5
pour y parvenir. Et, en effet, c'est une solution possible, mais ça n'est pas une bonne solution.
Comment allons-nous faire pour programmer ces briques ? Et bien nous allons créer un tableau. En C++, un tableau est une sorte de liste qui contient un nombre précis de variables. Si nous imaginons que les variables sont des boites, alors un tableau est une série de boites liées.
Au lieu d'avoir un nom par boite, on a un nom pour toutes les boites, briques
. Les tableaux sont très puissants en programmation car ils peuvent contenir n'importe quel type de variable (mais tous les éléments au sein d'un tableau sont du même type). Ici, on veut un tableau d'entiers. Pour accéder à un des éléments on écrit nomDuTableau[indice_de_l_element]
. Mais attention, les indices commencent avec 0, donc:
Premier élément : nomDuTableau[0] Deuxième élément : nomDuTableau[1] Troisième élément : nomDuTableau[2] ... N-ème élément : nomDuTableau[N - 1] ... Dernier élément : nomDuTableau[NBR_D_ELEMENTS - 1]
Okay, mais ça c'est pour interagir avec les valeurs dans le tableau. Comment fait-on pour créer un tableau ?
int monTableau[NBR_D_ELEMENTS];
Pour déclarer un tableau d'entiers, c'est presque comme déclarer un entier. On met d'abord le type des éléments du tableau (ici int
), son nom
(ici monTableau
), et enfin la taille du tableau entre crochets [ ]
.
int gamebuinoTeamAges[3] = {26, 23, 19};
Optionnellement, nous pouvons remplir le tableau de valeurs une première fois avec une paire d'accolades { }
. Attention, on ne peut faire ceci que lors de la déclaration. Si, après la déclaration, vous écrivez gamebuinoTeamAges = {34, 24, 12};
, votre code ne compilera pas!
Pour en revenir à notre jeu TapTap, il faut que nous déclarions un tableau briques
. Pour commencer, il n'y aura que quatre briques. Et nous utiliserons une constante pour savoir combien de briques nous avons. On considérera que la brique d'indice 0 est celle tout en bas, celle que le joueur doit frapper.
#include <Gamebuino-Meta.h> // Constantes // ... const int NBR_DE_BRIQUES = 4; int briques[NBR_DE_BRIQUES]; // Position des briques. Soit GAUCHE, soit DROITE int fleche = SANS_DIRECTION; // Correspond à la flèche appuyé par l'utilisateur. Soit GAUCHE, soit DROITE, soit SANS_DIRECTION int score = 0; void setup() { gb.begin(); // Créons des briques aléatoires briques[0] = random(GAUCHE, DROITE + 1); // +1 parce-que le deuxième paramètre n'est pas dans l'intervalle : random(1, 3) => 1 ou 2 (pas 3) briques[1] = random(GAUCHE, DROITE + 1); briques[2] = random(GAUCHE, DROITE + 1); briques[3] = random(GAUCHE, DROITE + 1); fleche = SANS_DIRECTION; } void loop() { while(!gb.update()); // ENTREES // // ... // MISE A JOUR // if (score > 0) { score -= 1; } if (fleche != SANS_DIRECTION) { if (briques[0] == fleche) { // Bonne flèche score += 15; // Décaler les briques vers le bas (donc la [0] devient la [1], etc...) briques[0] = briques[1]; briques[1] = briques[2]; briques[2] = briques[3]; briques[3] = random(GAUCHE, DROITE + 1); // Nouvelle brique } else { // Perdu :( score = 0; } fleche = SANS_DIRECTION; // L'entrée utilisateur a été prise en compte } // AFFICHAGE // gb.display.clear(); if (briques[0] == GAUCHE) { gb.display.fillRect(20, 40, 20, 10); } else { // DROITE gb.display.fillRect(40, 40, 20, 10); } if (briques[1] == GAUCHE) { gb.display.fillRect(20, 30, 20, 10); } else { // DROITE gb.display.fillRect(40, 30, 20, 10); } if (briques[2] == GAUCHE) { gb.display.fillRect(20, 30, 20, 10); } else { // DROITE gb.display.fillRect(40, 30, 20, 10); } if (briques[3] == GAUCHE) { gb.display.fillRect(20, 20, 20, 10); } else { // DROITE gb.display.fillRect(40, 20, 20, 10); } // Score gb.display.print(score); }
Maintenant nous avons donc un tableau d'entiers bricks
de taille 4. Dans setup()
on donne des valeurs aléatoires pour chacune des briques. Au niveau des entrées, rien à changer. Mais pour la logique et l'affichage, notre code doit s'adapter. Dans la partie de mise à jour, nous avons fait en sort que les briques "descendent". Chaque brique prend la valeur de celle du dessus, en commençant par la brique [0]
. La dernière brique ([3]
) prend une direction aléatoire. Dans la section d'affichage, il faut afficher toutes les briques. Donc nous avons presque fait un copié-collé pour chaque brique, avec seulement la hauteur du rectangle à afficher qui change. Ce n'est pas très joli comme code et ça prend de la place, mais dans l'atelier qui suit, nous verrons une méthode puissante et simple qui permet de réduire les "presque copié-collé" :D
Mais en avant d'aller au prochain atelier, c'est...
Cet atelier est le premier du chapitre 2, et a permis de présenter trois notions qui reviendrons dans quasiment tous les jeux que vous ferez plus tard, même hors des ateliers :O
La structure "Entrées, Mise à jour, Affichage" est très puissante. Elle permet de s'organiser et de dissocier les actions du joueur, la logique de jeu, et ce que voit le joueur. Les constantes nous permettent d'écrire du code plus facilement. Et les tableaux sont une nouvelle structure de données qui nous permet de grouper des variables sous un même nom.
Avec tout ça, nous avons construit un jeu, TapTap. Simple, mais amusant. Maintenant c'est à vous de l'améliorer. Voici ce que je propose. Commencez par implémenter un système de record. Puis, pour améliorer le gameplay, ajoutez une ou deux briques. Vous pouvez aussi rendre le jeu plus dur, en augmentant la vitesse à laquelle le score diminue en fonction du score actuel. Donc plus le joueur a de points, plus il les perd vite.
NBR_DE_BRIQUES
et modifier l'affichage pour qu'on les voit toutes./
.Montre nous ton jeu sur les réseaux avec #gamebuino #atelier #TapTap, on les regarde tous ;)
Si vous êtes en panne d'inspiration, voilà ce qu'on a fait de notre côté :)
#include <Gamebuino-Meta.h>const int GAUCHE = 1; const int DROITE = 2; const int SANS_DIRECTION = 3; const int NBR_DE_BRIQUES = 5;
int briques[NBR_DE_BRIQUES]; // Position des briques. Soit GAUCHE, soit DROITE int fleche; // Correspond à la flèche appuyé par l'utilisateur. Soit GAUCHE, soit DROITE, soit SANS_DIRECTION int score = 0; int highscore = 0;
void setup() { gb.begin();
// Mélanger les briques briques[0] = random(GAUCHE, DROITE + 1); briques[1] = random(GAUCHE, DROITE + 1); briques[2] = random(GAUCHE, DROITE + 1); briques[3] = random(GAUCHE, DROITE + 1); briques[4] = random(GAUCHE, DROITE + 1);
fleche = SANS_DIRECTION; }
void loop() { while (!gb.update());
// ENTREES // if (gb.buttons.released(BUTTON_LEFT) || gb.buttons.released(BUTTON_A)) { fleche = GAUCHE; } else if (gb.buttons.released(BUTTON_RIGHT) || gb.buttons.released(BUTTON_B)) { fleche = DROITE; }
// MISE A JOUR // // Diminuer le score en fonction du score if (score > 0) { score -= score / 60; }
if (fleche != SANS_DIRECTION) { if (briques[0] == fleche) {
score += 20; if (score > highscore) { // Record ?? highscore = score; }// Décaler tout vers le bas briques[0] = briques[1]; briques[1] = briques[2]; briques[2] = briques[3]; briques[3] = briques[4]; briques[4] = random(GAUCHE, DROITE + 1); // Nouvelle brique } else { // Mauvaise réponse :( score = 0; }
fleche = SANS_DIRECTION; // L'entrée utilisateur a été prise en compte }
// DRAW // gb.display.clear();
if (briques[0] == GAUCHE) { gb.display.fillRect(25, 40, 20, 10); // fillRect pour la brique du bas. drawRect pour les autres } else { gb.display.fillRect(35, 40, 20, 10); } if (briques[1] == GAUCHE) { gb.display.drawRect(25, 30, 20, 10); } else { gb.display.drawRect(35, 30, 20, 10); } if (briques[2] == GAUCHE) { gb.display.drawRect(25, 20, 20, 10); } else { gb.display.drawRect(35, 20, 20, 10); } if (briques[3] == GAUCHE) { gb.display.drawRect(25, 10, 20, 10); } else { gb.display.drawRect(35, 10, 20, 10); } if (briques[4] == GAUCHE) { gb.display.drawRect(25, 0, 20, 10); } else { gb.display.drawRect(35, 0, 20, 10); }
// Score gb.display.fillRect(0, gb.display.height() - (score / 5), 4, (score / 5)); // On divise le score par 5 pour qu'il rentre dans l'écran
// Highscore gb.display.drawFastHLine(0, gb.display.height() - (highscore / 5), 6); }
Par Julien Giovinazzo
Des questions / commentaires / suggestions ? Laissez-nous un commentaire plus bas!
NEW il y a 6 ans
Bon tuto. Les tableaux, c'est pas évident à intégrer. Il y a des petits erreurs dans l'article: "setUp" avec un "u" majuscule. J'ai un peu galéré à trouver pourquoi ça marchait pas.