HTML5 Platformer: portage complet du jeu XNA vers

grâce à EaselJS

Après quelques heures passées sur le clavier à coder en JavaScript, j’ai finalement complètement porté l’exemple de jeu Platformer issu des samples XNA 4.0 vers HTML5/Canvas à l’aide du framework EaselJS. Vous trouverez dans cet article le jeu en lui-même ainsi qu’un petit retour d’expérience sur les parties qui m’ont fait me poser le plus de questions. Si vous cherchez à savoir comment fonctionne le jeu en interne, le mieux est de tout simplement en lire le code. Il est disponible au téléchargement à la fin de cet article. Avant de rentrer dans le vif du sujet, j’aimerais attirer votre attention sur mes motivations principales à l’écriture de ce jeu. Je cherchais tout simplement à mieux apprendre JavaScript en écrivant du code purement JS (sans aucune forme de dépendance avec le DOM). Je cherchais également à écrire un jeu fonctionnant sur tous les navigateurs ainsi que sur les périphériques “compatible” HTML5 lorsque cela est possible. Cela vous aidera peut-être à mieux comprendre certains choix que j’ai effectués.

Vous pouvez jouer au jeu directement au sein de cette iframe (avec les flèches droite et gauche & W pour sauter). Il vous suffit d’appuyer sur le bouton “Start” pour lancer la séquence de téléchargement des ressources puis du jeu lui-même.

Vous pouvez également jouer au jeu dans une fenêtre séparée en suivant ce lien : HTML5 Platformer

Note 1: ce jeu a d'abord été publié en septembre 2011 avec EaselJS 0.3.2 et a été mis à jour pour EaselJS 0.4. Vous pouvez désormais tester l’usage de requestAnimationFrame avec les navigateurs le supportant pour vérifier si cela augmente vos performances ou non. Il vous suffit de cocher/décocher la case ci-dessus pour voir le résultat en temps réel.

Note 2: ce jeu a été testé avec succès sous IE9/10, Chrome 17, Firefox 11, IE9 sur Windows Phone et sous iPad 2. Le jeu ne fonctionne pas sous Opera 11.61 à cause d’un bug sur leur implémentation native de l’objet Image apparemment: About Issue in latest Opera. Pour finir, pour pouvoir bien jouer au jeu, un navigateur disposant d’une bonne couche d’accélération matérielle est conseillé. J’ai ainsi 60 images/seconde sous IE9/10 sur mon Sony Vaio Z13.

Vous trouverez à la fin de cet article le code source complet non compressé (minified) au sein d’une archive ZIP à télécharger. J’ai essayé de commenter le code autant que possible pour le rendre le plus explicite possible. J’espère que vous apprendrez des choses intéressantes en le lisant. Si vous avez d’ailleurs des retours ou suggestions à faire dessus, n’hésitez pas à les faire à travers les commentaires de ce billet de blog.

Pour rappel, j’ai également déjà écrit 2 articles abordant les bases de la logique se trouvant derrière ce jeu:

- Jeux HTML5: animation de sprites dans l’élément Canvas grâce à EaselJS 
- Jeux HTML5: construction des objets principaux & gestion des collisions avec EaselJS

Et je suis ensuite allé plus loin avec le même jeu à travers 3 autres articles :

- Tutorial: créez des applications avec HTML5 sur Windows Phone grâce à PhoneGap où je vous montre comment porter ce jeu sous PhoneGap
- Modernisez vos jeux HTML5 canvas partie 1: mise à l’échelle matérielle & CSS3 où j’utilise CSS3 3D Transform, Transition & Grid Layout pour améliorer l’expérience de jeu
- Modernisez vos jeux HTML5 canvas partie 2: Offline API, Drag’n’drop & File API où je rend le jeu fonctionnel même en mode déconnecté

Construction du gestionnaire de téléchargements

Dans mon article précédent, le gestionnaire de téléchargements ne notifiait pas l’utilisateur du niveau de progression globale des téléchargements. C’était très mal. Heureusement, l’ajout de cette fonctionnalité fut un jeu d’enfant. Vous n’avez en effet qu’à ajouter une méthode tick() à votre objet and mettre à jour un élément de type Text présent dans le Stage d’EaselJS pour afficher la progression. Vous pouvez le voir en lisant le code du fichier ContentManager.js.

Par ailleurs, mon objectif étant de rendre le jeu compatible avec tous les navigateurs, j’ai encodé la musique et les éléments sonores au format MP3 et OGG. Si vous souhaitez en savoir davantage sur cette histoire de codec autour de la balise audio d’HTML5, je vous recommande chaudement la lecture de l’article de mon collègue Stanislas: HTML5 - ce qu il faut savoir sur la balise Audio et également son cahier de vacances HTML5 sur audio/video: Cahiers de vacances HTML5

Bon du coup, de mon côté, je télécharge soit la version MP3 soit la version OGG en utilisant le code suivant :

 var canPlayMp3, canPlayOgg;
var audioExtension = ".none";

// Need to check the canPlayType first or an exception // will be thrown for those browsers that don't support it var myAudio = document.createElement('audio');

if (myAudio.canPlayType) {
    // Currently canPlayType(type) returns: "", "maybe" or "probably" canPlayMp3 = !!myAudio.canPlayType && "" != myAudio.canPlayType('audio/mpeg');
    canPlayOgg = !!myAudio.canPlayType && "" != myAudio.canPlayType('audio/ogg; codecs="vorbis"');
}

if (canPlayMp3)
    audioExtension = ".mp3";
else if (canPlayOgg) {
    audioExtension = ".ogg";
}

// If the browser supports either MP3 or OGG if (audioExtension !== ".none") {
    SetAudioDownloadParameters(this.globalMusic, "sounds/Music" + audioExtension);
    SetAudioDownloadParameters(this.playerKilled, "sounds/PlayerKilled" + audioExtension);
    SetAudioDownloadParameters(this.playerJump, "sounds/PlayerJump" + audioExtension);
    SetAudioDownloadParameters(this.playerFall, "sounds/PlayerFall" + audioExtension);
    SetAudioDownloadParameters(this.exitReached, "sounds/ExitReached" + audioExtension);
}

J’ai passé un peu plus de temps à essayer de trouver la meilleure façon de précharger ou de télécharger les fichiers audio avant de démarrer le jeu. Dans le cas des éléments de type image, on ne peut tout simplement pas démarrer le jeu si nous n’avons pas entièrement téléchargé toutes les images localement. Sinon, tenter dessiner sur le canvas avec des images non encore entièrement présentes généra une belle exception.

Pour les éléments audio, mon souhait était d’avoir également entièrement le fichier son pour être sûr de bien le jouer le moment venu. Si le joueur meurt, je ne veux pas que le son soit soit joué 2 secondes après sa mort. Il faut pas louper l’heure du son de sa mort. Malheureusement, il n’y a pas de réelle manière de faire cela dans tous les navigateurs à l’heure actuelle. L’élément audio d’HTML5 a plutôt été conçu manifestement pour lire un flux audio en streaming. Il joue donc le son dès qu’il y a suffisamment de données disponibles. Nous n’avons pas par exemple un évènement qui nous indiquerait : “le fichier audio a entièrement été téléchargé ”.

Ma dernière idée fut alors de télécharger une vesion encodée en base64 du fichier via un appel à XMLHTTPRequest et de passer le résultat à la balise audio ainsi par exemple : <source src="data:audio/mp3;base64,{base64_string}" />. Mais j’avoue ne pas avoir eu le courage/temps de tester si cela fonctionnait ou pas. Si de votre côté, vous avez fait des tests similaires, je suis très curieux du résultat. N’hésitez pas à les partager !

Finalement, la seule chose que je fais est d’appeler la méthode load() pour tenter de charger autant de données que possible avant de les utiliser. Sur un réseau lent, on pourrait alors tomber dans le cas où le jeu demande à jouer un son avant qu’il ne soit encore tout à fait disponible. Cela ne généra par d’erreurs mais le son ne sera tout simplement pas synchronisé avec l’action en cours.

Ruses autour des limitations de la balise audio HTML5

Pendant que je codais et testais le jeu, j’ai trouvé un autre souci avec l’élement <audio> d’HTML5 (j’avoue que je n’avais pas pensé passer autant de temps sur la partie audio du jeu!). Si le joueur prenais trop vite plusieurs diamants d’affilé, l’élément audio associé à cet évènement n’était pas capable de le gérer. Par exemple, au début, quand le joueur prenais 8 diamants d’affilé sur la même ligne, je ne pouvais entendre le son “GemCollected.mp3/ogg” qu’une à 3 fois. Il n’y avait pas de son de joué pour les 5 derniers éléments restant. Ensuite, après une certaine attente, le son fonctionnait à nouveau pour un nouveau diamant.

Après plusieurs expérimentations, je me suis finalement aperçu que tous les tests que j’effectuais étaient détaillés dans cet article : Multiple Channels for HTML5 Audio.

De mon côté, j’ai fini par utiliser une autre méthode de contournement. Dans l’objet ContentManager, je télécharge 8 fois le même son GemCollected au sein d’un tableau contenant des éléments de type Audio() :

 // Used to simulate multi-channels audio // As HTML5 Audio in browsers is today too limited // Yes, I know, we're forced to download N times to same file... for (var a = 0; a < 8; a++) {
    this.gemCollected[a] = new Audio();
    SetAudioDownloadParameters(this.gemCollected[a], "sounds/GemCollected" + audioExtension);
}

Puis pendant le jeu, je tourne de manière cyclique au sein de ce tableau pour simuler plusieurs canaux audio. Vous pouvez trouver cette astuce dans la méthode UpdateGems() dans le fichier Level.js

 /// <summary> /// Animates each gem and checks to allows the player to collect them. /// </summary> Level.prototype.UpdateGems = function () {
    for (var i = 0; i < this.Gems.length; i++) {
        if (this.Gems[i].BoundingRectangle().Intersects(this.Hero.BoundingRectangle())) {
            // We remove it from the drawing surface this.levelStage.removeChild(this.Gems[i]);
            this.Score += this.Gems[i].PointValue;
            // We then remove it from the in memory array this.Gems.splice(i, 1);

// And we finally play the gem collected sound using a multichannels trick this.levelContentManager.gemCollected[audioGemIndex % 8].play(); audioGemIndex++;

         }
    }
};

Je sais, je sais… Cela n’est pas joli, joli. Peut-être que les nouvelles APIs Audio nous aideront dans le futur à mieux gérer la partie audio de nos jeux. Je sais que Mozilla et Google travaillent sur de bonnes idées à ce sujet mais c’est pour l’instant loin d’être finalisé et adopté par tout le monde.

La nature mono-threadée de JavaScript

J’ai déjà abordé ce sujet à travers l’un de mes articles précédents : Introduction aux Web Workers d’HTML5 : le multithreading version JavaScript . Sans les WebWorkers, JavaScript est par nature mono-threadé. Même si les fonctions setInterval() et setTimeout() essaient de nous faire croire que plusieurs choses peuvent arriver en même temps, ce n’est pas le cas. Tout est sérialisé.

Dans mon contexte de jeu, cela engendre des problèmes sur la gestion des animations. Nous n’avons pas la garantie d’être appelé toutes les xxx millisecondes dans notre logique de mise à jour de nos éléments de jeu. Pour éviter cela, XNA fournit 2 boucles séparées : la boucle de dessin (Draw) et la boucle de mise à jour (Update). Dans mon cas, elles sont plus ou moins mélangées. Et c’est bien cela le problème. Imaginez par exemple que le temps écoulé entre 2 ticks est trop important (à cause du traitement mono-threadé global). Je pourrais alors louper un appel à ma méthode Update() qui devait normalement empêcher l’un de mes ennemies de sortir de son environnement. Les tests de collisions deviennent donc non prédictibles dans le temps et cela génère des problèmes très étranges… C’est pourquoi dans certaines parties du code, j’ai codé en dur le temps espéré entre 2 appels à 17 ms. Dans la version XNA, le temps écoulé est bien calculé et équivaut souvent à 16 ms environ (60 FPS). Une solution envisageable pour mieux gérer cette boucle de mise à jour (la méthode tick() dans EaselJS) serait d’utiliser des WebWorkers.

Du côté boucle de dessin, la solution pour gérer correctement les animations dans le jeux HTML5 pourrait venir dans le futur avec requestAnimationFrame(). Cependant, la spécification est aujourd’hui dans un état trop brouillonne et son implémentation varie d’un navigateur à l’autre. Par ailleurs, il y a actuellement des débats intéressants sur son utilisation ou non sur le web. Si le sujet vous intéresse, je vous conseille la lecture de ces 3 articles :

- Are We Fast Yet? de Dominic Szablewski, l’auteur du test HTML5 Benchmark (utilisant ImpactJS) : “At the moment requestAnimationFrame() is truly worthless for games. ” 
- requestAnimationFrame for smart animating de Paul Irish.
- requestAnimationFrame API de notre site IE Test Drive démontrant notre implémentation courante dans IE10.

Bonus spéciaux pour IE9

J’ai ajouté 2 petits bonus pour les utilisateurs d’IE9 venant tester mon petit jeu :

- un mode épinglé avec une icône haute résolution et une jump list vers des ressources tournant autour du jeu
- des boutons présents dans le mode “preview” de Windows 7 pour jouer au jeu d’une manière originale ! Clignement d'œil

Vous pouvez ainsi glisser/déposer l’onglet contenant le jeu dans la barre des tâches Windows 7. IE9 prendra alors les couleurs du thème du jeu :

Vous disposerez également d’une liste de raccourcis :

Le jeu sera donc présent au sein de la barre des tâches de l’utilisateur comme si c’était une application native.

Pour finir, vous pouvez même jouer discrètement au jeu en utilisant la fonction de prévisualisation d’une fenêtre de Windows 7 !

Il y a 3 petits boutons pour bouger à gauche/droite et même sauter dans ce mode particulier. Rire

Note : il y a une autre dédicace à IE plus ou moins caché dans l’un des niveaux. La trouverez-vous ?

Ajoutez de nouveaux niveaux au jeu

Les niveaux sont stockés dans le répertoire “/levels” dans des fichiers de type .TXT. Vous aurez 4 niveaux par défaut: 0.txt, 1.txt, 2.txt & 3.txt. Ils sont tout simplement téléchargés via une requête XMLHTTPRequest en mode asynchrone et ils sont parsés afin de construire le système de rendu et de collisions du jeu.

Voici par exemple le 2ème niveau du jeu :

.................... .................... ..........X......... .......######....... ..G..............G.. ####..G.G.G.G....### .......G.G.GCG...... ......--------...... ...--...........--.. ......GGGG.GGGG..... .G.G...GG..G....G.G. ####...GG..GGGG.#### .......GG..G........ .1....GGGG.GGGG..... ####################

Et voici le résultat de son interprétation dans le jeu :

Le caractère 1 indique où commence le joueur, X la sortie, G un diamant, # un bloc d’une plateforme, - un block que l’on peut traverser, C l’un des monstres, etc.

Donc si vous souhaitez ajouter un nouveau niveau au jeu, ajoutez simplement un nouveau fichier de type texte dans le répertoire et éditez le niveau vous-même avec… Notepad ! Vous devrez aussi modifier la valeur de la variable numberOfLevel contenue dans le fichier PlateformerGame.js.

Code source non compressé à télécharger

Chose promise, chose due, vous pouvez télécharger le code source ainsi que tous les éléments du jeu ici : HTML5 Platformer Non-minified.

J’espère que vous aurez apprécié cette série de 3 articles autour du jeu HTML5. Cela vous aidera peut-être à transformer vos idées de jeux en une réalité HTML5 ! N’hésitez pas à aller jeter un œil aux 3 autres articles qui ont suivi.

David