image

Windows 8 Metro et les navigateurs HTML5 sont aujourd’hui des candidats sérieux pour y développer des jeux.

Avec le canvas de HTML 5, vous avez maintenant accès à un espace accéléré matériellement ou vous allez pouvoir dessiner le contenu de votre jeu et avec quelques tuyaux vous serez capable de faire des rendus à 60 images par seconde.

Cette notion de fluidité est extrêmement importante dans les jeux car plus le rendu est fluide plus le joueur aura un bon ressenti.

Le but de cet article va donc être de vous donner quelques clefs pour obtenir le maximum de performances du canvas.

Pour servir de support à cet article, j’utiliserai une démonstration basée sur le rendu d’un tunnel 2D. C’est l’adaptation d’un des exemples que j’avais utilisé lors de la Coding4Fun aux TechDays 2012 (http://video.fr.msn.com/watch/video/techdays-2012-session-technique-coding4fun/zqy7cm8l).

L’effet en lui-même est inspiré de ce que je faisais quand j’étais un jeune demomaker sur Commodore AMIGA (il y a bien longtemps).

Ecrit à l’origine sur assembleur 68000, le code utilise aujourd’hui uniquement JavaScript et le canvas:

Le code complet est bien sur à votre disposition ici : http://www.catuhe.com/msdn/canvas/tunnel.zip

Le but de cet article n’étant pas d’expliquer comment le tunnel est codé, nous allons nous concentrer sur les différentes techniques d’optimisations. 

Utiliser un canvas en mémoire pour lire les données d’une image

Le premier point dont je voudrais parler concerne l’usage du canvas comme outil intermédiaire dans le cadre de la lecture de données dans une image. En effet, dans chaque jeu, vous avez besoin d’images et de ressources graphiques pour vos fonds, vos décors, vos sprites, etc.

Le canvas pour ce faire fournit une fonction très pratique : drawImage. Cette fonction peut être utilisée pour dessiner une sprite par exemple puisque elle peut prendre un rectangle source et un rectangle destination.

Toutefois ce n’est pas forcément toujours suffisant lorsque par exemple vous voulez appliquer des effets ou lorsque vous voulez lire le contenu de vos images pour vous en servir dans votre code.

Pour tous ces cas, vous avez besoin d’accéder finalement au tableau d’octets qui compose l’image. Hélas la balise Image ne fournit pas ce genre de service. Et c’est justement là que va intervenir notre ami canvas.

En effet, l’idée ici est la suivante : se servir du canvas dont on peut avoir simplement le contenu sous la forme d’un tableau d’octets pour aller lire notre image. Pour ce faire, il faut:

  • Charger une image dans une balise idoine
  • Dessiner l’image dans un canvas mémoire
  • Récupérer le contenu du canvas
  • Et le tour est joué!

Ce qui en terme de code nous donne:

var loadTexture = function (name, then) {
    var texture = new Image();
    var textureData;
    var textureWidth;
    var textureHeight;
    var result = {};

    // lors du chargement
    texture.addEventListener('load', function () {
        var textureCanvas = document.createElement('canvas'); // canvas en mémoire uniquement

        // Mise du canvas à la bonne taille
        textureCanvas.width = this.width; //<-- "this" définit l’image
        textureCanvas.height = this.height;

        result.width = this.width;
        result.height = this.height;

        var textureContext = textureCanvas.getContext('2d');
        textureContext.drawImage(this, 0, 0);

        result.data = textureContext.getImageData(0, 0, this.width, this.height).data;

        then();
    }, false);

    // Chargement
    texture.src = name;

    return result;
};

Pour utiliser ce code, vous avez juste à tenir compte de l’asynchronisme lors du chargement de l’image en faisant passer votre code de continuation dans le paramètre then:

// Texture
var texture = loadTexture("soft.png", function () {
    // Lancement du rendu
    QueueNewFrame();
});
      

Utilisation du redimensionnement matériel

Les navigateurs modernes et Windows 8 supportent les canvas de manière accélérée par le matériel. Cela veut dire par exemple que le GPU (Graphics Processor Unit, le cœur de votre carte graphique) va être utilisée pour toutes les opérations de zoom nécessaires sur le canvas.

Dans le cas de la création de notre tunnel, l’algorithme que j’utilise nécessite un traitement spécifique pour chaque pixel. Ainsi pour un canvas faisant 1024x768, il va donc falloir traiter 786432 pixels. Et afin d’être fluide, il va falloir le faire 60 fois par seconde soit 47185920 pixels par seconde !

Il est donc évident que tout ce qui peut nous permettre de réduire le nombre de pixels à traiter améliorera dramatiquement les performances globales.

Et bien entendu le canvas va pouvoir nous y aider. Le code suivant montre comment mettre en place un redimensionnement matériel au niveau du buffer de travail interne du canvas:

// Mise en place du redimensionnement matériel
canvas.width = 300;
canvas.style.width = window.innerWidth + 'px';
canvas.height = 200;
canvas.style.height = window.innerHeight + 'px';

Il est bon de noter ici la différence entre la taille du contrôle HTML positionné dans le DOM (canvas.style.width et canvas.style.height) et la taille du buffer de travail interne du canvas (canvas.width et canvas.height).

Lorsque ces deux tailles ne sont pas identiques, la carte graphique va effectuer un redimensionnement matériel en ajoutant un petit lissage de bon aloi. Ainsi dans notre exemple, nous générons une image en 300x200 qui sera automatiquement retaillée pour couvrir toute la taille de la fenêtre.

Cette fonctionnalité est bien gérée par Windows 8 et les navigateurs modernes et va vous permettre de gagner beaucoup de performances.

Optimiser votre boucle de rendu

Lorsque l’on écrit un jeu, il y a très souvent besoin d’avoir une boucle de rendu où l’on dessine les composants du jeu (fonds d’écran, décors, sprites, etc.). Cette boucle constitue l’épine dorsale et doit être turbo-optimisée pour être sur que votre jeu soit le plus fluide possible.

RequestAnimationFrame

Une des fonctionnalités intéressantes introduites par HTML5 est la fonction window.requestAnimationFrame. Au lieu d’utiliser window.setInterval pour créer un timer qui va bêtement essayer de faire exécuter la boucle de rendu toutes les (1000/60 ~ 16.7) millisecondes, il est désormais possible de déléguer la responsabilité de cadencer notre boucle au navigateur.

Avec window.requestAnimationFrame, vous allez indiquer que vous souhaitez être appelé dès que possible. Le navigateur (ou Windows 8) va alors vous inclure dans son cadencement en synchrone avec ses propres opérations de dessins et d’animations (CSS, transitions, etc.). Cette solution est d’autant plus intéressante qu’elle garantie que votre code est appelé aussi souvent que possible et n’est pas appelé si ce n’est pas nécessaire (fenêtre cachée, réduite ou autre onglet sélectionné).

Le navigateur va aussi pouvoir optimiser les rendus concurrents (si votre boucle est trop lente par exemple) et va donc permettre une meilleure fluidité.

Le code associé est relativement simple si l’on omet la gestion des préfixes des fournisseurs :

var intervalID = -1;
var QueueNewFrame = function () {
    if (window.requestAnimationFrame)
        window.requestAnimationFrame(renderingLoop);
    else if (window.msRequestAnimationFrame)
        window.msRequestAnimationFrame(renderingLoop);
    else if (window.webkitRequestAnimationFrame)
        window.webkitRequestAnimationFrame(renderingLoop);
    else if (window.mozRequestAnimationFrame)
        window.mozRequestAnimationFrame(renderingLoop);
    else if (window.oRequestAnimationFrame)
        window.oRequestAnimationFrame(renderingLoop);
    else {
        QueueNewFrame = function () {
        };
        intervalID = window.setInterval(renderingLoop, 16.7);
    }
};

Cette fonction est ensuite à appeler à la fin de votre boucle de rendu pour requérir un nouveau rendu dès que possible:

var renderingLoop = function () {
    ...

QueueNewFrame(); };
      

Accéder au DOM (Document Object Model)

Pour optimiser la boucle de rendu, il y a au moins une règle d’or : NE PAS ACCEDER AU DOM. Même si les navigateurs modernes sont de plus en plus optimaux sur ce sujet, lire ou modifier les propriétés d’objets du DOM va ralentir votre rendu.

Par exemple si on prend le code du tunnel, en utilisant le profileur d’Internet Explorer 10 (disponible dans la barre F12 de développement), on peut voir ce genre de choses :

image_thumb5

On peut donc noter qu’accéder aux propriétés width et height de notre canvas nous coute très cher.

Sachant que le code d’origine était:

var renderingLoop = function () {


    for (var y = -canvas.height / 2; y < canvas.height / 2; y++) {
        for (var x = -canvas.width / 2; x < canvas.width / 2; x++) {

            ...

        }
    }
};

Il suffit de remplacer canvas.width et canvas.height par des variables préalablement remplies:

var renderingLoop = function () {

    var index = 0;
    for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
        for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
            ...
        }
    }
};

Carrément simple, non? Et je peux vous dire que même si parfois cela peut être complexe à mettre en place, le résultat vaut le coup (voir le tableau plus bas).

Pré-calcul

Selon le profileur, la fonction Math.atan2 consomme un peu trop de puissance. Cela s’explique par le fait que cette fonction n’est pas une fonction native du processeur et que donc le moteur JavaScript doit la calculer et ce calcul est un peu fastidieux.

image_thumb8

Nous allons donc pré-calculer les résultats de cette fonction. Et d’une manière générale, dès que c’est possible il est intéressant de pré-calculer tout ce qui peut prendre du temps dans nos boucles. Ainsi pour le tunnel, avant de lancer le rendu, il suffit de faire un tableau et de mettre dedans le résultat voulu:

// pré-calcul de l’arctangent
var atans = [];

var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
    for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
        atans[index++] = Math.atan2(y, x) / Math.PI;
    }
}

Par la suite, plutôt que de faire appel à Math.atan2, je ferai juste une lecture dans mon tableau!

Evitez d’utiliser Math.round, Math.floor et parseInt

Le dernier point important issu de mon profilage concerne l’utilisation de parseInt:

image_thumb11

En effet, lorsque l’on se sert des canvas, il faut bien à un moment donné préciser des coordonnées qui sont des valeurs discrètes au format entier donc (x et y). Toutefois il y a de grandes chances que vos calculs eux soient fait en valeurs flottantes (physique, collisions, déplacement des sprites, etc.). Il faut donc bien convertir ces valeurs en entiers.

JavaScript fournit les fonctions Math.round, Math.floor ou bien encore parseInt pour effectuer cette conversion. Toutefois ces fonctions effectuent plus qu’une simple conversion (elles vérifient la source, vérifient les dépassements et dans le cas du parseInt convertissent la valeur d’entrée en chaine !). Et ce n’est pas une bonne chose dans le cadre d’une boucle de rendu.

En me souvenant de mon vieux code assembleur, j’ai retrouvé une petite astuce: Plutôt que de faire cette conversion couteuse on peut faire un décalage de bits mais sur une valeur de 0. L’idée ici est que l’opérateur >> (ou << ou | ou &) ne fonctionne que sur des registres entiers donc l’appliquer à une valeur flottante force le système à déplacer la valeur d’un registre flottant vers un registre entier et donc il doit faire une conversion matérielle extrêmement rapide. Le fait de faire cela sur 0 n’entraine aucune autre modification et le tour est joué Sourire

Donc plutôt que de faire cela :

u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u);

Il est bien plus efficace de faire cela :

u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u) >> 0;

Bien évidemment cette solution nécessite que vous soyez bien sur que votre valeur est un nombre correct Sourire.

Résultat final

Appliquer toutes les optimisations nous donne ce résultat dans le profiler :

image

Donc pour mémoire voici d’ou nous sommes partis :

Et après avoir appliqué toutes nos optimisations, voici le résultat définitif :

Voici également, l’impact de chacune des optimisations sur mon Internet Explorer 10 (Consumer Preview) :

image_thumb1

Aller plus loin

Avec ces quelques points clefs à l’esprit, vous allez pouvoir profiter pleinement des canvas pour faire de superbes jeux bien fluides pour Windows 8 ou pour les navigateurs modernes !