6 years ago
So you just finished your first game, let's improve on it with Artificial Intelligence.
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.
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.
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...
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 }
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.
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.
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 :)
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 ;)
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!
NEW 6 years 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.
NEW 6 years 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 !
NEW 6 years 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...
NEW 6 years 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 :)
NEW 6 years 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.
NEW 6 years 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 :
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"); } }
NEW 6 years 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
Durée: 30 minutes (à la louche)
Niveau: ancien débutant ayant suivi les premiers tutos
Prérequis
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.
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 ?
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] ;
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...
#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(""); } }
NEW 6 years 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
NEW 5 years ago
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);
}