SeaBattle is an HTML5 game that demonstrates the usefulness of HTML5’s Audio, Canvas, and Web Storage APIs. This article completes our five-part gaming series covering SeaBattle by exploring its draw()
and allResourcesLoaded()
functions. It also reviews these APIs, by discussing game enhancements, and by examining SeaBattle in a mobile context.
Drawing the Scene
The SeaBattle
object’s draw()
function, shown in Listing 1, is called to draw the game’s scene based on updated state.
draw: function() {
if (SeaBattle.state == SeaBattle.STATE_INIT)
if (!SeaBattle.allResourcesLoaded())
{
SeaBattle.ctx.fillStyle = "#000";
SeaBattle.ctx.fillRect(0, 0, SeaBattle.width, SeaBattle.height);
SeaBattle.ctx.fillStyle = "#fff";
SeaBattle.ctx.fillText("Initializing...",
SeaBattle.width/2, SeaBattle.height/2);
return;
}
else
SeaBattle.state = SeaBattle.STATE_TITLE;
if (SeaBattle.state == SeaBattle.STATE_TITLE)
{
SeaBattle.ctx.drawImage(SeaBattle.imgTitle, 0, 0);
return;
}
SeaBattle.ctx.drawImage(SeaBattle.imgSky, 0, 0);
SeaBattle.ctx.fillStyle = "#404040"
SeaBattle.ctx.fillRect(0, SeaBattle.height/3, SeaBattle.width, 2*SeaBattle.height/3);
SeaBattle.ctx.drawImage(SeaBattle.imgMoon, SeaBattle.width-65, 25);
SeaBattle.ctx.strokeStyle = "rgb(255, 102, 0)"; // orange
for (var i = 0; i < SeaBattle.width; i++)
{
SeaBattle.ctx.beginPath();
SeaBattle.ctx.moveTo(i, SeaBattle.hillTops[i]);
SeaBattle.ctx.lineTo(i, SeaBattle.height);
SeaBattle.ctx.stroke();
}
for (var i = 0; i < SeaBattle.MAX_DC; i++)
if (SeaBattle.dc[i] != null)
SeaBattle.dc[i].draw();
for (var i = 0; i < SeaBattle.MAX_TORP; i++)
if (SeaBattle.torp[i] != null)
SeaBattle.torp[i].draw();
if ((SeaBattle.ship != null && SeaBattle.explosion == null) ||
(SeaBattle.explosion != null && !SeaBattle.ship.exploded))
SeaBattle.ship.draw();
if ((SeaBattle.sub != null && SeaBattle.explosion == null) ||
(SeaBattle.explosion != null && !SeaBattle.sub.exploded))
SeaBattle.sub.draw();
if (SeaBattle.explosion != null)
SeaBattle.explosion.draw();
SeaBattle.ctx.fillStyle = "rgba(0, 0, 255, 0.1)";
SeaBattle.ctx.fillRect(0, SeaBattle.height/3, SeaBattle.width, SeaBattle.height);
SeaBattle.ctx.fillStyle = "#fff";
var align = SeaBattle.ctx.textAlign;
SeaBattle.ctx.textAlign = "left";
SeaBattle.ctx.fillText("Score: "+SeaBattle.score+"("+SeaBattle.hiScore+")", 10, 45);
SeaBattle.ctx.textAlign = align;
for (var i = 0; i < SeaBattle.lives-1; i++)
{
var x = SeaBattle.width-(i+1)*(SeaBattle.imgShipLeft.width+10);
var y = SeaBattle.height-SeaBattle.imgShipLeft.height;
SeaBattle.ctx.drawImage(SeaBattle.imgShipLeft, x, y);
}
if (SeaBattle.state == SeaBattle.STATE_WINLOSE ||
SeaBattle.state == SeaBattle.STATE_RESTART)
{
SeaBattle.ctx.fillStyle = "#fff";
SeaBattle.ctx.fillText(SeaBattle.msg, SeaBattle.width/2, SeaBattle.height/2);
}
}
Listing 1: An initializing screen is drawn until all game resources have loaded.
Listing 1 first determines if the game is in the initialization state. If so, and if not all game resources have loaded, a “white on black” initialization screen is presented. After all game resources have loaded, the state reverts to the title state and the title screen is presented.
If the game is not initializing or presenting a title, Listing 1 proceeds to draw the current scene based on the game’s current state. It draws the depth charges and torpedoes before drawing the ship and submarine so that their emergence from either game object looks more natural.
Before drawing the ship or submarine, either of two compound conditions must be satisifed. For the ship, its game object must exist and no explosion object must exist, or an explosion object must exist and the ship must still be exploding. The same conditions apply to the submarine.
After drawing the sky, water, moon, undersea terrain, and game objects, Listing 1 draws the current and high scores, and draws lives left. Lastly, it draws a message centered over the scene, but only when the game is in the win/lose or restart state.
Detecting All Resources Loaded
The draw()
function relies on SeaBattle
‘s allResourcesLoaded()
function to tell it when all image and audio game resources have finished loading. There’s no point in proceeding until all of these resources are available. Listing 2 presents this function’s implementation.
allResourcesLoaded: function() {
var status = SeaBattle.imgTitle.complete &&
SeaBattle.imgSky.complete &&
SeaBattle.imgMoon.complete &&
SeaBattle.imgShipLeft.complete &&
SeaBattle.imgShipRight.complete &&
SeaBattle.imgSubLeft.complete &&
SeaBattle.imgSubRight.complete;
for (var i = 0; i < SeaBattle.imgExplosion.length; i++)
status = status && SeaBattle.imgExplosion[i].complete;
status = status && SeaBattle.audBombLoaded;
return status;
}
Listing 2: The Boolean values of the complete
and audBombLoaded
properties are merged.
Listing 2 merges the Boolean true/false value of each Image
object’s complete
property with the Boolean true/false value of SeaBattle
‘s audBombLoaded
property to obtain a result that indicates whether or not all game resources have loaded.
Reviewing HTML5’s Audio, Canvas, and Web Storage APIs
SeaBattle wouldn’t exist as is without access to HTML5’s Audio, Canvas, and Web Storage APIs. This section briefly reviews Audio, Canvas, and Web Storage for the benefit of newcomers who want to fully understand this game but don’t have a solid grasp of these APIs.
Reviewing the Audio API
HTML5’s audio element lets you represent a sound or an audio stream. You can programmatically create and manipulate instances of this element by using the Audio()
constructor. Unfortunately, Safari doesn’t support this constructor, so I used the DOM’s createElement()
function instead.
The resulting instance is of type HTMLAudioElement
, whose play()
function is used to start playing an explosion sound effect. This function throws an exception on the Safari browser when QuickTime isn’t installed. The HTMLAudioElement
also provides an onloadeddata
event handler that I use to receive notification when the audio data has loaded, and assign true to SeaBattle
‘s audBombLoaded
property.
Reviewing the Canvas API
HTML5’s canvas element lets you allocate a rectangular region of a Web page and draw on this region. You can programmatically create and manipulate instances of this element by using the Canvas API.
Unlike the Audio API, there are no constructors for programmatically creating a canvas instance. Instead, you work with the DOM’s createElement()
function, as demonstrated in SeaBattle
‘s init(width, height)
function. The resulting instance is of type HTMLCanvasElement
. Before you can draw on the canvas, you need to obtain a drawing context. You can obtain this context by calling the getContext()
function.
You typically invoke this function with a "2D"
or "2d"
argument to return a 2D context. The returned object for a 2D context is of type CanvasRenderingContext2D
. CanvasRenderingContext2D
declares various function and non-function attributes. The init(width, height)
function demonstrated the font
and textAlign
attributes. Additional attributes were demonstrated earlier in this article.
Reviewing the Web Storage API
Web Storage provides persistent data storage of key-value pair data in browsers and other Web clients. SeaBattle relies on local storage to save the high score and retrieve this score the next time the game runs. The high score isn’t saved when the browser window is closed because Opera doesn’t provide a reliable means of detecting window closing events.
Detecting support for local storage amounts to first checking for the presence of a localStorage
property on the global window
object, and then making sure that this property’s value is neither null
nor undefined
.
Internet Explorer 9 doesn’t support local storage for local files. For more information, see stackoverflow’s local storage in IE9 fails when the website is accessed directly from the file system topic.
The localStorage
property is ultimately of type Storage
. Call this property’s void setItem(DOMString key, DOMString value)
function to store a key-value pair, and its DOMString getItem(DOMString key)
function to return the specified key’s value.
Each browser provides its own local storage. This means that a specific high score saved by one browser won’t be retrieved by another browser. For example, it is possible to have a high score of 500 on Firefox and 300 on Opera.
Enhancing SeaBattle
As far as I’m concerned, SeaBattle is complete. However, is a game ever finished? You can probably think of many enhancements for improving this game. For example, think about increasing the speed of a torpedo to make it harder for the destroyer to avoid.
Consider the following excerpt from the torpedo object:
this.move = function move() {
this.y--;
if (this.y < this.bound)
return false;
return true;
}
To make a torpedo move faster, simply decrement this.y
by a greater value; for example, this.y -= 2;
.
Perhaps it’s currently too easy to destroy the submarine, or too hard to avoid torpedoes. These possibilities bring up the idea of levels of play. A first level could be easy to win, and a second level could be harder. Perhaps a third level could implement multiple submarines, and so on.
An additional enhancement possibility is to introduce spurious animations. For example, the sky could occasionally reveal a meteor, or perhaps the stars could twinkle. How about introducing sea creatures that move across the undersea terrain?
Going Mobile
While thinking about SeaBattle enhancements, there’s one important enhancement to consider. The game should be tested on mobile device browsers. If you hope to monetize your HTML5 games, you cannot limit them to desktop browsers. You’ll probably first test your game on the iOS and Android platforms.
Having previously installed the Android 4.1 emulator, I decided to test SeaBattle on the default browser app. My first concern was being able to view the canvas in its entirety. It turns out that this isn’t a problem, as Figure 1 reveals.
Apart from experiencing sluggish gaming, I detected two problems while running SeaBattle in the browser app:
- Lack of audio, probably because WAV files aren’t supported.
- The brower occasionally gets stuck in a loop where it repeatedly displays the initialization screen followed by the game play screen.
As an exercise, verify the cause of the first problem and adapt the game to compensate. (Hint: You can identify the current browser via navigator.userAgent.indexOf()
and then act accordingly.) However, the second problem may prove harder to fix.
Conclusion
SeaBattle is an example of an interesting game that can be created with HTML5’s Audio, Canvas, and Web Storage APIs. Now that you have an understanding of how it interacts with these APIs, you may want to enhance the game. You can start by downloading the SeaBattle source code. If you plan to monetize your version of this game, don’t forget to fully test the game on various mobile devices. Good luck!
Jeff Friesen is a freelance tutor and software developer with an emphasis on Java and mobile technologies. In addition to writing Java and Android books for Apress, Jeff has written numerous articles on Java and other technologies for SitePoint, InformIT, JavaWorld, java.net, and DevSource.