Jeu Snake avec Canvas en VueJs

VueJs Déc 20, 2020 #canvas, #snake, #VueJs
snake vuejs canvas
Partagez

Dans cet article, nous allors apprendre à créer un jeu snake en VueJs et nous découvrirons ainsi de nouvelles fonctionnalités :

  • Les Canvas HTML et le dessin javascript sur ces canvas
  • Les timers et intervalles pour répéter une méthode toutes les x millisecondes
  • La manipulation de tableau
  • etc.

Créer la structure du projet

Je vais passer sur comment créer un projet vide. Vous pouvez le découvrir grâce à un précédent article : -> Formation de base VueJs <-

Créez donc un projet vide dans Visual Studio Code et allez dans votre App.vue pour mettre le code ci-dessous :

<template>
  <div id="app">
    <snake/>
  </div>
</template>

<script>
import Snake from './components/Snake';

export default {
  name: 'App',
  components: {
    Snake
  }
}
</script>

Comme vous pouvez le voir, on va créer un composant snake qui va représenter l’affichage d’un rectangle dans lequel le serpent va évoluer. Pour se faire, comme d’habitude : on fait un import, on ajoute le composant dans la liste « components » et on ajoute notre élément <snake/> dans le template html.

Créons à présent notre composant SNAKE avec un rectangle noir contenant un CANVAS (c’est sur ce canvas que le serpent évoluera et sera dessiné en javascript). Voici le code de base dans notre composant SNAKE :

<template>
  <div id="snake-div">
    <canvas ref="snake" id="snake" :width="width*cellSize" :height="height*cellSize" />
  </div>
</template>

<script>
export default {
  name: 'Snake',
  props: {
    msg: String
  },
  data() {
    return {    
      width: 45,
      height: 20,
      cellSize: 20
    }
  }
}
</script>
<style>
  body {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
  }

  #snake {
    border: 1px solid black;
  }
</style>

A présent, si vous lancez le projet grâce à la commande NPM RUN SERVE, vous pourrez voir notre rectangle vide dessiné en noir (grâce au STYLE #snake) et bien centré (grâce au STYLE du BODY).

Pour des raisons de facilités et de calcul pour le dessin du serpent, la taille du canvas et d’un carré du serpent (une cellule) sont stockés dans les data (variable).

Dessiner la tête du serpent

Pour dessiner la tête du serpent, on va ajouter une méthode RESET. Celle-ci remettra toutes les valeurs à 0 et recréera notamment la tête du serpent dans une zone aléatoire. Voici donc le code :

data() {
    return {
      snake: [{x:0,y:0}],
...
mounted() {
    this.boardContext = this.$refs.snake.getContext("2d");
    this.resetGame();
  },
  methods: {
    resetGame() {

      // Clear the canvas
      this.clear();

      // Create the head of the snake in the middle of the canvas
      this.snake = [{x: Math.round(this.width / 2), y: Math.round(this.height / 2)}];
      this.drawGame();
    },
    clear() {
      this.boardContext.clearRect(0, 0, this.width * this.cellSize, this.height * this.cellSize);
    },
    drawGame() {

      // Clear canvas
      this.clear();

      // Start drawing on the canvas
      this.boardContext.beginPath();

      // Draw the snake
      this.snake.forEach((snakePart) => {
          this.boardContext.rect(snakePart.x * this.cellSize, snakePart.y * this.cellSize, this.cellSize, this.cellSize);
          this.boardContext.fillStyle = "black";
          this.boardContext.fill();
      });

      // Stop drawing on the canvas
      this.boardContext.closePath();
    }

Dans le code précédent, on peut voir que pour dessiner sur le canvas, on doit récupérer le boardContext via : canvas.getContext(« 2d »). Le canvas a pour ref « snake » donc on l’obtient via this.$refs.snake. Tout cela se faire dans le « mount() » afin d’être effectué à la création du composant.

On va également ajouter dans la partie data le serpent qui en fait est un tableau de x et y : un tableau de carrés positionnés à des coordonnées x et y.

On va ensuite créer les méthodes dont on a besoin :

  • resetGame qui vide le canvas, qui vide également tous les éléments du serpent et qui le recrée juste avec sa tête (élément 0) positionné au centre du canvas.
  • clear : vide le canvas
  • drawGame : dessine tous les élements du jeux : pour le moment juste le serpent. Pour se faire, on parcourt chaque partie du serpent avec un foreach et on utilise chaque partie.x et .y dans le dessin.
    • Le dessin sur un canvas commence toujours par un .beginPath() et se termine par un closePath()
    • on dit ensuite ce qu’on va dessiner : ici un rectangle .rect(x, y, width, height) et on le remplit avec une couleur : .fillStyle= »color ». Le remplissage final se fait en appelant .fill()

Faire avancer le serpent

Pour faire avancer le serpent, la première chose à faire est de définir les différentes directions possible du serpent : on va donc créer dans la partie data un tableau de directions reprenant pour chacune : le code de la touche à enfoncer pour aller dans cette direction et ensuite, ce que ça produit comme mouvement sur l’axe des x et l’axe des y. Ainsi, on aura ceci :

nextDirection: null,
directions: [
        { // left
          keyCode: 37,
          move: {
            x: -1,
            y: 0
          }
        },
        { // top
          keyCode: 38,
          move: {
            x: 0,
            y: -1
          }
        },
        {
          direction: "right",
          keyCode: 39,
          move: {
            x: 1,
            y: 0
          }
        },
        {
          direction: "bottom",
          keyCode: 40,
          move: {
            x: 0,
            y: 1
          }
        }
      ]

On a également la direction choisie dans la variable nextDirection.

On va à présent appeler une méthode qui va s’exécuter tous les x millisecondes pour faire avancer le dessin de notre serpent. Le principe est simple :

  • On retire le dernier élément de la queue du serpent
  • Et on calcule un nouvel élément qui sera la tête en fonction de la nouvelle direction.

L’exécution d’une méthode toutes les x millisecondes se fait via le code suivant à mettre dans le mounted() {…} :

this.interval = setInterval(this.nextMove, 200);

Autre code à mettre dans le mounted() : l’abonnement à l’évènement keypressed afin d’être prévenu quand l’utilisateur appuie sur une touche :

window.addEventListener("keydown", this.onKeyPress);

Commençons par coder notre méthode nextMove() :

nextMove() {

      // If there is no direction for the moment : nothing to do
      if (this.nextDirection == null)
      {
        return;
      }

      // And we compute a new header depending on the next direction
      this.snake.unshift({ 
        x: this.snake[0].x + this.nextDirection.move.x,
        y: this.snake[0].y + this.nextDirection.move.y
      });

      // Else we can remove the last element of the snake
      this.snake.pop();

      // Draw on the canvas
      this.drawGame();
    }

Au démarrage de la page web, il n’y a pas de direction donc rien ne bouge (premier IF). Ensuite, on a une direction du coup, on ajoute un élément à notre serpent en fonction de sa tête (snake[0]) et de la direction en cours -> on ajoute le mouvement de la direction à la position actuelle de la tête. L’ajout au début d’un tableau se fait avec l’instruction unshift(). Ensuite, on supprime le dernier élément du serpent puisqu’on a avancé d’un élément -> instruction pop(). Et finalement, on dessine le tout avec notre méthode this.drawGame().

A présent, codons le onKeypress() :

onKeyPress(event) {
      
      // Get the new direction
      const newDirection = this.directions.find(c => c.keyCode === event.keyCode);

      // If the key pressed is no a direction key : nothing to do
      if (!newDirection) {
        return;
      }

      // From up, we can go right or left but not down and so on for other directions so we use the following code to know if the next direction can be set
      if (this.nextDirection == null || Math.abs(newDirection.keyCode - this.nextDirection.keyCode) !== 2) {
        this.nextDirection = newDirection;
      }
    }

Rien de bien compliqué :

  • On cherche la nouvelle direction à assigner en fonction de la touche enfoncée –> si pas de nouvelle direction : rien à faire
  • Sinon, on vérifie si on a déjà une direction en cours
    • NON -> on peut simplement assigner la nouvelle direction comme nextDirection
    • OUI -> on vérifie si on peut passer de la direction en cours à la nouvelle (de haut vers bas : pas possible, de gauche vers droite : pas possible, etc.) -> en vérifiant si la différence entre la nouvelle direction et l’ancienne direction est != 2, on vérifie si on peut passer de l’une à l’autre très facilement sans tester les cas un à un.

Ajouter et traiter la nourriture

A présent, on va placer un élement rouge dans la scène représentant la nourriture du serpent : ce qui le fait grandir d’un carré lorsqu’il le touche.

Commençons par déclarer cet élément dans la partie data :

      food: {x:0,y:0},

Dans la méthode resetGame(), on va ajouter avant le drawGame() l’instruction permettant de positionner la nourriture à un endroit aléatoire mais libre :


      // Create the food of the snake from an empty random place
      this.moveFoodToFreePlace();

On va donc ajouter cette méthodes avec son code :

moveFoodToFreePlace() {
      this.food = null;
      while (this.food === null)
      {
        // Random x and y
        const x = Math.floor(Math.random() * this.width);
        const y = Math.floor(Math.random() * this.height);

        // Check if this coord = a snake part
        const snakePart = this.snake.find(snakeP => snakeP.x == x && snakeP.y == y);

        // // If it is not a snake part, we can use it
        if (!snakePart)
        {
           this.food = { x, y };
        }
      }
    }

On génère donc des coordonnées aléatoires x et y entre 0 et width pour x et entre 0 et height pour y. Ensuite, tant qu’on est sur une partie du serpent (qu’on trouve donc dans le tableau des éléments du serpent une correspondance -> voir le find : on continue de chercher). Une fois qu’on a une position non utilisée par le serpent : on l’utilise comme position pour la nourriture.

Ensuite, il reste à dessiner la nourriture. Pour se faire, on va ajouter le code ci-dessous dans le drawGame(). Ce code permet de dessiner un carré rouge à l’emplacement de la nourriture. ATTENTION : comme on change de style de remplissage (couleur rouge ici), on doit faire un nouveau PATH -> nouveau beginPath et closePath :

// Draw the food
      if (this.food != null)
      {
        this.boardContext.beginPath();
        this.boardContext.rect(this.food.x * this.cellSize, this.food.y * this.cellSize, this.cellSize, this.cellSize);
        this.boardContext.fillStyle = "red";
        this.boardContext.fill();
        this.boardContext.closePath();
      }

Si vous testez déjà tel quel, vous avez votre carré rouge qui apparait et votre serpent peut continuer de se déplacer mais rien ne se passe évidemment quand notre serpent touche ce carré rouge.

On va donc agir lorsque le serpent touche le carré rouge. Pour se faire, on va modifier le nextMove() (on va modifier l’endroit où on a le this.snake.pop() par ce qui suit) :

... 
      // If we touch the food : we just create a new food place
      if (this.food != null && this.snake[0].x === this.food.x && this.snake[0].y === this.food.y)
      {
        this.moveFoodToFreePlace();
      }
      else
      {
        // Else we can remove the last element of the snake
        this.snake.pop();
      }
...

Ainsi, si la tête du serpent (snake[0]) est au même emplacement que la nourriture, on laisse le serpent augmenter de taille et on calcule simplement un nouvel emplacement pour la nourriture. Sinon, on continue de supprimer le dernier élément du serpent puisqu’on lui a déplacé sa tête d’un élement.

Empêcher les collisions (lui et bord)

A présent, il faut empêcher la collision entre le serpent et le bord du jeu ainsi qu’entre le serpent et lui-même. Pour se faire, toujours au même endroit dans le nextMove() :

... Après le unshift
// Check if the new position of the head of the snake is not out of the canvas
      if (this.snake[0].x < 0 || this.snake[0].x >= this.width || this.snake[0].y < 0 || this.snake[0].y >= this.height)
      {
        this.resetGame();
        window.alert('Le serpent est sorti du jeu : perdu !');
        return;
      } 

      // Check if the new position of the head of the snake is not in collision with its body
      for (var cpt = 1; cpt < this.snake.length; cpt++) {
        if (this.snake[cpt].x === this.snake[0].x &&
          this.snake[cpt].y === this.snake[0].y) {
            this.resetGame();
            window.alert('Le serpent entre en collision avec lui-même : perdu !');
            return;
        }
      }
...

On teste donc bien le fait de rentrer en collision avec un bord de la grille ou avec le corps du serpent en lui-même (la tête snake[0] est testée avec tous les autres éléments du serpent en dehors de la tête elle-même d’où le for (cpt = 1 et pas 0…).

Voilà, vous avez à présent un snake basique mais fonctionnel.

Pour finir

Vous pouvez ajouter un score, un message en overlay à la fin au lieu de la box d’alert toute simple mais j’ai voulu me concentrer sur le jeu snake au plus basic pour utiliser le canvas en s’amusant un peu, libre à vous maintenant de perfectionner le jeu et laisser libre cours à votre imagination et créativité.

J’espère que cet article vous aura plu. N’hésitez pas à liker, commenter et partager cet article de même qu’à vous inscrire sur ce blog pour ne rien manquer. Je vous dis à très bientôt sur le blog développement-web-facile.com.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *