Modernizing your HTML5 Canvas games Part 2: Offline API, Drag’n’drop & File API

We’ve seen in the previous article Modernizing your HTML5 Canvas games Part 1: hardware scaling & CSS3 how to use CSS3 3D Transform, Transitions & Grid Layout for a HTML5 Platformer game. We’re now going to see how to use the Offline API, Drag’n’drop & File API to leverage new ideas I had while coding my game.

If you haven’t read the first article, let me remind you that my objectives were to see how to use these new features to modernize my previous HTML5 game named HTML5 Platformer.

Agenda

  • Playing to the game in offline mode
    • Step 1: choosing the resources you’d like to cache
    • Step 2: modifying the logic for the levels
    • Step 3: checking online/offline & displaying a logo when launched in offline mode
    • Step 4: conditionally downloading the mp3 or ogg files and store them as blob in IndexedDB
  • Drag’n’dropping the levels from your desktop
  • Video, URL to play this demo & source code

Playing to the game in offline mode

The original version of the game only works if your device has access to Internet. For instance, let’s imagine you’d like to play to my awesome game while you’re in the train, in a taxi or in any other scenarios where your machine can’t access to the network, you’re stuck. It’s really sad as finally nothing in my game really needs a “live” connection to the webserver once all the resources have been downloaded. Hopefully, HTML5 can now address this scenario thanks to the offline APIs.

Step 1: choosing the resources you’d like to cache

It’s pretty simple to indicate to the browser which resources you’d like to cache for an offline use. But before going further, you should first read those 2 articles:

Building Offline Experiences with HTML5 AppCache and IndexedDB from our IE blog
Application Cache API (“AppCache”) from the MSDN Documentation

In my case, I’ve built a file named platformer.cache with this kind of entries:

CACHE MANIFEST
 
# Version 1.5

CACHE: 
index.html
modernplatformer.css
img/MonsterA.png
.. up to ..
img/MonsterD.png
img/Player.png
img/offlinelogoblack.png
img/Backgrounds/Layer0_0.png
.. up to ..
img/Backgrounds/Layer2_2.png 
img/Tiles/BlockA0.png 
.. up to ..
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/easeljs-0.5.0.min.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:
*

I’ve inserted all my PNG files containing my sprites, background layers & overlays, the needed JS files from the EaselJS framework as well as my own gaming logic and the main HTML & CSS files. Once done, you just have to indicate you’d like to use this manifest file in your main HTML page. It’s “index.html” in my case:

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

Please note also that your manifest file should be served as “text/cache-manifest”. As it’s stored in the blob storage of Windows Azure in my case, I’ve added a new “.cache” content type mapped to “text/cache-manifest” then.

Moreover, you need to understand that the specification doesn’t allow delta changes. This means that if only one of your files has changed and you’d like the browser to download the new version, you need to force a complete re-download. For that, add a simple change to your manifest file. Most of the time, we’re adding a version number, a date, a GUID – you named it – at the beginning of the file via a comment (“Version 1.5” in the above example). By simply changing its value, the browser will see that a change have been done to the file and will re-download all resources specified inside it.

Step 2: modifying the logic for loading the levels

The original version of my code was downloading each level via a XHR call to the webserver. As my game now needs to run in offline mode, I need to change that. Moreover, I’d like to indicate to the user that he’s currently playing in offline mode by adding the “officialHTML5 associated logo inside the gaming canvas.

Let’s first review the changes done for the levels. First time you’ll launch my game, I’ll download all the levels (described into {x}.txt files) into the local storage. This is widely supported since IE8 and very easy to use. Even more important, it’s available in offline mode.

Here is the code I’ve added inside the “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(/[nrt]/g, '');
            numberOfLevelDownloaded++;
        }
        else {
            // Loading a hard coded level in case of error
            window.localStorage["platformer_level_" + index] = hardcodedErrorTextLevel;
        }

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

We’re downloading all the levels in the PlateformerGame constructor in an asynchronous way. When all the levels have been downloaded (numberOfLevelDownloaded === numberOfLevels), we’re loading the first level. Here is the code of the new function:

// 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);
};

The beginning of the code handles the CSS3 transitions like described in the previous article. Then, we’re simply accessing to the local storage via the appropriate key to retrieve the previously downloaded content.

Step 3: checking online/offline & displaying a logo when launched in offline mode

There are 2 things you need to check to know & confirm you’re in offline mode. First of all, the most recent browsers implement the offline/online events. This is the first thing to check. If your browser says he’s offline, no need to go further and you should immediately switch to your offline logic. But most of the time, this simple check is not enough. Your browser says it’s online or not by simply checking the connectivity state of your network card. It doesn’t know if your webserver is still online or simply available from where you are. You then need to do a second check by trying to more or less ping your server with a simple XHR for instance.

Here my code checking those 2 points in my case:

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;
};

I’m just trying to download the first level. If it fails, I’m switching to the offline part of my code. Finally, here is the code launched in the constructor part of the 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();
}

and here is the code displaying the offline logo in Level.js in the CreateAndAddRandomBackground function:

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

Once done, here is the result you’ll have in your browser when launching my game without network connection:

image

The offline logo will be displayed just before the frame rate. This tells you that the game is currently working purely offline.

Step 4: conditionally downloading the mp3 or ogg files and store them as blob in IndexedDB

This is something I’ve not implemented in this version but I’d like to share the concept with you. See this section as a bonus you’ll need to implement yourself! Sourire

Maybe you’ll have noticed that I’ve not included the sound effects & the music of my game in the manifest file of step 1.

When writing this HTML5 game, my first goal was to be compatible with the biggest number of browsers (when possible). I have then 2 versions of the sounds: MP3 for IE and Safari, OGG for Chrome, Firefox & Opera. In my content download manager, I’m downloading only the type of codec supported by the current browser launching my game. Indeed, no need to download the OGG version of the files if I’m playing it inside IE as well as there is no need to download the MP3 version for Firefox.

The problem with the manifest file is that you can’t conditionally indicate which resource to load based on the current browser’s support. I’ve then imagined several solutions to work-around this limitation:

1 – Download both versions by putting all files references inside the manifest file. This is very simple to implement and works fine but you’re downloading some files that will be never used by some browsers…
2 – Build a server-side dynamic manifest file by sniffing the browser agent to guess the codec supported. This is definitely a very bad practice!
3 – Client-side feature detect the codec support in the content manager object and then download the appropriate file format in IndexedDB or in the local storage for offline use.

To my point of view, the 3rd solution is the best one but there are several things to keep in mind to do that:

1 – If you’re using local storage, you’ll need to encode in base64 the files and you’ll be potentially limited by the quota if you have too big/many files
2 – If you’re using IndexedDB, you can either store the base64 encoded version of the files or store them as a blob.

The blob approach is definitely the most efficient & smart approach but needs a very up-to-date browser like the last version of IE10 or Firefox. But if you’re curious about this one, check out our Facebook Companion demo from our IE Test Drive site here:

image

You’ll find more details about this demo in this article: IndexedDB Updates for IE10 and Metro style apps

In the version available with this article, I’ve been lazy. I’ve finally decided to cache all formats (solution 1). I’ll maybe enhance that in a future article by implementing an IndexedDB caching.

Drag’n’dropping the levels from your desktop

My idea here was to take advantage of the new Drag’n’drop & File APIs to implement a funny scenario. The user will be able to create/edit a level using his favorite text editor. Then, he will drag’n’drop it from his file explorer directly into the HTML5 game to play with it!

I won’t go into much details about drag’n’drop as it has been very well covered in this article: HTML5 Drag and Drop in IE10 explaining how the Magnetic Poetry demo was built. Be sure to read this article first if you’d like to understand the above code.

In my case, I’ve created the dragDropLogic.js file containing this code:

(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(/[snrt]/g, '');
                // Warning, there is no real check on the consistency
                // of the file. 
                _platformerGameInstance.LoadThisTextLevel(textLevel);
            }
            // Asynchronous read
            reader.readAsText(firstFileDropped);
        }
    };

    window.DragDropLogic = DragDropLogic;
})();

This code is called inside main.js in the startGame function:

// 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();
}

And you’re done! For instance, copy/paste this text block into a new “.txt” file:

………………..
………………..
………………..
.1………………
######………#####
………………..
………###……..
………………..
.G.G.GGG.G.G.G……
.GGG..G..GGG.G……
.G.G..G..G.G.GGG….
………………..
………………..
.X…………….C.
####################

And drag’n’drop it in my game. The new level will be normally loaded magically :

image

Video, URL to play this demo & source code

Here is a short video of IE10 demonstrating the features implemented in this article:

You can also play with this demo in IE10 or your favorite browser here: Modern HTML5 Platformer

image

At last, as you’ve been kind enough to read until the end, you can download the complete source code here: HTML5 Modern Platformer Source Code

David

6 thoughts on “Modernizing your HTML5 Canvas games Part 2: Offline API, Drag’n’drop & File API

  1. Very nice tutorial, thank you for detailing HTML5 APIs core technologies using this simple platformer as an example.

    However, the source code download link you provide doesn't match the improvements brought by these two last articles : no drag and drop, no css transitions, no rescaling, no offline, etc…

    Keep up the good work =)

Leave a Reply

Your email address will not be published. Required fields are marked *