Guide pratique des jeux HTML5

Daniel X. Moore
Daniel X. Moore

Introduction

Vous souhaitez créer un jeu à l'aide de Canvas et de HTML5 ? Suivez ce tutoriel et vous serez prêt en un rien de temps.

Ce tutoriel suppose que vous disposez au moins de connaissances intermédiaires en JavaScript.

Vous pouvez d'abord jouer au jeu ou accéder directement à l'article et afficher le code source du jeu.

Créer le canevas

Pour dessiner des éléments, nous devons créer un canevas. Comme il s'agit d'un guide sans pleurs, nous allons utiliser jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Boucle de jeu

Afin de simuler un gameplay fluide et continu, nous souhaitons mettre à jour le jeu et redessiner l'écran juste plus vite que l'esprit et l'œil humain ne peuvent le percevoir.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Pour l'instant, nous pouvons laisser les méthodes de mise à jour et de dessin vides. L'important à savoir est que setInterval() s'occupe de les appeler régulièrement.

function update() { ... }
function draw() { ... }

Hello World

Maintenant que nous avons une boucle de jeu en cours, mettons à jour notre méthode de dessin pour dessiner du texte à l'écran.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

C'est plutôt bien pour le texte fixe, mais comme nous avons déjà configuré une boucle de jeu, nous devrions pouvoir le faire bouger assez facilement.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Essayez maintenant. Si vous suivez, il devrait bouger, mais aussi conserver les précédentes fois où il a été dessiné à l'écran. Prenez quelques instants pour deviner pourquoi. En effet, nous ne nettoyons pas l'écran. Ajoutons du code de nettoyage de l'écran à la méthode de dessin.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Maintenant que du texte se déplace à l'écran, vous êtes à mi-chemin d'un vrai jeu. Il suffit de renforcer les commandes, d'améliorer le gameplay et d'ajuster les graphismes. Ok, peut-être un septième du chemin vers un vrai jeu, mais la bonne nouvelle est que le tutoriel est bien plus complet.

Créer le lecteur

Créez un objet pour stocker les données du joueur et être responsable de tâches telles que le dessin. Ici, nous créons un objet joueur à l'aide d'un littéral d'objet simple pour contenir toutes les informations.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Pour le moment, nous utilisons un simple rectangle coloré pour représenter le joueur. Lorsque nous dessinons le jeu, nous effaçons le canevas et dessinons le joueur.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Commandes au clavier

Utiliser les raccourcis jQuery

Le plug-in jQuery Hotkeys facilite grandement la gestion des touches dans les navigateurs. Plutôt que de pleurer sur des problèmes keyCode et charCode inter-navigateurs indéchiffrables, nous pouvons lier des événements comme suit:

$(document).bind("keydown", "left", function() { ... });

Le fait de ne pas avoir à se soucier des détails des clés associées aux codes est un avantage considérable. Nous voulons simplement pouvoir dire des choses comme "Lorsque le joueur appuie sur le bouton "Haut", faites quelque chose". jQuery Hotkeys permet de le faire facilement.

Mouvement du joueur

La façon dont JavaScript gère les événements de clavier est entièrement basée sur les événements. Cela signifie qu'il n'existe aucune requête intégrée pour vérifier si une clé est en panne. Nous devrons donc utiliser la nôtre.

Vous vous demandez peut-être pourquoi ne pas simplement utiliser une approche basée sur les événements pour gérer les touches ? En effet, la fréquence de répétition du clavier varie d'un système à l'autre et n'est pas liée au timing de la boucle de jeu. Le gameplay peut donc varier considérablement d'un système à l'autre. Pour créer une expérience cohérente, il est important que la détection des événements de clavier soit étroitement intégrée à la boucle de jeu.

La bonne nouvelle est que j'ai inclus un wrapper JS de 16 lignes qui permettra de mettre à disposition la requête d'événements. Il s'appelle key_status.js et vous pouvez interroger l'état d'une clé à tout moment en vérifiant keydown.left, etc.

Maintenant que nous pouvons interroger si des touches sont enfoncées, nous pouvons utiliser cette méthode de mise à jour simple pour déplacer le joueur.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Allez-y, essayez.

Vous remarquerez peut-être que le lecteur peut être déplacé hors de l'écran. Limitons la position du joueur pour qu'il reste dans les limites. De plus, le lecteur semble un peu lent. Nous allons donc aussi augmenter la vitesse.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Ajouter d'autres entrées sera tout aussi simple. Ajoutons donc des projectiles.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Ajouter d'autres objets de jeu

Projectiles

Ajoutons maintenant les projectiles. Tout d'abord, nous avons besoin d'une collection pour les stocker tous:

var playerBullets = [];

Nous avons ensuite besoin d'un constructeur pour créer des instances de puces.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Lorsque le joueur tire, nous devons créer une instance de balle et l'ajouter à la collection de balles.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Nous devons maintenant ajouter la mise à jour des puces à la fonction de mise à jour de l'étape. Pour éviter que la collection de puces ne se remplisse indéfiniment, nous filtrons la liste des puces pour n'inclure que les puces actives. Cela nous permet également de supprimer les balles qui ont heurté un ennemi.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

La dernière étape consiste à dessiner les puces:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Ennemis

Il est maintenant temps d'ajouter des ennemis de la même manière que nous avons ajouté les balles.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Charger et dessiner des images

C'est sympa de voir toutes ces boîtes voler, mais il serait encore plus intéressant de les voir avec des images. Le chargement et le dessin d'images sur un canevas sont généralement une expérience éprouvante. Pour éviter cette souffrance et cette misère, nous pouvons utiliser une classe utilitaire simple.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Détection de collision

Tous ces éléments volent à l'écran, mais ils n'interagissent pas les uns avec les autres. Pour que tout le monde sache quand exploser, nous devons ajouter une sorte de détection de collision.

Utilisons un algorithme de détection de collision rectangulaire simple:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Nous souhaitons vérifier quelques collisions:

  1. Balles du joueur => Vaisseaux ennemis
  2. Joueur => Vaisseaux ennemis

Créons une méthode pour gérer les collisions que nous pouvons appeler à partir de la méthode de mise à jour.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Nous devons maintenant ajouter les méthodes d'explosion au joueur et aux ennemis. Ils seront alors signalés pour suppression et une explosion sera ajoutée.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Son

Pour compléter l'expérience, nous allons ajouter quelques effets sonores sympas. Les sons, comme les images, peuvent être difficiles à utiliser en HTML5, mais grâce à notre formule magique sans larmes sound.js, les sons peuvent être extrêmement simples.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Bien que l'API soit désormais sans larmes, ajouter des sons est actuellement le moyen le plus rapide de planter votre application. Il n'est pas rare que les sons soient coupés ou que l'intégralité de l'onglet du navigateur soit bloqué. Préparez donc vos mouchoirs.

Farewell

Pour rappel, voici la démo complète du jeu fonctionnel. Vous pouvez également télécharger le code source au format ZIP.

J'espère que vous avez apprécié découvrir les bases de la création d'un jeu simple en JavaScript et HTML5. En programmant au bon niveau d'abstraction, nous pouvons nous protéger des parties les plus difficiles des API, et être résilients face aux futures modifications.

Références