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.

Web Audio API has two implementations in wild, the WebKit (iOS, Safari, Chrome) and the Standard (latest Firefox). Fortunately differences are small.

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)
                    // support both webkitAudioContext or standard AudioContext
                    source.noteOn ? source.noteOn(0) : source.start(0);
                };
                // default volume
                // support both webkitAudioContext or standard AudioContext
                asset.gain = context.createGain ? context.createGain() : context.createGainNode();
                asset.gain.connect(context.destination);
                asset.gain.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.