Un jeu HTML5 : canvas, pixi et sprites

HTML c’est le langage du web qui est né dans les années 90 et qui a subi de nombreuses évolutions au cours du temps. Je ne vais pas vous en faire l’historique mais seulement vous montrer comment utiliser ses dernières possibilités avec la version 5.

Dans ce chapitre je vais présenter l’élément canvas et la librairie Pixi.js, le chargement des images et leur transformation en textures utilisables, la création de sprites et leur manipulation. On verra enfin les structures de base du code pour l’animation.

Canvas

Les créateurs de jeux ont été les premiers à comprendre l’intérêt du HTML5, essentiellement avec l’arrivée de l’élément canvas qui permet de créer des graphismes dynamiques. Pour comprendre de quoi il s’agit prenons un exemple :

<!DOCTYPE HTML>
<html>
<head>
  <title>Cours pixi</title>
  <meta charset="UTF-8">
  <style>
    canvas {
      margin: 0 auto;
      display: block;
    }
  </style>
</head>
<body>
  <canvas id="moncanvas" width="200" height="250"></canvas>
  <script>
    var moncanvas = document.getElementById('moncanvas'),
    ctx = moncanvas.getContext('2d');

    ctx.fillStyle = 'cyan';
    ctx.strokeStyle = 'blue';
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.arc(50, 50, 35, 0, Math.PI * 2, true);
    ctx.fill();
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(150, 50, 25, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.fillStyle = 'red';
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(100, 100);
    ctx.lineTo(75, 150);
    ctx.lineTo(125, 150);
    ctx.lineTo(100, 100);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.fillStyle = 'chartreuse';
    ctx.strokeStyle = 'green';
    ctx.fillRect(40, 180, 120, 40);
    ctx.strokeRect(40, 180, 120, 40);

  </script>
</body>
</html>

img01

L’intégration dans une page HTML est des plus simples :

<canvas id="moncanvas" width="200" height="250"></canvas>

 On utilise la balise canvas  et on définit les dimensions en pixels, ce qui a pour effet de créer sur la page une zone où on va dessiner.

Je ne vais pas entrer dans le détail de ce code parce que nous n’aurons pas à utiliser directement canvas. Cet exemple a pour seul objectif de vous montrer qu’on peut facilement dessiner avec lui. Sachez aussi qu’on peut tout aussi facilement modifier dynamiquement ce qu’on dessine et vous commencerez à avoir une idée de ce qu’il est possible de réaliser ainsi.

Il est nécessaire par contre de connaître le système de coordonnées utilisé. L’origine se situe en haut à gauche :

img02

Chaque point est ainsi défini par ses coordonnées x et y.

Pixi

Pixi est une librairie JavaScript qui gère le canvas à votre place et il sait parfaitement bien le faire ! Vous trouverez cette librairie sur ce site avec des exemples et la documentation. Le code est hébergé sur Github :

img03

Cliquez sur le bouton Download ZIP pour télécharger le code. Vous obtenez plusieurs dossiers, la librairie se trouve dans le dossier bin :

img04

Vous disposez de deux versions :

  • pixi.js : contient le code lisible avec ses commentaires

  • pixi.min.js : contient le code minifié

La  première est utile pendant la phase de développement si vous voulez aller voir le code. La seconde, plus légère à charger, est à prévoir pour le déploiement.

Voici la prestation minimale pour utiliser Pixi :

<!DOCTYPE HTML>
<html>
<head>
  <title>Cours pixi</title>
  <meta charset="UTF-8">
  <script src="../js/pixi.js"></script>
  <style>
    canvas {
      margin: 0 auto;
      display: block;
    }
  </style>
</head>
<body>
  <script>
    var renderer = PIXI.autoDetectRenderer(200, 250);
    document.body.appendChild(renderer.view);
    var stage = new PIXI.Container();
    renderer.render(stage);
  </script>
</body>
</html>

 Voyons ça de plus près…

On a besoin d’un objet renderer  qui va créer le canvas, on détecte automatiquement avec la méthode autoDetectRenderer  (les dimensions par défaut sont 800 x 600) si on dispose de WebGL :

var renderer = PIXI.autoDetectRenderer(200, 250);

 WebGL est une API d’HTML5 qui permet d’utiliser le standard OpenGL ES qui présente comme principal avantage d’utiliser le processeur graphique pour l’affichage.

Ensuite on ajoute le canvas créé à la page HTML :

document.body.appendChild(renderer.view);

 On crée alors un objet conteneur qui va constituer la scène :

var stage = new PIXI.Container();

 Pour terminer on demande au renderer d’afficher la scène :

renderer.render(stage);

 Le résultat de tout cela se résume à l’affichage d’un rectangle noir puisqu’on n’a pas demandé d’afficher quelque chose en particulier :

img05

Mais on a maintenant tout ce qu’il nous faut pour le faire !

Par exemple pour obtenir le même résultat que vu ci-dessus on a besoin de ce code :

<script>
  var renderer = PIXI.autoDetectRenderer(200, 250);
  document.body.appendChild(renderer.view);
  var stage = new PIXI.Container();
  renderer.backgroundColor = 0xFFFFFF;

  var graphics = new PIXI.Graphics();

  graphics.beginFill(0x00ffff);
  graphics.lineStyle(5, 0x0000ff, 1);
  graphics.drawCircle(50, 50, 35);
  graphics.drawCircle(150, 50, 25);

  graphics.beginFill(0xff0000);
  graphics.lineStyle(5, 0x0000ff, 1);
  graphics.moveTo(100, 100);
  graphics.lineTo(75, 150);
  graphics.lineTo(125, 150);
  graphics.lineTo(100, 100);
  graphics.endFill();

  graphics.beginFill(0x7fff00);
  graphics.lineStyle(5, 0x00aa00, 1);
  graphics.drawRect(40, 180, 120, 40);
  graphics.endFill();

  stage.addChild(graphics);
  renderer.render(stage);

</script>

img01

Le code est déjà moins verbeux avec l’utilisation de méthodes spécifiques pour dessiner cercles et rectangles. Mais on va voir que ce n’est pas vraiment là que Pixi va se révéler performant ! Ne vous tracassez pas encore pour le code utilisé ici, c’est juste un exemple pour se mettre en jambes…

Le conteneur

On a utilisé dans le code ci-dessus cette ligne :

var stage = new PIXI.Container();

 On crée ainsi un conteneur pour tout ce qu’on veux afficher sur la scène. On peut avoir plusieurs conteneurs mais il en faut obligatoirement un. Pensez à ce conteneur comme une grosse boîte vide au départ dans laquelle vous allez placer tout ce dont vous avez besoin pour l’affichage. Ainsi le renderer pourra ensuite tout afficher avec :

renderer.render(stage);

 Par exemple ci dessus on a créé un objet particulier de Pixi pour le graphisme :

var graphics = new PIXI.Graphics();

 Et on l’a ensuite placé dans le conteneur :

stage.addChild(graphics);

 Donc travailler avec Pixi consiste à créer des objets pour les mettre dans le conteneur et finalement les afficher avec le renderer.

Mais que va-t-on mettre dans le conteneur ?

On a vu un exemple avec des objets graphiques construits. Mais ce qu’on va surtout y placer ce sont des sprites.

Les textures et les sprites

Pour créer un sprite il faut une image, mais pas n’importe quelle image, il faut qu’elle soit prête pour le WebGL. Pixi dispose d’un cache de texture qui permet de charger une image quelconque et de la convertir en texture utilisable. Prenons un exemple :

<script>
  var renderer = PIXI.autoDetectRenderer();
  document.body.appendChild(renderer.view);
  var stage = new PIXI.Container();

  var texture = PIXI.Texture.fromImage('img/image.png');

  texture.on('update', function() {
    var sprite = new PIXI.Sprite(texture);
    stage.addChild(sprite);
    renderer.render(stage);
  });
</script>

 On charge l’image et on la transforme en texture avec cette ligne :

var texture = PIXI.Texture.fromImage('img/image.png');

 On attend que le chargement soit terminé (avec l’événement update) et on crée le sprite avec la texture :

var sprite = new PIXI.Sprite(texture);

 Il suffit ensuite de mettre le sprite dans la boîte :

stage.addChild(sprite);

 Et finalement afficher :

renderer.render(stage);

img06

Le sprite se retrouve dans le coin supérieur gauche qui l’origine des coordonnées parce qu’on n’en a pas indiqué pour lui. On verra tout ça plus tard, pour le moment on va voir une façon plus efficace pour charger des images et les transformer en textures.

Le loader

Il n’est pas vraiment pratique de mettre en place un événement pour savoir quand les images sont chargées. Heureusement Pixi propose un loader qui se charge de tout ça. Voici le code corrigé avec utilisation du loader :

<script>
  var renderer = PIXI.autoDetectRenderer();
  document.body.appendChild(renderer.view);
  var stage = new PIXI.Container();

  PIXI.loader
    .add('img/image.png')
    .once('complete', function () {
    var sprite = new PIXI.Sprite(PIXI.loader.resources['img/image.png'].texture);
      stage.addChild(sprite);
      renderer.render(stage);
    })
    .load();
</script>

 On dispose de la méthode add  pour ajouter une ou plusieurs images et ensuite la méthode once  est appelée lorsque tout est chargé. Il suffit alors de créer le sprite en allant cherche la texture dans les ressources. Le résultat est le même :

img06

Lorsqu’on a beaucoup d’images il peut être pratique de les nommer plutôt que d’utiliser le chemin par défaut :

...
.add('titi', 'img/image.png')
.once('complete', function () {
  var sprite = new PIXI.Sprite(PIXI.loader.resources['titi'].texture);
...

 Si on charge de nombreuses images ça peut prendre un certain temps, il est possible de visualiser la progression. Voici un exemple :

<div id="message"></div>
<script>
  var renderer = PIXI.autoDetectRenderer();
  document.body.appendChild(renderer.view);
  var stage = new PIXI.Container();

  PIXI.loader
    .add([
      'img/img01.jpg',
      'img/img02.jpg',
      'img/img03.jpg',
      'img/img04.jpg',
      'img/img05.jpg'
    ])
    .on('progress', loadProgressHandler)
    .once('complete', setup)
    .load();

  function setup() {
    var sprite = new PIXI.Sprite(PIXI.loader.resources['img/img01.jpg'].texture);
    stage.addChild(sprite);
    renderer.render(stage);
  }

  function loadProgressHandler(loader, resource) {
    document.getElementById("message").innerHTML = 'Url : ' + resource.url + ', Pourcentage : ' + loader.progress;
  }
</script>

 On charge 5 images et on peut suivre la progression avec un simple texte affiché. C’est évidemment très sommaire mais au moins vous avez le principe.

Position, taille et rotation

Une fois qu’on a un sprite on peut le manipuler. En particulier on peut le positionner, changer sa taille ou sa rotation.

Position

La position d’un sprite est définie par ses propriétés x et y :

function setup() {
    ...
    sprite.x = 40;
    sprite.y = 60;
    ...
}

 img07

Ce qui peut aussi s’écrire avec cette simple ligne :

sprite.position.set(40, 60);

 Le point d’origine du sprite est en haut à gauche. On verra plus loin qu’on peut le modifier, ce qui sera intéressant pour les rotations.

Taille

Mon sprite a pour dimensions 48 x 48. Pour changer ces dimensions il suffit de modifier les propriétés width et height :

sprite.width = 30;
sprite.height = 60;

 img08

Une autre façon de procéder est d’utiliser la propriété scale qui a la valeur 1 par défaut :

sprite.scale.x = 1.2;
sprite.scale.y = 0.8;

 img09

C’est surtout cette propriété qu’on va utiliser, plutôt que la précédente. La valeur est un pourcentage des dimensions du sprite, de 0 (0%) à n (n * 100%). Dans le cas ci-dessus on a donc 120% et 80%.

Ce qui peut aussi s’écrire avec une seule ligne :

sprite.scale.set(1.5, 0.8);

Rotation

Essayons maintenant de faire tourner le sprite :

sprite.rotation = 0.5;

 img10

J’ai parlé plus haut du point de référence du sprite qui se situe dans la partie supérieure gauche, la rotation s’effectue autour de ce point.

La valeur est en radians, donc 0.5 correspond en gros à 28 degrés.

On peut changer la position du point de référence :

sprite.anchor.x = 0.5;
sprite.anchor.y = 0.5;
sprite.rotation = 0.5;

 img11

Les valeurs vont de 0 (origine) à 1 (côté opposé). Ici avec la valeur 0.5 on est pile au milieu.

 Ce qui peut aussi s’écrire avec une seule ligne :

sprite.anchor.set(0.5, 0.5);

 Action !

Pour le moment notre sprite ne bouge pas beaucoup ! Le rendu est effectué une fois pour toutes et plus rien ne change. Evidemment pour faire de l’animation on a besoin de rafraîchir régulièrement l’affichage. On va donc créer une boucle infinie. regardez cet exemple :

<script>
  var renderer = PIXI.autoDetectRenderer();
  document.body.appendChild(renderer.view);
  var stage = new PIXI.Container();

  PIXI.loader
    .add('img/image.png')
    .once('complete', setup)
    .load();

  var sprite;

  function setup() {
    sprite = new PIXI.Sprite(PIXI.loader.resources['img/image.png'].texture);
    sprite.x = 40;
    sprite.y = 60;
    sprite.anchor.set(0.5, 0.5);
    stage.addChild(sprite);
    gameLoop();
  }

  function gameLoop(){
    requestAnimationFrame(gameLoop);
    sprite.rotation += 0.01;
    renderer.render(stage);
  }
</script>

 La nouveauté par rapport à ce qu’on a vu ci-dessus est la création d’une fonction récursive gameLoop . Comme j’ai prévu de changer la valeur de la rotation en l’incrémentant de 0.01 radian à chaque fois on va avoir une rotation régulière du sprite :

img12

On a ici les bases de notre moteur de jeu. En gros :

  • on crée un canvas (renderer),

  • on ajoute le canvas à la page (appendChild),

  • on crée un conteneur (stage),

  • on charge les ressources (loader),

  • on initialise les sprites (setup) et on les met dans le conteneur (addChild),

  • on crée une boucle infinie pour faire les changements à l’affichage.

Les bases sont là mais on a encore du chemin à parcourir !

En résumé

Dans ce chapitre on a vu :

  • L’élément canvas du HTML5 qui permet de dessiner.

  • La libairie Pixi.js qui facilite l’utilisation du canvas.

  • Le conteneur de Pixi qui permet de stocker les textures.

  • La création de sprite et leur modifications : position, taille et rotation.

  • La création d’une boucle pour animer la scène.

Laisser un commentaire