Modernisez vos jeux HTML5 canvas partie 2: Offline API, Drag’n’drop & File API

Nous avons vu dans l’article précédent Modernisez vos jeux HTML5 canvas partie 1: mise à l’échelle matérielle & CSS3 comment utiliser CSS3 3D Transform, les transitions & Grid Layout pour améliorer un jeu de plateforme HTML5. Nous allons maintenant voir comment utiliser Offline API, Drag’n’drop et File API pour mettre en place de nouvelles idées que j’ai eu pendant que je développais mon jeu.

Si vous n’avez pas lu le premier article, laissez moi vous rappelez quels étaient mes objectifs. Je souhaitais voir comment jouer avec les dernières spécifications pour moderniser mon jeu HTML5 nommé HTML5 Platformer. Voici ce qui nous attend au menu de ce 2ème article.

Agenda

  • Rendre le jeu jouable en mode déconnecté 
    • Etape 1: choisir les ressources que vous souhaitez mettre en cache
    • Etape 2: modifier la logique du jeu pour le chargement des niveaux
    • Etape 3: vérifier le statut en ligne/hors ligne & afficher un logo en mode déconnecté
    • Etape 4: chargement conditionnel des fichiers mp3 ou ogg et stockage en blob dans IndexedDB
  • Glisser/Déposer des niveaux depuis votre bureau
  • Vidéo, URL pour jouer avec la démo & code source à télécharger 

Rendre le jeu jouable en mode déconnecté

La version initiale du jeu ne fonctionne uniquement que si votre périphérique dispose d’un accès à Internet. Ainsi, imaginons qu’il vous prend la subite envie de jouer à mon superbe jeu pendant que vous êtes dans le train, dans le taxi ou dans n’importe quel endroit où l’accès au réseau est inexistant et bah… vous êtes fichu. C’est quand même dommage dans la mesure où finalement mon jeu n’a pas réellement besoin d’une connexion permanente vers le serveur web une fois que toutes les ressources ont été téléchargées. Heureusement, HTML5 peut désormais adresser ce scénario grâce aux APIs gérant la déconnection.

Etape 1: choisir les ressources que vous souhaitez mettre en cache

C’est assez simple d’indiquer au navigateur quelles sont les ressources que vous souhaitez mettre en cache pour un usage hors connexion. Mais avant d’aller plus loin, je vous invite à lire ces 2 articles décrivant le fonctionnement de cette spécification :

- Building Offline Experiences with HTML5 AppCache and IndexedDB sur le site du blog officiel IE
- Application Cache API ("AppCache") présent dans la documentation MSDN

Dans mon cas, j’ai construit un fichier manifeste appelé platformer.cache avec ce type d’entrées :

 CACHE MANIFEST
 
# Version 1.5

CACHE:  
index.html
modernplatformer.css
img/MonsterA.png
 .. jusqu’à .. 
img/MonsterD.png
img/Player.png
img/offlinelogoblack.png
img/Backgrounds/Layer0_0.png
 .. jusqu’à .. 
img/Backgrounds/Layer2_2.png 
img/Tiles/BlockA0.png 
 .. jusqu’à .. 
img/Tiles/BlockA6.png
img/Tiles/BlockB0.png
img/Tiles/BlockB1.png
img/Tiles/Gem.png
img/Tiles/Exit.png
img/Tiles/Platform.png
overlays/you_died.png
overlays/you_lose.png
overlays/you_win.png
src/dragDropLogic.js
src/main.js
src/ModernizrCSS3.js
src/easeljs/utils/UID.js
src/easeljs/geom/Matrix2D.js
src/easeljs/events/MouseEvent.js
src/easeljs/utils/SpriteSheetUtils.js
src/easeljs/display/SpriteSheet.js
src/easeljs/display/Shadow.js
src/easeljs/display/DisplayObject.js
src/easeljs/display/Container.js
src/easeljs/display/Stage.js
src/easeljs/display/Bitmap.js
src/easeljs/display/BitmapAnimation.js
src/easeljs/display/Text.js
src/easeljs/utils/Ticker.js
src/easeljs/geom/Rectangle.js
src/easeljs/geom/Point.js
src/easeljs/XNARectangle.js
src/easeljs/PlatformerHelper.js
src/easeljs/ContentManager.js
src/easeljs/Tile.js
src/easeljs/Gem.js
src/easeljs/Enemy.js
src/easeljs/Player.js
src/easeljs/Level.js
src/easeljs/PlatformerGame.js

NETWORK: 
*

J’y ai inséré l’ensemble de mes fichiers PNG contenant mes sprites, mes différents calques d’arrière plan, les fichiers JavaScript du framework EaselJS ainsi que ma propre logique de jeu et pour finir les fichiers HTML & CSS principaux. Une fois cela spécifié, il ne vous reste plus qu’à indiquer que ce fichier de manifeste sera la source de mise en cache au sein de la page HTML principale. C’est la page “index.html” dans mon cas :

 <!DOCTYPE html>
<html manifest="platformer.cache">
<head>
    <title>HTML5 Platformer Game</title>
    // ... 
</head>
</html>

Il faut également faire attention à servir le fichier avec le type “text/cache-manifest”. Dans mon cas, comme le fichier est stocké dans le blob storage de Windows Azure, j’a ajouté un nouveau “content type” nommé “ .cache" mappé sur le type “text/cache-manifest” donc.

Par ailleurs, vous devez savoir que la spécification ne permet pas des mises à jours partielles. Ainsi, même si un seul de vos fichiers a changé et que vous souhaitez que le navigateur télécharge la nouvelle version, vous êtes obligé de forcer un nouveau téléchargement complet. Pour cela, il suffit d’effectuer un simple changement au fichier manifest lui-même. La plupart du temps, on ajoute ainsi un numéro de version, une date, un GUID – peu importe finalement – au début du fichier via un commentaire (“Version 1.5” dans l’exemple précédent). En changeant simplement cette valeur, le navigateur va voir qu’un changement a été effectué au fichier et va alors procéder au téléchargement complet de toutes les ressources spécifiées au sein du même fichier.

Etape 2: modifier la logique du jeu pour le chargement des niveaux

La version initiale de mon code s’occupait de télécharger chacun des niveaux via un appel XHR vers le serveur web. Comme mon jeu doit maintenant fonctionner hors connexion, il me faut changer cette partie là. Par ailleurs, j’aimerais indiquer à l’utilisateur qu’il joue en mode déconnecté en ajoutant le logo “officiel” HTML5 au sein du canvas de jeu.

Commençons par analyser les changements effectuées pour le chargement des niveaux. La première fois que vous lancerez mon jeu, mon code s’occupera de télécharger tous les niveaux (décrit au sein des fichiers {x}.txt) vers le local storage. Cette spécification est largement supporté depuis IE8 et facile à utiliser. Encore plus important dans notre cas, cela fonctionne même en mode déconnecté.

Voici le code que j’ai ajouté au sein du fichier “PlatformerGame.js” :

 PlatformerGame.prototype.DownloadAllLevels = function () {
    // Searching where we are currently hosted
    var levelsUrl = window.location.href.replace('index.html', '') + "levels/";
    var that = this;

    for (var i = 0; i < numberOfLevels; i++) {
        try {
            var request = new XMLHttpRequest();
            request.open('GET', levelsUrl + i + ".txt", true);
            request.onreadystatechange = makeStoreCallback(i, request, that);
            request.send(null);
        }
        catch (e) {
            // Loading the hard coded error level to have at least something to play with
            //console.log("Error in XHR. Are you offline?"); 
            if (!window.localStorage["platformer_level_0"]) {
                window.localStorage["platformer_level_0"] = hardcodedErrorTextLevel;
            }
        }
    }
};

// Closure of the index 
function makeStoreCallback(index, request, that) {
    return function () {
        storeLevel(index, request, that);
    }
}

function storeLevel(index, request, that) {
    if (request.readyState == 4) {
        // If everything was OK
        if (request.status == 200) {
            // storing the level in the local storage
            // with the key "platformer_level_{index}
            window.localStorage["platformer_level_" + index] = request.responseText.replace(/[\n\r\t]/g, '');
            numberOfLevelDownloaded++;
        }
        else {
            // Loading a hard coded level in case of error
            window.localStorage["platformer_level_" + index] = hardcodedErrorTextLevel;
        }

        if (numberOfLevelDownloaded === numberOfLevels) {
            that.LoadNextLevel();
        }
    }
}

On télécharge tous les niveaux pendant la phase de construction de l’objet PlateformerGame de manière asynchrone. Lorsque tous les niveaux sont téléchargés (numberOfLevelDownloaded === numberOfLevels), on charge le premier niveau. Voici le code de la nouvelle fonction s’occupant de charger le prochain niveau :

 // Loading the next level contained into the localStorage in platformer_level_{index}
PlatformerGame.prototype.LoadNextLevel = function () {
    this.loadNextLevel = false;
    // Setting back the initialRotation class will trigger the transition
    this.platformerGameStage.canvas.className = "initialRotation";
    this.levelIndex = (this.levelIndex + 1) % numberOfLevels;
    var newTextLevel = window.localStorage["platformer_level_" + this.levelIndex];
    this.LoadThisTextLevel(newTextLevel);
};

Le début du code s’occupe de gérer les transitions CSS3 comme déjà vu dans l’article précédent. Ensuite, on accède simplement au “local storage” via la bonne clé pour retrouver un niveau téléchargé précédemment.

Etape 3: vérifier le statut en ligne/hors ligne & afficher un logo en mode déconnecté

Il y a 2 choses que vous devez savoir et vérifier pour être sûr d’être en mode déconnecté. Tout d’abord, les navigateurs les plus récents implémentent les évènements offline/online. C’est la 1ère chose à vérifier. Si votre navigateur vous dit qu’il est hors ligne, faites lui confiance et n’allez pas plus loin en vous rendant directement dans votre logique de code gérant la déconnection. Cependant, la plupart du temps, cette simple vérification n’est pas suffisante. En effet, votre navigateur indique qu’il est en ligne ou non en vérifiant simplement l’état de votre carte réseau. Il ne sait pas si votre serveur web est toujours en ligne ou simplement accessible depuis là où vous êtes. Vous devez alors effectuer une 2ème vérification en essayant plus ou moins de toquer votre serveur pour vérifier qu’il est toujours en vie via une petite requête XHR par exemple.

Voici le code que j’ai utilisé pour vérifier ces 2 points dans mon cas :

 PlatformerGame.prototype.CheckIfOnline = function () {
    if (!navigator.onLine) return false;

    var levelsUrl = window.location.href.replace('index.html', '') + "levels/";

    try {
        var request = new XMLHttpRequest();
        request.open('GET', levelsUrl + "0.txt", false);
        request.send(null);
    }
    catch (e) {
        return false;
    }

    if (request.status !== 200)
        return false;
    else
        return true;
};

J’essaie simplement de télécharger le premier niveau. Si cela échoue, je bascule sur la partie “offline” de mon code. Pour finir, voici le code exécuté pendant la phase de construction de PlateformerGame.js :

 PlatformerGame.IsOnline = this.CheckIfOnline();

// If we're online, we're downloading/updating all the levels
// from the webserver
if (PlatformerGame.IsOnline) {
    this.DownloadAllLevels();
}
// If we're offline, we're loading the first level
// from the cache handled by the local storage
else {
    this.LoadNextLevel();
}

Et voici le code s’occupant d’afficher le logo “offline” dans la fonction CreateAndAddRandomBackground du fichier Level.js :

 if (!PlatformerGame.IsOnline) {
    offlineLogo.x = 710;
    offlineLogo.y = -1;
    offlineLogo.scaleX = 0.5;
    offlineLogo.scaleY = 0.5;
    this.levelStage.addChild(offlineLogo);
}

Vous devriez avoir ce résultat en lançant mon jeu sans connexion réseau :

image

Le logo de déconnection sera affiché juste à côté du taux de rafraichissement. Cela vous indiquera que le jeu fonctionne actuellement entièrement en mode hors ligne.

Etape 4: chargement conditionnel des fichiers mp3 ou ogg et stockage en blob dans IndexedDB

C’est une partie que je n’ai pas implémentée dans cette version mais je voulais en partager avec vous le concept. Voyez donc cette section comme une section bonus. Ce sera à votre charge de l’implémenter ! Sourire

Peut-être aurez-vous noté que je n’ai pas inclus les effets sonores et la musique de mon jeu dans le fichier de manifeste de l’étape 1.

Lorsque j’ai écrit ce jeu HTML5, mon but premier était d’être compatible avec le plus grand nombre de navigateurs pour être le plus portable possible. J’ai ainsi 2 versions des fichiers sons: MP3 pour IE et Safari, OGG pour Chrome, Firefox & Opera. Dans mon gestionnaire de téléchargement, je télécharge ainsi uniquement le type de codec supporté par le navigateur lançant actuellement mon jeu. En effet, nul besoin de télécharger les versions OGG des fichiers lorsque je joue au jeu dans IE et, de la même manière, aucun intérêt à télécharger les versions MP3 pour Firefox.

Le problème avec le fichier de manifeste est que vous ne pouvez pas indiquer de manière conditionnelle quelles ressources vous souhaitez charger en fonction du support du navigateur. J’ai donc réfléchi à plusieurs types de solutions pouvant contourner cette limitation :

1 – Télécharger chacune des versions en insérant toutes les références vers les fichiers .mp3 et .ogg dans le fichier de manifeste. C’est très simple à implémenter et marche bien mais vous téléchargez certains fichiers qui ne seront jamais utilisés par certains navigateurs…

2 – Fabriquer un fichier manifeste de manière dynamique côté serveur en analysant le “user agent” envoyé et en essayant de deviner le codec supporté. Nous savons tous que c’est une mauvaise pratique !

3 – Détecter côté client le codec supporté dans le gestionnaire de téléchargement pour ensuite ne télécharger uniquement que le bon format de fichier dans IndexedDB ou dans le local storage pour un usage hors ligne.

Selon moi, la 3ème solution est de loin la meilleure mais il y a plusieurs choses à avoir en tête pour l’implémenter :

1 – Si vous utilisez le “local storage”, vous aurez besoin d’encoder en base64 les fichiers et vous serez potentiellement limité par le quota du navigateur si vous avez des fichiers trop gros ou trop nombreux

2 – Si vous utilisez IndexedDB, vous pouvez soit stocker la version base64 soit les stocker sous la forme de blob

L’approche du stockage blob est définitivement la plus performante mais elle nécessite un navigateur très récent comme IE10 (PP5) ou Firefox (11). Mais si ce sujet vous rend curieux (et c’est bien naturel !), vous pouvez jeter un oeil à notre démo Facebook Companion hébergée sur notre site IE Test Drive ici :

image

Vous trouverez d’ailleurs davantage de détails sur cette démo au sein de cet article : IndexedDB Updates for IE10 and Metro style apps

Pour la version disponible au sein de cet article, j’ai fait mon faignant. J’ai finalement décidé de mettre en cache tous les formats (solution 1). J’améliorais peut-être cela dans un futur article en implémentant un cache basé sur IndexedDB.

Glisser/Déposer des niveaux depuis votre bureau

Mon idée ici était de bénéficier des nouvelles API de Drag’n’drop et de manipulation de fichiers pour tester des scénarios amusants. L’utilisateur sera ainsi capable de créer/éditer ses propres niveaux via son éditeur de texte préféré. Ensuite, il glissera/déposera ses fichiers depuis l’explorateur de fichiers directement dans le jeu HTML5 pour jouer avec.

Je ne rentrerais pas plus en détails sur le fonctionnement du drag’n’drop puisqu’il a été très bien décrit dans cet article : HTML5 Drag and Drop in IE10 expliquant comment la démo Magnetic Poetry a été réalisée. Assurez vous d’avoir lu cet article avant si vous souhaitez comprendre le code ci-dessous.

Dans mon cas, j’ai créé le fichier dragDropLogic.js contenant le code suivant :

 (function () {
    "use strict";

    var DragDropLogic = DragDropLogic || {};

    var _elementToMonitor;
    var _platformerGameInstance;

    // We need the canvas to monitor its drag&drop events
    // and the platformer game instance to trigger the loadnextlevel function
    DragDropLogic.monitorElement = function (elementToMonitor, platformerGameInstance) {
        _elementToMonitor = elementToMonitor;
        _platformerGameInstance = platformerGameInstance;

          _elementToMonitor.addEventListener("dragenter", DragDropLogic.drag, false);
          _elementToMonitor.addEventListener("dragover", DragDropLogic.drag, false);
          _elementToMonitor.addEventListener("drop", DragDropLogic.drop, false);
    };

    // We don't need to do specific actions
    // enter & over, we're only interested in drop
    DragDropLogic.drag = function (e) {
        e.stopPropagation();
        e.preventDefault();
    };

    DragDropLogic.drop = function (e) {
        e.stopPropagation();
        e.preventDefault();

        var dt = e.dataTransfer;
        var files = dt.files;

        // Taking only the first dropped file
        var firstFileDropped = files[0];

        // Basic check of the type of file dropped
        if (firstFileDropped.type.indexOf("text") == 0) {
            var reader = new FileReader();
            // Callback function
            reader.onload = function (e) {
                // get file content
                var text = e.target.result;
                var textLevel = text.replace(/[\s\n\r\t]/g, '');
                // Warning, there is no real check on the consistency
                // of the file. 
                _platformerGameInstance.LoadThisTextLevel(textLevel);
            }
            // Asynchronous read
            reader.readAsText(firstFileDropped);
        }
    };

    window.DragDropLogic = DragDropLogic;
})();

Ce code est appelé au sein de main.js dans la fonction startGame :

 // Callback function once everything has been downloaded
function startGame() {
    platformerGame = new PlatformerGame(stage, contentManager, 800, 480, window.innerWidth, window.innerHeight);
    window.addEventListener("resize", OnResizeCalled, false);
    OnResizeCalled();
    DragDropLogic.monitorElement(canvas, platformerGame);
    platformerGame.StartGame();
}

Et c’est tout ! Par exemple, copiez/collez ce bloc de texte dans un nouveau fichier “.txt” :

....................

....................

....................

.1..................

######.........#####

....................

.........###........

....................

.G.G.GGG.G.G.G......

.GGG..G..GGG.G......

.G.G..G..G.G.GGG....

....................

....................

.X................C.

####################

Puis glissez/déposez le dans mon jeu. Un nouveau niveau devrait alors être magiquement chargé :

image

Vidéo, URL pour jouer avec la démo & code source à télécharger

Voici une courte vidéo illustrant le résultat de cet article dans IE10 :





Poster Image

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

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

image

Pour finir, comme vous avez été suffisamment patient et concentré pour me lire jusqu’au bout, je vous offre la possibilité de télécharger le code source complet ici : HTML5 Modern Platformer Source Code

David