Créer un jeu Snake en JavaScript : Partie 2 – améliorations

Dans notre premier guide sur la création d’un jeu Snake, nous avons conçu une version simple du classique en JavaScript. Dans cette deuxième partie, on va pimenter un peu les choses ! Au programme : un système de score et de high score, un écran de fin de partie, et même un effet sonore quand le serpent mange une nourriture. On trouvait que notre jeu était si réussi qu’on lui a même donné un nom : Java Snake. Et oui, on l’a ajouté à la liste des jeux sur la page principale ! Cliquez ici pour jouer à Java Snake.

Prêt à donner à votre Snake une dimension encore plus immersive ?

Ajouter le score et le high score

Commençons avec le score et le high score. Chaque fois que le serpent mange une nourriture, notre score grimpe de 10 points. Et si ce score dépasse le high score enregistré, alors le high score est mis à jour. On le garde en mémoire à l’aide d’un cookie, ce qui le rend persistant pour chaque session (pas de panique, c’est juste du stockage local !).

let score = 0;
let highScore = getHighScore(); // Récupère le high score à partir des cookies

function drawGame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Affiche le score et le high score en haut à droite
  ctx.fillStyle = 'white';
  ctx.font = '16px Arial';
  ctx.textAlign = 'right';
  ctx.fillText(`Score: ${score}`, canvas.width - 10, 20);
  ctx.fillText(`High Score: ${highScore}`, canvas.width - 10, 40);
}

Dans ce code (Snippet 1), nous initialisons deux variables, score et highScore. À chaque coup de dent dans une nourriture, le score augmente, et si ce score bat le record, on le sauvegarde comme nouveau high score.

Mettre à jour le score et le high Score

Maintenant, ajoutons la logique pour mettre à jour le score et le high score à chaque fois que le serpent mange une nourriture.

if (newHead.x === food.x && newHead.y === food.y) {
  score += 10;  // Incrémenter le score de 10 points
  if (score > highScore) {
    highScore = score;
    setHighScore(highScore); // Enregistrer le high score dans les cookies
  }
}

Snippet 2 s’occupe de mettre à jour le score de notre serpent : dès qu'il mange, on vérifie si le score dépasse le high score. Si c’est le cas, on met à jour le high score dans le cookie. Simple mais efficace, non ?

L'Écran de fin de partie

Pour rendre l’expérience encore plus interactive, ajoutons un écran “Fin de partie” qui affiche le score et le high score actuels, et inclut un bouton “Start” pour relancer la partie.

function drawGame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (gameStarted) {
    // Affiche le serpent, le score, le high score, et la nourriture
  } else {
    // Affiche l'écran "Fin de partie"
    ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = 'white';
    ctx.font = '24px Arial';
    ctx.textAlign = 'center';
    ctx.fillText("Fin de partie", canvas.width / 2, canvas.height / 2 - 20);
    ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2 + 10);
    ctx.fillText(`High Score: ${highScore}`, canvas.width / 2, canvas.height / 2 + 40);

    // Dessine le bouton Start
    ctx.fillStyle = 'lime';
    ctx.fillRect(canvas.width / 2 - 50, canvas.height / 2 + 60, 100, 30);
    ctx.fillStyle = 'black';
    ctx.font = '16px Arial';
    ctx.fillText("Start", canvas.width / 2, canvas.height / 2 + 80);
  }
}

Snippet 3 construit un écran de fin de partie en affichant "Fin de partie" avec le score actuel et le high score. Un bouton “Start” permet de recommencer le jeu, en un seul clic, et donne ainsi une seconde chance !

Ajouter un effet sonore

Et pour la touche finale, ajoutons un effet sonore ! Téléchargez un fichier audio (comme eatSound.mp3) et ajoutez ce son au moment où le serpent mange la nourriture.

const eatSound = new Audio('eatSound.mp3');

if (newHead.x === food.x && newHead.y === food.y) {
  eatSound.play(); // Jouer le son "manger"
}

Dans Snippet 4, on ajoute l’effet sonore pour une bouchée immersive ! En téléchargeant un fichier audio et en l’activant avec eatSound.play(), chaque morceau de nourriture devient plus satisfaisant.

Optimisation et code final complet

Pour finir, une petite optimisation : nous avons veillé à ce que la nourriture n'apparaisse jamais sur le corps du serpent en générant chaque nouvelle nourriture en dehors des segments du serpent. Nous avons également ajouté quelques autres fonctionnalités pratiques, comme la possibilité de redémarrer le jeu d'une simple pression sur la touche "Enter". Chaque section de code comporte un commentaire indiquant sa fonction.

Pour vous faciliter la tâche, voici le code complet avec toutes les nouvelles fonctionnalités ajoutées. En exécutant ce code, le jeu fonctionnera parfaitement avec les dernières améliorations !

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');

// Paramètres du grille et du canvas
const gridSize = 20;  // Chaque carré fait 20x20 pixels
const canvasSize = canvas.width / gridSize;  // Nombre de cases sur le canvas

let snake = [{ x: 10, y: 10 }];
let direction = { x: 0, y: 0 };
let food = placeFood();

let gameOver = false;
let gameStarted = false;
let score = 0;
let highScore = getHighScore(); // Charger le high score à partir des cookies

// Charger le son de "manger"
const eatSound = new Audio('eatSound.mp3');

// Dessiner un carré sur le canvas
function drawSquare(x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
}

// Dessiner le serpent, la nourriture et le score
function drawGame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (gameStarted) {
    // Dessiner le serpent
    snake.forEach(segment => drawSquare(segment.x, segment.y, 'lime'));

    // Dessiner la nourriture
    drawSquare(food.x, food.y, 'red');

    // Afficher le score et le high score en haut à droite en texte blanc
    ctx.fillStyle = 'white';
    ctx.font = '16px Arial';
    ctx.textAlign = 'right';
    ctx.fillText(`Score: ${score}`, canvas.width - 10, 20);
    ctx.fillText(`High Score: ${highScore}`, canvas.width - 10, 40);
  } else {
    // Afficher l'écran "Fin de partie" avec un bouton Jouer
    ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = 'white';
    ctx.font = '24px Arial';
    ctx.textAlign = 'center';
    ctx.fillText("Fin de partie", canvas.width / 2, canvas.height / 2 - 20);
    ctx.fillText(`Score: ${score}`, canvas.width / 2, canvas.height / 2 + 10);
    ctx.fillText(`High Score: ${highScore}`, canvas.width / 2, canvas.height / 2 + 40);

    // Dessiner le bouton Jouer
    ctx.fillStyle = 'lime';
    ctx.fillRect(canvas.width / 2 - 50, canvas.height / 2 + 60, 100, 30);
    ctx.fillStyle = 'black';
    ctx.font = '16px Arial';
    ctx.fillText("Jouer", canvas.width / 2, canvas.height / 2 + 80);
  }
}

// Générer une position de nourriture qui n'est pas sur le corps du serpent
function placeFood() {
  let newFood;
  while (true) {
    newFood = { x: Math.floor(Math.random() * canvasSize), y: Math.floor(Math.random() * canvasSize) };
    // Vérifier si la position de la nourriture est sur le serpent
    const onSnake = snake.some(segment => segment.x === newFood.x && segment.y === newFood.y);
    if (!onSnake) break;
  }
  return newFood;
}

// Mettre à jour la position du serpent
function updateSnake() {
  const newHead = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };

  // Si le serpent mange la nourriture
  if (newHead.x === food.x && newHead.y === food.y) {
    food = placeFood(); // Placer une nouvelle nourriture qui ne chevauche pas le serpent
    score += 10;
    eatSound.play(); // Jouer le son de "manger"
    if (score > highScore) {
      highScore = score;
      setHighScore(highScore); // Mettre à jour le high score dans les cookies
    }
  } else {
    snake.pop();
  }

  snake.unshift(newHead);
}

// Vérifier les collisions
function checkCollision() {
  const head = snake[0];

  // Collision avec le mur
  if (head.x < 0 || head.x >= canvasSize || head.y < 0 || head.y >= canvasSize) {
    gameOver = true;
  }

  // Collision avec le corps
  for (let i = 1; i < snake.length; i++) {
    if (head.x === snake[i].x && head.y === snake[i].y) {
      gameOver = true;
    }
  }

  if (gameOver) {
    gameStarted = false;
    drawGame();
  }
}

// Écouter les touches du clavier pour démarrer le jeu
document.addEventListener('keydown', (event) => {
  if (!gameStarted && event.key === 'Enter') {
    startGame();
  }

  if (!gameStarted) return;

  switch (event.key) {
    case 'ArrowUp':
      if (direction.y === 0) direction = { x: 0, y: -1 };
      break;
    case 'ArrowDown':
      if (direction.y === 0) direction = { x: 0, y: 1 };
      break;
    case 'ArrowLeft':
      if (direction.x === 0) direction = { x: -1, y: 0 };
      break;
    case 'ArrowRight':
      if (direction.x === 0) direction = { x: 1, y: 0 };
      break;
  }
});

// Événement de clic pour le bouton de démarrage
canvas.addEventListener('click', (event) => {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // Vérifier si le bouton "Jouer" est cliqué
  if (!gameStarted && x > canvas.width / 2 - 50 && x < canvas.width / 2 + 50 && y > canvas.height / 2 + 60 && y < canvas.height / 2 + 90) {
    startGame();
  }
});

// Changer le curseur en pointeur lorsque l'on survole le bouton Jouer
canvas.addEventListener('mousemove', (event) => {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  if (!gameStarted && x > canvas.width / 2 - 50 && x < canvas.width / 2 + 50 && y > canvas.height / 2 + 60 && y < canvas.height / 2 + 90) {
    canvas.style.cursor = 'pointer';
  } else {
    canvas.style.cursor = 'default';
  }
});

// Démarrer une nouvelle partie
function startGame() {
  gameStarted = true;
  gameOver = false;
  score = 0;
  snake = [{ x: 10, y: 10 }];
  direction = { x: 0, y: 0 };
  food = placeFood();
}

// Définir le high score dans les cookies
function setHighScore(score) {
  document.cookie = `highScore=${score}; path=/; max-age=${60 * 60 * 24 * 365}`;
}

// Récupérer le high score à partir des cookies
function getHighScore() {
  const match = document.cookie.match(/(?:^|; )highScore=([^;]*)/);
  return match ? parseInt(match[1]) : 0;
}

// Boucle principale du jeu
function gameLoop() {
  if (gameStarted) {
    updateSnake();
    checkCollision();
  }
  drawGame();
}

// Exécuter la boucle toutes les 100 ms
setInterval(gameLoop, 100);

Snippet 5 montre comment la nourriture est générée aléatoirement, en vérifiant qu’elle n’apparaisse pas sur le corps du serpent. Propre et efficace !

Voilà, Java Snake est maintenant prêt pour l’action. Mais ce n’est pas tout ! Dans notre prochain blog, nous pousserons ce jeu encore plus loin avec une version 2.0 qui introduira de nouvelles surprises. Restez à l'écoute et amusez-vous en attendant !