Intelligence artificielle

Créations

Aurélien Rodot

il y a 5 ans

Vous avez fini votre premier jeu, améliorons-le avec de l'intelligence artificielle.

Notre première IA

Durée 30 minutes (et plus si affinité)

Niveau débutant

Prérequis

Dans l'atelier précédent, Pong, vous avez créé un jeu à deux joueurs fonctionnel. On utilisait les flèches pour bouger la raquette de gauche, et les boutons A et B pour la raquette de droite. Avec ce qu'on a vu dans l'atelier compteur d'invités, vous avez aussi implémenté un système de points. Enfin, nous avons utilisé des if avec plusieurs conditions pour faire rebondir la balle. Pong à deux c'est sympa, mais comment faire si on veut jouer tout seul? Figurez-vous qu'une petite intelligence artificielle vous permettra de jouer contre votre Gamebuino ;)

Mais, qu'est-ce qu'une intelligence artificielle au juste? L'intelligence artificielle, aussi appelée IA consiste à donner le pouvoir à l'ordinateur de prendre des décisions par lui-même. C'est le rendre vivant!

Une IA peut être très puissante. C'est un domaine très populaire aujourd'hui, et il peut prendre des formes assez complexe. Il y a même des diplômes universitaires spécialisés dans l'IA. Dans cet atelier, nous allons revenir aux bases de l'IA, aux origines. On va créer une intelligence pour une des raquettes de notre jeu Pong.

La manière la plus simple de programmer une IA est d'utiliser des déclarations conditionnelles if plus poussées. Avec de l'aléatoire, nous rendrons l'ordinateur moins prévisible et plus amusant dans la partie 2.

Notre premier ennemi virtuel

Commençons par examiner comment l'ordinateur pourra atteindre ses objectifs. L'ordinateur contrôlera la raquette de droite, appelée raquette2 dans le pong de l'atelier précédent. L'objectif de cette IA est de taper la balle quand elle arrive de son côté. Autrement dit, il faut que l'ordinateur puisse suivre la balle avec la raquette.

La logique derrière les coulisses

L'algorithme devrait donc avoir cette forme:

Si la balle est au-dessus du centre de la raquette
    Alors, déplacer la raquette vers le haut
Sinon, si la raquette est en dessous du centre de la raquette
    Alors, déplacer la raquette vers le bas

Avec cet algorithme, on veut toujours faire une de deux choses: aller vers le haut, ou aller vers le bas. De manière généralle, on veut centrer la raquette sur la balle. Pour faire ça, on regarde si la balle est au-dessus ou en dessous du centre de la raquette. Vous avez vu que la variable raquette2_posY contient la position du haut de la raquette. Si on veut le centre, il faut donc ajouter la moitié de la hauteur de la raquette. On a donc raquette2_centreY = raquette2_posY + raquette_hauteur / 2.

L'algorithme introduit aussi quelque chose de nouveau pour nous: le sinon. Il arrive que vous vouliez exprimer l'idée de "Si A est vrai, alors on fait une chose, sinon on fait une autre chose." En C/C++, ça se traduit par:

if (A) {
  // Faire une chose
}
else {
  // Faire une autre chose
}

Ceux d'entre vous qui ne parlent pas anglais auront peut-être deviné que "else" signifie "sinon" en français ;) Une autre particularité très pratique est le else if. Quand notre sinon est directement suivi par un if, alors on peut écrire else if () {...} à la place de else { if () {...} }. C'est plus lisible et surtout on peut placer autant de else if à la suite que l'on souhaite (alors qu'on peut ne mettre qu'un seul else). Par exemple, ceci est permis:

if (A) {
  // Faire une chose
}
else if (B) {
  // Faire une autre chose
}
else if (C) {
  // Faite encore autre chose
}
else {  // Si A, B, et C sont faux
  // Faire autre chose
} 

Pour en revenir à notre algorithme, on a donc un if et un else if. En appliquant ce qu'on vient de voir, il suffit donc de trouver le code correspondant à l'algorithme. Maintenant c'est...

Notre IA en code

Vous avez tous les outils à votre disposition pour pouvoir programmer votre première intelligence artificielle. Commencez par relire l'algorithme, puis modifiez le code du dernier atelier (Pong - deux joueurs) à l'aide de ce qu'on vient de voir. Si vous avez une idée mais que vous n'êtes pas sûr, essayez-la quand même! Vous avez le code, vous avez la console, quelle meilleure façon de savoir si ça marche que de l'essayer?

Petit coup de main: pour trouver le centre de la raquette, relisez le début du tutoriel.

J'ai mis la solution juste en dessous, donc si vous n'avez pas cherché par vous-même, c'est votre dernière chance :) Encore une fois, essayer par soi-même est une des meilleurs techniques pour apprendre rapidement.

Et voilà, c'est fait! Vous pouvez jouer contre votre Gamebuino à Pong! Mais... en jouant vous avez peut-être remarqué que la raquette ennemie suit le mouvement exact de la balle et fait peu de fautes. Nous allons corriger ça en améliorant notre IA.

Exemple de Solution

Ok, donc regardons si vous avez quelque chose de similaire:



void loop() {
  //// MAJ de la raquette1 (raquette du joueur)

  // MAJ de la raquette2 (raquette de l'ordinateur)
  if (balle_posY > raquette2_posY + raquette_hauteur / 2) {  // Si la balle est plus basse que le centre
    raquette2_posY = raquette2_posY + 1;                     // Se déplacer vers le bas
  } 
  else if (balle_posY < raquette2_posY + raquette_hauteur / 2) {  // Si la balle est plus haute que le centre
    raquette2_posY = raquette2_posY - 1;                          // Se déplacer vers le haut
  }

  //// MAJ de la balle et collisions
  //// Afficher la balle, les raquettes et le score 
}

Une IA moins prévisible

Vous avez conçu une intelligence artificielle. L'ordinateur est capable de suivre la balle pour ne pas perdre. Il joue contre vous mais son comportement est très prévisible. 'Si la balle est en dessous de la raquette, je descends. Sinon je monte'. Ce comportement n'est pas très naturel. Ça serait mieux si notre adversaire se déplaçait plus chaotiquement et ratait la balle de temps en temps.

Random()

Mais comment concevoir un comportement imprévisible? Et bien il existe une fonction qui nous permet de générer un nombre aléatoire:

random(int min, int max)

random() est une fonction qui retourne un nombre entier aléatoire. Le nombre renvoyé sera entre min et max - 1. C'est-à-dire que si a = random(0, 4);, alors a peut valoir 0, 1, 2, ou 3 (mais pas 4!). Avant d'attaquer notre IA avancée, utilisons random() pour améliorer notre jeu.

Quand vous avez créé votre jeu Pong, la balle est replacée sur l'écran à chaque fois qu'un joueur marque un point. Voici le code qui vous permettait de le faire:

// Vérifier si la balle est sortie de l'écran
if (balle_posX < 0) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = 20;
  balle_speedX = 1;
  balle_speedY = 1;
  // incrémenter le score du joueur 2
  score2 = score2 + 1;
}
if (balle_posX > gb.display.width()) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = 20;
  balle_speedX = 1;
  balle_speedY = 1;
  // incrémenter le score du joueur 1
  score1 = score1 + 1;
}

En faisant comme ça, toutes les parties commencent de la même manière: la balle apparait en position (20, 20) et part en bas à droite. C'est un peu répétitif non? random() peut nous sauver!

// Vérifier si la balle est sortie de l'écran
if (balle_posX < 0) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
  balle_speedX = 1;

  if (random(0, 2) == 1) {  // 50% du temps
    balle_speedY = 1;
  } 
  else {  // 50% du temps
    balle_speedY = -1;
  }

  // incrémenter le score du joueur 2
  score2 = score2 + 1;
}

if (balle_posX > gb.display.width()) {
  // Replacer la balle sur l'écran
  balle_posX = 20;
  balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
  balle_speedX = 1;

  if (random(0, 2) == 1) {  // 50% du temps
    balle_speedY = 1;
  } 
  else {  // 50% du temps
    balle_speedY = -1;
  }

  // incrémenter le score du joueur 1
  score1 = score1 + 1;
}

Maintenant, la balle réapparait en (20, Y), avec Y un nombre aléatoire. Aussi, le if (random(0, 2) == 1) est vrai une fois sur deux car random(0, 2) retourne soit 0, soit 1. Donc la balle part dans une direction aléatoire: en haut ou en bas. Ce sont des petites modifications comme ça qui différencient les jeux moyens des bons jeux ;)

Remarque: En C/C++, si on veut tester une égalité dans une condition, il faut utiliser un double égal ==. Donc if (a == b) est vrai quand a vaut b. C'est TRÈS important car si vous mettez un seul signe égal, votre jeu fera des choses inattendues.


Moins prévisible == plus amusant

Améliorons notre IA avec random()! Pour que l'ordinateur semble plus "humain", il lui faut des réflexes plus longs. Pour l'instant, la raquette est toujours en face de la balle, mais avec random, on peut faire en sorte que la raquette ait plus de mal à suivre. Pour compenser un peu ses erreurs, on va aussi augmenter sa vitesse de déplacement. Avec tout ça on aura un ennemi bien plus dynamique et amusant.

Tout d'abord, pour que la raquette ait un mouvement fluide, il faut créer une variable de vitesse comme on a fait pour la balle: raquette2_speedY. Regardons son utilité:

int raquette2_speedY = 0;  // Vitesse verticale de la raquette2

void loop() {
  //// MAJ de la raquette1 (raquette du joueur)

  // MAJ de la raquette2 (raquette de l'ordinateur)
  if (balle_posY > raquette2_posY + raquette_hauteur / 2 && random(0, 3) == 1) {
    raquette2_speedY = 2;  // Vers le bas
  } else if (balle_posY < raquette2_posY + raquette_hauteur / 2 && random(0, 3) == 1) {
    raquette2_speedY = -2;  // Vers le haut
  }

  raquette2_posY = raquette2_posY + raquette2_speedY;  // Mettre à jour la position de la raquette2

  //// MAJ de la balle et collisions
  //// Afficher la balle, les raquettes et le score 
}

Ici, nous avons ajouté la condition random(0, 3) == 1. Donc le premier if est vrai une fois sur 3 quand la balle est plus basse que la raquette (de même quand la balle est au-dessus de la raquette). Pour mieux comprendre, imaginez le scénario suivant: la balle est sous la raquette, la raquette se dirige vers le bas (raquette2_speedY = 2). Quand la raquette dépasse la balle, sa direction devrait changer, mais avec le random() peut-être que la raquette continuera de descendre. Avec un peu de chance, elle ira trop loin et vous avez gagné!



Dans le code que je vous donne, on a random(0, A) == 1 avec A = 3. Logiquement, plus A est grand plus la condition devient improbable. Donc la valeur de A détermine la vitesse de réaction de notre IA car il lui faudra plus d'essais avant de changer de direction. On peut exploiter ce fait pour faire varier la difficulté, mais pour ça, je vous laisse faire.

A vous de jouer!

Dans cet atelier, nous avons créé notre IA puis nous l'avons améliorée avec random(). Comme exercice de fin d'atelier, je vous propose d'implémenter une nouvelle fonctionnalité: changer de difficulté. En appuyant sur le bouton MENU, le joueur peut changer entre "facile" et "difficile".

Astuce: Pour faire varier la difficulté, relisez la fin de cet atelier (juste avant le "Encore à vous de jouer").

A vous de continuer de perfectionner votre IA, ou de vous en inspirer pour faire une IA dans un autre jeu ;)

Montrez votre talent sur les réseaux sociaux avec #gamebuino #Pong #IA , on vous suit de près ;)

Exemple de solution

Si vous êtes en panne d'inspiration, voilà ce qu'on a fait de notre côté :)

#include <Gamebuino-Meta.h>

 




// Caractéristiques de la balle
int balle_posX = 20;
int balle_posY = 20;
int balle_speedX = 1;
int balle_speedY = 1;
int balle_taille = 3;




// Caractéristiques des raquettes
int raquette1_posX = 10;
int raquette1_posY = 30;
int raquette2_posX = gb.display.width() - 13;
int raquette2_posY = 30;
int raquette_hauteur = 10;
int raquette_largeur = 3;




// Pour l'IA
int raquette2_speedY = 0;  // Vitesse verticale de la raquette2




// Scores
int score1;  // Score du joueur 1
int score2;  // Score du joueur 2




int difficulte = 3;  // Niveau de difficulté. 3 = FACILE et 2 = DIFFICILE




void setup() {
  gb.begin();
}




void loop() {
  while (!gb.update());
  gb.display.clear();




  // Changement de difficulté
  if (gb.buttons.pressed(BUTTON_MENU)) {
    if (difficulte == 3) { // Facile
      difficulte = 2;  // Changer de difficulté
    }
    else {  // Difficile
      difficulte = 3;  // Changer de difficulté
    }
  }




  // MAJ raquette1
  if (gb.buttons.repeat(BUTTON_UP, 0)) {
    raquette1_posY = raquette1_posY - 1;
  }
  if (gb.buttons.repeat(BUTTON_DOWN, 0)) {
    raquette1_posY = raquette1_posY + 1;
  }




  // MAJ Raquette2 - Intelligence Artificielle
  if (balle_posY > raquette2_posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquette2_speedY = 2;
  } else if (balle_posY < raquette2_posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquette2_speedY = -2;
  }
  raquette2_posY = raquette2_posY + raquette2_speedY;  // Mettre à jour la position de la raquette2




  // MAJ balle
  balle_posX = balle_posX + balle_speedX;
  balle_posY = balle_posY + balle_speedY;




  // Collisions avec les murs (haut et bas)
  if (balle_posY < 0) {
    balle_speedY = 1;
  }
  if (balle_posY > gb.display.height() - balle_taille) {
    balle_speedY = -1;
  }




  // Collision balle/raquette1
  if ( (balle_posX == raquette1_posX + raquette_largeur)
       && (balle_posY + balle_taille >= raquette1_posY)
       && (balle_posY <= raquette1_posY + raquette_hauteur) ) {
    balle_speedX = 1;
  }
  // Collision balle/raquette2
  if ( (balle_posX + balle_taille == raquette2_posX)
       && (balle_posY + balle_taille >= raquette2_posY)
       && (balle_posY <= raquette2_posY + raquette_hauteur) ) {
    balle_speedX = -1;
  }




  // Vérifier si la balle est sortie de l'écran
  if (balle_posX < 0) {
    // Replacer la balle sur l'écran
    balle_posX = 20;
    balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
    balle_speedX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle_speedY = 1;
    } 
    else {  // 50% du temps
      balle_speedY = -1;
    }




    // incrémenter le score du joueur 2
    score2 = score2 + 1;
  }
  if (balle_posX > gb.display.width()) {
    // Replacer la balle sur l'écran
    balle_posX = 20;
    balle_posY = random(20, gb.display.height() - 20);  // Position aléatoire au centre de l'écran
    balle_speedX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle_speedY = 1;
    } 
    else {  // 50% du temps
      balle_speedY = -1;
    }




    // incrémenter le score du joueur 1
    score1 = score1 + 1;
  }




  // Afficher la balle
  gb.display.fillRect(balle_posX, balle_posY, balle_taille, balle_taille);
  // Afficher la raquette1
  gb.display.fillRect(raquette1_posX, raquette1_posY, raquette_largeur, raquette_hauteur);
  // Afficher la raquette2
  gb.display.fillRect(raquette2_posX, raquette2_posY, raquette_largeur, raquette_hauteur);




  // Afficher les scores
  gb.display.setCursor(35, 5);
  gb.display.print(score1);
  gb.display.setCursor(42, 5);
  gb.display.print(score2);




  // Afficher la difficulté
  gb.display.setCursor(33, gb.display.height() - 5);
  if (difficulte == 3) {
    gb.display.print("EASY");
  }
  else {
    gb.display.print("HARD");
  }
}



Bien joué! Vous avez finis l'atelier sur les ennemis commandés par ordinateur simples :D

Par Julien Giovinazzo

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

Voir la création

jicehel

NEW il y a 5 ans

Bon tuto  ;) Quelques petites remarques secondaire: 

Dans l'intro:  aujourd'hui, et il est peut prendre des formes assez complexe.

=> aujourd'hui, et il peut prendre des formes assez complexe.

Ça serais mieux si... => Ça serait mieux si

compencer => compenser.

L'image de la Solution du "A vous de jouer" ne s'affiche pas.

un seul signe égale => un seul signe égal

 IA car il li faudra plus d'essais =>  IA car il lui faudra plus d'essais


Bon j'ai fait une petite relecture rapide car j'avais remarqué quelques petites fautes de typo. J'espère que ça aidera à avoir un tuto encore plus parfait mais sinon les explications sont super claires et c'est le plus important. 

JulienGio

il y a 5 ans

Merci :) tes remarques ont été prises en compte.

JulienGio

NEW il y a 5 ans

jicehel jicehel

Merci :) tes remarques ont été prises en compte.

Max

NEW il y a 5 ans

Heu, ça fait un bout de temps qu'il n'y a plus rien de neuf en atelier... Où sont les tutos de jeux de plateformes, de shoot'em UP, des jeux vue de dessus, etc. ?

J'espère qu'en 2018 le concept d'apprendre ne se limite pas à des tutos juste pour faire un Pong, rassurez moi !

Aurélien Rodot

il y a 5 ans

C'est pour bientôt ;)

jicehel

NEW il y a 5 ans

Tu as raison Max, ne les laisse pas s'endormir... Allez la team Aadalie: oubliez que les journée ne font que 24 heures et on se retrousse les manches, on dépoussière le clavier, on s'aère les neurones et on se remet à bosser... 

deeph

NEW il y a 5 ans

Si j'ai le temps (et la motivation), j'essaierais de faire un tuto sur comment faire un petit RPG type Picomon.

Peut-être durant mes congés en août :)

jicehel

NEW il y a 5 ans

oui oui deeph, on peut tous contribuer et apporter notre pierre à l’édifice ça ne peut qu'être un plus pour tout le monde, c'est certain.

Aurélien Rodot

NEW il y a 5 ans

Max Max

C'est pour bientôt ;)

jicehel

NEW il y a 5 ans

Aurélien, je n'ai pas testé, j'ai écrit ça au boulot (c'est mal) mais avant de tester ce soir et de mettre en forme éventuellement, je voudrais ça voir si ça t’intéresse et / ou si tu verrais ça autrement:


Structurer les objets de votre PROGRAMME

Vous avez ajouté votre premier adversaire en lui donnant de l'intelligence artificielle, le jeu est jouable alors commençons à nous structurer pour le prochain qui sera plus compliqué.


Durée :      30 minutes (et plus si affinité)

Niveau :      débutant (mais bon plus tant que ça déjà)

Prérequis :

  • Avoir une Gamebuino META
  • Avoir fait l'atelier Installation de la Gamebuino META
  • Avoir fait les ateliers hello, world, compteur d'invités, balle rebondissante, Pong (deux joueurs) et Artifical intelligence (Pong)


Dans l'atelier précédent, Artificial intelligence (Pong), vous avez ajouté un adversaire à votre jeu en lui programmant un comportement. Nous avions également rajouté une seconde raquette qui avait les mêmes propriétés que la première mais avec des valeurs différentes.

Avant de compliquer un peu le programme, nous allons voir une autre façon de coder ses propriétés d’un objet : les structures



Les structures

Vous pouvez créer vos propres types de variables. Des « types de variables personnalisés » permettant de de s’y retrouver plus facilement quand on cherche à faire des programmes plus complexes.

En effet dans les tutoriaux, on a bien pris soin d’utiliser des noms parlant comme balle_posX ou balle_posY, mais une autre solution consiste à utiliser un « objet » balle ayant des propriétés définies.



Définir une structure

Une structure est un assemblage de variables qui peuvent avoir différents types : long, char, int, double, …

Une définition de structure commence par le mot-clé struct, suivi du nom de votre structure (par exemple s_balle, ou s_raquette).

On peut également adopter des règles de nommage pour ces structures. Par exemple, on peut choisir de mettre la première lettre en majuscule pour pouvoir faire la différence ou lui mettre un préfixe (par exemple s_).

Après le nom de votre structure, vous ouvrez les accolades et les fermez plus loin, comme pour une fonction.

Attention, ici c'est particulier : vous DEVEZ mettre un point-virgule après l'accolade fermante.

Vous ajoutez ensuite les variables dont est composée votre structure.

Faisons un exemple complet :

Pour la balle, nous utilisions 5 variables :

// Caractéristiques de la balle
int balle_posX = 20;
int balle_posY = 20;
int balle_speedX = 1;
int balle_speedY = 1;
int balle_taille = 3;

Nous allons les transformer en structure, nous allons donc définir son nom en tant que s_balle et y mettre 5 variables de type entier : posX, posY, vitesseX, vitesseY et taille, ce qui nous donne le code suivant :

// Définition des structures balles et raquette
struct s_balle{int posX; int posY; int vitesseX; int vitesseY; int taille;};

Comme vous le voyez, la création d'un type de variable personnalisé n'est pas bien complexe. Toutes les structures que vous verrez sont en fait des « assemblages » de variables de types définis (long, int, double, etc…)

OK, on a défini une structure mais bon on n’a toujours pas de valeur et pour cause, on doit maintenant définir une variable de ce type que l’on pourra alors initialiser, mais voyons ça plus en détail…


Utiliser une structure

Maintenant que notre structure s_balle est définie, on va pouvoir l'utiliser en créant une variable de ce type :

// Définition des objets utilisant les structures définies
s_balle balle;

Nous avons ainsi créé une variable balle de type s_balle. Cette variable est automatiquement composée de cinq sous-variables : posX, posY, vitesseX, vitesseY et taille (respectivement son abscisse, son ordonnée, sa vitesse latérale, sa vitesse horizontale et sa taille en pixels).

Maintenant que notre variable balle est créée, nous voulons modifier ses coordonnées.
Comment accéder aux variable posX et posY de la variable balle?

Comme ceci :

// Caractéristiques de la balle
balle.posX = 20;
balle.posY = 20;

On a ainsi modifié balle, en lui donnant une abscisse de 20 et une ordonnée de 20.

Pour accéder donc à chaque composante de la structure, vous devez écrire :

variable.nomDeLaComposante

Le point sert de séparation entre la variable et la composante à laquelle on souhaite accéder.

Pour les structures comme pour les variables, l'initialisation peut également se faire un peu comme pour un tableau en enchainant entre accolades les valeurs des composantes, séparées par des virgules dans l’ordre de leur déclaration.

Pour la balle, cela nous donnerait donc :

// Définition des objets utilisant les structures définies
s_balle balle = {20, 20, 1, 1, 3};


Cela définira, dans l'ordre, de déclaration les composantes de l’objet, c’est-à-dire posX, posY, vitesseX, vitesseY et taille.



Synthèse avec le programme Pong

Essayez d’utiliser ce que l’on vient de voir pour déclarer 3 objets (la balle mais ça on l’a déjà fait dans l’exemple), la raquette de gauche, la raquette de droite pour l’ordinateur.



Solution

#include <Gamebuino-Meta.h>

// Définition des constantes
#define EspaceBordRaquette    10
#define HauteurRaquette       10
#define LargeurRaquette       3
#define MargeInitialeBalleX   20
#define MargeInitialeBalleY   20

// Définition des structures balles et raquette
struct s_balle{int posX; int posY; int vitesseX; int vitesseY; int taille;};
struct s_raquette{int posX; int posY; int largeur; int hauteur; int vitesse;};

// Définition des objets utilisant les structures définies
s_balle balle = {MargeInitialeBalleX,MargeInitialeBalleY,1,1,3};
s_raquette raquetteAGauche = {EspaceBordRaquette,((gb.display.height()-HauteurRaquette) / 2),LargeurRaquette,HauteurRaquette,0};
s_raquette raquetteADroite = {(gb.display.width() - EspaceBordRaquette - LargeurRaquette),((gb.display.height()-HauteurRaquette) / 2),LargeurRaquette,HauteurRaquette,0};

// Scores
int scoreGauche;  // Score du joueur 1
int scoreDroite;  // Score du joueur 2

int difficulte = 3;  // Niveau de difficulté. 3 = FACILE et 2 = DIFFICILE

void setup() {
  gb.begin();
}

void loop() {
  while (!gb.update());
  gb.display.clear();

  // Changement de difficulté
  if (gb.buttons.pressed(BUTTON_MENU)) {
    if (difficulte == 3) { // Facile
      difficulte = 2;  // Changer de difficulté
    }
    else {  // Difficile
      difficulte = 3;  // Changer de difficulté
    }
  }

  // MAJ raquetteAGauche
  if (gb.buttons.repeat(BUTTON_UP, 0)) {
    raquetteAGauche.posY = raquetteAGauche.posY - 1;
  }

  if (gb.buttons.repeat(BUTTON_DOWN, 0)) {
    raquetteAGauche.posY = raquetteAGauche.posY + 1;
  }

  // MAJ raquetteADroite - Intelligence Artificielle
  if (balle.posY > raquetteADroite.posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquetteADroite.vitesse = 2;
  } else if (balle.posY < raquetteADroite.posY + raquette_hauteur / 2 && random(0, difficulte) == 1) {
    raquetteADroite.vitesse = -2;
  }
  raquetteADroite.posY = raquetteADroite.posY + raquetteADroite.vitesse;  // Mettre à jour la position de la raquetteADroite

  // MAJ balle
  balle.posX = balle.posX + balle.vitesseX;
  balle.posY = balle.posY + balle.vitesseY;

  // Collisions avec les murs (haut et bas)
  if (balle.posY < 0) {
    balle.vitesseY = 1;
  }
  if (balle.posY > gb.display.height() - balle_taille) {
    balle.vitesseY = -1;
  }

  // Collision balle/raquetteAGauche
  if ( (balle.posX == raquetteAGauche.posX + raquette_largeur)
       && (balle.posY + balle_taille >= raquetteAGauche.posY)
       && (balle.posY <= raquetteAGauche.posY + raquette_hauteur) ) {
    balle.vitesseX = 1;
  }
  // Collision balle/raquetteADroite
  if ( (balle.posX + balle_taille == raquetteADroite.posX)
       && (balle.posY + balle_taille >= raquetteADroite.posY)
       && (balle.posY <= raquetteADroite.posY + raquette_hauteur) ) {
    balle.vitesseX = -1;
  }

  // Vérifier si la balle est sortie de l'écran
  if (balle.posX < 0) {
    // Replacer la balle sur l'écran
    balle.posX = MargeInitialeBalleX;
    balle.posY = random(MargeInitialeBalleY, gb.display.height() - MargeInitialeBalleY);  // Position aléatoire au centre de l'écran
    balle.vitesseX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle.vitesseY = 1;
    } 
    else {  // 50% du temps
      balle.vitesseY = -1;
    }

    // incrémenter le score du joueur 2
    scoreDroite = scoreDroite + 1;
  }
  if (balle.posX > gb.display.width()) {
    // Replacer la balle sur l'écran
    balle.posX = MargeInitialeBalleX;
    balle.posY = random(MargeInitialeBalleY, gb.display.height() - MargeInitialeBalleY);  // Position aléatoire au centre de l'écran
    balle.vitesseX = 1;
    if (random(0, 2) == 1) {  // 50% du temps
      balle.vitesseY = 1;
    } 
    else {  // 50% du temps
      balle.vitesseY = -1;
    }

    // incrémenter le score du joueur 1
    scoreGauche = scoreGauche + 1;
  }

  // Afficher la balle
  gb.display.fillRect(balle.posX, balle.posY, balle_taille, balle_taille);
  // Afficher la raquetteAGauche
  gb.display.fillRect(raquetteAGauche.posX, raquetteAGauche.posY, raquetteAGauche.largeur, raquetteAGauche.hauteur);
  // Afficher la raquetteADroite
  gb.display.fillRect(raquetteADroite.posX, raquetteADroite.posY, raquetteADroite.largeur, raquetteADroite.hauteur);

  // Afficher les scores
  gb.display.setCursor(35, 5);
  gb.display.print(scoreGauche);
  gb.display.setCursor(42, 5);
  gb.display.print(scoreDroite);

  // Afficher la difficulté
  gb.display.setCursor(33, gb.display.height() - 5);
  if (difficulte == 3) {
    gb.display.print("Facile");
  }
  else {
    gb.display.print("Difficile");
  }
}

Aurélien Rodot

il y a 5 ans

Hello !

On travaille sur la suite de notre côté, on va mettre ça en ligne très bientôt.

Mais ça ne t'empêche pas de faire des tutos, bien au contraire, tu devrais faire des Créations pour qu'il soient mieux mis en avant et que les personnes puissent réagir directement :D

jicehel

NEW il y a 5 ans

Bon en attendant la réponse, sinon il y aurait une suite pour changer un peu du Pong avec un Sokoban. Pour ne pas faire trop long, il serait bien sûr divisé en morceaux. Le premier sur la réflexion sur le jeu et sur les tableaux, la seconde partie serait sur les graphiques avec déplacements et la 3ème serait l'organisation du programme en onglet pour s'y retrouver plus facilement et la fin de la création du jeu.

Dis moi si ça t’intéresse et si je fais la suite. En attendant, voici le tuto sur la partie 1 de la création d'un Sokoban


Le jeu de Sokoban

Partie 1 : Préparons notre jeu


Durée: 30 minutes (à la louche)

Niveau: ancien débutant ayant suivi les premiers tutos

Prérequis

  • Avoir une Gamebuino META
  • Avoir fait l'atelier Installation de la Gamebuino META
  • Avoir suivi les tutoriels jusqu’au Pong pour avoir les bases


Réfléchissons sur ce que nous voulons faire

Le jeu de Sokoban est un jeu où l’on est le gardien d’un entrepôt où se trouve, dans un petit labyrinthe dans lequel se trouve des caisses que l’on doit ranger à certains emplacements.

C’est un jeu classique, on a donc plein d’exemple de ce jeu. Selon moi, ce genre de jeu avec de principes simples est idéal pour commencer à essayer d’atteindre un objectif, mais bien sûr on peut aussi commencer un jeu sans modèle si l’on est créatif.

Dans un premier temps renseignons nous sur ce que l’on aura à faire :

Contrôler un personnage qui pourra se déplacer dans le labyrinthe s’il n’y a ni mur ni caisse pour le bloquer.

Le joueur pourra pousser une caisse s’il n’y a rien derrière cette caisse

Le niveau est terminé quand toutes les caisses sont sur les emplacements

Bon on va déjà se concentrer sur ces 3 éléments et voir les autres points sur lesquels on peut avoir à réfléchir.

Par exemple :

Est-ce que l’on dessine les niveaux ou est-ce que l’on utilise des sprites ?
Pour cet exemple, on va utiliser des sprites pour intégrer la couleur plus facilement. Pour les couleurs, on utilisera les couleurs de la palette de la META pour répondre à la charte de qualité.

Observons un jeu de Sokoban déjà existant :

On voit que le jeu peut être représenté par une grille de positions. Chaque case de cette grille pouvant contenir quelque chose : Un mur, le personnage, une caisse ou un point d’arrivée. On fera attention toutefois, une caisse peut être sur un point d’arrivée (c’est même le but du jeu) mais si on la déplace de nouveau, on doit pouvoir retrouver le point d’arrivée.


Commençons a écrire notre jeu

Nous avons vu dans le tuto « Hello World » la structure d’un programme et le principe des 2 fonctions : setup() et loop()

#include <Gamebuino-Meta.h>

void setup() {
  gb.begin();
}

void loop() {
  while(!gb.update());
  gb.display.clear();

  // C'est ici que le plus gros du programme se déroule
}

Que doit faire la boucle loop ?

  • Surveiller l’appui sur les touches
  • Faire respecter la logique du jeu
  • Afficher les graphismes
  • Jouer les bruitages


OK, sinon, il faut que l’on définisse notre niveau.

Pour ça on va définir sa taille en nombre de lignes et en nombre de colonnes à l’aide de la directive #define

En regardant sur Wikipedia sur le sujet du Sokoban, on apprend que l’on peut échanger de niveaux via des fichiers au format .xsb. Il y a même le premier niveau de la première version du jeu que nous allons utiliser pour notre programme :

    #####
    #   #
    #$  #
  ###  $##
  #  $ $ #
### # ## #   ######
#   # ## #####  ..#
# $  $          ..#
##### ### #@##  ..#
    #     #########
    #######


Comptons les lignes et les colonnes de ce niveau : On a 11 lignes et 19 colonnes

On écrira donc 

#define NB_LIGNES_NIVEAUX 11
#define NB_COLONNES_NIVEAUX 19


Légende :
# : mur
$ : caisse
. : destination
* : caisse sur une zone de rangement (pas présente dans ce niveau)
@ : personnage
+ : personnage sur une zone de rangement (pas présent dans ce niveau)


Tiens, on découvre donc sur Wikipédia comment ils ont résolu le problème du stockage de l’information quand on pousse une caisse sur une destination : ils remplacent le caractère tout simplement et si on retire la caisse, on remettra le « . » dans la case de la destination et « $ » sur la case contenant la caisse si ce n’est pas une zone de rangement (auquel cas il faudra mettre un « * ».

On remarque aussi le « + » et oui on avait oubli un cas : le personnage peut lui aussi se trouver sur une case de destination ponctuellement. Ils ont donc également ajouté le caractère « + » qui reprend les codes « @ » et « . »

Bien, bien, on avance. C’est une étape importante avant de coder. Bien penser à ce que l’on veut faire et lister les étapes et réfléchir à comment on va le faire.

Pour que notre niveau soit compatible avec les xsb, nous allons stocker les caractères dans un tableau de caractères que l’on va appeler « niveau » vu notre imagination débordante

On appelle tableau une variable composée de données de même type, stockée de manière contiguë en mémoire (les unes à la suite des autres).


donnée

donnée

donnée

...

donnée

donnée

donnée



Lorsque le tableau est composé de données de type simple, on parle de tableau monodimensionnel (ou vecteur). Sa syntaxe est la suivante :

type Nom_du_tableau [Nombre d'éléments]

type définit le type d'élément contenu dans le tableau définissant donc la taille d'une case du tableau en mémoire

Nom_du_tableau est le nom que l'on décide de donner au tableau, le nom du tableau suit les mêmes règles qu'un nom de variable

Nombre d'éléments est un nombre entier qui détermine le nombre de cases que le tableau doit comporter

Voici par exemple la définition d'un tableau de 8 éléments de type char :

char Tableau [8];

On peut également initialiser un tableau lors de sa définition ainsi:

type Nom_du_tableau [Taille1][Taille2]...[TailleN] = {a1, a2, ... aN};

Pour le tableau de 8 caractères, on pourrait écrire par exemple

char Tableau [8] = {'A',’B ', 'C', 'D', 'E', 'F', 'G', 'H'};


Lorsqu’un tableau contient lui-même d'autres tableaux on parle alors de tableaux multidimensionnels (aussi matrice ou table). Il se définit de la manière suivante :

type Nom_du_tableau [a1][a2][a3] ... [aN]

Chaque élément entre crochets désigne le nombre d'éléments dans chaque dimension

Le nombre de dimensions n'est pas limité

Un tableau d'entiers positifs à deux dimensions (3 lignes, 4 colonnes) se définira avec la syntaxe suivante :

int Tableau [3][4];

On peut représenter un tel tableau de la manière suivante :


Tableau[0][0]

Tableau[0][1]

Tableau[0][2]

Tableau[0][3]

Tableau[1][0]

Tableau[1][1]

Tableau[1][2]

Tableau[1][3]

Tableau[2][0]

Tableau[2][1]

Tableau[2][2]

Tableau[2][3]




Voilà, on a la solution pour notre tableau pour notre niveau. Nous voulons un tableau qui va contenir un caractère correspondant au contenu de chaque case de notre niveau. Nous allons donc créer une matrice de caractère (type char). Nous l’appellerons niveau pour lui donner un nom parlant et ses dimensions seront NB_LIGNES_NIVEAUX x NB_COLONNES_NIVEAUX afin de pouvoir accéder à chacune des cases de notre niveau. On le déclarera donc ainsi :

char niveau[NB_LIGNES_NIVEAUX][NB_COLONNES_NIVEAUX] = 
{
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', ' ', ' ', '#', '$', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', '#', '#', ' ', ' ', '$', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ ' ', ' ', '#', ' ', ' ', '$', ' ', '$', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', },
{ '#', '#', '#', ' ', '#', ' ', '#', '#', ' ', '#', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', },
{ '#', ' ', ' ', ' ', '#', ' ', '#', '#', ' ', '#', '#', '#', '#', '#', ' ', ' ', '.', '.', '#', },
{ '#', ' ', '$', ' ', ' ', '$', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '.', '.', '#', },
{ '#', '#', '#', '#', '#', ' ', '#', '#', '#', ' ', '#', '@', '#', '#', ' ', ' ', '.', '.', '#', },
{ ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', },
{ ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }
};


La première accolade indique que l’on initialise le tableau et chaque ligne en dessous correspond à un tableau avec le contenu de cette ligne dans notre tableau. On ferme ensuite notre tableau de lignes.

Attention n’oubliez pas que les tableaux commencent à l’indice 0, donc la première ligne à le numéro 0 et sa 3ème case à le numéro 2. Pur charger la variable Résultat avec le contenu de la 3ème colonne de la ligne 1, je devrais donc écrire :

Résultat = niveau[0][2] ;


A vous de jouer

Bon assez parlé, à vous de travailler. Faites un petit programme qui charge le tableau et affiche le contenu au format texte.

Attention, limitez le nombre de ligne affichée à 10 pour que ça tienne à l'écran, sinon ce ne sera pas joli...



SOLUTION

#include <Gamebuino-Meta.h>



#define NB_LIGNES_NIVEAUX 11

#define NB_COLONNES_NIVEAUX 19

int NB_Lignes;

char niveau[NB_LIGNES_NIVEAUX][NB_COLONNES_NIVEAUX] = { { ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, { ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, { ' ', ' ', ' ', ' ', '#', '$', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, { ' ', ' ', '#', '#', '#', ' ', ' ', '$', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, { ' ', ' ', '#', ' ', ' ', '$', ' ', '$', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, { '#', '#', '#', ' ', '#', ' ', '#', '#', ' ', '#', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', }, { '#', ' ', ' ', ' ', '#', ' ', '#', '#', ' ', '#', '#', '#', '#', '#', ' ', ' ', '.', '.', '#', }, { '#', ' ', '$', ' ', ' ', '$', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '.', '.', '#', }, { '#', '#', '#', '#', '#', ' ', '#', '#', '#', ' ', '#', '@', '#', '#', ' ', ' ', '.', '.', '#', }, { ' ', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', '#', '#', }, { ' ', ' ', ' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', } };

void setup() { gb.begin(); }

void loop() { while(!gb.update()); gb.display.clear();

// Astuce: avec print sans rien modifier, par défaut, on ne peut afficher que 10 lignes, pour l'affichage, on va donc brider le nombre de lignes à 10 // On a de la chance on a moins de 20 colonnes. On n'a donc pas besoin de brider le nombre de colonnes à afficher dans notre programme de test NB_Lignes = NB_LIGNES_NIVEAUX ; if(NB_Lignes > 10) NB_Lignes = 10;

// On affiche le contenu de chacune des cases for (int ligne=0;ligne<NB_Lignes;ligne++) { for (int colonne=0;colonne<NB_COLONNES_NIVEAUX;colonne++) { gb.display.printf("%c",niveau[ligne][colonne]); } gb.display.println(""); } }


Aurélien Rodot

NEW il y a 5 ans

jicehel jicehel

Hello !

On travaille sur la suite de notre côté, on va mettre ça en ligne très bientôt.

Mais ça ne t'empêche pas de faire des tutos, bien au contraire, tu devrais faire des Créations pour qu'il soient mieux mis en avant et que les personnes puissent réagir directement :D

jicehel

NEW il y a 5 ans

OK, je vais faire 2 créations pour ces tutos  ;)

Max_NN

NEW il y a 5 ans

Bonjour tout le monde ! J'ai un problème, je veux faire un programme qui dessine un cercle avec une taille aléatoire, mais pour le moment la taille est toujours la même. Voici mon programme. Merci !

#include <Gamebuino-Meta.h>

int ballSize = random(1, 15);

void setup() {
  gb.begin();
}

void loop() {
  while (!gb.update());
  gb.display.clear();

 
  gb.display.print(ballSize);
  gb.display.setColor(WHITE);
  gb.display.drawCircle(39, 31, ballSize);
}