Moving Ball > Problème de test de sortie d'écran non effectué

Bonjour,

Je me remets doucement à la programmation de ma GameBuino en refaisant les tutoriaux de débutant.

J’ai un souci avec ce bout de code : Moving_Ball · GitHub

J’ai fait un test pour ne pas dépasser le bord de l’écran, sauf que dès que j’atteins une valeur supérieure à 3 pour la vitesse de mouvement de la balle, le test de retour au centre de la balle n’est pas exécuté et elle continue d’avancer hors de l’écran :thinking:

D’avance merci pour votre aide car j’aurai d’autres questions !

Cric

Salut @Cric,

screen-bounds

Penses-tu que ce test soit le bon ? :wink:

if (positionX == gb.display.width())
2 Likes

Ca semble être la bonne question en effet :wink:

La réponse de @Steph t’as elle débloqué ?

@Steph et @JMP

Curieusement, quand on met le bon test, ça marche :wink:
Merci pour votre aide.

Comme le souligne Steph, la bonne condition du test est la suivante :

if (positionX > gb.display.width()) {
      positionX = 32;
  }
1 Like

:joy: Rien de “curieux” ici… Pour mieux visualiser ce qui (peut) se passe(r), il est souvent très utile de se faire un p’tit crobar :wink:

2 Likes

C’est en faisait des petites erreurs qu’on apprends.

Bravo et bonne continuation.
N’hésite pas à demander en cas de problèmes.
Jean-Marie

1 Like

Bon, j’ai terminé le tutorial de Pong et j’aimerais ajouter un score à la fin en affichant le vainqueur (le premier atteignant 10 points), le code est consultable ici : Pong_1_Player · GitHub.

  if (score1 >= 10) {
    gb.display.setCursor(10, 30);
    gb.display.print("Le joueur gagne");
  } else if (score2 >= 10) {
    gb.display.setCursor(17, 30);
    gb.display.print("La GB gagne");
  }

Sauf que, une fois le vainqueur affiché, j’aimerais recommencer une nouvelle partie en appuyant sur une touche (A par exemple).
Pour ce faire, @Jicehel m’avait parlé de gérer des états, mais je n’ai pas d’exemples sous la main.
Du coup, comment faire ?

D’avance merci pour votre aide.

Salut @Cric,

Le principe consiste à définir ce que l’on appelle un automate, dont le comportement est prédéterminé par un ensemble d’états et de règles permettant de passer d’un état à un autre.

Voici un exemple simple d’implémentation d’un automate permettant de gérer 3 phases élémentaires d’un jeu :

  • start
  • play
  • game_over

Les règles de transition d’un état à un autre sont ici régies par l’appui sur les boutons de la console. Je te laisse déchiffrer le code pour les identifier (car c’est en forgeant qu’on devient forgeron…) :wink:

Fais défiler le code pour pouvoir le lire entièrement…

#include <Gamebuino-Meta.h>

enum class State : uint8_t {
    
    start,
    play,
    game_over
    
};

struct Game {
    
    State   state;
    uint8_t score;
    
};

Game game;

void start() {

    if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;

    gb.display.print(10, 29, "PRESS A TO PLAY");
    
}

void play() {

         if (gb.buttons.pressed(BUTTON_A)) game.score++;
    else if (gb.buttons.pressed(BUTTON_B)) game.state = State::game_over;

    gb.display.printf(34, 29, "%03u", game.score);

}

void gameOver() {

    if (gb.buttons.pressed(BUTTON_A)) game.state = State::start;

    gb.display.print(22, 17, "GAME OVER");
    gb.display.printf(20, 29, "SCORE: %03u", game.score);
    gb.display.print(4, 41, "PRESS A TO RESTART");

}

void setup()  {

    gb.begin();

    game.state = State::start;
    game.score = 0;
    
}

void loop()  {

    gb.waitForUpdate();
    gb.display.clear();

    switch (game.state) {

        case State::start:
            start();
            break;

        case State::play:
            play();
            break;

        case State::game_over:
            gameOver();

    }

}

Ce petit bout de code devrait te permettre de transposer facilement le principe d’implémentation d’un automate dans ton propre code… En cas de difficulté, n’hésite pas à revenir poser des questions.

2 Likes

Le code est très élégant @Steph !

2 Likes

@Steph, un grand merci pour ta réponse !

Je vais étudier cela de près et reviendrai avec mes questions au fur et à mesure de mes difficultés.

Avec plaisir :slightly_smiling_face:

Ha et, en passant, tu as posté ta requête initiale dans la mauvaise catégorie (#creations).
Il eût mieux valu la poster dans #french-francais:fr-demandes-daide me semble-t-il…

Je ne sais pas si tu peux encore la reclasser (?)
Mais @JMP devrait pouvoir le faire dans le cas contraire.

2 Likes

C’est fait, topic déplacé .
Merci @Steph

1 Like

Bonjour @Steph,

J’ai redispatché le programme à partir ton code.
Bon bah ça marche Nickel :smiley: Pong 1 Player · GitHub
Un grand merci à toi pour ton aide.

Seul truc que je n’ai pas réussi à faire, c’est d’éviter de repartir sur l’état :start: (qui nécessite d’appuyer deux fois sur A) plutôt que l’état :play: car dans ce cas, la réinitialisation des scores ne se fait pas.

J’ai même dû la déplacer dans le void start() (voir code ci-après) car le void setup() n’est pas réexécuté après l’état :gameOver: (ou je ne sais pas faire pour l’exécuter si l’état est :play:).

void start() {
    if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;
    gb.display.print(12, 10, "PONG 1 PLAYER");
    gb.display.print(12, 30, "APPUYER SUR A");
    gb.display.print(12, 40, "POUR DEMARRER");
    game.score1 = 0;
    game.score2 = 0;
}

J’ai tenté de réinitialiser les scores comme ceci, mais ça ne fonctionne pas…

void gameOver() {
  gb.display.setCursor(25, 5);
  gb.display.print(game.score1);
  gb.display.print(" - ");
  gb.display.print(game.score2);
  if (gb.buttons.pressed(BUTTON_A)) game.state = State::start;
    if (game.score1 >= 10) {
      gb.display.print(15, 20, "LE JOUEUR GAGNE");
      gb.display.print(12, 40, "APPUYER SUR A");
      gb.display.print(12, 50, "POUR DEMARRER");
      game.score1 = 0;
      game.score2 = 0;
    } else if (game.score2 >= 10) {
      gb.display.print(15, 20, "LA GB GAGNE");
      gb.display.print(12, 40, "APPUYER SUR A");
      gb.display.print(12, 50, "POUR DEMARRER");   
      game.score1 = 0;
      game.score2 = 0; 
  }
}

D’avance merci pour ton aide, car mon code n’est pas très élégant :frowning:

En synthèse, ta gestion des états m’ouvre de nouvelles portes, ce qui va me permettre de perfectionner mon programme.

Hello @Cric,

La structure d’un code Arduino s’articule nécessairement autour des 2 fonctions setup() et loop() :

void setup() {

  // Le code placé ici permet d'initialiser le programme.
  // La fonction setup() n'est exécutée qu'une seule fois
  // => au démarrage du programme.

}

void loop() {

  // Ensuite la fonction loop() prend le relais et est exécutée en boucle...
  // C'est donc ici qu'il faut mettre en place toute la logique de contrôle
  // du programme.

}

Par conséquent, le fait de réinitialiser les scores après la fin d’une partie ne peut se faire dans la fonction setup(), mais bien dans la fonction start() comme tu l’as justement fait.

D’ailleurs tu peux aussi implémenter cette fonctionnalité de la manière suivante :

struct Game {

  State state;
  uint8_t score1;
  uint8_t score2;

  void resetScores() {
    score1 = score2 = 0;
  }

};

// [ ... ]

void start() {

  if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;

  gb.display.print(12, 10, "PONG 1 PLAYER");
  gb.display.print(12, 30, "APPUYER SUR A");
  gb.display.print(12, 40, "POUR DEMARRER");

  game.resetScores();

}

Cela permet de transférer à l’objet game la capacité de réinitialiser les scores (puisque ce sont des variables qui relèvent de sa responsabilité).

Ensuite, concernant l’articulation des phases de jeu :

  start --> play --> game_over
    ^                    |
    |                    |
    +--------------------+

Cela correspond bien à la logique d’enchaînement des états, comme tu l’as implémentée. Une fois la partie terminée, il faut bien afficher les scores, puis attendre que le joueur appuie sur le bouton A pour revenir sur l’écran d’accueil défini par la fonction start(). Le joueur doit ensuite rappuyer sur le bouton A pour relancer une nouvelle partie. Rien ne me choque ici !

Par ailleurs, lorsque le joueur rate la balle, le repositionnement est un peu violent :

balle.posX = 20;

Tu devrais peut-être replacer la balle au centre du terrain sur l’axe horizontal…

De manière générale, ton code est perfectible, certes, mais c’est un bon début :slightly_smiling_face:

L’expérience viendra au fur et à mesure. L’apprentissage se fait par étapes, et il ne faut pas vouloir les brûler trop vite :wink: Tu as de nombreux concepts à intégrer ; prends le temps de bien les comprendre et de parvenir à les mettre en oeuvre correctement.

Merci @Steph pour tes explications.

J’ai bien créé la fonction resetScores() comme dans ton exemple, cependant, ça n’y change rien, le score reste à 10 et la partie se termine directement… Pong 1 Player · GitHub

Autre truc que je n’arrive pas à trouver : existe t’il une fonction qui fasse clignoter du texte à l’écran ?
J’ai tenté ce bout de code, mais ça ne fonctionne pas (quel boulet !), et je ne comprends pas pourquoi car ce code devrait s’exécuter en boucle tant que le bouton A n’est pas appuyé, non ?

void start() {
    if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;
    gb.display.print(17, 10, "PONG 1 PLAYER");
    for (int i=0; i<1000; ++i); {
      gb.display.print(17, 30, "APPUYER SUR A");
      gb.display.print(17, 40, "POUR DEMARRER");
    }
    for (int i=0; i<1000; ++i); {
      gb.display.print(17, 30, "            ");
      gb.display.print(17, 40, "             ");
    }
    game.resetScores();
}

J’en ai profité pour repositionner la balle au centre :wink:

Comment ça le score reste à 10 ? Je ne comprends pas… :thinking:

Par contre, tu t’es trompé à la ligne 133 :

if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;

Le nouvel état n’est pas State::play mais State::start :

if (gb.buttons.pressed(BUTTON_A)) game.state = State::start;

Curieux… Tu l’avais pourtant bien écrit dans ta v1… :slightly_smiling_face:

En ce qui concerne le clignotement, tu fais l’erreur de raisonnement que beaucoup de débutants font :

for (int i=0; i<1000; ++i); {
  gb.display.print(17, 30, "APPUYER SUR A");
  gb.display.print(17, 40, "POUR DEMARRER");
}
for (int i=0; i<1000; ++i); {
  gb.display.print(17, 30, "            ");
  gb.display.print(17, 40, "             ");
}

Si tu écris les choses comme ça, les boucles sont bien effectuées… mais dans le même cycle de la fonction loop(). Autrement dit, avant que le moindre raffraîchissement graphique n’ait pu avoir lieu. En effet, souviens toi que le chef d’orchestre est la fonction loop() :

void loop() {

  gb.waitForUpdate();
  gb.display.clear();

  switch (game.state) {

    case State::start:
      start();
      break;

    case State::play:
      play();
      break;

    case State::game_over:
      gameOver();

  }
  
}

À chaque cycle de la fonction loop() la première instruction permet d’effectuer les tâches internes de la lib Gamebuino-Meta :

gb.waitForUpdate();

C’est cette instruction qui permet d’effectuer la lecture de l’état des boutons, la gestion du son, la gestion des LEDs, etc… et surtout le raffraîchissement de l’écran. Tout ceci s’exécute 25 fois par secondes (valeur par défaut, si tu ne modiifies pas le frame rate).

Autrement dit, tout ce que tu dessines à chaque cycle de la fonction loop() ne sera visible qu’à partir du moment où le prochain cycle s’exécute. Par conséquent, tu ne peux pas gérer le clignotement comme tu l’as fait.

L’idée consiste en fait à répartir l’affichage ou non de l’élément que tu souhaites faire clignoter sur plusieurs cycles de la fonction loop(). La librairie fournit un compteur de cycles : gb.frameCount. Tu peux donc t’appuyer sur la valeur de ce compteur pour compter les cycles de raffraîchissements de l’écran et déterminer lesquels doivent afficher ou non l’élément à faire clignoter. Une manière très simple de faire cela consiste, par exemple, à utiliser l’opérateur de congruence % (modulo) :

void start() {

  if (gb.buttons.pressed(BUTTON_A)) game.state = State::play;
    
  gb.display.print(17, 10, "PONG 1 PLAYER");

  if (gb.frameCount % 25 < 13) {

    gb.display.print(17, 30, "APPUYER SUR A");
    gb.display.print(17, 40, "POUR DEMARRER");

  }

  game.resetScores();

}

À chaque cycle de la fonction loop(), l’expression suivante est évaluée :

gb.frameCount % 25 < 13

Note que l’opérateur % est ici prioritaire devant l’opérateur <, sinon il aurait fallu écrire :

(gb.frameCount % 25) < 13

a % b retourne le reste de la division euclidienne de a par b. Autrement dit, quelle que soit la valeur de gb.frameCount, l’expression :

gb.frameCount % 25

retournera toujours une valeur comprise entre 0 et 24. Et comme gb.frameCount est incrémentée d’une unité à chaque cycle de la fonction loop(), tu es sûr de parcourir indéfiniment la séquence de valeurs 0, 1, 2, … 24.

L’astuce consiste donc simplement à comparer la valeur courante (dans cet intervalle) à une valeur pivot pour équilibrer les phases d’affichage et de non affichage de l’élément à faire clignoter. Ici j’ai pris la valeur 13 comme pivot, donc le texte est affiché pour les valeurs comprises entre 0 et 12, mais ne sera pas affiché pour les valeurs comprises entre 13 et 24.

Et le tour est joué ! Tu obtiens bien le clignotement désiré.

Tu peux jouer sur la valeur limite (25) et la valeur pivot (13) pour obtenir des clignotements plus ou moins rapides. À toi de déterminer quelles valeurs conviennent à l’effet de clignotement que tu préfères.

J’espère avoir été assez clair…

1 Like

C’est très clair merci.
J’ai corrigé mon erreur et inséré ton code qui marche exactement comme je le souhaitais Pong 1 Player · GitHub.

Cependant, je ne te cache pas qu’il va falloir que je relise plusieurs fois ton explication à tête reposée pour bien comprendre le fonctionnement de l’instruction

gb.waitForUpdate();

et de ses implications…

Edit :

@Steph,
Tentative de créer un nouvel état pour saisir le niveau de difficulté (Pong_1_Player_V2 · GitHub) , mais ça ne fonctionne pas…
En fait, je peux afficher la difficulté et choisir la valeur entre FACILE et DIFFICILE, mais le bouton A n’est pas pris en compte pour passer à l’état suivant (:play:).
J’ai l’impression que je ne peux pas à la fois afficher du texte à l’écran et à la fois afficher un menu…

Edit : j’avais oublié d’ajouter l’état menu_diff dans le void loop() :astonished:
Cependant, il y a une difficulté car le bouton A est à la fois utilisé pour choisir le niveau de difficulté dans le menu, et à la fois pour changer d’état… J’ai essayé le changement d’état avec le bouton B mais rien ne se passe.

1 Like

C’est pourtant très simple, en suivant exactement la même logique que tout ce que tu as fait jusque là :slightly_smiling_face:

Commençons par définir les niveaux de difficulté :

enum class Difficulty : uint8_t {

  easy,
  hard

};

Puis ajoutons une propriété à notre modèle Game pour prendre en compte le niveau de difficulté choisi par le joueur :

struct Game {

  State      state;
  Difficulty difficulty;
  uint8_t    score1;
  uint8_t    score2;

  void resetScores() {
    
    score1 = score2 = 0;
  
  }

};

Définissons ensuite une fonction menu() chargée d’afficher l’écran de sélection du niveau de difficulté :

void menu() {

       if (gb.buttons.pressed(BUTTON_UP))   game.difficulty = Difficulty::easy;
  else if (gb.buttons.pressed(BUTTON_DOWN)) game.difficulty = Difficulty::hard;
  else if (gb.buttons.pressed(BUTTON_A))    game.state      = State::play;

  gb.display.print(6, 10, "SELECT DIFFICULTY");
  gb.display.print(32, 30, "EASY");
  gb.display.print(32, 40, "HARD");
  gb.display.fillRect(20, 31 + 10*(uint8_t)game.difficulty, 4, 4);

}

Ici, on préfèrera utiliser les boutons BUTTON_UP et BUTTON_DOWN pour sélectionner le niveau de difficulté, et le bouton BUTTON_A pour enregistrer le choix du joueur. Ça me paraît plus logique en terme d’ergonomie…

Puis faisons en sorte qu’une fois passé l’écran d’accueil (géré par la fonction start()), on aboutisse sur l’écran de sélection du niveau de difficulté :

void start() {

  // C'est ici qu'on modifie la règle de changement d'état :
  if (gb.buttons.pressed(BUTTON_A)) game.state = State::menu;
    
  gb.display.print(17, 10, "PONG 1 PLAYER");

  if (gb.frameCount % 25 < 13) {

    gb.display.print(17, 30, "APPUYER SUR A");
    gb.display.print(17, 40, "POUR DEMARRER");

  }

  game.resetScores();

}

Il ne reste plus qu’à faire en sorte que le contrôleur principal prenne en charge l’état State::menu pour transférer le contrôle à la fonction menu() :

void loop() {

  gb.waitForUpdate();
  gb.display.clear();

  switch (game.state) {

    case State::start:
      start();
      break;

    case State::menu:
      menu();
      break;

    case State::play:
      play();
      break;

    case State::game_over:
      gameOver();

  }
  
}

Et le tour est joué :wink:

Par ailleurs, tu peux aller examiner le code de la bilbiothèque Gamebuino-Meta pour déchiffrer tout ce qui découle de l’appel à la fonction gb.waitForUpdate().

Tu noteras en particulier l’appel à la fonction updateDisplay() (ligne 391) qui effectue le transfert du tampon graphique de gb.display vers l’écran.

2 Likes

C’est très clair merci et ça fonctionne bien.

Cela m’amène 3 questions :

  • Que se passe t’il si je souhaite avoir 3 options dans le menu de difficulté (EASY, MEDIUM, HARD) ? Car de ce que je comprends du code, je n’ai que 2 options possibles (BUTTON_UP et BUTTON_DOWN)
  • Quelle est la signification de (uint8_t) dans le code permettant d’afficher le carré en face du choix ?
  • Comment modifier une élément graphique tous les X frames à partir de la fonction gb.frameCount ? Par exemple, tous les 4 ou 6 frames (sur 24) ?

D’avance merci pour ton aide.