Dans cet article, je vais vous guider à travers la création d’un jeu Snake en utilisant uniquement HTML, CSS et JavaScript.
Nous n’aurons recours à aucune bibliothèque supplémentaire ; le jeu sera entièrement fonctionnel dans votre navigateur. La conception de ce jeu est un excellent exercice pour stimuler vos capacités de résolution de problèmes et renforcer vos compétences en développement web.
Aperçu du projet
Le principe de Snake est simple : vous dirigez un serpent à travers un espace, en le guidant vers de la nourriture tout en évitant les obstacles. Chaque fois que le serpent mange, il grandit. Au fur et à mesure que le jeu avance, le serpent devient de plus en plus long, ce qui augmente la difficulté.
Le serpent doit éviter de se heurter aux murs ou à lui-même. Cette contrainte fait que plus le serpent grandit, plus le jeu devient délicat.
Ce tutoriel a pour objectif de vous permettre de créer un jeu Snake comme celui-ci :
Le code source complet est disponible sur mon GitHub. Une démo en direct est hébergée sur GitHub Pages.
Prérequis
Ce projet sera développé en utilisant HTML, CSS et JavaScript. Nous utiliserons du HTML et du CSS basique. L’accent sera mis sur JavaScript. Par conséquent, il est préférable que vous ayez déjà des connaissances de base en JavaScript pour suivre ce tutoriel. Si ce n’est pas le cas, je vous suggère de consulter notre article sur les meilleures ressources pour apprendre JavaScript.
De plus, vous aurez besoin d’un éditeur de code pour écrire votre code, ainsi qu’un navigateur, que vous avez très probablement si vous lisez cet article.
Mise en place du projet
Commençons par la configuration des fichiers. Dans un nouveau dossier vide, créez un fichier index.html et ajoutez le code suivant.
<meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="https://wilku.top/javascript-snake-tutorial-explained/./styles.css" /> <title>Snake</title> <div id="game-over-screen"> <h1>Game Over</h1> </div> <canvas id="canvas" width="420" height="420"> </canvas> <script src="./snake.js"></script>
Ce balisage HTML crée un écran basique « Game Over », dont la visibilité sera gérée via JavaScript. Il définit également un élément canvas sur lequel le jeu (le labyrinthe, le serpent et la nourriture) sera dessiné. De plus, il relie la feuille de style CSS et le code JavaScript.
Créez ensuite un fichier styles.css pour les styles visuels et ajoutez-y les règles suivantes.
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', Courier, monospace; } body { height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: #00FFFF; } #game-over-screen { background-color: #FF00FF; width: 500px; height: 200px; border: 5px solid black; position: absolute; align-items: center; justify-content: center; display: none; }
Dans le sélecteur ‘*’, nous ciblons tous les éléments et réinitialisons les marges et les rembourrages. Nous définissons également la police pour tous les éléments et utilisons une méthode de dimensionnement plus prévisible, border-box. Pour le body, nous définissons la hauteur pour qu’elle couvre toute la hauteur de la fenêtre, et nous centrons tous les éléments. Nous lui attribuons également une couleur de fond turquoise.
Enfin, nous stylisons l’écran « Game Over » en lui donnant une hauteur de 200 pixels et une largeur de 500 pixels. Nous lui donnons également un fond magenta et une bordure noire. Nous positionnons cet élément de manière absolue pour qu’il soit hors du flux normal du document et centré à l’écran. Nous centrons également son contenu. Par défaut, nous cachons l’écran en définissant son affichage sur « none ».
Ensuite, créez un fichier snake.js, que nous compléterons dans les sections suivantes.
Création de variables globales
L’étape suivante consiste à définir certaines variables globales qui seront utilisées tout au long du jeu. Dans le fichier snake.js, ajoutez les déclarations de variables suivantes au début :
// Creating references to HTML elements let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Creating context which will be used to draw on canvas let ctx = canvas.getContext("2d");
Ces variables stockent des références à l’écran « Game Over » et à l’élément canvas. Nous créons ensuite un contexte qui servira à dessiner sur le canvas.
Ajoutez ensuite ces variables sous le premier groupe.
// Maze definitions let gridSize = 400; let unitLength = 10;
La première variable définit la taille de la grille en pixels. La seconde définit l’unité de longueur dans le jeu. Cette unité sera utilisée pour définir plusieurs aspects, comme l’épaisseur des murs du labyrinthe, l’épaisseur du serpent, la taille de la nourriture, ainsi que les incréments de déplacement du serpent.
Ensuite, ajoutez ces variables qui suivent l’état du jeu :
// Game play variables let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
La variable snake maintient la trace des positions occupées par le serpent. Le serpent est constitué d’unités, et chaque unité occupe une position dans le canvas. La position occupée par chaque unité est stockée dans un tableau. Chaque position a des coordonnées x et y. Le premier élément du tableau représente la queue, tandis que le dernier élément représente la tête.
À chaque déplacement, nous ajouterons des éléments à la fin du tableau (pour faire avancer la tête), et nous supprimerons le premier élément (la queue) pour que le serpent conserve sa longueur.
La variable foodPosition stocke l’emplacement actuel de la nourriture à l’aide de coordonnées x et y. La variable direction stocke la direction dans laquelle le serpent se déplace, et la variable collided est un booléen qui passe à true lorsqu’une collision a été détectée.
Déclaration de fonctions
Le jeu sera structuré en fonctions pour faciliter l’écriture et la gestion du code. Dans cette section, nous allons déclarer ces fonctions, ainsi que leurs rôles. Les sections suivantes définiront les fonctions et expliqueront leurs algorithmes.
function setUp() {} function doesSnakeOccupyPosition(x, y) {} function checkForCollision() {} function generateFood() {} function move() {} function turn(newDirection) {} function onKeyDown(e) {} function gameLoop() {}
En résumé, la fonction setUp configure le jeu. La fonction checkForCollision vérifie si le serpent a percuté un mur ou lui-même. La fonction doesSnakeOccupyPosition prend une position (coordonnées x et y) et vérifie si le corps du serpent occupe cette position. Cette fonction sera utile pour trouver une position libre pour la nourriture.
La fonction move déplace le serpent dans la direction indiquée, tandis que la fonction turn change cette direction. La fonction onKeyDown détectera les pressions sur les touches fléchées afin de changer la direction du serpent. Enfin, la fonction gameLoop déplacera le serpent et vérifiera les collisions.
Définir les fonctions
Nous allons maintenant définir les fonctions déclarées précédemment et expliquer comment elles fonctionnent. Chaque fonction sera brièvement décrite avant son code, avec des commentaires pour expliquer chaque ligne si nécessaire.
Fonction setup
La fonction setUp effectuera 3 tâches :
- Dessiner les bordures du labyrinthe sur le canvas.
- Initialiser le serpent en ajoutant ses positions à la variable snake et en le dessinant sur le canvas.
- Générer la position initiale de la nourriture.
Le code correspondant sera le suivant :
// Drawing borders on canvas // The canvas will be the size of the grid plus thickness of the two side border canvasSideLength = gridSize + unitLength * 2; // We draw a black square that covers the entire canvas ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // We erase the center of the black to create the game space // This leaves a black outline for the that represents the border ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Next, we will store the initial positions of the snake's head and tail // The initial length of the snake will be 60px or 6 units // The head of the snake will be 30 px or 3 units ahead of the midpoint const headPosition = Math.floor(gridSize / 2) + 30; // The tail of the snake will be 30 px or 3 units behind the midpoint const tailPosition = Math.floor(gridSize / 2) - 30; // Loop from tail to head in unitLength increments for (let i = tailPosition; i <= headPosition; i += unitLength) { // Store the position of the snake's body and drawing on the canvas snake.push({ x: i, y: Math.floor(gridSize / 2) }); // Draw a rectangle at that position of unitLength * unitLength ctx.fillRect(x, y, unitLength, unitLength); } // Generate food generateFood();
doesSnakeOccupyPosition
Cette fonction prend les coordonnées x et y comme position et vérifie si le corps du serpent occupe cette position. Elle utilise la méthode find des tableaux pour chercher une position qui correspond aux coordonnées.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
checkForCollision
Cette fonction vérifie si le serpent a percuté quelque chose et définit la variable collided à true. Nous allons d’abord vérifier les collisions avec les murs gauche et droit, puis les murs du haut et du bas, puis avec le serpent lui-même.
Pour les collisions avec les murs de gauche et de droite, nous vérifions si la coordonnée x de la tête du serpent dépasse la taille de la grille ou est inférieure à 0. Pour les collisions avec les murs du haut et du bas, nous effectuons la même vérification, mais avec les coordonnées y.
Ensuite, nous allons vérifier les collisions avec le corps du serpent ; nous vérifions si une autre partie de son corps occupe la position de sa tête. En combinant tout cela, le corps de la fonction checkForCollision ressemble à ceci :
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Check for collisions against left and right walls if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Check for collisions against top and bottom walls if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Check for collisions against the snake itself const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
generateFood
La fonction generateFood utilise une boucle do-while pour chercher une position non occupée par le serpent. Une fois trouvée, la position de la nourriture est enregistrée et dessinée sur le canvas. Le code de la fonction generateFood doit ressembler à ceci :
function generateFood() { let x = 0, y = 0; do { x = Math.floor((Math.random() * gridSize) / 10) * 10; y = Math.floor((Math.random() * gridSize) / 10) * 10; } while (doesSnakeOccupyPosition(x, y)); foodPosition = { x, y }; ctx.fillRect(x, y, unitLength, unitLength); }
move
La fonction move commence par faire une copie de la position de la tête du serpent. Ensuite, selon la direction actuelle, elle augmente ou diminue la valeur de la coordonnée x ou y du serpent. Par exemple, augmenter la coordonnée x revient à se déplacer vers la droite.
Après cela, nous ajoutons la nouvelle headPosition au tableau snake. Nous dessinons aussi la nouvelle headPosition sur le canvas.
Ensuite, nous vérifions si le serpent a mangé de la nourriture durant ce déplacement. Nous le faisons en vérifiant si la headPosition est égale à la foodPosition. Si le serpent a mangé la nourriture, nous appelons la fonction generateFood.
Si le serpent n’a pas mangé, nous supprimons le premier élément du tableau snake. Cet élément représente la queue, et le supprimer permet de maintenir la longueur du serpent tout en donnant une impression de mouvement.
function move() { // Create a copy of the object representing the position of the head const headPosition = Object.assign({}, snake.slice(-1)[0]); switch (direction) { case "left": headPosition.x -= unitLength; break; case "right": headPosition.x += unitLength; break; case "up": headPosition.y -= unitLength; break; case "down": headPosition.y += unitLength; } // Add the new headPosition to the array snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // Check if snake is eating const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // Generate new food position generateFood(); } else { // Remove the tail if the snake is not eating tailPosition = snake.shift(); // Remove tail from grid ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
turn
La dernière fonction importante est la fonction turn. Cette fonction prend une nouvelle direction et change la variable direction avec cette nouvelle direction. Cependant, le serpent ne peut tourner que dans une direction perpendiculaire à celle dans laquelle il se déplace actuellement.
Ainsi, le serpent ne peut tourner à gauche ou à droite que s’il se déplace vers le haut ou vers le bas. Et inversement, il ne peut monter ou descendre que s’il se déplace vers la gauche ou la droite. En gardant ces contraintes à l’esprit, la fonction turn ressemble à ceci :
function turn(newDirection) { switch (newDirection) { case "left": case "right": // Only allow turning left or right if they were originally moving up or down if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // Only allow turning up or down if they were originally moving left or right if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
onKeyDown
La fonction onKeyDown est un gestionnaire d’événements qui va appeler la fonction turn avec la direction correspondant à la touche fléchée qui a été enfoncée. La fonction ressemble donc à ceci :
function onKeyDown(e) { switch (e.key) { case "ArrowDown": turn("down"); break; case "ArrowUp": turn("up"); break; case "ArrowLeft": turn("left"); break; case "ArrowRight": turn("right"); break; } }
gameLoop
La fonction gameLoop sera appelée régulièrement pour que le jeu continue de fonctionner. Cette fonction va appeler move et checkForCollision. Elle vérifie aussi si collided est à true. Si c’est le cas, elle arrête le timer que nous utilisons pour exécuter le jeu et affiche l’écran « game over ». La fonction ressemble à ceci :
function gameLoop() { move(); checkForCollision(); if (collided) { clearInterval(timer); gameOverScreen.style.display = "flex"; } }
Démarrer le jeu
Pour démarrer le jeu, ajoutez ces lignes de code :
setUp(); document.addEventListener("keydown", onKeyDown); let timer = setInterval(gameLoop, 200);
Tout d’abord, nous appelons la fonction setUp. Ensuite, nous ajoutons l’écouteur d’événement « keydown ». Enfin, nous utilisons la fonction setInterval pour démarrer le timer.
Conclusion
À ce stade, votre fichier JavaScript devrait être similaire à celui disponible sur mon GitHub. Si quelque chose ne fonctionne pas, veuillez consulter le dépôt. Ensuite, vous pourriez être intéressé par l’apprentissage de la création d’un carrousel d’images en JavaScript.