Gaming: Battle on the High Seas, Part 2

Jeff Friesen
Tweet
This entry is part 2 of 5 in the series Battle on the High Seas

Battle on the High Seas

Last week, I introduced an HTML5 game known as SeaBattle, as a demonstration of what you can accomplish with HTML5’s Audio, Canvas, and Web Storage APIs. I then showed you how to embed this game in a Web page, and overviewed its architecture. This article starts to dig into this architecture by focusing on initialization. It explores the SeaBattle object’s init(width, height) and related functions.

Initializing SeaBattle

Listing 1 presents the implementation of the init(width, height) function.

init: function(width, height) {
  var canvas = $("<canvas width='"+width+"' height='"+height+"'></canvas>");
  canvas.appendTo("body");
  SeaBattle.ctx = canvas.get(0).getContext("2d");
  SeaBattle.ctx.font = "30px Arial";
  SeaBattle.ctx.textAlign = "center";

  var seed = 5*height/6;
  SeaBattle.hillTops = new Array();
  for (var i = 0; i < width; i++)
  {
    SeaBattle.hillTops.push(seed);
    var x = SeaBattle.rnd(seed);
    if (x < seed/4)
    {
      if (--seed < 2*height/3)
        seed = 2*height/3;
    }
    else
    if (x > 3*seed/4)
    {
      if (++seed > height-1)
        seed = height-1;
    }
  }

  SeaBattle.width = width;
  SeaBattle.height = height;
  SeaBattle.dc = new Array(SeaBattle.MAX_DC);
  SeaBattle.torp = new Array(SeaBattle.MAX_TORP);
  SeaBattle.explosion = null;
  SeaBattle.msg = "";
  SeaBattle.score = 0;
  SeaBattle.hiScore = 0;
  if (SeaBattle.supports_html5_storage())
  {
    var temp = localStorage.getItem("hiScore");
    if (temp != undefined)
      SeaBattle.hiScore = temp;
  }
  SeaBattle.lives = 4;
  window.keydown = {};
  function keyName(event)
  {
    return jQuery.hotkeys.specialKeys[event.which] ||
           String.fromCharCode(event.which).toLowerCase();
  }
  $(document).bind("keydown", function(event) {
    keydown[keyName(event)] = true;
  });
  $(document).bind("keyup", function(event) {
    keydown[keyName(event)] = false;
  });

  SeaBattle.imgTitle = new Image();
  SeaBattle.imgTitle.src = "images/title.png";
  SeaBattle.imgSky = new Image();
  SeaBattle.imgSky.src = "images/sky.png";
  SeaBattle.imgMoon = new Image();
  SeaBattle.imgMoon.src = "images/moon.png";
  SeaBattle.imgShipLeft = new Image();
  SeaBattle.imgShipLeft.src = "images/shipLeft.png";
  SeaBattle.imgShipRight = new Image();
  SeaBattle.imgShipRight.src = "images/shipRight.png";
  SeaBattle.imgSubLeft = new Image();
  SeaBattle.imgSubLeft.src = "images/subLeft.png";
  SeaBattle.imgSubRight = new Image();
  SeaBattle.imgSubRight.src = "images/subRight.png";
  SeaBattle.imgExplosion = new Array();
  for (var i = 0; i < 17; i++)
  {
    var image = new Image();
    image.src = "images/ex"+i+".png";
    SeaBattle.imgExplosion.push(image);
  }

  SeaBattle.imgTorpedo = new Image();
  SeaBattle.imgTorpedo.src = "images/torpedo.png";
  SeaBattle.imgDC = new Image();
  SeaBattle.imgDC.src = "images/dc.png";
  SeaBattle.audBombLoaded = false;
  SeaBattle.audBomb = document.createElement("audio");
  SeaBattle.audBomb.onloadeddata = new function() {
    SeaBattle.audBombLoaded = true;
  };
  SeaBattle.audBomb.src = (navigator.userAgent.indexOf("MSIE") == -1)
                           ? "audio/bomb.wav" : "audio/bomb.mp3";
  SeaBattle.state = SeaBattle.STATE_INIT;
}

Listing 1: Game initialization involves canvas and undersea terrain creation/initialization, hotkey binding, game resource loading, and more.

Listing 1 first uses jQuery to create a <canvas> element node, and then install it in the browser’s Document Object Model (DOM) tree. It accomplishes this task as follows:

  1. Invoke the jQuery(html) constructor to parse the html string, create DOM nodes from the parsed HTML, and create/return a jQuery object that refers to these nodes. Listing 1 creates a single <canvas> DOM node.
  2. Invoke appendTo("body") on this new jQuery object to attach the parsed HTML’s DOM nodes to the Web page’s <body> element node. Listing 1 attaches the <canvas> node to the page’s <body> node.

The canvas’s context is obtained via canvas.get(0).getContext("2d") and assigned to SeaBattle‘s ctx property. Next, the 2D drawing context’s font and textAlign properties are initialized to specify that text is to be drawn in the Arial font with a 30-pixel height, and to make it easy to horizontally center the text.

Listing 1 proceeds to generate undersea terrain by randomly choosing the top locations of hills. The leftmost hill top is at the midpoint of the bottom third of the canvas. Each hill top to the right is relative to the previous hill top.

Continuing, the width and height values passed to init(width, height) are saved in same-named SeaBattle properties so that they can be accessed from other functions. Additionally, the following SeaBattle properties are initialized:

  • dc is initialized to an array that will store at most MAX_DC depth charge objects.
  • torp is initialized to an array that will store at most MAX_TORP torpedo objects.
  • explosion is initialized to null. The update() function tests this property to find out if an explosion is in progress. When an explosion is occurring, explosion is assigned a reference to an explosion object.
  • msg is initialized to the empty string. When the ship or submarine wins, a suitable message is assigned to this property, for subsequent display in the draw() function.
  • score is initialized to zero and reflects the player’s current score. This score appears in the upper-left corner of the canvas.
  • hiScore is initialized to zero and reflects the player’s highest previous score. If the current browser supports the local aspect of HTML5 Web Storage, and if this score was previously saved, hiScore is set to the saved value. The high score appears in parentheses after the current score.
  • lives is initialized to four and reflects the total number of destroyer lives that can be lived out before the game ends. This count decrements by one each time the destroyer is destroyed.

Games that involve keyboard input typically recognize hotkeys, which are keys that trigger various operations when pressed. Also, each operation usually repeats while its hotkey is held down. For example, an object keeps moving left until the left arrow key is released.

Differences in how browsers interpret a key event object’s keyCode and charCode properties along with other factors make it challenging to implement your own logic for responding to hotkeys. However, this task needn’t be too difficult to accomplish, as the following steps reveal:

  1. Attach key down and up event listeners to the canvas, as in canvas.onkeydown = keyDown; and canvas.onkeydown = keyUp;. keyDown and keyUp identify functions that respond to key down and up events, respectively.
  2. Create an initially empty associative array and assign it to the window object, as in window.keydown = {}. Each entry’s key will be the name of a key that has been pressed, and its value will be true when the key is down or false when the key is up.
  3. For each of keyDown() and keyUp(), invoke a function that returns the name of the key, which is either a character key or a non-character (a special) key. Then, use the result as an index into the keydown array. For keyDown(), assign true to this array entry. For keyUp(), assign false instead.

Implementing this solution can be bothersome. For example, charCode is always undefined in Opera. Why not let jQuery and the jQuery HotKeys plugin handle most of this work for you?

jQuery offers a powerful binding capability that makes it easy to register event-handling functions. Furthermore, the HotKeys plugin facilitates returning the name of a character or special key. Listing 1 leverages these capabilities to install key event handling as previously discussed.

Listing 1 now starts to load image resources, which are stored in the images directory, by instantiating the Image object and assigning the location and name of the image to the object’s src property. It also starts loading an audio resource, which is stored in the audio directory. Unlike in other browsers, Safari doesn’t provide an Audio object. To ensure consistent cross-browser behavior, document.createElement("audio") is used to create an equivalent object.

When an image finishes loading, the Image object assigns true to its complete property. To detect that an audio file has finished loading, an onloadeddata handler function that assigns true to SeaBattle‘s audBombLoaded property is assigned to the “Audio” object.

Except for Internet Explorer, all browsers mentioned in the first part of this series support the WAV format. Instead, Internet Explorer supports MP3. Listing 1 detects whether or not the current browser is Internet Explorer before choosing the proper audio file to load. The expression navigator.userAgent.indexOf("MSIE") returns a value other than -1 when the current browser is Internet Explorer. This fact helps Listing 1 choose between audio/bomb.wav and audio/bomb.mp3, which is assigned to the “Audio” object’s src property.

Listing 1’s final task is to add a state property to the SeaBattle object and assign STATE_INIT to this property. This state results in the canvas presenting a centered Initializing... message until all game resources have finished loading.

Obtaining Random Integers

The init(width, height) function relies on SeaBattle‘s rnd(limit) function to return random integers so that it can generate terrain. Listing 2 presents rnd(limit)‘s implementation.

rnd: function(limit) {
  return (Math.random()*limit)|0;
}

Listing 2: Bitwise operators cause JavaScript to convert floating-point numbers to integers.

Listing 2 returns a randomly selected integer from zero through limit - 1. Because an integer result is desired, and because Math.random()*limit returns a number with a fraction, |0 is used to truncate the result to an integer. To learn more about JavaScript’s conversion to integer capability, check out the Javascript Type-Conversion FAQ. Specifically, read the FAQ’s ToInt32 section to learn about a JavaScript implementation’s ToInt32 function.

Detecting HTML5 Local Storage

The init(width, height) function also relies on SeaBattle‘s supports_html5_storage() function to detect the local aspect of Web storage. Listing 3 presents supports_html5_storage()‘s implementation.

supports_html5_storage: function() {
  try
  {
    return 'localStorage' in window &&
            window['localStorage'] !== null &&
            window['localStorage'] !== undefined;
  }
  catch (e)
  {
    return false;
  }
}

Listing 3: Older versions of Firefox raise an exception when cookies are disabled.

Listing 3 detects support for the local aspect of Web storage by checking the global window object for the presence of a localStorage property. When this property exists and isn’t null or undefined, this function returns true; otherwise, it returns false.

Conclusion

The init(width, height) function works with the rnd(limit) and supports_html5_storage() functions to properly initialize the SeaBattle object. The next step in understanding the SeaBattle gaming experience is to explore the update() function, which is the subject of the third part in this series. Next Friday, you’ll also learn how the ship object is implemented.

Battle on the High Seas

<< Gaming: Battle on the High Seas, Part 1Gaming: Battle on the High Seas, Part 3 >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments