Gaming: Battle on the High Seas, Part 2
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:
- Invoke the
jQuery(html)
constructor to parse thehtml
string, create DOM nodes from the parsed HTML, and create/return ajQuery
object that refers to these nodes. Listing 1 creates a single<canvas>
DOM node. - Invoke
appendTo("body")
on this newjQuery
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 mostMAX_DC
depth charge objects.torp
is initialized to an array that will store at mostMAX_TORP
torpedo objects.explosion
is initialized tonull
. Theupdate()
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 thedraw()
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:
- Attach key down and up event listeners to the canvas, as in
canvas.onkeydown = keyDown;
andcanvas.onkeydown = keyUp;
.keyDown
andkeyUp
identify functions that respond to key down and up events, respectively. - Create an initially empty associative array and assign it to the
window
object, as inwindow.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. - For each of
keyDown()
andkeyUp()
, 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 thekeydown
array. ForkeyDown()
, assign true to this array entry. ForkeyUp()
, 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.