Guide pratique des jeux HTML5

Daniel X. Moore
Daniel X. Moore

Introduction

Vous voulez créer un jeu en utilisant Canvas et HTML5 ? Suivez ce tutoriel, et vous serez opérationnel en un rien de temps.

Il nécessite au moins un niveau de connaissance intermédiaire de JavaScript.

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

Créer le canevas

Pour dessiner, nous devons créer un canevas. Comme il s'agit d'un guide "No Tears", 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

Pour simuler une expérience de jeu fluide et continue, nous voulons mettre à jour le jeu et redessiner l'écran un peu plus rapidement que ce que l'œil et l'esprit humains peuvent 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. Il est important de savoir que setInterval() se charge de les appeler périodiquement.

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

Hello World

Maintenant qu'une boucle de jeu est 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 intéressant pour le texte stationnaire, mais comme nous avons une boucle de jeu déjà configurée, nous devrions pouvoir la déplacer 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. Si vous suivez le mouvement, il devrait se déplacer, mais conserver les dates d'affichage précédentes à l'écran. Prenez un moment pour comprendre pourquoi cela peut être le cas. En effet, nous n'effaçons pas l'écran. Ajoutons donc du code d'effacement de l'écran à la méthode draw.

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 véritable jeu. Il vous suffit de resserrer les commandes, d'améliorer le gameplay et de retoucher les éléments graphiques. 1/7e du chemin pour avoir un vrai jeu, mais la bonne nouvelle, c'est que le tutoriel est bien plus complexe.

Créer le lecteur

Créez un objet pour stocker les données du joueur et être responsable de certains aspects, comme les dessins. Ici, nous créons un objet "player" à 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 de couleur pour représenter le joueur. Lorsque nous dessinons le jeu, nous effaçons le canevas et nous dessinons le lecteur.

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

Commandes au clavier

Utilisation des touches d'accès rapide jQuery

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

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

Vous n'avez pas à vous soucier des informations sur les clés qui présentent des codes qui représentent une grande victoire. Nous voulons simplement pouvoir dire : "lorsque le joueur appuie sur le bouton Haut, faire quelque chose". Les raccourcis jQuery permettent de le faire.

Mouvement du joueur

La manière dont JavaScript gère les événements de clavier est entièrement basée sur les événements. Cela signifie qu'il n'existe pas de requête intégrée permettant de vérifier si une clé est indisponible. Nous devons donc utiliser la nôtre.

Vous vous demandez peut-être : "Pourquoi ne pas simplement utiliser une méthode basée sur des événements pour gérer les touches ?" C'est parce que le taux de répétition du clavier varie d'un système à l'autre et n'est pas lié à la durée de la boucle de jeu. Le gameplay peut donc considérablement varier 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, c'est que j'ai inclus un wrapper JS de 16 lignes qui permettra d'interroger des événements. Il s'agit du fichier "key_status.js". Vous pouvez interroger l'état d'une clé à tout moment en vérifiant keydown.left, etc.

Maintenant que nous pouvons vérifier si les touches sont désactivées, nous pouvons utiliser cette méthode de mise à jour simple pour déplacer le lecteur.

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é en dehors de l'écran. Nous allons limiter la position du joueur pour qu'il respecte les limites. De plus, le lecteur semble assez lent, alors accélérons aussi 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 facile, alors ajoutons une sorte de 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 des objets de jeu

Projectiles

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

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 puce et l'ajouter à l'ensemble de puces.

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 l'étape de mise à jour. Pour éviter que l'ensemble des 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 touché 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 puces.

  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();
  });
}

Chargement et dessin d'images

C'est cool de regarder toutes ces boîtes s'envoler, mais avoir des images pour elles serait encore plus cool. Le chargement et le dessin d'images sur un canevas sont généralement une expérience bâclée. Pour éviter cela, 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 contrats s'affichent à l'écran, mais ils n'interagissent pas les uns avec les autres. Afin de tout faire savoir quand exploser, nous devrons ajouter une sorte de détection de collision.

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

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;
}

Il y a quelques collisions que nous voulons vérifier:

  1. Puces 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 "update".

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 doivent alors être supprimés, et une explosion est 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 de jolis effets sonores. Les sons, comme les images, peuvent s'avérer difficiles à utiliser en HTML5, mais grâce à notre formule magique sound.js, le son peut être simplifié.

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

function Enemy(I) {
  ...

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

Bien que l'API ne présente plus de déchirures, l'ajout de sons est actuellement le moyen le plus rapide de faire planter votre application. Il n'est pas rare que les sons enlèvent ou suppriment tout l'onglet du navigateur, alors préparez vos mouchoirs.

Adieu

Pour rappel, voici la démonstration complète du jeu. Vous pouvez également télécharger le code source sous forme de fichier ZIP.

J'espère que vous avez apprécié apprendre les bases de la création d'un jeu simple en JavaScript et HTML5. En programmant un niveau d'abstraction approprié, nous pouvons nous isoler des parties les plus difficiles des API et être résilients face aux changements futurs.

Références