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

Salut @Cric,

Pour déclencher une action à intervalles réguliers, par exemple toutes les 4 frames, tu peux utiliser gb.frameCount de la manière suivante :

if (gb.frameCount % 4 == 0) {

  // et là tu fais ce que t'as à faire...

}

Chaque appel à la fonction loop() implique une incrémentation d’une unité sur la propriété gb.frameCount. Par conséquent, l’expression :

gb.frameCount % 4

génèrera indéfiniment, la succession des valeurs suivantes :

0, 1, 2, 3  // puis ça recommence à 0, etc.

Du coup, il suffit d’intercepter l’une de ces valeurs pour déclencher une action à intervalles réguliers. Par exemple, lorsqu’on atteint la valeur 0 :

gb.frameCount % 4 == 0

(l’opérateur % est prioritaire sur ==)

Maintenant, pour en revenir à ce que tu souhaites faire, c’est-à-dire la possibilité de rétrécir ou élargir la raquette, j’t’ai pondu un bout de code qui fait le boulot. Teste-le sur ta Meta :

  • BUTTON_UP pour déplacer la raquette vers le haut
  • BUTTON_DOWN pour déplacer la raquette vers le bas
  • BUTTON_A pour rétrécir la raquette
  • BUTTON_B pour élargir la raquette

J’en ai profité pour t’orienter davantage sur la programmation orientée objet et te faire découvrir une autre façon de programmer, un peu plus structurée que du code monolithique pondu au kilomètre… :joy:

On définit un modèle nommé Paddle qui décrit :

  • sa structure (de quoi il est fait) => ce sont ses attributs (qui sont des variables internes),
  • son comportement (de quelle manière il peut agir sur lui même ou sur le reste du “monde”) => ce sont ses méthodes (qui sont des fonctions)

Le modèle peut être vu comme un plan de construction, à partir duquel on va pourvoir construire des objets, qui respectent tous ce plan de construction. Mais chaque propriété pourra s’exprimer différemment chez un objet ou un autre. Par exemple, le modèle Paddle précise que chaque instance dispose des attributs x et y :

struct Paddle {

  uint8_t x;
  uint8_t y;

};

Mais si on construit deux instances de ce modèle :

Paddle player_1;
Paddle player_2;

player_1.x = 4;
player_1.y = 32;

player_2.x = 73;
player_2.y = 32;

Tu vois qu’on peut attribuer des valeurs différentes aux propriétés x et y de chaque instance.

On peut d’ailleurs faire en sorte de paramétrer la construction d’une instance en définissant un constructeur qui accepte des arguments. Un constructeur est une méthode un peu particulière qui porte nécessairement le nom du modèle auquel il est rattaché. Par exemple, on pourrait définir le constructeur suivant :

struct Paddle {

  uint8_t x;
  uint8_t y;

  Paddle(uint8_t a, uint8_t b) {

    x = a;
    y = b;

  }

};

Et construire deux instances ainsi :

Paddle player_1(4, 32);
Paddle player_2(73, 32);

Ceci permet d’affecter des valeurs initiales aux propriétés x et y de chaque instance du modèle Paddle, au moment de leur création.

On pourrait également écrire le constructeur de cette manière :

struct Paddle {

  uint8_t x;
  uint8_t y;

  Paddle(uint8_t x, uint8_t y) {

    this->x = x;
    this->y = y;

  }

};

Dans ce cas, les arguments portent les mêmes noms que les attributs. Pour pouvoir les différencier dans le corps du constructeur, C++ fournit un opérateur d’autoréférence nommé this qui est un pointeur sur l’instance elle-même. Par conséquent quand on écrit this->x on fait référence à l’attribut x de l’objet.

Lorsque le constructeur accepte des arguments nommés x et y, ces arguments sont déclarés comme des variables locales au constructeur, mais viennent masquer les attributs x et y définis au niveau du modèle… Ça pose problème, car si on écrivait :

struct Paddle {

  uint8_t x;
  uint8_t y;

  Paddle(uint8_t x, uint8_t y) {

    x = x;
    y = y;

  }

};

Il y aurait une indétermination lié au conflit de nommage… de quel x on parle (à gauche comme à droite) quand on écrit :

x = x;  // ???...

En réalité, ici, on ne ferait que réaffecter à l’argument x sa propre valeur… ce qui ne servirait évidemment à rien…

L’opérateur this permet de lever l’ambigüité :

this->x = x;
  • à gauche on fait référence à l’attribut de l’objet,
  • à droite on fait référence à l’argument du constructeur.

C++ nous permet également de définir un constructeur avec une liste d’initialisation des attributs avant que le corps du constructeur soit exécuté. Par exemple, on pourrait réécrire notre constructeur de la façon suivante :

struct Paddle {

  uint8_t x;
  uint8_t y;

  Paddle(uint8_t x, uint8_t y) : x(x), y(y) {

    // et on n'a rien à faire de plus ici...

  }

};

C’est une manière plus concise d’écrire exactement la même chose que précédemment. Ici il n’y pas d’ambigüité, ce qui se trouve entre parenthèses fait référence aux arguments du constructeur. Alors que ce qui précède la parenthèse ouvrante fait référence à l’attribut de l’objet.

Voilà, ceci devrait te permettre de mieux comprendre l’écriture du constructeur que j’ai défini dans mon code :

Paddle(uint8_t x, uint8_t y) : x(x), y(y), h(H), th(h), sizing(false) {}

Tu noteras également que j’ai défini quelques constantes à l’intérieur du modèle Paddle :

struct Paddle {

  static const uint8_t W        = 3;
  static const uint8_t H        = 16;
  static const uint8_t H_SHRUNK = 8;
  static const uint8_t VY       = 2;

};

Ces constantes sont définies dans l’espace de nommage du modèle Paddle et sont accessibles par toutes les instances. Mais aucune copie de ces constantes ne sont reportées au sein de chaque instance. Elles sont définies de manière unique et sont partagées par toutes les instances. C’est ce que précise le mot-clef static.

Bon, je ne vais pas te pondre ici un cours complet sur la programmation orientée objet. Il y aurait beaucoup trop à dire, surtout avec les subtilités du C++, et tu serais très vite rassasié… Mais je trouvais intéressant de t’amener à t’y intéresser :wink:

Si c’est imbitable pour toi, n’hésite pas à me le dire, et je reviendrai à un style de programmation plus procédural.

Concernant le rétrécissement ou l’élargissement de la raquette, c’est dans la méthode update() du modèle Paddle que ça se passe. J’ai implémenté deux approches pour donner deux effets visuels différents :

  • une première approche où la variation de la largeur de la raquette suit une progression arithmétique (donc linéaire),
  • et une seconde approche où la variation de la largeur de la raquette suit une progression géométrique (donc exponentielle).

Une macro en début de code permet de déterminer quelle progression tu veux appliquer :

/**
 * 0 for an arithmetical progression
 * 1 for a geometrical progression
 */
#define GEOMETRIC 0

Je te laisse tester les 2, tu appliqueras celle que tu préfères…

Progression arithmétique de raison 2 :

demo-arithmetical

Progression géométrique de raison 0.5 :

demo-geometrical

On pourrait utiliser gb.frameCount pour espacer les variations par un nombre constant de frames (comme expliqué au début de ce post), mais l’effet obtenu est saccadé… même avec gb.frameCount % 2 == 0 qui n’opère qu’une frame sur deux. Donc je n’ai pas utilisé cette possibilité qui me semble insatisfaisante.

Je suppose que tu n’auras pas trop de mal à comprendre comment tout s’articule. Mais si une zone d’ombre subsiste, n’hésite pas à revenir poser de nouvelles questions :wink:

1 Like