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:
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 () {
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) {
if (!window.localStorage["platformer_level_0"]) {
window.localStorage["platformer_level_0"] = hardcodedErrorTextLevel;
}
}
}
};
function makeStoreCallback(index, request, that) {
return function () {
storeLevel(index, request, that);
}
}
function storeLevel(index, request, that) {
if (request.readyState == 4) {
if (request.status == 200) {
window.localStorage["platformer_level_" + index] = request.responseText.replace(/[\n\r\t]/g, '');
numberOfLevelDownloaded++;
}
else {
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:
PlatformerGame.prototype.LoadNextLevel = function () {
this.loadNextLevel = false;
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
implemented offline/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 (PlatformerGame.IsOnline) {
this.DownloadAllLevels();
}
else {
this.LoadNextLevel();
}
And here is the code displaying
the offline logo in Level.js in the CreateAndAddRandomBackground
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 then download
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;
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);
};
DragDropLogic.drag = function (e) {
e.stopPropagation();
e.preventDefault();
};
DragDropLogic.drop = function (e) {
e.stopPropagation();
e.preventDefault();
var dt = e.dataTransfer;
var files = dt.files;
var firstFileDropped = files[0];
if (firstFileDropped.type.indexOf("text") == 0) {
var reader = new FileReader();
reader.onload = function (e) {
var text = e.target.result;
var textLevel = text.replace(/[\s\n\r\t]/g, '');
_platformerGameInstance.LoadThisTextLevel(textLevel);
}
reader.readAsText(firstFileDropped);
}
};
window.DragDropLogic = DragDropLogic;
})();
This code is called inside main.js
in the startGame function:
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