Modernisez vos jeux HTML5 canvas partie 1: mise à l’échelle matérielle & CSS3

Les dernières versions des navigateurs commencent à implémenter des versions stables de certaines spécifications HTML5 particulièrement intéressantes comme Offline APIs, Drag’n’drop & File APIs. Ces nouvelles fonctionnalités offrent aux développeurs web de nouveaux scénarios de jeux et nous mène vers une nouvelle ère d’applications web. De notre côté, IE10 les implémente toutes et les mêmes fonctionnalités sont présentes également au sein des applications Windows 8 HTML5 au style Metro. D’un point de vue graphique, le moteur d’IE10 offre une couche d’accélération matérielle pouvant être bien utile pour gérer la mise à l’échelle vers différentes résolutions ainsi que pour jouer des animations de manière fluide. Nous nous concentrerons d’ailleurs en premier lieu sur la partie graphique dans cet article en utilisant CSS3 Grid, CSS3 Transition, CSS3 3D Transform.  

Dans ces 2 articles, nous allons voir comment j’ai utilisé ces nouvelles spécifications pour moderniser mon jeu HTML5 précédent que j’ai nommé HTML5 Platformer. J’espère que ces 2 tutoriaux vous aideront à imaginer de nouvelles choses pour vos propres jeux :

- Modernisez vos jeux HTML5 canvas partie 1: mise à l’échelle matérielle & CSS3 (cet article)
- Modernisez vos jeux HTML5 canvas partie 2: Offline APIs, File APIs & Drag’n’drop APIs (prochain article)

Note : vous trouverez l’URL de la démo complète pour la jouer au sein de votre navigateur préféré à la fin de cet article ainsi qu’une vidéo du résultat dans IE10. Le code source sera disponible au téléchargement à la fin du prochain article. Le jeu a été testé avec succès sous IE10, Firefox 11, Chrome 18 et Opera 12.

Gérer la mise à l’échelle sur tous les périphériques

Si vous développez un jeu en HTML5, c’est probablement parce que vous êtes intéressé par la nature cross-plateformes de ce standard. Mais faire tourner du code sur une grande variété de périphériques soulève rapidement la question de la gestion de leurs multiples résolutions. Or, comparé à SVG, Canvas peut apparaitre rapidement comme mal préparé à ce scénario.

Heureusement, avec des jeux dit “casuals” basés sur des sprites, il y a une solution simple que vous pouvez implémenter qui apporte un niveau de qualité graphique souvent satisfaisant. Cela a d’ailleurs été très bien décrit par mon ami David Catuhe sur son blog ici: Libérez la puissance du canvas de HTML5 pour vos jeux dans la section “utilisation du redimensionnement matériel”.

L’idée est plutôt simple et efficace. Vous travaillez dans un canvas de taille fixe avec une résolution prédictible et vous l’étirez à la résolution courante en jouant sur les propriétés de canvas.style.

Etape 1: étirer

Dans mon cas, avec mon petit jeu de plateforme HTML5, les ressources graphiques et la logique du jeu ont été pensées pour une résolution de 800x480. Donc si je veux remplir un écran 1080p ou une tablette de 1366x768, j’aurais besoin de fabriquer des sprites d’une résolution plus haute. Sinon, on peut mettre en place une opération de mise à l’échelle qui, combinée à un bon anti-crénelage, peut apporter suffisamment de qualité. Ok, testons cette solution !

Cette opération peut être simplement réalisée grâce à ce code :

 window.addEventListener("resize", OnResizeCalled, false);

function OnResizeCalled() {
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
}

Ou de manière encore plus simple via cette règle CSS :

 #canvas 
{
    width: 100%;
    height: 100%;
}

Mais vous comprendrez plus tard pourquoi je m’enregistre au près de l’évènement resize.

A part ça, c’est tout ! Grâce aux nouveaux navigateurs utilisant l’accélération matérielle, cette opération est effectuée par votre GPU gratuitement. Vous aurez même un anti-crénelage d’activé. En effet, l’opération de mise à l’échelle est simplement faite par le GPU grâce aux 100% appliqués aux propriétés width & height. Les navigateurs récents s’occuperont également d’appliquer un filtre d’anti-crénelage pour vous lors de cette mise à l’échelle. C’est pourquoi mon collègue David Catuhe en parlait dans son article dédié aux performances de canvas.

D’ailleurs, cela n’est pas spécifique à HTML5 au passage. La plupart des jeux tournant sur les consoles modernes ne sont pas calculés en interne en 720p ou 1080p. La quasi-totalité d’entre eux sont rendus dans des résolutions plus petites (comme 1024x600 par exemple) et ils laissent la puce vidéo faire le processus de mise à l’échelle/anti-crénelage. Cela peut vous aider à augmenter les FPS dans la plupart des cas mais baisse légèrement la qualité graphique en contre partie.

Cependant, se contenter de faire cela pose un problème de ratio. En effet, quand mon canvas avait une résolution fixe de 800x480, son ratio était naturellement fixé et sous contrôle. Mais maintenant, lorsque je redimensionne la fenêtre du navigateur, je peux obtenir ce genre de résultats pour le moins étrange :

imageimage

Le jeu reste jouable mais l’expérience est loin d’être optimale.

Etape 2: contrôler le ratio

L’idée ici est de contrôler la manière de remplir l’écran lorsque la fenêtre du navigateur sera redimensionnée. Plutôt que d’étirer complètement sur toute la largeur et la hauteur, nous allons ajouter de l’espace à droite si la fenêtre est trop large ou en dessous si la fenêtre est trop haute. Cela est effectué grâce à ce code dans mon cas :

 var gameWidth = window.innerWidth;
var gameHeight = window.innerHeight;
var scaleToFitX = gameWidth / 800;
var scaleToFitY = gameHeight / 480;

var currentScreenRatio = gameWidth / gameHeight;
var optimalRatio = Math.min(scaleToFitX, scaleToFitY);

if (currentScreenRatio >= 1.77 && currentScreenRatio <= 1.79) {
    canvas.style.width = gameWidth + "px";
    canvas.style.height = gameHeight + "px";
}
else {
    canvas.style.width = 800 * optimalRatio + "px";
    canvas.style.height = 480 * optimalRatio + "px";
}

J’ai ajouté une exception grâce au “if”. En effet, si vous pressez la touche F11 de votre navigateur pour basculer en plein écran et que vous utilisez un écran 16/9 (comme mon écran 1920x1080 de mon Vaio Z ou 1366x768 d’une tablette Samsung), le jeu sera entièrement étiré. J’ai trouvé que le résultat fournissait une expérience super cool. Clignement d'œil

A part cette exception, voici le type de résultat que vous devriez avoir:

image image

Remarquez les bandes noires en dessous et sur la droite du jeu. Elles sont ici pour contrôler notre ratio. Mais ça serait encore plus cool si le jeu était centré pour avoir un effet type écran large/cinémascope non?

Etape 3: centrer le jeu avec CSS3 Grid

Centrer un élément HTML peut être une tâche compliquée dans certains scénarios. Il y a plusieurs façons d’opérer et vous trouverez des tas de ressources bien faites sur le web pour vous aider. J’aimerais utiliser ici une nouvelle spécification nommée CSS Grid Layout actuellement uniquement supportée par IE10 et qui représente la base de notre nouvelle mise en page Metro dans Windows 8. Grâce à cette nouvelle spécification, centrer un élément est l’enfance de l’art. Il vous suffit de basculer le display du conteneur de votre élément en display:grid, de définir 1 colonne et 1 ligne puis de centrer l’élément intérieur avec les propriétés column-align et row-align.

Voici le CSS utilisé dans mon cas:

 .canvasHolder {
    width: 100%;
    height: 100%;
    display: -ms-grid;
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr;
}

#platformerCanvas {
    -ms-grid-column: 1;
    -ms-grid-row: 1;
    -ms-grid-column-align: center;
    -ms-grid-row-align: center;
}

Vous aurez remarqué que c’est actuellement préfixé par “-ms” pour IE10. Mozilla a récemment annoncé qu’ils supporteront également la spécification dans Firefox en 2012. C’est une excellente nouvelle ! En attendant, cette solution ne marche que sous IE10 et voici le type de résultat que vous aurez avec ce navigateur :

imageimageimage

Dans IE10, comme avec votre écran large, vous aurez ainsi des barres verticales ou horizontales en fonction de la taille de la fenêtre. Sur les autres navigateurs, vous aurez le même résultat qu’à l’étape 2 car la spécification CSS3 Grid Layout sera simplement ignorée.

Mise en place d’animations fluides

Maintenant que nous gérons de multiples résolutions possibles via une opération de mise à l’échelle, il serait aussi sympa de jouer une transition fluide lorsque l’utilisateur redimensionne la fenêtre. Il serait aussi cool d’avoir une animation sympatôche entre chaque niveau de jeu. Pour cela, nous ferons usage de CSS3 Transitions & CSS3 3D Transforms. Cela a le bon gout d’être à nouveau accéléré matériellement par la plupart des plateformes.

Animation des changements effectués sur chacune des propriétés du canvas

CSS3 Transitions est facile à utiliser et produit des animations efficaces & fluides. Si vous souhaitez découvrir comme les utiliser, vous pouvez lire l’excellente introduction de mon collègue David: Introduction aux transitions CSS3 ou jouez avec directement sur notre site IE Test Drive:  Hands On transitions.

Dans mon cas, j’ai mis en place une transition globale sur l’ensemble des propriétés de mon canvas grâce à ces règles:

 #platformerCanvas {
    -ms-grid-column: 1;
    -ms-grid-row: 1;
    -ms-grid-column-align: center;
    -ms-grid-row-align: center;

    -ms-transition-property: all;
    -ms-transition-duration: 1s;
    -ms-transition-timing-function: ease;
}

Le canvas avec l’identifiant “platformerCanvas” s’occupera maintenant automatiquement de prendre en compte chacun des changements effectués à ses propriétés via une animation d’1 seconde via une fonction “d’easing”. Désormais, grâce à nette nouvelle règle, si vous redimensionnez la fenêtre du navigateur, une animation sera jouée pour réduire/agrandir la taille du canvas. J’adore l’effet que cela produit. Et tout cela en n’ajoutant finalement que 3 lignes de CSS! Les transitions CSS sont vraiment épatantes pour cela.

Note: j’ai également ajouté dans mon code les prefixes –moz, –webkit & –o pour Mozilla, Webkit & Opera afin d’être compatible avec le plus grand nombre de navigateurs. N’oubliez jamais de faire la même chose et ne laissez pas un navigateur sur le carreau!

Mise en place d’une animation cool entre chaque niveau

J’aimerais maintenant utiliser CSS3 3D Transform pour faire disparaitre temporairement mon canvas. Cela se fera via une animation d’une rotation de 90 degrés sur l’axe des Y. Ensuite, nous chargerons le niveau suivant une fois que l’animation aura atteinte les 90° pour finalement revenir à l’état initial de 0°. Pour mieux comprendre l’effet que j’essaie péniblement de vous décrire à l’écrit, le mieux est de jouer avec notre démo Hands On 3D Transforms sur notre site IE Test Drive:

image

Nous allons également jouer avec la propriété “scale” en conjonction de la propriété “rotateY” pour fabriquer notre animation. Pour cela, je vous propose d’ajouter ces 2 règles CSS visant 2 classes particulières:

 .moveRotation
{
    -ms-transform: perspective(500px) rotateY(-90deg) scale(0.1);
    -webkit-transform: perspective(500px) scale(0);
    -moz-transform: perspective(500px) rotateY(-90deg) scale(0.1);
    -o-transform: scale(0);
}

.initialRotation
{
    -ms-transform: perspective(500px) rotateY(0deg) scale(1);
    -webkit-transform: perspective(500px) scale(1);
    -moz-transform: perspective(500px) rotateY(0deg) scale(1);
    -o-transform: scale(1);
}

Mon canvas aura alors la classe initialRotation en 1er lieu:

 <canvas id="platformerCanvas" width="800" height="480" class="initialRotation"></canvas>

Maintenant l’idée est d’attendre que le joueur ait gagné le niveau courant. Une fois fait, nous allons changer la classe du canvas de initialRotation à moveRotation. Cela aura pour effet de déclencher automatiquement la transition CSS mise en place précédemment et travaillant sur toutes les propriétés.

Afin de savoir si l’animation est terminée, il y a un évènement qui est levé. Il est nommé différemment dans chaque navigateur pour l’instant. Voici mon code s’occupant de s’enregistrer à cet évènement et visant IE10, Firefox, Webkit & Opera:

 // Registering to the various browsers vendors transition end event
PlatformerGame.prototype.registerTransitionEndEvents = function () {
    // IE10, Firefox, Chrome & Safari, Opera
    this.platformerGameStage.canvas.addEventListener("MSTransitionEnd", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("transitionend", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("webkitTransitionEnd", onTransitionEnd(this));
    this.platformerGameStage.canvas.addEventListener("oTransitionEnd", onTransitionEnd(this));
};

Et voici le morceau de code qui sera rappelé:

 // Function called when the transition has ended
// We're then loading the next level
function onTransitionEnd(instance) {
    return function () {
        if (instance.loadNextLevel === true) {
            instance.LoadNextLevel();
        }
    }
};

Pour finir, voici le code de mon jeu qui positionne la classe moveRotation sur le canvas:

 // Perform the appropriate action to advance the game and
// to get the player back to playing.
PlatformerGame.prototype.HandleInput = function () {
    if (!this.wasContinuePressed && this.continuePressed) {
        if (!this.level.Hero.IsAlive) {
            this.level.StartNewLife();
        }
        else if (this.level.TimeRemaining == 0) {
            if (this.level.ReachedExit) {
                // If CSS3 Transitions is supported
                // We're using smooth & nice effects between each levels
                if (Modernizr.csstransitions) {
                    this.loadNextLevel = true;
                    // Setting the moveRotation class will trigger the css transition
                    this.platformerGameStage.canvas.className = "moveRotation";
                }
                // If CSS3 Transition is not supported, we're jumping directly
                // to the next level
                else {
                    this.LoadNextLevel();
                }
            }
            else
                this.ReloadCurrentLevel();
        }
        this.platformerGameStage.removeChild(statusBitmap);
        overlayDisplayed = false;
    }

    this.wasContinuePressed = this.continuePressed;
};

Vous remarquerez que j’utilise Modernizr pour faire de la détection de fonctionnalité des transition CSS. Ensuite, nous remettons finalement en place la classe initialRotation dans la fonction LoadNextLevel :

 // Loading the next level 
PlatformerGame.prototype.LoadNextLevel = function () {
    this.loadNextLevel = false;
    // Setting back the initialRotation class will trigger the transition
    this.platformerGameStage.canvas.className = "initialRotation";
    // ... loadNextLevel logic stuff...
};

Note: vous aurez peut-être remarqué que je n’utilise pas la même transformation (et donc la même animation) pour WebKit que pour IE10 et Firefox dans le bloc de CSS précédent. C’est dû au fait que Chrome ne se comporte pas de la même manière qu’IE10 et Firefox 11 dans mon cas. J’ai poussé une petite repro sur jsFiddle ici: https://jsfiddle.net/5H8wg/2/ si vous souhaitez y jeter un œil. Opera ne supporte pas CSS3 3D Transform pour le moment.

Vidéo & URL pour jouer avec la démo

Voici une courte vidéo vous démontrant le résultat de toutes ces fonctionnalités au sein d’IE10:






Poster Image

Download Video: MP4, WebM, HTML5 Video Player by VideoJS

Vous pouvez aussi jouer avec cette démo dans IE10 ou votre navigateur préféré ici: Modern HTML5 Platformer

image

Dans le 2ème article, nous allons voir comment j’ai implémenté les API gérant le mode déconnecté pour faire fonctionner le jeu même en l’absence de connexion réseau. Nous verrons aussi comme j’ai utilisé les API de manipulation de fichiers et de la gestion du glisser/déposer pour implémenter une fonctionnalité amusante.

David