Modernize your HTML5 Canvas game
Part 2: Offline, Drag-and-Drop, and File APIs
HTML5 features in modern browsers like Internet Explorer 10 are making possible a whole new class of web applications and gaming scenarios. This two-part article demonstrates how I've used some of these new features to modernize my last HTML5 game, HTML5 Platformer .
In part 1 of this article, I covered how to use CSS3 3D Transform, Transitions, and Grid Layout for an HTML5 Platformer game. In part 2, I'll show you how to use the Offline, Drag-and-Drop, and File APIs to implement some interesting new ideas.
Playing a game in offline mode
The original version of my game only worked if your device was currently connected to the Internet. So, if you wanted to play to my fabulous game while you were on the train, in a taxi, or somewhere else without an Internet connection, you were out of luck-stuck without access to the awesomeness. And that's too bad because there really isn't anything in my game that needs a "live" connection to the web server once all the resources have been downloaded. Fortunately, offline APIs provide a solution for this in HTML5.
Step 1: Choosing the resources you'd like to cache
It's actually pretty easy to tell the browser which resources you'd like to cache for offline use. But before you go any further, I recommend reading these two articles:
- Building Offline Experiences with HTML5 AppCache and IndexedDB from our IE Blog
- Application Cache API ("AppCache") from the MSDN Documentation
For my game, I built a file named platformer.cache that looks like this:
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/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: *
I inserted all the PNG files containing my sprites, background layers, and overlays; the necessary JS files from the EaselJS framework; as well as my own gaming logic and the main HTML and CSS files. Then, I simply indicate that I'd like to use this manifest file in my main HTML page. In this case, it's " index.html":
<!DOCTYPE html> <html manifest="platformer.cache"> <head> <title>HTML5 Platformer Game</title> // ... </head> </html>
Please note that your manifest file should be served as "text/cache-manifest". For my game, I added a new ".cache" content type mapped to "text/cache-manifest" because it's stored in the blob storage of Windows Azure.
Be aware that this specification doesn't allow delta changes. Even if only one of your files has changed, you need to force a complete re-download for the browser to update to the new version. However, any change to your manifest file will be detected by the browser, which will then re-download all the resources specified inside it. The change could be a version number, a date, a GUID-whatever works for you-at the beginning of the file via a comment ("Version 1.5" in the above example).
Step 2: Modifying the logic for loading the levels
The original version of my code downloaded each level via an XHR call to the web server. I will need to change that to make my game run in offline mode. Also, I'd like to indicate to the user that he's currently playing in offline mode by adding the "official" HTML5 associated logo inside the gaming canvas.
Let's go through the changes to make it happen. The first time a user launches my game, all the levels will be downloaded (described into {x}.txt files) into the local storage. This is widely supported (since IE8) and very easy to use. And most important, it's available in offline mode.
Here is the code I 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(/[\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(); } } }
All the levels in the PlatformerGame constructor will be downloaded asynchronously. Once all the levels have been downloaded (numberOfLevelDownloaded === numberOfLevels), the first level loads. Here is the code for 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 as described in the previous article . The game will simply access the local storage via the appropriate key to retrieve the previously downloaded content.
Step 3: Checking online/offline and displaying a logo when launched in offline mode
Two tests are required to confirm a game is running in offline mode. The first is whether the browser has implementedoffline/online events, as most modern browsers do. If the browser says the user is offline, it's confirmed, and the game should immediately switch to the offline logic. Often, this simple check is not enough, though. The browser may say it's online, but it doesn't know if the web server is still online or not. So you need to do a second check by pinging the server with a simple XHR.
Here is my code for both these checks:
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; };
My test is to try to download the first level. If that fails, it switches to the offline part of my code. Now, here's the code launched in the constructor part of the PlatformerGame.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 theCreateAndAddRandomBackground function:
if (!PlatformerGame.IsOnline) { offlineLogo.x = 710; offlineLogo.y = -1; offlineLogo.scaleX = 0.5; offlineLogo.scaleY = 0.5; this.levelStage.addChild(offlineLogo); }
With these changes implemented, here is what my game looks like when launched without network connection:
The offline logo is displayed just before the frame rate, indicating to the user that the game is currently running purely offline.
Step 4: Conditionally downloading the MP3 or OGG files and storing them as blob in IndexedDB
This is something I've not implemented, but I'd like to share the concept with you as a bonus so you can implement it yourself!
You may have noticed that I didn't include my game's sound effects and music in the manifest file of step 1. When I wrote this HTML5 game, my first goal was to be compatible with the largest number of browsers possible. To accomplish that, I have two versions of the sounds:MP3 for IE and Safari, and OGG for Chrome, Firefox, and Opera. The content download manager only downloads the type of codec supported by the current browser launching my game. That's because there's no need to download the OGG version of the files if I'm playing it inside IE, and 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 come up with three solutions to work around this limitation:
- Download both versions by putting all file versions inside the manifest file. This is very simple to implement and works fine, but users will be downloading some files that will be never used by some browsers.
- Build a server-side dynamic manifest file by sniffing the browser agent to guess the codec supported. This is definitely a very bad practice!
- Use a client-side feature to detect the codec support in the content manager object and thendownload the appropriate file format in IndexedDB or in the local storage for offline use.
I believe the 3rd solution is the best, but you need to be mindful of a couple things to make it work:
- If you're using local storage, you'll need to encode the files in base64, and you may run into quota limits if you have too big/many files.
- 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 smarter and more efficient solution, but it requires a very up-to-date browser like the last version of IE10 (PP5) or Firefox (11). If you're curious about this idea, check out our Facebook Companion demo from our IE Test Drive site here:
You'll find more details about this demo in this article: IndexedDB Updates for IE10 and Metro style apps
In the game version supplied with this article, I decided to cache all formats (solution 1). I may update that in a future article by implementing an IndexedDB caching. Stay tuned!
Drag-and-drop and File APIs
Here's a fun new feature that takes advantage of the new Drag-and-Drop and File APIs. The user can create/edit a level using his favorite text editor, then simply drag and drop it from his file explorer directly into the HTML5 game and play it!
I won't go into too much detail about drag and drop as it has been very well covered in this article: HTML5 Drag and Drop in IE10 , which explains how the Magnetic Poetry demo was built. I recommend reading the article first to fully understand the code below.
For my game, I 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(/[\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; })();
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(); }
That's all there is to it! 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 and drop it into my game. The new level will be loaded like magic!
Demo and source code
If you'd like to see a demonstration in IE10 of all the features implemented in this article, check out this short video:
Download Video:
MP4 , WebM , HTML5 Video Player by VideoJS
You can also play with this demo in IE10 or your favorite browser here: Modern HTML5 Platformer
Since you've been kind enough to read this entire article, please enjoy the complete source code here: HTML5 Modern Platformer Source Code