TapTap

Créations

JulienGio

il y a 5 ans

Commençons le chapitre 2 avec un jeu qui nous apprendra certains concepts fondamentaux des jeux vidéo.

Tap Tap, à quelle vitesse peux-tu taper ?

Durée 45 minutes

Niveau Débutant

Prérequis

  • Avoir complété le chapitre 1

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 :)

Entrées, Mise à jour, Affichage

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 :

  1. 1. Obtenir les entrées de l'utilisateur
  2. 2. Mettre à jour la logique du jeu
  3. 3. Mettre à jour l'affichage

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 ;)

Un premier prototype

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

Les Constantes

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 :


  • Si le joueur n'a pas appuyé sur une flèche, ne rien faire
  • Sinon, si la flèche en question correspond au côté de la brique, +15 points et une nouvelle brique est choisie.
  • Sinon, le score revient à zéro

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 :

  • Si le joueur a appuyé sur une flèche :
    • Si la flèche en question correspond au côté de la brique, +1 point et une nouvelle brique est choisie.
    • Sinon, le score revient à zéro

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 :

  • +=
  • -=
  • *=
  • /=
  • et d'autres encore...

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.

Les Tableaux

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...

A vous de jouer !

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.

  • Astuce #1 : Pour ajouter des briques, il faudra changer NBR_DE_BRIQUES et modifier l'affichage pour qu'on les voit toutes.
  • Astuce #2 : Pour rendre le jeu plus dur quand le score augmente, vous pouvez soustraire une partie du score à chaque image avec l'aide de l'opérateur de division /.

Montre nous ton jeu sur les réseaux avec #gamebuino #atelier #TapTap, on les regarde tous ;)

Exemple de Solution

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); }


Atelier Suivant

Par Julien Giovinazzo

Des questions / commentaires / suggestions ? Laissez-nous un commentaire plus bas!

Voir la création

jicehel

NEW il y a 5 ans

Super, très clair. PS: Le bouton Atelier suivant apparaît en "Next Workshop" sur les derniers tutos.

JulienGio

il y a 5 ans

Merci :)

JulienGio

NEW il y a 5 ans

jicehel jicehel

Merci :)

Juice_Lizard

NEW il y a 5 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.

Aurélien Rodot

il y a 5 ans

Merci, c'est corrigé ! :)

Aurélien Rodot

NEW il y a 5 ans

Juice_Lizard Juice_Lizard

Merci, c'est corrigé ! :)