Artificial Intelligence

Creations

Aurélien Rodot

8 months ago

So you just finished your first game, let's improve on it with Artificial Intelligence.

Our first AI

Length 30 minutes

Level Beginner

Prerequisites

In the previous workshop, Pong, you built a functional two-player game. We used the arrows to move the left paddle, and the A and B buttons to move the right paddle. With what we saw in the tally counter workshop, you also implemented a way to track and display the scores. Finally, you used if statements with multiple conditions to make the ball bounce. Well, a 2-player tennis game is nice, but what if we wanted to play alone? Turns out, a small Artificial Intelligence can go a long way ;)

But, what is an artificial intelligence? Artificial Intelligence, also called AI, boils down to giving the computer the power to decide things by itself. It is just like bringing the computer to life!

An AI can be very powerful. It has been hot topic for quite some time now, and it can come in many shapes and forms, including some very complex ones. There are even university degrees specialized in AI. But for this workshop, we will go back to the roots of AI. Our AI will control one of the paddles in our Pong game.

First we will see the simplest way to program an AI, by using if statements. Then, in a second part, with the help of randomness, we will make the computer less predictable and way more fun.

Our first computer controlled enemy

Let's start by taking a look at how the computer might play. The computer will control the right paddle (called paddle2 in the previous workshop). Our AI's goal is to prevent the ball to reach the right side of the screen. In other words, the computer must follow the ball with its paddle.

The logic behind the scenes

Such an algorithm would look like this:

If the ball is above the center of the paddle
    Then, move the paddle up
Else, if the ball is below the center of the paddle
    Then, move the paddle down

Here, we always want to do one of two things: go up or go down. Generally speaking, we want to center the paddle on the ball. To do so, we check if the ball is below the center of the paddle. Since the variable paddle2_posY keeps track of the top of the paddle, then we have to add half the paddle's height to get it's center's Y position. So this give us: paddle2_center = paddle2_posY + paddle_height / 2.

The algorithm introduces a new concept for us: the else. Sometimes you might want to express "If A is true, then do thing 1, else do thing 2". In C/C++, this is translated by :

if (A) {
  // Do thing 1
}
else {
  // Do thing 2
}

Another awesome aspect of C++ is the else if {...}. Whenever an else is directly followed by an if, then we can write else if () {...} instead of else { if () {...} }. This improves readability quite a bit, but more importantly, we can put as many else ifs as we want in a row (whereas we can only put at most one else per if statement. For example, this is allowed:

if (A) {
  // Do a thing
}
else if (B) {
  // Do another thing
}
else if (C) {
  // Do something else
}
else {  // A, B, and C are false
  // Do yet again another thing
} 

So, to go back to our algorithm, we have an if and an else if. By simply using what we have just learned, it should be easy enough to translate the algorithm. So now...

Coding the AI

You now have all the necessary tools to start making you first artificial intelligence. Start by going over the algorithm, then modify the previous' workshop code (Pong - two players) to match the algorithm. If you have an idea but you are not sure about it, try it out! You have the code, you have the console, it is easy to test something. There is no better way to know if it works than by testing it :)

Small help: to find the center of the paddle, go back to the beginning of this tutorial.

I have put the solution for this right below, so if you still have not searched on you own, this is you last chance ;) Once again, the best way to progress is to do (and not just read about) the thing you want to learn!

Ok, so it is done, your own artificial intelligence! You can now play against your Gamebuino at Pong. But... as you play, you might heve noticed that the opposing paddle follows the exact movement of the ball and rarely makes any mistakes. Let's fix that right away.

Solution

This code is our implementation of the AI, you should have something similar:











void loop() {
  //// Update paddle1 (player's paddle)

  // Update paddle2 (AI)
  if (ball_posY > paddle2_posY + paddle_height / 2) {  // If the ball is below the center of the paddle
    paddle2_posY = paddle2_posY + 1;                      // Move downwards
  } 
  else if (ball_posY < paddle2_posY + paddle_height / 2) {  // If the ball is above the center of the paddle
    paddle2_posY = paddle2_posY - 1;                           // Move upwards
  }

  //// Update ball movement and collisions
  //// Draw the ball, the paddles, and the score
}

Making our AI less predictable

You have just built a working AI. The computer is capable of following the ball and not losing. It plays correctly, however its behavior is very predictable. 'If the ball is below me, I go down. If the ball is above me, I go up'. This behavior in not very natural looking. The game would be a lot more fun if the AI moved in a more "human" way, and missed the ball from time to time.

Random()

So how do we go about making an unpredictable movement? Well, there is a legend about a function that returns random numbers! And guess what? It's called random():

random(int min, int max)

random() is a function that spits out a random integer. This integer is always between min and max-1. So, the instruction a = random(0, 4); means that a could be 0, 1, 2, or 3 (but not 4!). Before starting to work on our advanced AI, let's put random() to the test.

When you built your Pong game, the ball always spawned in the same position whenever a point was scored. Here is the code we wrote:

// Check if the ball exited the screen
if (ball_posX < 0) {
  // Reset ball
  ball_posX = 20;
  ball_posY = 20;
  ball_speedX = 1;
  ball_speedY = 1;
  // Increment player 2's score
  score2 = score2 + 1;
}
if (ball_posX > gb.display.width()) {
  // Reset ball
  ball_posX = 20;
  ball_posY = 20;
  ball_speedX = 1;
  ball_speedY = 1;
  // Increment player 1's score
  score1 = score1 + 1;
}

By doing so, ALL games started out the same way: the ball spawns in (20, 20) and moved down and to the right. This is a bit repetitive no? random() can save us!

// Check if the ball exited the screen
if (ball_posX < 0) {
  // Reset the ball
  ball_posX = 20;
  ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
  ball_speedX = 1;

  if (random(0, 2) == 1) {  // 50% of the time, this is true
    ball_speedY = 1;
  } 
  else {  // Other 50% of the time
    ball_speedY = -1;
  }

  // Increment player 2's score
  score2 = score2 + 1;
}

if (ball_posX > gb.display.width()) {
  // Reset ball
  ball_posX = 20;
  ball_posY = random(20, gb.display.height() - 20);  // Random position along the Y axis
  ball_speedX = 1;

  if (random(0, 2) == 1) {  // 50% of the time, this is true
    ball_speedY = 1;
  } 
  else {  // Other 50% of the time
    ball_speedY = -1;
  }

  // Increment player 1's score
  score1 = score1 + 1;
}

So now, the ball will respawn at (20, Y), where Y is a random number. Also, we use if (random(0, 2) == 1) because it is true only every other time. You see, random(0, 2) returns either 0 or 1, so there is a 50% chance of getting a 1. With this, we set the ball to start with an upward motion half of the time, and a downward motion the rest of the time. It is these slight modifications that differentiate a 'meh' game from a great game ;)






In C/C++, if we want to test an equality in a condition, we have to use a double equals sign ==. So if (a == b) is true when a IS EQUAL TO b. This is very important to keep in mind, because if you put a single equals sign, your game will do strange things.



Less predictable == more enjoyable

Let's improve our AI. To make the computer behave more naturally, it needs to not be perfect at following the ball. With random, we can make it so that the enemy paddle has a harder time following the ball. To help the AI out a bit, we will also increase it's speed. With these changes, the game should a lot more fun to play.

Let's start by making a more fluid movement. To do so, we need to create a variable that keeps track of the computer's paddle's speed, just like we did for the ball: paddle2_speedY. Let's look at what we will do with it:

int paddle2_speedY = 0;  // Vertical speed of the AI's paddle

void loop() {
  //// Update paddle1 (player's paddle)

  // Update paddle2 (AI's paddle)
  if (ball_posY > paddle2_posY + paddle_height / 2 && random(0, 3) == 1) {
    paddle2_speedY = 2;  // Move down
  } else if (ball_posY < paddle2_posY + paddle_height / 2 && random(0, 3) == 1) {
    paddle2_speedY = -2;  // Move up
  }

  paddle2_posY = paddle2_posY + paddle2_speedY;  // Update paddle2's position

  //// Update ball movement and collisions
  //// Draw the ball, the paddles, and the score
}

Here, we added the random(0, 3) == 1 condition. So when the ball is below the paddle's center, the first if is true about once every 3 frames (and it the same is true for the second if). To better understand why this works, imagine the following scenario: the ball is below the paddle, and the paddle is already moving downwards (paddle2_speedY = 2). When the ball ends up above the paddle's center, the paddle's direction should change, but with the random condition, it should take a few frames before the paddle actually decides to go up. And with a little bit of luck, the paddle will miss the ball, and you score a point!



In the code above, we can generalize random(0, 3) == 1 with random(0, A) == 1 where A = 3. And the bigger A gets, the less likely the condition becomes. So in this case, A is related to the AI's reaction speed because if A is bigger, the more frames are necessary for the AI to change directions. We can exploit this finding to change the AI's difficulty, but I will leave this up to you :)

It's your turn!

In this workshop, we created our first AI, then we improved upon it with the random() function. Like at the end of every workshop, I propose a feasible exercise to implement/improve a functionality. Here I want you to make it so the AI can change difficulty with the press of a button. When the player presses the MENU button, the AI switches between "easy" and "hard" play styles.

Tip: To make the difficulty vary, go over the last part of this tutorial.

It's up to you to polish your AI, or to make another one for another game ;)


Show off your talent on social networks with #gamebuino #Pong #AI, we go through them all the time ;)

Solution Example

If you ran out of ideas, here is what we did on our side :)

#include 

// ball attributes int ball_posX = 20; int ball_posY = 20; int ball_speedX = 1; int ball_speedY = 1; int ball_size = 3;

// paddle1 attributes int paddle1_posX = 10; int paddle1_posY = 30;

// paddle2 attributes int paddle2_posX = gb.display.width() - 13; int paddle2_posY = 30;

// Dimensions for both paddles int paddle_height = 10; int paddle_width = 3;

// For the AI int paddle2_speedY = 0; // Vertical speed of the AI's paddle

// Scores int score1; // Player 1's score int score2; // Player 2's score

int difficulty = 3; // Level of difficulty. 3 = EASY et 2 = HARD

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

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

// Difficulty switch if (gb.buttons.pressed(BUTTON_MENU)) { if (difficulty == 3) { // Easy difficulty = 2; // Change difficulty } else { // Hard difficulty = 3; // Change difficulty } }

// Update paddle 1 (player controlled paddle) if (gb.buttons.repeat(BUTTON_UP, 0)) { paddle1_posY = paddle1_posY - 1; } if (gb.buttons.repeat(BUTTON_DOWN, 0)) { paddle1_posY = paddle1_posY + 1; }

// Update paddle2 (AI controlled paddle) if (ball_posY > paddle2_posY + paddle_height / 2 && random(0, difficulty) == 1) { paddle2_speedY = 2; } else if (ball_posY < paddle2_posY + paddle_height / 2 && random(0, difficulty) == 1) { paddle2_speedY = -2; } paddle2_posY = paddle2_posY + paddle2_speedY; // Update paddle2's position

// Update ball ball_posX = ball_posX + ball_speedX; ball_posY = ball_posY + ball_speedY;

// Collisions with walls if (ball_posY < 0) { ball_speedY = 1; } if (ball_posY > gb.display.height() - ball_size) { ball_speedY = -1; }

// Collision with paddle1 if ( (ball_posX == paddle1_posX + paddle_width) && (ball_posY + ball_size >= paddle1_posY) && (ball_posY <= paddle1_posY + paddle_height) ) { ball_speedX = 1; } // Collision with paddle2 if ( (ball_posX + ball_size == paddle2_posX) && (ball_posY + ball_size >= paddle2_posY) && (ball_posY <= paddle2_posY + paddle_height) ) { ball_speedX = -1; }

// Check if the ball exited the screen if (ball_posX < 0) { // Reset the ball ball_posX = 20; ball_posY = random(20, gb.display.height() - 20); // Random position along the Y axis ball_speedX = 1;

if (random(0, 2) == 1) { // 50% of the time, this is true ball_speedY = 1; } else { // Other 50% of the time ball_speedY = -1; }

// Increment player 2's score score2 = score2 + 1; }

if (ball_posX > gb.display.width()) { // Reset ball ball_posX = 20; ball_posY = random(20, gb.display.height() - 20); // Random position along the Y axis ball_speedX = 1;

if (random(0, 2) == 1) { // 50% of the time, this is true ball_speedY = 1; } else { // Other 50% of the time ball_speedY = -1; }

// Increment player 1's score score1 = score1 + 1; }

// Draw ball gb.display.fillRect(ball_posX, ball_posY, ball_size, ball_size); // Draw paddle1 gb.display.fillRect(paddle1_posX, paddle1_posY, paddle_width, paddle_height); // Draw paddle2 gb.display.fillRect(paddle2_posX, paddle2_posY, paddle_width, paddle_height);

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

// Draw difficulty gb.display.setCursor(33, gb.display.height() - 5); if (difficulty == 3) { gb.display.print("EASY"); } else { gb.display.print("HARD"); } }



Good job you finished the workshop of simple computer controlled enemies :D

By Julien Giovinazzo

Any questions / comments / suggestions, be sure to drop a comment down below!

View full creation

jicehel

NEW 8 months ago

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

8 months ago

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

JulienGio

NEW 8 months ago

jicehel jicehel

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

Max

NEW 6 months ago

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

5 months ago

C'est pour bientôt ;)

jicehel

NEW 6 months ago

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 6 months ago

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 6 months ago

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 5 months ago

Max Max

C'est pour bientôt ;)

jicehel

NEW 5 months ago

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

5 months ago

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 5 months ago

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 5 months ago

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 5 months ago

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

You must be logged in in order to post a message on the forum

Log in