Simple Slot machine game using HTML5 Part 4: Offline mode

This is the fourth part of the Slot machine game in HTML5 (previous parts 1, 2, and 3) and this time we modify the game to support HTML5 offline mode, also known as HTML5 Application Cache.

Slots Offline

Try out offline supported version here.

Word of warning. HTML5 offline mode is powerful but very fragile feature. It’s tricky to get right, but once you get it to work the mobile user experience can be very native app like.

Some problems you will encounter

  • Browser refresh logic is confusing. Especially the fact that browser does not use updated manifest and resources when they change but only after next reload. Fortunately Javascript workarounds exists.
  • Application cache file maintenance needs diligence. For example, browser will not reload any assets if this file is not modified.
  • Externally linked resources do not generally work offline. This makes CDN use difficult.
  • Web server has to use right MIME type and cache settings to reliably use application cache files. Most web servers don’t do this in default configuration.
  • No reliable way to detect if page was loaded in online or offline mode.
  • Chrome bypasses some restrictions (e.g. cross-domain issues) in the specification and what works in Chrome may not work anywhere else.
  • Each browser has slightly different meaning and heuristic for using offline mode. For example if browser can load some unrelated pages but can’t currently load your app page it may not show your page in offline mode and simply shows “Can not reach the server error”. This may happen especially if it knows from last load that manifest has been updated. Then at other times, it may load page few times in offline mode even when connectivity has returned.

Manifest file

Web page must use application cache manifest file to support offline mode. This manifest file is specified in html tag of the page.

<!DOCTYPE html>
<html manifest="slots.appcache">
<head>
 ...

The file has listing of all content that page needs. For detailed explanation of each section, refer to Beginners guide to HTML5 application cache

CACHE MANIFEST
# version 10

NETWORK:
# here goes resources that must be never cached
js/online.json

CACHE:
audio/nowin.mp3
audio/nowin.ogg
audio/roll.mp3
audio/roll.ogg
audio/slot.mp3
audio/slot.ogg
audio/win.mp3
audio/win.ogg
css/webfonts.css
css/reset.css
css/slot.css
css/Slackey.ttf
img/build-64.png
img/cash-64.png
img/energy-64.png
img/gold-64.png
img/goods-64.png
img/loading.gif
img/staff-64.png
js/slot.js
js/jquery.min.js
index.html

Web Fonts

Web fonts must be hosted locally if you want to use them offline. Note that some web fonts may have licensing restrictions for local hosting.

<link type='text/css' rel='stylesheet' href="css/webfonts.css"/>

The webfont.css defines the font face and loads true type file.

@font-face {
  font-family: 'Slackey';
  font-style: normal;
  font-weight: 400;
  src: local('Slackey'), url(Slackey.ttf) format('truetype');
}

The file Slackey.ttf is hosted locally in css directory.

Web Server Support

Web server must use correct MIME type for text/cache-manifest application cache manifest. For example, in NGINX web server edit the mime.types and add following file type to MIME type mapping.

text/cache-manifest		appcache;

Browsers should check manifest every time page is loaded online, but it may not do this often enough if cache control is too long. Therefore, set short cache lifetime for the manifest files by adding this inside server section of NGINX configuration file. This forces cache lifetime of 1 minute to all *.appcache files.

# set 1 minute cache life for HTML5 offline manifests
location ~* \.(appcache)$ {
   expires 1m;
}

Verify with cURL that server response Content-Type has right MIME type and that the Expires and/or Cache-Control have correct 1 minute cache life time. If you get 404 error, make sure that site root configuration is set in http section.

$ curl -I http://localhost:8081/slots.appcache
HTTP/1.1 200 OK
Server: nginx/1.4.0
Date: Sun, 05 May 2013 04:43:32 GMT
Content-Type: text/cache-manifest
Content-Length: 38
Last-Modified: Sun, 05 May 2013 04:25:28 GMT
Connection: keep-alive
ETag: "5185df38-26"
Expires: Sun, 05 May 2013 04:44:32 GMT
Cache-Control: max-age=60
Accept-Ranges: bytes

Detecting online status

Currently only “reliable” method to do is to make Ajax request and check the response. There are some caveats

  • Request may fail for other reasons, and this does not mean browser is in offline mode
  • Offline status may change while user is in page, you may want to do repeat polling check.
  • User may be in public WiFi that redirects requests to login server. This can confuse your app that gets response but is not what was expected.

Slots game checks online status in parallel while game loads and only on startup. Slots game does not really need to know if it’s online or offline, but just writes the status on screen for debugging purposes.

<script type="text/javascript">$(function () {

    var game = SlotGame();

    // Attempt loading static json file from server to detect online or offline mode.
    // The url has unique random parameter to avoid browser or proxy caches
    $.ajax({
        url: 'js/online.json?ts=' + (~~new Date()),
        dataType: 'json',
        success: function(data) { 
            if ( data.online ) { 
                game.setOnlineStatus(true);
            } else {
                // might be online, but we didn't get expected response. Could be
                // e.g. Wifi login page.
                game.setOnlineStatus(false);
            }
        },
        error: function() {
            game.setOnlineStatus(false);
        }
    });

});</script>

Otherwise loading images, audio and other content should be fully transparent to your app. Think twice before doing separate logic for online and offline as things will get difficult. Best advice I can give is to that you write code for online use with proper handling for Ajax errors. In this way when app loads in online mode but loses network later in session (e.g. when user goes in subway tunnel), the experience does not break completely.

Testing

This is the part where things get interesting, offline is tricky to test because of caching and browser reload logic. See detailed lamentation about subject here in Dive into HTML5.

These are the best practices I’ve come up with. First, set browser manually to offline mode to try things out. e.g. in Firefox this is enabled from File->Work Offline.

Firefox offline

Second, if you develop the game from local server, do not use http://localhost as host, but use real domain name that resolves to localhost. In this example I’ve used http://hexxie.com that supports wildcard subdomain. Any subdomain resolves to address 127.0.0.1.

$ nslookup anything-goes-here.hexxie.com
Server:		192.168.1.254
Address:	192.168.1.254#53

Non-authoritative answer:
Name:	anything-goes-here.hexxie.com
Address: 127.0.0.1

In this way you can always start from scratch the offline debugging simply by changing subdomain name. For example I just used slotsoff1.hexxie.com, slotsoff2.hexxie.com, … etc.:

Trick url

Note that at least Firefox asks each time if you allow offline content.
Accept offline

Before each deploy, remember to increment the version comment in manifest file, so web server notices that the file has changed and browser will refresh it on next load. Server does not look inside the manifest file, so it does not matter how you change the file, as long as it’s changed.

Good Luck!

Code is available in Github.

Simple Slot machine game using HTML5 Part 3: Loading

UPDATE: See also Simple Slot machine game using HTML5 Part 4: Offline mode

In previous part 1 and part 2 I implemented simple slots machine purely in HTML5 with audio support.
After adding audio, the initial loading time increased a lot up to several seconds. This slowdown is easy to miss during development, as developer has always primed browser cache and content is loaded from development server that is hosted locally or even in developers machine.
It’s very important to occasionally clear the browser cache and put the game to remote server and reload the game to get realistic estimation of new users loading time (i.e. the empty cache experience).

This part improves the landing experience by adding loading progress bar so users know that the game is loading and will start soon.

Loading screen

Try out version with loading bar here.

How it works

Loading bar is simple nested div, where parent draws the outline and the child is the filling progress bar.

<div id="progressbar">
   <div id="progress"></div>
</div>

The CSS rules set the colors and position.

#progressbar
{
    margin-top: 10px;
    background: black;
    border: 1px solid mediumaquamarine;
    width: 80%;
    height: 15px;
    margin-left: auto;
    margin-right: auto;
}
#progressbar #progress
{
    height: 100%;
    position: relative;
    width: 0%;
    left: 0;
    background: #77e0fb;
}

Indicating progress is now simply matter of updating the width of #progress div.

Any HTML5 game loading bar implementation should be done purely with HTML and CSS and not use any images, as this just makes total loading experience slower and loading bar does not appear immediately when page renders. If you must use images, consider using base64 embedded images.

#progressbar {
  background-image:url(data:image/png;base64,iAE28w0KGgoABBANSAxhEegBB54DIA...);
  ...
}

If possible, void building the loading view with javascript as your page must load javascript before being able to show anything.
For best experience you should always have the CSS file in the <head> section of the HTML file and the javascript at the end of file, just before closing </body>. This way page can render with proper layout before any javascript has been loaded.

<!DOCTYPE HTML>
<html>
<head>
    ...
    <link href='http://fonts.googleapis.com/css?family=Slackey' rel='stylesheet' type='text/css'/>
    <link type="text/css" rel="stylesheet" href="css/reset.css"/>
    <link type="text/css" rel="stylesheet" href="css/slot.css"/>

</head>
<body>
<div id="viewport">
    ...
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="js/slot.js"></script>
<script type="text/javascript">$(function () { SlotGame(); });</script>
</body>
</html>

Both image and audio assets are loaded with same preloader, that keeps track of the progress and updates the progressbar. When loading is completed it hides the loading view. See the code for details how the preloader is used to load images and web/html5 audio files.

var progressCount = 0; // current progress count
var progressTotalCount = 0; // total count

function updateProgress( inc ) {
    progressCount += (inc || 1);
    if ( progressCount >= progressTotalCount ) {
        // done, complete progress bar and hide loading screen
        $('#progress').css('width', '100%');
        $('#loading').slideUp(600);
    } else {
        // Update progress bar
        $('#progress').css('width', parseInt( 100 * progressCount / progressTotalCount)  + '%');
    }
}

// Generic preloader handler, it calls preloadFunction for each item and
// passes function to it that it must call when done.
function preloader( items, preloadFunction, callback ) {

    var itemc = items.length;
    var loadc = 0;

    // called by preloadFunction to notify result
    function _check( err, id ) {
        updateProgress(1);
        if ( err ) {
            alert('Failed to load ' + id + ': ' + err);
        }
        loadc++;
        if ( itemc == loadc ) callback();
    }

    progressTotalCount += items.length;

    // queue each item for fetching
    items.forEach(function(item) {
        preloadFunction( item, _check);
    });
}

Improving loading time

You can further improve initial loading time by conventional web tricks

  • Enable HTTP gzip compression in the web server
  • Include jquery and library scripts from google api url, user may have those already in the cache
  • Merge and minify the javascript e.g. with uglify-js
  • Merge and minify the CSS with CSS compressor
  • Minify PNG image files with pngout. Use JPG where possible.
  • Compose all images in single montage and use that with CSS sprites and canvas ctx.drawImage
  • Convert audio files to mono to save half of the size. Use e.g. Audacity or ffmpeg
  • Host asset files in CDN, such as Akamai or Amazon S3. Note that if assets are loaded from different domain than the HTML document, you may encounter security problems if Cross-origin policy is not properly set for the content.

Use Chrome browsers audit tool get idea what takes most time.

Code is available in Github.

Continue to Simple Slot machine game using HTML5 Part 4: Offline mode

Simple Slot machine game using HTML5 Part 2: Audio

UPDATE: See also Simple Slot machine game using HTML5 Part 3: Loading.

In part 1 I presented simple slots machine purely in HTML5. This part extends the basic implementation with audio support. The game itself is simple slot machine that has only one winning line and we add effects for roll start, reel stop and win/loss.

Slots with audio

Try audio enabled game here. Note that loading time is significantly longer in audio enabled version. Debug text under button tells what audio system game uses for your browser.
Original non audio version from part 1 is here.

How to support Audio

Web game can implement audio in 3 main ways.

1. Flash player audio. (e.g. use SoundManager2 library)
2. HTML5 Audio (Buzz is easy way to use it)
3. Web Audio API. (See HTML5 Rocks for tutorial)

Flash audio is pretty much deprecated now, as only older browsers will still need it that most of don’t support HTML5 canvas anyway.
HTML5 Audio works very well for desktop browsers, but has only nominal support for mobile (Android & iOS). It’s generally too moody for tablets or mobile.
Web Audio API is supported only by latest browsers, but it works reliably e.g. in Safari iOS 6.0.

The libraries listed above simplify implementation a lot, but it’s easier to understand how these technologies work with simple examples. So I implemented both methods 2 and 3 from scratch.

Some caveats with audio.

  • Game initial loading time will increase. Audio files can be pretty large and they must be usually preloaded so they can be played on game start
  • iOS (iPad/iPhone) does not allow autoplay for audio. Audio must be enabled by playing some sound in click event handler.

Implementation

Initialization function accepts array of objects that have required audio file name in id property and callback that is called after audio has been initialized and loaded.
First code checks if mp3 or ogg is supported. Firefox requires .ogg and it’s easy to convert at least in OS/X or Linux with ffmpeg. Exact command line depends little on ffmpeg version.

$ ffmpeg -i win.mp3 -strict experimental -acodec vorbis -ac 2 win.ogg

When format is known, the code checks wether to use Web Audio API or normal HTML5 Audio.

function initAudio( audios, callback ) {

    var format = 'mp3';
    var elem = document.createElement('audio');
    if ( elem ) {
        // Check if we can play mp3, if not then fall back to ogg
        if( !elem.canPlayType( 'audio/mpeg;' ) && elem.canPlayType('audio/ogg;')) format = 'ogg';
    }

    var AudioContext = window.webkitAudioContext || window.mozAudioContext || window.MSAudioContext || window.AudioContext;

    if ( AudioContext ) {
        // Browser supports webaudio
        return _initWebAudio( AudioContext, format, audios, callback );
    } else if ( elem ) {
        // HTML5 Audio
        return _initHTML5Audio(format, audios, callback);
    } else {
        // audio not supported
        audios.forEach(function(item) {
            item.play = function() {}; // dummy play
        });
        callback();
    }
}

Both initialization functions attempt to load the desired format of needed audio files and sets play function in objects that is used to play the effect. If audio initialization fails, this play function is set to dummy empty function.

HTML5 Audio initialization function creates Audio objects and sets src to point to the audio file. Downloading is handled automagically by the browser.

function _initHTML5Audio( format, audios, callback ) {

    function _preload( asset ) {
        asset.audio = new Audio( 'audio/' + asset.id + '.' + format);
        asset.audio.preload = 'auto';
        asset.audio.addEventListener("loadeddata", function() {
            // Loaded ok, set play function in object and set default volume
            asset.play = function() {
                asset.audio.play();
            };
            asset.audio.volume = 0.6;
        }, false);

        asset.audio.addEventListener("error", function(err) {
            // Failed to load, set dummy play function
            asset.play = function() {}; // dummy
        }, false);

    }
    audios.forEach(function(asset) {
        _preload( asset );
    });
...

Web Audio initialization is bit more complicated, it needs to download the audio with XHR requests.

function _initWebAudio( AudioContext, format, audios, callback ) {

    var context = new AudioContext();

    function _preload( asset ) {
        var request = new XMLHttpRequest();
        request.open('GET',  'audio/' + asset.id + '.' + format, true);
        request.responseType = 'arraybuffer';

        request.onload = function() {
            context.decodeAudioData(request.response, function(buffer) {

                asset.play = function() {
                    var source = context.createBufferSource(); // creates a sound source
                    source.buffer = buffer;                    // tell the source which sound to play
                    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
                    source.noteOn(0);                          // play the source now
                };
                // default volume
                asset.gainNode = context.createGainNode();
                asset.gainNode.connect(context.destination);
                asset.gainNode.gain.value = 0.5;

            }, function(err) {
                asset.play = function() {};
            });
        };
        request.onerror = function(err) {
            asset.play = function() {};
        };
        request.send();
    }

    audios.forEach(function(asset) {
        _preload( asset );
    });

}

NOTE: Chrome supports XMLHttpRequest only when loading pages over HTTP. If you load the HTML file locally you’ll see erros like this in the error console:
XMLHttpRequest cannot load file:///Users/teemuikonen/work/blog/slot2/audio/roll.mp3. Cross origin requests are only supported for HTTP.

After audio has initialized, the game can play any effect simply by calling play function for the effect. If audio initialization or loading failed, the play is simply a dummy function.

$('#play').click(function(e) {
    // start game on play button click
    $('h1').text('Rolling!');
    game.audios[0].play(); // Play start audio
    ...

Code is available in Github.

Continue to Simple Slot machine game using HTML5 Part 3: Loading.

Ad-hoc static file web server for development use with Node.js

There is often need to serve local files through web server so you can access them with browser at http://localhost:8000/ for development and debugging.

Python has good tool, SimpleHTTPServer that can run standalone simply by command python -m SimpleHTTPServer in the directory you want to serve.

Node.js has something similar, http-server that works essentially in the same way.

That said, Today I thought that what internet needs is yet another example of http server with Node.js. Instead of the tools above I tend to use my own one-file implementation as it’s easy to expand and modify for testing purposes, for example to add some mock up Ajax endpoints.
The code also demonstrates some common node.js programming patterns so learning programmers might find it useful.

Code

Server is based on express framework and uses send and async modules to serve index.html and directory listings.

Minimum viable static file server is only few lines in express.

var express = require('express'),
    path = require('path');

var mainapp = express();
mainapp.use(express.static( process.cwd() ));
mainapp.listen(8000);

In case file is not found, we need fallback handler for checking the index.html or if that does not exists then build directory listing. Note that this listens only HTTP GET requests, not POST or HEADs.

mainapp.get('*', function(req, res) {
   var pathname = url.parse(req.url).pathname;
   pathname = path.join(dir, pathname);

    fs.stat(pathname, function(err, stat) {
        // Check if path is directory
        if ( !stat || !stat.isDirectory() ) return res.send(404);

        // check for index.html
        var indexpath = path.join(pathname, 'index.html');
        fs.stat(indexpath, function(err, stat) {
           if ( stat &amp;&amp; stat.isFile() ) {
               // index.html was found, serve that
               send(res, indexpath)
                   .pipe(res);
               return;

           } else {
               // No index.html found, build directory listing
               fs.readdir(pathname, function(err, list) {
                  if ( err ) return res.send(404);
                  return directoryHTML( res, req.url, pathname, list );
               });
           }
        });
    });
});

There is to default 404 handler, as express does this automatically.

Function that builds the HTML page from directory listing is surprisingly messy, as it’s not easy to do directory iteration with asynchronous node.js fs api. Directory listing use same HTML layout as python’s SimpleHTTPServer.

function directoryHTML( res, urldir, pathname, list ) {
    var ulist = [];

    function sendHTML( list ) {
        res.setHeader('Content-Type', 'text/html');
        res.send('<!DOCTYPE html>' +
            '<html>\n' +
            '<title>Directory listing for '+urldir+'</title>\n' +
            '<body>\n' +
            '<h2>Directory listing for '+urldir+'</h2>\n' +
            '<hr><ul>\n' +
            list.join('\n') +
            '</ul><hr>\n' +
            '</body>\n' +
            '</html>');
    }

    if ( !list.length ) {
        // Nothing to resolve
        return sendHTML( ulist );
    }

    // Check for each file if it's a directory or a file
    var q = async.queue(function(item, cb) {
        fs.stat(path.join(pathname, item), function(err, stat) {
           if ( !stat ) cb();
           if ( stat.isDirectory() ) {
               ulist.push('<li><a href="'+item+'/">'+item+'/</a></li>')
           } else {
               ulist.push('<li><a href="'+item+'">'+item+'</a></li>')
           }
            cb();
        });
    }, 4); // 4 parallel tasks
    // Push directory listing in workqueue
    list.forEach(function(item) {
        q.push(item);
    });
    // Set drain handler that is called when all tasks are completed
    q.drain = function() {
       sendHTML(ulist);
    };
}

Function uses async librarys queue to control the execution flow.

Running

Running directly with node. Server accepts web root directory as argument.
Download and see full code in Github.

$ git clone git://github.com/tikonen/blog.git
$ cd blog/simplehttpserver
$ node simplehttpserver.js ~/work
Listening port 8000 root dir /Users/teemuikonen/work

Server prints out request access log for debugging purposes.

127.0.0.1 - - [Sun, 2 Apr 2013 12:48:08 GMT] "GET / HTTP/1.1" 200 730 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:20.0) Gecko/20100101 Firefox/20.0"

You also install the server globally either from the checkout or from NPM repository. Install globally with

$ npm install -g simplehttpserver
$ simplehttpserver ~/work
Listening port 8000 root dir /Users/teemuikonen/work

Test server for Apple push notification and feedback integration

Here is a test server that you can use to verify your integration to Apple Push notification service and push notification feedback server. It should be good enough for testing out your application behavior and pdu format. Server helps you to get more debug info than just connecting directly to apple.

See these blog posts on details how to send push notifications

Test server runs under node.js and listens both SSL and plain ports where your application can connect. Get code from Github and generate the SSL keys (see quick howto in cert-howto.txt) and start up server

$ node server.js 
Waiting for connections in ports
Listening feedback port 2296 
Listening feedback port 2196 SSL
Listening push port 2295 
Listening push port 2195 SSL

Note that you may need to install binary module to run the server. Use npm install binary.

Successful push notification sending should look like following. The server dumps the data from your app in binary format for debugging and tries to parse it as Push PDU. Server prints out the fields so you can verify the data.

Accepted push connection 2195 1 SSL
=== RECEIVED DATA (1) ====
00000000: 0100 0000 0151 7261 1d00 206b 4628 de93  .....Qra...kF(^.
00000010: 17c8 0edd 1c79 1640 b58f dfc4 6d21 d0d2  .H.].y.@5._Dm!PR
00000020: d135 1687 239c 44d8 e30a b100 1e7b 2261  Q5..#.DXc.1..{"a
00000030: 7073 223a 7b22 616c 6572 7422 3a22 4865  ps":{"alert":"He
00000040: 6c6c 6f20 5075 7368 227d 7d              llo.Push"}}

=== PDU ====
{ command: 1,
  pduid: 1,
  expiry: Sat Apr 20 2013 17:34:21 GMT+0800 (SGT),
  tokenlength: 32,
  token: '6B4628DE9317C80EDD1C791640B58FDFC46D21D0D2D1351687239C44D8E30AB1',
  payloadlength: 30,
  payload: { aps: { alert: 'Hello Push' } } }

Test server does not validate the data, but it tries to parse JSON message in push notifications and prints error if it fails. Also if command was not set to 1, it sends back error pdu and closes connection. This should be good enough for testing. For example here I made HTTP request to the server to get some error output.

Accepted push connection 2295 1 SSL
=== RECEIVED DATA (1) ====
00000000: 4745 5420 2f20 4854 5450 2f31 2e31 0d0a  GET./.HTTP/1.1..
00000010: 5573 6572 2d41 6765 6e74 3a20 6375 726c  User-Agent:.curl
00000020: 2f37 2e32 392e 300d 0a48 6f73 743a 206c  /7.29.0..Host:.l
00000030: 6f63 616c 686f 7374 3a32 3239 350d 0a41  ocalhost:2295..A
00000040: 6363 6570 743a 202a 2f2a 0d0a 0d0a       ccept:.*/*....

=== PDU ====
{ command: 71,
  pduid: 1163141167,
  expiry: Sun Mar 01 1987 23:31:32 GMT+0800 (SGT),
  tokenlength: 20527,
  token: '312E310D0A557365722D4167656E743A206375726C2F372E32392E300D0A486F73743A206C6F63616C686F73743A323239350D0A4163636570743A202A2F2A0D0A0D0A',
  payloadlength: null,
  payload: 'ERROR: INVALID JSON PAYLOAD [SyntaxError: Unexpected end of input]' }
=== SEND ERROR: 08014554202F
Connection terminated 1

When your app connects successfully to the feedback test service, it sends back few feedback tokens and closes connection after one minute. Edit the actual tokens in the server source code.

Accepted feedback connection 2296 1
SEND: 2696A21000207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60
SEND: 2696A21100207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60
SEND: 2696A21100207518B1C2C7686D3B5DCAC8232313D5D0047CF0DC0ED5D753C017FFB64AD25B60

Source code is available in Github.

Minesweeper clone in HTML5

In my two previous block entries I wrote about one possible ways to do simple Slots and Car games on HTML5 technologies. This writeup combines some of those methods and introduces new ones to implement Minesweeper game with a twist.

mine

This game is actually “reverse” minesweeper, or should we say Applesweeper. Players mission is to find all the apples without clicking on of the empty tiles. Score is increased when player finds an apple and is decreased when he misses. Game ends when all the apples are found.

Try out the game here: http://www.nonstop-games.com/examples/mine/

As in previous games, hud is a html div, and the game grid is drawn on a canvas. Grey slab that hides tiles content is procedurally generated on offscreen canvas at the startup.

this.slab = document.createElement('canvas');
var ctx = this.slab.getContext('2d');
this.slab.width = this.resolution;
this.slab.height = this.resolution;

ctx.fillStyle = 'grey';
ctx.fillRect(0, 0, this.resolution, this.resolution);

ctx.beginPath();
ctx.fillStyle = 'white'
ctx.moveTo(0, 0);
ctx.lineTo(this.resolution, 0);
ctx.lineTo(this.resolution, this.resolution);
ctx.lineTo(0, 0);
ctx.closePath();
ctx.fill();

ctx.fillStyle = 'lightgrey';
ctx.fillRect(4, 4, this.resolution-8, this.resolution-8);

The this.slab holds off-screen canvas that contains the generated slab image.

Game area size and number of apples are function of screen size, to adapt to different screensizes.

var width  = window.innerWidth;
var height = window.innerHeight;

GRID_W = Math.min( 12, ~~(width / GRID_RESOLUTION));
GRID_H = Math.min( 12, ~~(height / GRID_RESOLUTION)) - 1;
var APPLE_COUNT = ~~((GRID_W * GRID_H) / 8);

For example, Here is the game on iPhone 3Gs.

mine_iphone

Game main loop is passive, when user clicks on the screen click handler sets the location on object that holds the clicked tile x and y coordinates

$('#container').click( function( e ){
    var p = $('#canvas').offset();        
    game.click = {
        x:parseInt((e.pageX-p.left)/game.resolution),
        y:parseInt((e.pageY-p.top)/game.resolution)};
    }
);

Main update handler is called on each frame and it checks if button has been clicked and updates the grid and redraws it if required. Grid is simple array where x and y are mapped as position.

Game.prototype.pos = function( x, y ) {
    return y*this.width+x;
}
Game.prototype.xy = function( pos ) {
    return {
        x:parseInt(pos%this.width),
        y:parseInt(pos/this.width)
    }
}

Grid array values are integers where content is bit masked. Higher bits are used to flag if grid location has slab and apple, empty or number.

var SLAB_MASK = Math.pow(2, 16);
var APPLE_MASK = Math.pow(2, 15);

// grid location (5, 6) has slab and number 3
var pos = this.pos(5, 6);
this.grid[pos] = 3;
this.grid[pos] |= SLAB_MASK;

The slab is removed from location just by negating it

this.grid[pos] &= ~SLAB_MASK;

Draw loop just checks for each position and checks with mask what it contains

for ( var y=0; y < this.height; y++ ) {
    for ( var x=0; x < this.width; x++ ) {
        // Draw each tile
        var s = this.pos(x,y);
       if (s & SLAB_MASK) {
          // Still covered tile
         this.ctx.drawImage( this.slab, x * this.resolution, y * this.resolution )

       } else if (s & APPLE_MASK) {
         // Uncovered apple
         this.ctx.drawImage( tile, x * this.resolution + 2, y * this.resolution + 2 )
       } else if (s > 0) {
         // Neighbour number
         this.ctx.fillText( '' + s ,
                            x * this.resolution + this.resolution/2,
                            y * this.resolution + this.resolution/2)
       }
    }
}

When player clicks on empty tile, recursive function walks through the grid clearing slabs from adjacent empty tiles.

function _empty( x, y, force ) {
    if ( x < 0 || x >= that.width || y < 0 || y >= that.height ) return;

    var pos = that.pos(x, y);
    var d = that.grid[pos];

    if (d && (d & SLAB_MASK) && (force || !(d & APPLE_MASK))) {

        that.grid[pos] &= ~SLAB_MASK; // clear out slab

        // Clear next neighbor if this is empty tile
        if (that.grid[pos] == 0) {
            _empty(x, y - 1) // north
            _empty(x, y + 1) // south
            _empty(x - 1, y) // west
            _empty(x - 1, y - 1) // north west
            _empty(x - 1, y + 1) // south east
            _empty(x + 1, y) // east
            _empty(x + 1, y - 1) // north east
            _empty(x + 1, y + 1) // south east
        }
    }
}

Code is available at GitHub.

Car Game on HTML5

This is continuation to my previous blog post, that discussed how to make simple Slots machine with HTML5. The basic principle in this driving game demo is similar, but in addition to that it adds dynamic graphics using canvas.

HTML5 Drive

Try it out here: http://www.nonstop-games.com/examples/drive/

Game road is div with background image that is translated down and back up to create illusion of moving road. This same method is used in Slots machine.

Car spritecar2

Cars are drawn on canvas on top of the road so while road moves cars appear moving forward. Each click starts moving the player car from current point to the click point using smooth  Bezier curve. Car acceleration is increased or decreased depending on click location. Player car also has smoke plume animation which intensity depends on acceleration.

function _bezier_quad(t, p0, p1, p2) {
     return {
         x: (1 - t)*( (1 -t) * p0.x + t*p1.x) + t * ( (1 - t) * p1.x + t * p2.x),
         y: (1 - t)*( (1 -t) * p0.y + t*p1.y) + t * ( (1 - t) * p1.y + t * p2.y)
     }
 }

After car has travelled long enough, the update loop starts slowing down road speed and moves in sync  div that holds Finish text and flags.

finish

Collision detection is done on each update. In case player hits on other cars, the other cars may blurb random bubbles that are added as normal div elements with rounded borders. Element location is updated based on car location.

bubble

In collision both cars are bumped so player can make room by hitting to other cars. Game loop is run by animation frames, so it stops if browser window goes in background. This is not optimal but makes implementation simpler. Better option is to run update with setInterval and draw on animation frames.

...
    var that = this;
    (function gameLoop() {
        that.clear(); // clear canvas objects
        that.update(); // update game state
        that.draw(); // draw game
        requestAnimFrame( gameLoop );
    })();
...

Update and draw in separate loops. This is better solution for complex games because browser will always call setInterval callback but requestAnimFrame callback only if browser window is visible and active. Callback is not executed if browser window is minimized or it’s tab is on background. If you rely only on requestAnimFrame the game is paused on background.

    var that = this;
    setInterval(function() {
        that.update(); // update game state
    }, 1000/30); // 30 frames

    (function gameLoop() {
        that.clear(); // clear canvas objects
        that.draw(); // draw game
        requestAnimFrame( gameLoop );
    })();

Game code is available in GitHub.

Follow

Get every new post delivered to your Inbox.

Join 51 other followers