HTML5 Zone is brought to you in partnership with:

Austin Hallock is a co-founder of clay.io, an app store for HTML5 games and a developer API for tedious features like payment processing, social integration, leaderboards, achievements, and more. He has worked on 4 games (thus far) using HTML5: three of them can be found here, and the fourth on his blog. Austin has posted 3 posts at DZone. You can read more from them at their website. View Full User Profile

Developing a Cross-Platform HTML5 Game: Part 1

04.25.2012
| 18394 views |
  • submit to reddit

HTML5 is great for game development -- sure, there are a lot of skeptics, but the ability to create a game that runs on all platforms with very little modification is unparalleled. This is part one of a three part series where I'll be sharing my tips for developing a cross-platform HTML5 game.

Part 1: Getting the game to look great and run well across all platforms

Part 2: Handling the various input types of each platform

Part 3: Dealing with security for your game

Part 1 covers:

  • CSS3 media queries for UI
  • Scaling the game
  • Retina displays
  • "Full screen" on mobile
  • "Installing" the game on mobile
  • Improving FPS through use of subcanvases.

CSS3 Media Queries for UI

By now you're probably well aware of CSS media queries - they're a great way to custom-fit your site or game to various-sized screens. These will just be useful for the UI of your games as the game itself will likely all run within the canvas element.

For our games and site, we typically just have to make tweaks for layouts smaller than 800px wide. Here's how that's done:
@media screen and (max-width: 800px) {
    /* Whatever CSS you want here */
}
We use media queries to hide or move UI elements that aren't critical for mobile devices.

Another thing you'll have to deal with is how your game looks in different orientations. For desktops, we're used to landscape, but on a mobile device you might want your game to look good in portrait mode.

With Word Wars, we have some JavaScript that detects if the view is landscape or portrait (if the window width is less than window height, it's portrait), and we add a .portrait class to the <html> element. Then in our CSS we have some specific properties for html.portrait children (e.g. hiding certain elements, scaling them differently). If you take a look at http://wordwars.clay.io on a mobile device (or just scale down your browser) you'll see how we show the info bar and timer up top for portrait and to the left for landscape.

Portrait Landscape

Here's a bit of code to detect portrait and add the portrait class:
window.onresize = function() // this needs to be called onorientationchange as well
{
    var htmlTag = document.getElementsByTagName('html')[0];
    if(window.innerWidth < window.innerHeight)
        htmlTag.className += ' portrait';
    else
        htmlTag.className = htmlTag.className.replace(' portrait', '');
}


Scaling the Game

It's easy enough to scale the canvas element to the full window width. The hard part is scaling all the contents inside.

To scale the canvas to full width and height, just make sure your <html> and <body> have no padding or margin, and with a bit of JavaScript, you can set the canvas width and height to full and have it update when the window is resized (do note that an orientation change on a mobile device doesn't call onresize, just onorientationchange):
window.onresize = function() // You can alternatively add an event listener for onresize
{
    var canvas = document.getElementById(‘canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
window.onload = function()
{
    window.onresize(); // Call onload to set the width & height initially
}
When you're adding items to the canvas context, you specify a height and width -- for example: <code>ctx.fillRect(0, 0, 100, 100);</code> draws a 100px by 100px rectangle, which isn't fluid at all.

To make this fluid, you'll want to scale according to the window height and/or width. For example, if you want an item to take up 10% of the screen vertically and 10% horizontally, you would use:
var rectangle = { // Be sure to update this onresize
    width: window.innerWidth / 10,
    height: window.innerHeight / 10
};


Getting the Game to Look Good on Retina Screens

Another issue to deal with is getting the game to look good on retina screens. To do this we do two things. First, we set the CSS width and height of the canvas element to be the window.innerWidth and window.innerHeight. Additionally, we set the width and height attributes of the canvas element to be those values multiplied by the devicePixelRatio. Second, we multiply the size of each item drawn in the canvas by the device's pixel ratio (typically this is 1, but for devices like the iPhone 4 it's 2). Following the above example, we would get:
var devicePixelRatio = window.devicePixelRatio || 1; // > 1 for retina displays
var canvas = document.getElementById(‘canvas');
/* Be sure to update all these values onresize */
// Width & height attributes (scaled according to pixel ratio)
canvas.width = window.innerWidth * devicePixelRatio;
canvas.height = window.innerHeight * devicePixelRatio;
// CSS width & height
canvas.style.width = window.innerWidth;
canvas.style.height = window.innerHeight;
var rectangle = {
    width: devicePixelRatio * window.innerWidth / 10,
    height: devicePixelRatio * window.innerHeight / 10
};


"Full Screen" on Mobile

The address bar on a mobile device can be pretty annoying when it comes to games, but it's fairly easy to scroll past with this bit of code.

Another thing to consider to try and retain more mobile users is a prompt for users to ‘install' your game as a bookmark on iOS (which adds an icon to the home screen). The other benefit of these bookmarks is when your game is opened from the home screen, the bottom bar in Safari doesn't show, so there is more real-estate.

For this, you'll need to specify a 57x57 icon to use (in <head>):

<!-- Works for iOS and Android -->
<link rel="apple-touch-icon-precomposed" href="images/apple-touch-icon.png" />
This bit of JavaScript code written by my partner, Joe, will show the prompt to install. Feel free to use that snippet on your game!

Retain iOS gamers

Of course, if you put your game on clay.io, we take care of all of this for you.

Improving FPS with Subcanvases

In our most recent game, Word Wars, the FPS in mobile Safari was pretty abysmal at first.

Because each tile in our game has gradients and what-not to make it look good, redrawing those gradients 60 times per second is really CPU intensive. Word Wars has four types of tiles, gray (unselected), blue (selected), green (correct), red (incorrect). The solution to our low framerate was, for each type, when we initally load the page, we draw them and save them to a subcanvas and use that subcanvas in drawImage(). Here's a look at how we do that:
var insetCircle = document.createElement('canvas');
var redCircle   = document.createElement('canvas');
var blueCircle  = document.createElement('canvas');
var greenCircle = document.createElement('canvas');
var renderTile = function(circle, color1, color2, outglow1, outglow2, strokeColor)
{
    circle.width = size; // size is something we set elsewhere for fluid width & height
    circle.height = size;
    var innerGlow = ctx.createRadialGradient(0, 0, size/2-4, 0, 0, size/2); // Gradient for looks
    innerGlow.addColorStop(0, 'rgba(255,255,255,0)');
    innerGlow.addColorStop(1, 'rgba(255,255,255,.2)');
    var cctx = circle.getContext('2d');
    cctx.translate(size/2, size/2); // center of circle
    var grdRedLin  = cctx.createLinearGradient(0, -size/2, 0, size/2);
    grdRedLin.addColorStop(0, color1);
    grdRedLin.addColorStop(1, color2);
    cctx.save();
    outerGlow(outglow1, outglow2, cctx);
    cctx.beginPath();
    cctx.arc(0, 0, size/2, 0, 2*Math.PI);
    cctx.strokeStyle = strokeColor;
    cctx.lineWidth = 4;
    cctx.stroke();   
    cctx.fillStyle = grdRedLin;
    cctx.fill();
    cctx.fillStyle = innerGlow;
    cctx.fill();
}
var renderAllTiles = function() {
    // Render each tile in its own canvas
    renderTile(redCircle, '#a52222', '#7e0101', '#a52222', 'rgba(165, 34, 34, 0)', '#520101');
    renderTile(blueCircle, '#1d97c9', '#0a7cab', '#7bb5c5', 'rgba(123, 181, 197, 0)', '#105a78');
    renderTile(greenCircle, '#1dc924', '#0faf15', '#1dc924', 'rgba(29, 201, 36, 0)', '#107814');
    insetCircle.width = size;
    insetCircle.height = size;
    var cctx = insetCircle.getContext('2d');
    cctx.translate(size/2+50, size/2+50);
    cctx.lineWidth = Math.floor(size * 0.1);
    cctx.strokeStyle = 'rgba(255,255,255,.25)';
    // draw the rest of the tile
    cctx.beginPath();
    cctx.arc(0, 0, size/2, 0, 2*Math.PI);   
    cctx.stroke();
    var grd = ctx.createRadialGradient(0, 0, 10, 0, 0, size/2);
    grd.addColorStop(0, 'rgba(255,255,255,.1)');
    grd.addColorStop(1, '#a1aaae');
    cctx.fillStyle = grd;
    cctx.fill();
}
renderAllTiles();

// Actually rendering them (render() is called with requestAnimationFrame())
var circles = [insetCircle, blueCircle, greenCircle, redCircle]; // These correspond to tile states
var render = function()
{
    // ...
    ctx.drawImage(circles[tiles[i].state], Math.floor(-size/2), Math.floor(-size/2), size, size);
    // ...
}
The code above renders each of the 4 types of tiles to its own canvas, and when we need to render it in the main canvas we reference the subcanvas in ctx.drawImage(). We then add anything that's dynamic on top of the tiles (the letters in our case).


That's it for part 1. Check back often for parts 2 and 3 where I'll cover handling various types of input, and security for games with JavaScript & a backend.

If you're interested in HTML5 game development, check out clay.io's developer API where we take care of achievements, leaderboards, payment processing, social integration, user login, multiplayer rooms, screenshots and a few of the things I mentioned in this post. If you have any feedback on the API, or ideas of how we can make your life easier, let me know in the comments!
Published at DZone with permission of its author, Austin Hallock.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)