Chapter 4. HTML5 Sound and Processing Optimization

HTML5 brings game designers much more than the canvas element. Native support for sound (and video) is one key piece, letting you write games for which you manage sound in the same JavaScript environment as the graphics. Other pieces improve your JavaScript, whether breaking up tasks with Web Workers or letting you keep information on the player’s device with local and session storage.

Adding Sound with the Audio Element

Previous versions of the HTML spec supported three ways of listening to an audio file in our page. We could:

  • Use the object or embed tags to embed a file or a plugin such as LiveAudio (Netscape Navigator) or an ActiveMovie Control (Internet Explorer). As time went by, other plugins started to enter the market, such as Macromedia Flash (now Adobe Flash), REAL Player, or Apple’s QuickTime, among others. To embed a MIDI or WAV file, we could either use <embed src="music.mid" autostart="true" loop="true"> or embed a proprietary third-party plugin such as the Macromedia Flash Player and play the sound through our SWF file.

  • Insert a Java Applet and play the sound through it.

  • Add the bgsound attribute to the body element of the page (Internet Explorer only).

Browsers used to include varying sets of plugins, which meant that our sounds might play in one browser but not on another, even if both were installed on the same computer.

Luckily, with the advent of HTML5, we also gained the ability to play audio and video files natively through the use of the <audio> and <video> tags.

Unfortunately, the technological limitations in previous versions of HTML have been replaced by legal limitations in HTML5. Audio (and video) is encoded or decoded with the use of codecs, which are small software libraries that lets us encode/decode an audio or video data file or stream implementing a particular algorithm. Some algorithms are optimized for speed; other algorithms are optimized for lossless quality; just like conventional software, some of them are royalty-free and open and others are licensed.

In the case of “open” codecs, some companies such as Apple and Microsoft worry that they might be infringing a patent, which could make them liable in the case of a lawsuit—this is why they don’t support them in their browsers. The opposite scenario is that other companies, such as the Mozilla Foundation or Opera Software, haven’t made the necessary arrangements needed in order to use some licensed codecs yet.

Figure 4-1 shows which audio codecs are supported by each browser.

Support for audio codecs
Figure 4-1. Support for audio codecs

Hopefully, by the time the HTML5 spec is finished, all browser vendors will have agreed to use a generic codec that works across all platforms. In the meantime, the W3C provides an elegant way of handling these sorts of problems with the ability to define “fallback audio sources.”

To embed an audio player, all we need to do is to create an audio tag:

<audio src="../sounds/song.ogg" type="audio/ogg" controls />

which would show an audio player with a play/pause control (thanks to the control attribute’s presence). If we press the “Play” button, it will attempt to play the sound identified by the src attribute. However, we may find ourselves using a browser that doesn’t support that particular file format/codec, in which case we can do this:

<audio controls>
  <source src="../sounds/song.mp3" type="audio/mpeg">
  <source src="../sounds/song.ogg" type="audio/ogg">
</audio>

Instead of defining a single src parameter, the HTML5 audio tag allows us to define multiple audio files. If song.mp3 can’t be played for some reason, the browser will attempt to play the alternative song.ogg. If we had more sources, it would try to play every single one on the list until all of them failed or one of them worked. In addition to the control attribute, the HTML5 audio tag also supports other optional attributes:

loop

Lets us loop the media file specified in the src attribute or in a source tag

autoplay

Starts playing the sound as soon as it finishes loading the source

preload

Lets us define our source preload strategy:

preload="none”

Doesn’t preload the file, which will be loaded when the user presses the play button

preload="metadata”

Preloads only the metadata of the file

preload="auto”

Lets the browser handle the preload of the file (which usually means to preload the entire file)

Of course, we can also create and use an HTML5 audio object using JavaScript instead of including an audio element in the document, as shown in Example 4-1.

Example 4-1. Creating HTML5 audio with JavaScript
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Example 15 (HTML5 Audio)</title>
    <script>
      window.onload = function() {

        // Define an array with the audio files to try
        var sources = [
          ["../sounds/song.mp3", "audio/mpeg"],
          ["../sounds/song.ogg", "audio/ogg"]
        ];

        // Create the HTML5 Audio tag
        var audio = document.createElement('audio');

        // Cycle the "sources" array
        for (var i = 0; i < sources.length; i++) {

          // Later, you will learn how to check if a browser supports a given type

          // Create a source parameter
          var src = document.createElement('source');
          // Add both src and type attributes
          src.setAttribute("src", sources[i][0]);
          src.setAttribute("type", sources[i][1]);

          // Append the source to the Audio tag
          audio.appendChild(src);
        }

        // Attempt to play the sound
        audio.play();

      }
    </script>
  </head>
  <body>
    HTML5 Audio tag example.
  </body>
</html>

Note

The code repository also contains a more efficient example, ex15-audioPlayer-alt.html, with an alternative way of figuring out if the audio format is supported by the browser.

Along with the HTML5 Audio and Video objects, modern browsers trigger a new set of events called Media Events; a complete list is available at https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox.

Some of the events that we will be using in this book and in our game are:

canplaythrough

Triggered when the file download has almost finished and can be played in its entirety

playing

Informs us if a sound is being played

ended

Triggered when playback has finished

Playback of HTML5 audio (and video) files is controlled using the following methods and variables:

play()

Plays the media.

pause()

Pauses the media.

currentTime

Allows us to get or set the current playback time, expressed in milliseconds.

Volume

Allows us to get or set the current volume, as a value ranging from 0.0 to 1.0.

Note

An ongoing project in the Mozilla Foundation called the Audio Data API allows developers to create and manipulate sounds with better accuracy and a greater degree of control. (At the time of writing of this book, the API is available only in Firefox and in the latest versions of Chromium.) For more information, see https://wiki.mozilla.org/Audio_Data_API.

Now that you understand the basics on how to use this technology and its capabilities, you should also be aware of its limitations:

  • Once you create an HTML5 audio object and start playing a sound, you can play that sound only once at a time. If you want to play the same sound two times simultaneously, you need an additional HTML5 audio object.

  • There’s a limit to the number of simultaneous playbacks; this limit varies from platform to platform. If you exceed that limit, you may experience errors that also vary from platform to platform.

A good rule of thumb is to keep the number of simultaneous audio playbacks to three (or fewer), as that is the playback limit on mobile OSs. Other platforms (such as Firefox running in a PC) can support a larger number of simultaneous playbacks.

When discussing the HTML Canvas section, we combined several images in a single image (called a sprite sheet) to optimize the number of requests made to the server, and then we could reference a particular image inside the sprite sheet by showing the rectangle of X1, Y1 and X2, Y2 (where X1, Y1, X2, Y2 are pixel coordinates). We can use a similar technique with sounds by combining them into a single file (called a sound sheet)—and instead of using pixel coordinates, we need to use time coordinates. (If you are feeling fancy, you could also put different sounds in the left and right audio channels, which could help you optimize requests even more, at the expense of losing one channel and playing sounds in mono.)

In our game, we’re going to be using a utility called SoundUtil that will handle sound sheets and take care of maintaining a pool of audio objects for effective memory and resource management.

SoundUtil uses a different approach than the one used in Example 7. Instead of creating an HTML5 audio tag, it creates HTML5 audio objects. When you request the utility to play a sound, you pass some parameters:

  • An array containing the files themselves and the format in which each file is encoded

  • A start time, expressed in seconds

  • An end time, expressed in milliseconds

  • A volume value

  • A boolean indicating whether you want the sound to loop forever, in which case, it will only respect the start time the first time it plays and won’t respect the value passed as the end time

The play() method will call another method, getAudioObject(), that maintains a pool of audio objects available for reuse and keeps track of the maximum number of simultaneous playbacks. If no objects are available on the pool, it will automatically generate one unless the number of objects on the pool is equal to the maximum number of simultaneous playbacks, in which case it will return a null value and no sound will be played.

Once a sound finishes playing, we need to “free” the audio object to put it back in the pool of available audio objects by calling the freeAudioObject() method.

The entire utility can be seen in Example 4-2.

Example 4-2. SoundUtil.js
// Maximum number of sound objects allowed in the pool
var MAX_PLAYBACKS = 6;
var globalVolume = 0.6;

function SoundUtil(maxPlaybacks) {
  this.maxPlaybacks = maxPlaybacks;
  this.audioObjects = []; // Pool of audio objects available for reutilization
}

SoundUtil.prototype.play = function(file, startTime, duration, volume, loop) {

  // Get an audio object from pool
  var audioObject = this.getAudioObject();
  var suObj = this;

  /**
   * No audio objects are available on the pool. Don't play anything.
   * NOTE: This is the approach taken by toy organs; alternatively you
   * could also add objects into a queue to be played later on
   */
  if (audioObject !== null) {
    audioObject.obj.loop = loop;
    audioObject.obj.volume = volume;

    for (var i = 0; i < file.length; i++) {
      if (audioObject.obj.canPlayType(file[i][1]) === "maybe" ||
        audioObject.obj.canPlayType(file[i][1]) === "probably") {
        audioObject.obj.src = file[i][0];
        audioObject.obj.type = file[i][1];
        break;
      }
    }

    var playBack = function() {
      // Remove the event listener, otherwise it will
      // keep getting called over and over agian
      audioObject.obj.removeEventListener('canplaythrough', playBack, false);
      audioObject.obj.currentTime = startTime;
      audioObject.obj.play();

      // There's no need to listen if the object has finished
      // playing if it's playing in loop mode
      if (!loop) {
        setTimeout(function() {
          audioObject.obj.pause();
          suObj.freeAudioObject(audioObject);
        }, duration);
      }
    }

    audioObject.obj.addEventListener('canplaythrough', playBack, false);
  }
}

SoundUtil.prototype.getAudioObject = function() {
  if (this.audioObjects.length === 0) {
    var a = new Audio();
    var audioObject = {
      id: 0,
      obj: a,
      busy: true
    }

    this.audioObjects.push (audioObject);

    return audioObject;
  } else {
    for (var i = 0; i < this.audioObjects.length; i++) {
      if (!this.audioObjects[i].busy) {
        this.audioObjects[i].busy = true;
        return this.audioObjects[i];
      }
    }

    // No audio objects are free. Can we create a new one?
    if (this.audioObjects.length <= this.maxPlaybacks) {
      var a = new Audio();
      var audioObject = {
        id: this.audioObjects.length,
        obj: a,
        busy: true
      }

      this.audioObjects.push (audioObject);

      return audioObject;
    } else {
        return null;
    }
  }
}

SoundUtil.prototype.freeAudioObject = function(audioObject) {
    for (var i = 0; i < this.audioObjects.length; i++) {
        if (this.audioObjects[i].id === audioObject.id) {
            this.audioObjects[i].currentTime = 0;
            this.audioObjects[i].busy = false;
        }
    }
}

To demonstrate how to use SoundUtil, we’re going to combine it with the very first example shown in this book: the title screen. Example 4-3, ex16-soundUtil.html, is in the examples folder of the code repository included with this book.

Example 4-3. Adding sounds to our title screen
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Example 16 - Title Screen with Sound</title>

    <!-- We're included the soundutil as an external file -->
    <script src="soundutil.js" charset="utf-8"></script>
    <script>

      window.onload = function () {
        var su = null;
        var sources = [
          ["../sounds/title.mp3", "audio/mp3"],
          ["../sounds/title.ogg", "audio/ogg"]
        ];

        var canvas = document.getElementById('myCanvas');
        var c = canvas.getContext('2d');

        var State = {
          _current: 0,
          INTRO: 0,
          LOADING: 1,
          LOADED: 2
        }

        window.addEventListener('click', handleClick, false);
        window.addEventListener('resize', doResize, false);

        doResize();

        // Check if the current browser supports playing MP3 or OGG files
        if (soundIsSupported()) {
          // Play the title screen music
          playTitleMusic();
        }

        function playTitleMusic() {
          if (su) {
            su.play(sources, 0, 156000, globalVolume, false);
          }
        }

        function soundIsSupported() {
          var a = new Audio();
          var failures = 0;

          for (var i = 0; i < sources.length; i++) {
            if (a.canPlayType(sources[i][1]) !== "maybe" &&
              a.canPlayType(sources[i][1]) !== "probably") {
              failures++;
            }
          }

          if (failures !== sources.length) {
            su = new SoundUtil()
            return true;
          } else {
            return false;
          }
        }

        function handleClick() {
          if (State._current !== State.LOADING) {
            State._current = State.LOADING;
            fadeToWhite();
          }
        }

        function doResize() {
          canvas.width = document.body.clientWidth;
          canvas.height = document.body.clientHeight;

          switch (State._current) {
            case State.INTRO:
              showIntro ();
              break;
          }
        }

        function fadeToWhite(alphaVal) {
          // If the function hasn't received any parameters, start with 0.02
          var alphaVal = (alphaVal == undefined) ? 0.02 : parseFloat(alphaVal) + 0.02;


          // Set the color to white
          c.fillStyle = '#FFFFFF';
          // Set the Global Alpha
          c.globalAlpha = alphaVal;

          // Make a rectangle as big as the canvas
          c.fillRect(0, 0, canvas.width, canvas.height);

          if (alphaVal < 1.0) {
            setTimeout(function() {
              fadeToWhite(alphaVal);
            }, 30);
          } else {
            State._current = State.LOADED;
          }
        }

        function showIntro () {
          var phrase = "Click or tap the screen to start the game";

          // Clear the canvas
          c.clearRect (0, 0, canvas.width, canvas.height);

          // Make a nice blue gradient
          var grd = c.createLinearGradient(0, canvas.height, canvas.width, 0);
          grd.addColorStop(0, '#ceefff');
          grd.addColorStop(1, '#52bcff');

          c.fillStyle = grd;
          c.fillRect(0, 0, canvas.width, canvas.height);

          var logoImg = new Image();
          logoImg.src = '../img/logo.png';

          // Store the original width value so that we can
          // keep the same width/height ratio later
          var originalWidth = logoImg.width;

          // Compute the new width and height values
          logoImg.width = Math.round((50 * document.body.clientWidth) / 100);
          logoImg.height = Math.round((logoImg.width * logoImg.height) /
originalWidth);

          // Create an small utility object
          var logo = {
            img: logoImg,
            x: (canvas.width/2) - (logoImg.width/2),
            y: (canvas.height/2) - (logoImg.height/2)
          }

          // Present the image
          c.drawImage(logo.img, logo.x, logo.y, logo.img.width, logo.img.height);

          // Change the color to black
          c.fillStyle = '#000000';
          c.font = 'bold 16px Arial, sans-serif';

          var textSize = c.measureText (phrase);
          var xCoord = (canvas.width / 2) - (textSize.width / 2);

          c.fillText (phrase, xCoord, (logo.y + logo.img.height) + 50);
        }
      }
    </script>
    <style type="text/css" media="screen">
      html { height: 100%; overflow: hidden }
      body {
        margin: 0px;
        padding: 0px;
        height: 100%;
      }
    </style>

  </head>
  <body>
    <canvas id="myCanvas" width="100" height="100">
      Your browser doesn't include support for the canvas tag.
    </canvas>
  </body>
</html>

We’re also going to modify ex14-gui.html to add a background music to our game. The example can be found online as ex14-gui-sound.html.

Managing Computationally Expensive Work with the Web Workers API

Now that we have managed to develop a high-performance graphics rendering function for our final game, it would be nice to implement a path-finding function, which will be useful to build roads or to display characters going from point A to point B.

In a nutshell, path-finding algorithms discover the shortest route between two points in an n-dimensional space, usually 2D or 3D.

Usually, path finding is one of the few areas that only a few selected people can get right—and that many people (hopefully not including us) will get wrong. It is one of the most expensive processes to execute, and the most efficient solution usually requires us to modify the algorithm to customize it to our product.

One of the best algorithms to handle path finding is A*, which is a variation of Dijkstra’s algorithm. The problem with any path-finding algorithm—or, for that matter, any computationally expensive operation that needs more than a couple of milliseconds to get solved—is that in JavaScript, they produce an effect called “interface locking” in which the browser freezes until the operation has finished.

Fortunately, the HTML5 specification also provides a new API called Web Workers. Web Workers (usually just called “workers”) allow us to execute relatively computational expensive and long-lived scripts in the background without affecting the main user interface of the browser.

Note

Workers are not silver bullets that will magically help us to solve tasks that are eating 100% of our CPU processing capabilities. If a task is processor-intensive using the conventional approach, it will probably also be processor-intensive when using workers and will wind up affecting the user experience anyway. However, if a task is consuming 30% of the CPU, workers can help us minimize the impact on the user interface by executing the task in parallel.

Also, there are some limitations:

  • Because each worker runs in a totally separate, thread-safe context from the page that executed it (also known as a sandbox), they won’t have access to the DOM and window objects.

  • Although you can spawn new workers from within a worker (this feature is not available in Google Chrome), be careful, because this approach can lead to bugs that are very difficult to debug.

Workers can be created with this code:

var worker = new Worker(PATH_TO_A_JS_SCRIPT);

where PATH_TO_A_JS_SCRIPT could be, for example, astar.js. Once our worker has been created, we can terminate the execution at any given time by calling worker.close(). If a worker has been closed and we need to perform a new operation, we’ll need to create a new worker object.

Back and forth communication with Web Workers is accomplished by using the worker.postMessage(object) method to send a message and defining a callback function on the worker.onmessage event. Additionally, you can define an onerror handler to process errors on the worker.

Just like a conventional page, Web Workers also allow us to call external scripts by using the function importScripts(). This function accepts zero or multiple parameters (each parameter is a JavaScript file).

An example implementation of the A* algorithm in JavaScript called using Web Workers can be found in the code repository as ex17-grid-astar.html. Figure 4-2 shows that worker’s progress. The code is shown in Example 17 and an A* implementation developed in JavaScript.

in action
Figure 4-2. Example 4-4 in action
Example 4-4. Path-finding HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Example 17 - (A* working on a grid with unset indexes using
           web workers)</title>

    <script>

      window.onload = function () {
        var tileMap = [];

        var path = {
          start: null,
          stop: null
        }

        var tile = {
          width: 6,
          height: 6
        }

        var grid = {
          width: 100,
          height: 100
        }

        var canvas = document.getElementById('myCanvas');
        canvas.addEventListener('click', handleClick, false);

        var c = canvas.getContext('2d');

        // Generate 1000 random elements
        for (var i = 0; i < 1000; i++) {
          generateRandomElement();
        }

        // Draw the entire grid
        draw();

        function handleClick(e) {
          // When a click is detected, translate the mouse
          // coordinates to pixel coordinates
          var row = Math.floor((e.clientX - 10) / tile.width);
          var column = Math.floor((e.clientY - 10) / tile.height);

          if (tileMap[row] == null) {
            tileMap[row] = [];
          }

          if (tileMap[row][column] !== 0 && tileMap[row][column] !== 1) {
            tileMap[row][column] = 0;

            if (path.start === null) {
              path.start = {x: row, y: column};
            } else {
              path.stop = {x: row, y: column};

              callWorker(path, processWorkerResults);

              path.start = null;
              path.stop = null;
            }

            draw();
          }
        }

        function callWorker(path, callback) {
          var w = new Worker('astar.js');
          w.postMessage({
            tileMap: tileMap,
            grid: {
              width: grid.width,
              height: grid.height
            },
            start: path.start,
            stop: path.stop
          });
          w.onmessage = callback;
        }

        function processWorkerResults(e) {
          if (e.data.length > 0) {
            for (var i = 0, len = e.data.length; i < len; i++) {
              if (tileMap[e.data[i].x] === undefined) {
                tileMap[e.data[i].x] = [];
              }

              tileMap[e.data[i].x][e.data[i].y] = 0;
            }
          }

          draw();
        }

        function generateRandomElement() {
          var rndRow = Math.floor(Math.random() * (grid.width + 1));
          var rndCol = Math.floor(Math.random() * (grid.height + 1));


          if (tileMap[rndRow] == null) {
            tileMap[rndRow] = [];
          }
          tileMap[rndRow][rndCol] = 1;
        }

        function draw(srcX, srcY, destX, destY) {
          srcX = (srcX === undefined) ? 0 : srcX;
          srcY = (srcY === undefined) ? 0 : srcY;
          destX = (destX === undefined) ? canvas.width : destX;
          destY = (destY === undefined) ? canvas.height : destY;

          c.fillStyle = '#FFFFFF';
          c.fillRect (srcX, srcY, destX + 1, destY + 1);
          c.fillStyle = '#000000';

          var startRow = 0;
          var startCol = 0;
          var rowCount = startRow + Math.floor(canvas.width / tile.width) + 1;
          var colCount = startCol + Math.floor(canvas.height / tile.height) + 1;

          rowCount = ((startRow + rowCount) > grid.width) ? grid.width : rowCount;
          colCount = ((startCol + colCount) > grid.height) ? grid.height : colCount;

          for (var row = startRow; row < rowCount; row++) {
            for (var col = startCol; col < colCount; col++) {
              var tilePositionX = tile.width * row;
              var tilePositionY = tile.height * col;

              if (tilePositionX >= srcX && tilePositionY >= srcY &&
                tilePositionX <= (srcX + destX) &&
                tilePositionY <= (srcY + destY)) {

                if (tileMap[row] != null && tileMap[row][col] != null) {
                  if (tileMap[row][col] == 0) {
                    c.fillStyle = '#CC0000';
                  } else {
                    c.fillStyle = '#0000FF';
                  }

                  c.fillRect(tilePositionX, tilePositionY, tile.width, tile.height);
                } else {
                  c.strokeStyle = '#CCCCCC';
                  c.strokeRect(tilePositionX, tilePositionY, tile.width, tile.height);
                }
              }
            }
          }
        }
      }
    </script>
    </head>
    <body>
    <canvas id="myCanvas" width="600" height="300"></canvas>
    <br />

    </body>
</html>
Example 4-5. JavaScript A* class
// The worker will take care of the instantiation of the astar class
onmessage = function(e){
  var a = new aStar(e.data.tileMap, e.data.grid.width, e.data.grid.height, e.data.start, e.data.stop);
  postMessage(a);
}


// A* path-finding class adjusted for a tileMap with noncontiguous indexes

/**
 * @param tileMap: A 2-dimensional matrix with noncontiguous indexes
 * @param gridW: Grid width measured in rows
 * @param gridH: Grid height measured in columns
 * @param src: Source point, an object containing X and Y
 *             coordinates representing row/column
 * @param dest: Destination point, an object containing
 *              X and Y coordinates representing row/column
 * @param createPositions: [OPTIONAL] A boolean indicating whether
 *                         traversing through the tileMap should
 *                         create new indexes (default TRUE)
 */
var aStar = function(tileMap, gridW, gridH, src, dest, createPositions) {
  this.openList = new NodeList(true, 'F');
  this.closedList = new NodeList();
  this.path = new NodeList();
  this.src = src;
  this.dest = dest;
  this.createPositions = (createPositions === undefined) ? true : createPositions;
  this.currentNode = null;

  var grid = {
    rows: gridW,
    cols: gridH
  }

  this.openList.add(new Node(null, this.src));

  while (!this.openList.isEmpty()) {
    this.currentNode = this.openList.get(0);
    this.currentNode.visited = true;

    if (this.checkDifference(this.currentNode, this.dest)) {
      // Destination reached :)
      break;
    }

    this.closedList.add(this.currentNode);
    this.openList.remove(0);

    // Check the 8 neighbors around this node
    var nstart = {
      x: (((this.currentNode.x - 1) >= 0) ? this.currentNode.x - 1 : 0),
      y: (((this.currentNode.y - 1) >= 0) ? this.currentNode.y - 1 : 0),
    }

    var nstop = {
      x: (((this.currentNode.x + 1) <= grid.rows) ? this.currentNode.x + 1 : grid.rows),
      y: (((this.currentNode.y + 1) <= grid.cols) ? this.currentNode.y + 1 : grid.cols),
    }

    for (var row = nstart.x; row <= nstop.x; row++) {
      for (var col = nstart.y; col <= nstop.y; col++) {

        // The row is not available on the original tileMap, should we keep going?
        if (tileMap[row] === undefined) {
          if (!this.createPositions) {
            continue;
          }
        }

        // Check for buildings or other obstructions
        if (tileMap[row] !== undefined && tileMap[row][col] === 1) {
          continue;
        }

        var element = this.closedList.getByXY(row, col);
        if (element !== null) {
          // this element is already on the closed list
          continue;
        } else {
          element = this.openList.getByXY(row, col);
          if (element !== null) {
            // this element is already on the closed list
            continue;
          }
        }

        // Not present in any of the lists, keep going.
        var n = new Node(this.currentNode, {x: row, y: col});
        n.G = this.currentNode.G + 1;
        n.H = this.getDistance(this.currentNode, n);
        n.F = n.G + n.H;

        this.openList.add(n);
      }
    }
  }

  while (this.currentNode.parentNode !== null) {
    this.path.add(this.currentNode);
    this.currentNode = this.currentNode.parentNode;
  }

  return this.path.list;
}

aStar.prototype.checkDifference = function(src, dest) {
  return (src.x === dest.x && src.y === dest.y);
}

aStar.prototype.getDistance = function(src, dest) {
  return Math.abs(src.x - dest.x) + Math.abs(src.y - dest.y);
}

function Node(parentNode, src) {
  this.parentNode = parentNode;
    this.x = src.x;
    this.y = src.y;
    this.F = 0;
    this.G = 0;
    this.H = 0;
}

var NodeList = function(sorted, sortParam) {
  this.sort = (sorted === undefined) ? false : sorted;
  this.sortParam = (sortParam === undefined) ? 'F' : sortParam;
  this.list = [];
  this.coordMatrix = [];
}

NodeList.prototype.add = function(element) {
  this.list.push(element);

  if (this.coordMatrix[element.x] === undefined) {
    this.coordMatrix[element.x] = [];
  }

  this.coordMatrix[element.x][element.y] = element;

  if (this.sort) {
    var sortBy = this.sortParam;
    this.list.sort(function(o1, o2) { return o1[sortBy] - o2[sortBy]; });
  }
}

NodeList.prototype.remove = function(pos) {
  this.list.splice(pos, 1);
}

NodeList.prototype.get = function(pos) {
  return this.list[pos];
}

NodeList.prototype.size = function() {
  return this.list.length;
}

NodeList.prototype.isEmpty = function() {
  return (this.list.length == 0);
}

NodeList.prototype.getByXY = function(x, y) {
  if (this.coordMatrix[x] === undefined) {
    return null;
  } else {
    var obj = this.coordMatrix[x][y];

    if (obj == undefined) {
      return null;
    } else {
      return obj;
    }
  }
}
NodeList.prototype.print = function() {
  for (var i = 0, len = this.list.length; i < len; i++) {
    console.log(this.list[i].x + ' ' + this.list[i].y);
  }
}

Local Storage and Session Storage

One limitation that web developers had to deal with in the past was that the size of cookies wasn’t enough to save anything too big or important; it was limited to just 4K. Nowadays, modern web browsers are including support for Web Storage, a tool that can help us save at least 5 megabytes (MB) on the user’s local HDD (hard disk drive). However, when we say “at least 5 MB,” it can actually be more or less than that amount, depending on which browser we’re using, and—in some cases (as in Opera)—the space quota that we defined in our settings panel. In some cases, the Web Storage capabilities could be disabled entirely. If we exceed the 5 MB of storage, the browser will throw a QUOTA_EXCEEDED_ERR exception, so it’s very important to surround calls to localStorage or sessionStorage in a try-catch block and—just like we should be doing with the rest of our code—handle any exceptions appropriately.

Expect Web Storage to behave very similarly to cookies:

  • The functionality could be disabled.

  • We have a maximum amount of elements that we can store—4K in the case of cookies and 5MB in the case of Web Storage, but it could be less than that as well.

  • Users can delete or manually create and/or modify the contents of our storage folder at any given time.

  • Browsers can also decide to automatically “expire” the content of our storage folder.

  • The quota is given per domain name, and shared by all the subdomains (i.e., site1.example.com, site2.example.com, and site3.example.com share the same folder).

However, Web Storage differs from cookies in the following ways:

  • The data saved with the Web Storage API can only be queries made by the client, not by the server.

  • The contents do not travel “back and forth” on every request.

  • With the exception of sessionStorage (which deletes its contents once the session is over), we can’t explicitly specify an expiration date.

  • Just as with cookies, it’s best to not use Web Storage to save anything important.

Other than that, it’s a great tool that can help us do things that we couldn’t do before, like caching objects to increase loading performance the next time the user opens our application, saving a draft of the document we’re working on, or even using it as a virtual memory container.

Once you understand the limitations and capabilities of Web Storage, the rest is pretty straightforward:

  • Data is stored in an array of key-value pairs that are treated and stored like strings.

  • The difference between localStorage and sessionStorage is that localStorage stores the data permanently (or until the user or the browser decides to dispose of it), and sessionStorage stores it only for the duration of the “session” (until we close the tab/window).

The Web Storage API consists of only four methods:

localStorage.setItem(key, value)

Adds an item

localStorage.getItem(key)

Queries an existing item

localStorage.removeItem(key)

Removes a specific item

localStorage.clear()

Entirely deletes the contents of our localStorage folder

Example 4-6 shows how to use Web Storage.

Example 4-6. Using Web Storage
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Canvas Example 18 (LocalStorage)</title>

    <script>
      window.onload = function () {
        var binMatrix = null;
        var matrixSize = 25;

        // This is the key name under which we will be storing,
        // loading or removing the data
        var KEY_NAME = "matrix";

        var matrix = document.getElementById('matrix');
        var load = document.getElementById('load');
        var save = document.getElementById('save');
        var clear = document.getElementById('clear');

        binMatrix = initializeMatrix(matrixSize);
        printMatrix(binMatrix, matrix);

        // Handle click events on the buttons
        load.addEventListener('click', handleLoad, false);
        save.addEventListener('click', handleSave, false);
        clear.addEventListener('click', handleClear, false);

        function handleLoad() {
          var m = localStorage.getItem(KEY_NAME);

          try {
            // If we haven't set the key yet, or we removed its
            // contents with the "m" variable,
            // will be null.
            if (m == null) {
              alert("You haven't stored a matrix yet.");
            } else {
              // Otherwise, we need to "parse" the contents back to an array.
              binMatrix = JSON.parse(m);

              // Clear the original matrix
              matrix.innerHTML = null;

              // And reprint it
              printMatrix(binMatrix, matrix);
            }          } catch(e) {
            alert("The following error occurred while trying to load the matrix: " + e);
          }
        }

        function handleSave() {
          try {
            // Read the values of the checkbox inside the "matrix" div
            // and replace them accordingly in the array
            for (var i = 0; i < matrixSize; i++) {
              for (var j = 0; j < matrixSize; j++) {
                var pos = (i + j) + (i * matrixSize);

                if (matrix.childNodes[pos].tagName == "INPUT") {
                  binMatrix[i][j] = (matrix.childNodes[pos].checked) ? 1 : 0;
                }
              }
            }
            // Finally, stringify the matrix for storage and save it
            localStorage.setItem(KEY_NAME, JSON.stringify(binMatrix));
          } catch(e) {
            alert("The following error occurred while trying to save the matrix: " + e);
          }
        }

        function handleClear() {
          if (confirm("Are you sure that you want to empty the matrix?")) {
            try {
              localStorage.removeItem(KEY_NAME);

              // Clear the original matrix
              matrix.innerHTML = null;
              binMatrix = null;

              // Regenerate the matrix
              binMatrix = initializeMatrix(matrixSize);

              // And reprint it
              printMatrix(binMatrix, matrix);
            } catch(e) {
              alert("The following error occurred while trying to remove the matrix: " + e);
            }
          }
        }
      }

      /**
       * Generic matrix initialization routine
       */
      function initializeMatrix(size) {
        var m = [];

        for (var i = 0; i < size; i++) {
          m[i] = [];
          for (var j = 0; j < size; j++) {
            m[i][j] = 0;
          }
        }

        return m;
      }

      /**
       * The following function gets the matrix and converts it to a long string
       * of checkboxes, to then insert it inside the "matrix" <div>
       * It is considered a good practice, unless you really need to
       * do otherwise, to use strings to generate HTML elements
       * in order to avoid having to create a new object for every new
       * element that you want to add.
       * Concatenate all the strings together and insert them to the
       * object "all at once" in order to prevent
       * unnecessary and performance-heavy browser reflows.
       */
      function printMatrix(m, elem) {
        var str = "";

        for (var i = 0, x = m.length; i < x; i++) {
          for (var j = 0, r = m[i].length; j < r; j++) {
            str += '<input type="checkbox" class="' + i + ' - ' + j + '" ';
            str += (m[i][j] == 1) ? 'checked' : '';
            str += ' />';
            str += ((j + 1) == r) ? '<div class="clb"></div>' : '';
          }
        }

        elem.innerHTML = str;
      }
    </script>

    <style type="text/css" media="screen">
      body {
        margin: 20px;
        padding: 0px;
      }

      #matrix input {
        float: left;
        padding: 0px;
        margin: 0px;
      }
      div.clb { clear: both; }
    </style>
  </head>
  <body>
    <input type="button" id="load" value="Load Matrix" />
    <input type="button" id="save" value="Save Matrix" />
    <input type="button" id="clear" value="Clear Matrix" />

    <br /><br />

    <div id="matrix"></div>
  </body>
</html>

The complete code for Example 4-6 is stored as ex18-localStorage.html in the examples folder of the code repository.

Note

For more information, refer to the Web Storage section of the HTML5 specification (still a draft at the time of writing of this book): http://dev.w3.org/html5/webstorage/.

We’re not going to be using localStorage in our game. However, if you’re working with very large grids full of elements, it’s recommended to work only with “portions” of the matrix containing all of our objects. By slightly modifying the code presented here, you can download additional tile positions from a server as you scroll around the grid and leave them ready to be swapped with the current matrix by storing the result on localStorage.

Get Making Isometric Social Real-Time Games with HTML5, CSS3, and JavaScript now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.