*A handful of game developers are pushing the envelope of mobile HTML5 games at the moment. Check out the likes of Nutmeg and Lunch Bug for some shining examples. The great thing about these titles is that they work equally well on both mobile and desktop using the same code.
Could HTML5 finally fulfill the holy grail of “write once, run anywhere”?*
*Getting Started
Before you start sketching the next Temple Run or Angry Birds, you should be aware of
a few things that could
dampen your excitement: *
*Performance
Mobile browsers are not traditionally
known for their blazing JavaScript
engines. With iOS 6 and Chrome beta
for Android, though,
things are improving fast.
A veritable cornucopia of Android
Hope you enjoy the sound of silence.
Audio support in mobile browsers is poor,
to say the least. Lag is a major problem,
as is the fact that most devices offer only
a single channel. iOS won’t even load a
sound until the user initiates the action.
My advice is to hold tight and wait for
browser vendors to sort this out. *
*
Now, as a Web developer you’re used to
dealing with the quirks of certain
browsers and degrading gracefully and
dealing with fragmented platforms. So,
a few technical challenges won’t put you
off, right? What’s more, all of these
performance and audio problems are
temporary. The mobile browser landscape
is changing so quickly that these concerns
will soon be a distant memory.
In this tutorial, we’ll make a relatively
simple game that takes you through the
basics and steers you away from pitfalls.
The result will look like this: *
*
*
*It’s a fairly simple game, in which the
user bursts floating bubbles before they
reach the top of the screen. Imaginatively,
I’ve titled our little endeavour Pop. *
*We’ll develop this in a number
of distinct stages: *
*
- Cater to the multitude of viewports
- and optimize for mobile;
- Look briefly at using the canvas API
- to draw to the screen;
- Capture touch events;
- Make a basic game loop;
- Introduce sprites, or game “entities”;
- Add collision detection and some
- simple maths to spice things up;
- Add a bit of polish and some basic
- particle effects. *
Enough of the background story. Fire up
your favorite text editor, pour a strong
brew of coffee, and let’s get our
hands dirty.
As mentioned, there is a plethora of
resolution sizes and pixel densities
across devices. This means we’ll have to
scale our canvas to fit the viewport.
This could come at the price of a loss
in quality, but one clever trick is to make
the canvas small and then scale up, which
provides a performance boost.
Let’s kick off with a basic HTML shim: *
*
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content=
"width=device-width,
user-scalable=no, initial-scale=1,
maximum-scale=1, user-scalable=0" />
<meta name="apple-mobile-web-app-
capable" content="yes" />
<meta name="apple-mobile-web-app-
status-bar-style" content="black-translucent" />
<style type="text/css">
body { margin: 0; padding: 0; background: #000;}
canvas { display: block; margin: 0 auto;
background: #fff; }
</style>
</head>
<body>
<canvas> </canvas>
<script>
// all the code goes here
</script>
</body>
</html>
The meta
viewport tag tells mobile browsers to disable user scaling and to
render at full size rather than shrink
the page down.
The subsequent
apple-
prefixed meta tags allow the game to be bookmarked.
On the iPhone, bookmarked apps do not
display the toolbar at the bottom of the
page, thus freeing up valuable real estate.
Take a look at the following: *
*
// namespace our game
var POP = {
// set up some initial values
WIDTH: 320,
HEIGHT: 480,
// we'll set the rest of these
// in the init function
RATIO: null,
currentWidth: null,
currentHeight: null,
canvas: null,
ctx: null,
init: function() {
// the proportion of width to height
POP.RATIO = POP.WIDTH / POP.HEIGHT;
// these will change when the
screen is resized
POP.currentWidth = POP.WIDTH;
POP.currentHeight = POP.HEIGHT;
// this is our canvas element
POP.canvas = document.
getElementsByTagName('canvas')[0];
// setting this is important
// otherwise the browser will
// default to 320 x 200
POP.canvas.width = POP.WIDTH;
POP.canvas.height = POP.HEIGHT;
// the canvas context enables us to
// interact with the canvas api
POP.ctx = POP.canvas.getContext('2d');
// we're ready to resize
POP.resize();
},
resize: function() {
POP.currentHeight = window.innerHeight;
// resize the width in proportion
// to the new height
POP.currentWidth =
POP.currentHeight * POP.RATIO;
// this will create some extra
space on the
// page, allowing us to scroll past
// the address bar, thus hiding it.
if (POP.android || POP.ios) {
document.body.style.height =
(window.innerHeight + 50) + 'px';
}
// set the new canvas style
width and height
// note: our canvas is still
320 x 480, but
// we're essentially scaling
it with CSS
POP.canvas.style.width =
POP.currentWidth + 'px';
POP.canvas.style.height =
POP.currentHeight + 'px';
// we use a timeout here because
some mobile
// browsers don't fire if there is not
// a short delay
window.setTimeout(function() {
window.scrollTo(0,1);
}, 1);
}
};
window.addEventListener
('load', POP.init, false);
window.addEventListener
('resize', POP.resize, false);
First, we create the POP
namespace forour game. Being good developers,
we don’t want to pollute the global
namespace. In keeping good practice,
we will declare all variables at the
start of the program.
Most of them are obvious:
canvas
refers to the
canvas
element in the HTML,and
ctx
enables us to access it via the JavaScript canvas API.
In
POP.init
, we grab a reference to our canvas element, get its context
and adjust the canvas element’s
dimensions to 480 × 320.
The
resize
function, which is fired on resize and load events, adjusts the
canvas’
style
attribute for width and height accordingly while maintaining
the ratio. Effectively, the canvas is
still the same dimensions but has been
scaled up using CSS. Try resizing your
browser and you’ll see the canvas scale to fit.
If you tried that on your phone, you’ll
notice that the address bar is still
visible. Ugh! We can fix this by adding
a few extra pixels to the document and
then scrolling down to hide
the address bar, like so:
// we need to sniff out Android and iOS
// so that we can hide the address bar in
// our resize function
POP.ua = navigator.userAgent.toLowerCase();
POP.android = POP.ua.indexOf('android') >
-1 ? true : false;
POP.ios = ( POP.ua.indexOf('iphone') >
-1 || POP.ua.indexOf('ipad') > -1 ) ?
true : false;
The code above sniffs out the user agent, flagging for Android and iOS
if present. Add it at the end of
POP.init
, before the call to
POP.resize()
. **Then, in the
resize
function,if
android
or ios
is true
, we add another 50 pixels to the document’s
height — i.e. enough extra space to be
able to scroll down past the address bar.
// this will create some extra space on the
// page, enabling us to scroll past
// the address bar, thus hiding it.
if (POP.android || POP.ios) {
document.body.style.height =
(window.innerHeight + 50) + 'px';
}
Notice that we do this only for Android and iOS devices; otherwise,
nasty scroll bars will appear. Also,
we need to delay the firing of
scrollTo
to make sure it doesn’t get ignored
on mobile Safari. *
*2. A Blank Canvas
Now that we’ve scaled our canvas
snuggly to the viewport, let’s add
the ability to draw some shapes.
*Note: In this tutorial, we’re going to
stick with basic geometric shapes.
iOS 5 and Chrome beta for Android can
handle a lot of image sprites at a high
frame rate. Try that on Android 3.2 or
lower and the frame rate will
drop exponentially.
Luckily, there is not much overhead
when drawing circles,
so we can have a lot of bubbles in our
game without hampering performance
on older devices.
Below, we’ve added a basic
Draw
object
that allows us to clear the screen, draw a
rectangle and circle, and add some text.
Nothing mind-blowing yet.
Mozilla Developers Network has excellent
resources as always,
*
// abstracts various canvas operations into
// standalone functions
POP.Draw = {
clear: function() {
POP.ctx.clearRect
(0, 0, POP.WIDTH, POP.HEIGHT);
},
rect: function(x, y, w, h, col) {
POP.ctx.fillStyle = col;
POP.ctx.fillRect(x, y, w, h);
},
circle: function(x, y, r, col) {
POP.ctx.fillStyle = col;
POP.ctx.beginPath();
POP.ctx.arc
(x + 5, y + 5, r, 0, Math.PI * 2, true);
POP.ctx.closePath();
POP.ctx.fill();
},
text: function(string, x, y, size, col) {
POP.ctx.font =
'bold '+size+'px Monospace';
POP.ctx.fillStyle = col;
POP.ctx.fillText(string, x, y);
}
};
Our Draw
object has methods for clearing the screen and drawing rectangles,
circles and text.
The benefit of abstracting these
operations is that we don’t have to
remember the exact canvas API calls,
and we can now draw a circle with
one line of code, rather than five.
Let’s put it to the test:
// include this at the end of POP.init function
POP.Draw.clear();
POP.Draw.rect(120,120,150,150, 'green');
POP.Draw.circle(100, 100, 50, 'rgba(255,0,0,0.5)');
POP.Draw.text('Hello World', 100, 100, 10, '#000');
Include the code above at the end of the
POP.init
function, and you should see a couple
of shapes drawn to the canvas.
3. The Magic Touch
Just as we have theclick
event, mobile browsers provide methods
for catching touch events.
The interesting parts of the code below
are the
touchstart
, touchmove
and
touchend
events. With the standard
click
event, we canget the coordinates from
e.pageX
and
e.pageY
. Touch events are slightly different. They contain a
touches
array, each element of which contains
touch coordinates and other data.
We only want the first touch,
and we access it like so:
e.touches[0]
. Note: Android provides JavaScript
access to multi-touch actions only
since version 4.
We also call
e.preventDefault();
when each event is fired to disable scrolling,
zooming and any other action that
would interrupt the flow of the game.
Add the following code
to the
POP.init
function.// listen for clicks
window.addEventListener('click', function(e) {
e.preventDefault();
POP.Input.set(e);
}, false);
// listen for touches
window.addEventListener('touchstart', function(e) {
e.preventDefault();
// the event object has an array
// named touches; we just want
// the first touch
POP.Input.set(e.touches[0]);
}, false);
window.addEventListener('touchmove', function(e) {
// we're not interested in this,
// but prevent default behaviour
// so the screen doesn't scroll
// or zoom
e.preventDefault();
}, false);
window.addEventListener('touchend', function(e) {
// as above
e.preventDefault();
}, false);
You probably noticed that the code above passes the event data to
an
Input
object, which we’ve yet to define. Let’s do that now:
// + add this at the bottom of your code,
// before the window.addEventListeners
POP.Input = {
x: 0,
y: 0,
tapped :false,
set: function(data) {
this.x = data.pageX;
this.y = data.pageY;
this.tapped = true;
POP.Draw.circle
(this.x, this.y, 10, 'red');
}
};
Now, try it out. Hmm, the circles are not appearing. A quick scratch of the
head and a lightbulb moment! Because
we’ve scaled the canvas, we need to
account for this when mapping the
touch to the screen’s position.
First, we need to subtract the offset
from the coordinates.
var offsetTop = POP.canvas.offsetTop,
offsetLeft = POP.canvas.offsetLeft;
this.x = data.pageX - offsetLeft;
this.y = data.pageY - offsetTop;
*
**
*
Then, we need to take into account the
factor by which the canvas has been
scaled so that we can plot to the actual
canvas (which is still 320 × 480).
var offsetTop = POP.canvas.offsetTop, offsetLeft = POP.canvas.offsetLeft; scale = POP.currentWidth / POP.WIDTH; this.x = ( data.pageX - offsetLeft ) / scale; this.y = ( data.pageY - offsetTop ) / scale;
** *
*
If your head is starting to hurt, a practical
example should provide some relief. Imagine
the player taps the 500 × 750 canvas above
at400,400
. We need to translate that
to 480 × 320 because, as far as the JavaScript
is concerned, those are the dimensions of the
canvas. So, the actualx
coordinate is 400
divided by the scale; in this case
, 400 ÷ 1.56 = 320.5.
Rather than calculating this on each touch
event, we can calculate them after resizing.
Add the following code to the start of the
program, along with the other
variable declarations:
In our resize function, after adjusting the// let's keep track of scale // along with all initial declarations // at the start of the program scale: 1, // the position of the canvas // in relation to the screen offset = {top: 0, left: 0},
canvas’ width and height, we make note of
the current scale and offset:
Now, we can use them in the// add this to the resize function. POP.scale = POP.currentWidth / POP.WIDTH; POP.offset.top = POP.canvas.offsetTop; POP.offset.left = POP.canvas.offsetLeft;
set
method of
ourPOP.Input
class:
this.x =
(data.pageX - POP.offset.left) / POP.scale; this.y =
(data.pageY - POP.offset.top) / POP.scale;
***4. In The Loop
A typical game loop goes something like this:
- Poll user input,
- Update characters and process collisions,
- Render characters on the screen,
- Repeat.
We could, of course, usesetInterval
, butthere’s a shiny new toy in town namedrequestAnimationFrame
. It promises smootheranimation and is more battery-efficient.The bad news is that it’s not supportedconsistently across browsers. But Paul Irishhas come to the rescue with a handy shim.Let’s go ahead and add the shim to the start
of our current code base.
// http://paulirish.com/2011/
requestanimationframe-for-smart-animating // shim layer with setTimeout fallback window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function( callback ){ window.setTimeout
And let’s create a rudimentary game loop:(callback, 1000 / 60); }; })();
We call the loop at the end of// Add this at the end of POP.init; // it will then repeat continuously POP.loop(); // Add the following functions after POP.init: // this is where all entities will be moved // and checked for collisions, etc. update: function() { }, // this is where we draw all the entities render: function() { POP.Draw.clear(); }, // the actual loop // requests animation frame, // then proceeds to update // and render loop: function() { requestAnimFrame( POP.loop ); POP.update(); POP.render(); }
POP.init
.
ThePOP.loop
function in turn calls ourPOP.
update
andPOP.render
methods.
requestAnimFrame
ensures that the loop is
called again, preferably at 60 frames per
second. Note that we don’t have to worry
about checking for input in our loop because
we’re already listening for touch and click
events, which is accessible through ourPOP.
Input
class.
The problem now is that our touches from the
last step are immediately wiped off the screen.
We need a better approach to remember what was
drawn to the screen and where. **
*
5. Spritely Will Do It
First, we add an entity array to keep track
of all entities. This array will hold a
reference to all touches, bubbles,
particles and any other dynamic thing we
want to add to the game.
Let’s create a// put this at start of program entities: [],
Touch
class that draws a
circle at the point of contact, fades it out
and then removes it.
POP.Touch = function(x, y) { this.type = 'touch'; // we'll
need this later this.x = x; // the x coordinate this.y = y; // the y coordinate this.r = 5; // the radius this.opacity = 1; // initial
opacity; the dot will fade out this.fade = 0.05; // amount by
which to fade on each game tick this.remove = false; // flag for
removing this entity. POP.update // will take
care of this this.update = function() { // reduce the opacity accordingly this.opacity -= this.fade; // if opacity if 0 or less,
flag for removal this.remove =
(this.opacity < 0) ? true : false; }; this.render = function() { POP.Draw.circle
(this.x, this.y, this.r, 'rgba
The(255,0,0,'+this.opacity+')'); }; };
Touch
class sets a number of properties
when initiated. The x and y coordinates are
passed as arguments, and we set the radius
this.r
to 5 pixels. We also set an initial
opacity to 1 and the rate by which the
touch fades to 0.05. There is also aremove
flag that tells the main game loop whether
to remove this from the entities array.
Crucially, the class has two main methods:
update
andrender
. We will call these from
the corresponding part of our game loop.
We can then spawn a new instance ofTouch
in
the game loop, and then move them via
the update method:
// POP.update function update: function() { var i; // spawn a new instance of Touch // if the user has tapped the screen if (POP.Input.tapped) { POP.entities.push(new POP.Touch
(POP.Input.x, POP.Input.y)); // set tapped back to false // to avoid spawning a new touch // in the next cycle POP.Input.tapped = false; } // cycle through all entities and
update as necessary for (i = 0; i < POP.entities.
Basically, iflength; i += 1) { POP.entities[i].update(); // delete from array if remove property // flag is set to true if (POP.entities[i].remove) { POP.entities.splice(i, 1); } } },
POP.Input.tapped
istrue
,
then we add a new instance ofPOP.Touch
to
our entities array. We then cycle through the
entities array, calling theupdate
method
for each entity. Finally, if the entity is
flagged for removal, we delete it from the array.
Next, we render them in thePOP.render
function.
// POP.render function render: function() { var i; POP.Draw.rect(0, 0, POP.WIDTH,
POP.HEIGHT, '#036'); // cycle through all entities and
render to canvas for (i = 0; i < POP.
Similar to our update function, we cycleentities.length; i += 1) { POP.entities[i].render(); } },
through the entities and call theirrender
method to draw them to the screen.
So far, so good. Now we’ll add aBubble
class
that will create a bubble that floats up for
the user to pop.
POP.Bubble = function() { this.type = 'bubble'; this.x = 100; this.r =
5; // the radius of the bubble this.y = POP.HEIGHT +
100; // make sure it starts off screen this.remove = false; this.update = function() { // move up the screen by 1 pixel this.y -= 1; // if off screen, flag for removal if (this.y < -10) { this.remove = true; } }; this.render = function() { POP.Draw.circle
The(this.x, this.y, this.r, 'rgba(255,255,255,1)'); }; };
POP.Bubble
class is very similar to the
Touch
class, the main differences being that
it doesn’t fade but moves upwards. The motion
is achieved by updating they
position,
this.y
, in the update function. Here, we also
check whether the bubble is off screen; if so,
we flag it for removal.
Note: We could have created a baseEntity
class
that bothTouch
andBubble
inherit from. But,
I’d rather not open another can of worms about
JavaScript prototypical inheritance versus
classic at this point.
// Add at the start of the program // the amount of game ticks until // we spawn a bubble nextBubble: 100, // at the start of POP.update // decrease our nextBubble counter POP.nextBubble -= 1; // if the counter is less than zero if (POP.nextBubble < 0) { // put a new instance of bubble into
our entities array POP.entities.push(new POP.Bubble()); // reset the counter with a random value POP.nextBubble =
Above, we have added a random timer to our( Math.random() * 100 ) + 100; }
game loop that will spawn an instance of
Bubble
at a random position. At the start
of the game, we setnextBubble
with a value
of 100. This is subtracted on each game tick
and, when it reaches 0, we spawn a bubble and
reset thenextBubble
counter.
6. Putting It Together
First of all, there is not yet any notion ofcollision detection. We can add that with asimple function. The math behind this isbasic geometry, which you can// this function checks if two circles overlap POP.collides = function(a, b) { var distance_squared =
( ((a.x - b.x) * (a.x - b.x)) + ((a.y - b.y)
* (a.y - b.y))); var radii_squared =
(a.r + b.r) * (a.r + b.r); if (distance_squared < radii_squared) { return true; } else { return false; } }; // at the start of POP.update, we set a
flag for checking collisions var i, checkCollision = false; // we only
need to check for a collision // if the user
tapped on this game tick // and then incorporate into the main logic if (POP.Input.tapped) { POP.entities.push(new POP.Touch
(POP.Input.x, POP.Input.y)); // set tapped back to false // to avoid spawning a new touch // in the next cycle POP.Input.tapped = false; checkCollision = true; } // cycle through all entities and
update as necessary for (i = 0; i < POP.entities.length; i += 1) { POP.entities[i].update(); if (POP.entities[i].type ===
'bubble' && checkCollision) { hit = POP.collides(POP.entities[i], {x: POP.Input.
The bubbles are rather boring; they allx, y: POP.Input.y, r: 7}); POP.entities[i].remove = hit; } // delete from array if remove property // is set to true if (POP.entities[i].remove) { POP.entities.splice(i, 1); } }
travel at the same speed on a very predictable
trajectory. Making the bubbles travel at
random speeds is a simple task:
POP.Bubble = function() { this.type = 'bubble'; this.r = (Math.random() * 20) + 10; this.speed = (Math.random() * 3) + 1; this.x = (Math.
random() * (POP.WIDTH) - this.r); this.y = POP.HEIGHT +
And let’s make them oscillate from side to(Math.random() * 100) + 100; this.remove = false; this.update = function() { this.y -= this.speed; // the rest of the class is unchanged
side, so that they are harder to hit:
// the amount by which the bubble // will move from side to side this.waveSize = 5 + this.r; // we need to remember the original // x position for our sine wave calculation this.xConstant = this.x; this.remove = false; this.update = function() { // a sine wave is commonly a
function of time var time = new Date().getTime() * 0.002; this.y -= this.speed; // the x coordinate to follow a sine wave this.x = this.waveSize *
Math.sin(time) + this.xConstant; // the rest of the class is unchanged
Again, we’re using some basic geometry toachieve this effect; in this case, a sine wave.While you don’t need to be a math whiz tomake games, basic understanding goes a long way.The articleAnimations With JavaScript"should get you started.Let’s also show some statistics on screen.
To do this, we will need to track various
actions throughout the game.
Put the following code, along with all of
the other variable declarations, at the beginning
of the program.
Now, in the// this goes at the start of the program // to track players's progress POP.score = { taps: 0, hit: 0, escaped: 0, accuracy: 0 },
Bubble
class we can keep track
ofPOP.score.escaped
when a bubble goes off screen.
In the main update loop, we increase// in the bubble class, when a bubble makes it to // the top of the screen if (this.y < -10) { POP.score.escaped += 1; // update score this.remove = true; }
POP.
score.hit
accordingly:
// in the update loop if (POP.entities[i].type ===
'bubble' && checkCollision) { hit = POP.collides(POP.entities[i], {x: POP.Input.x, y: POP.
In order for the statistics to be accurate,Input.y, r: 7}); if (hit) { POP.score.hit += 1; } POP.entities[i].remove = hit; }
we need to record all of the taps
the user makes:
Accuracy is obtained by dividing the number// and record all taps if (POP.Input.tapped) { // keep track of taps; needed to // calculate accuracy POP.score.taps += 1;
of hits by the number of taps,
multiplied by 100, which gives us a nice
percentage. Note that~~(POP.score.accuracy)
is a quick way (i.e. a hack) to round floats
down to integers.
// Add at the end of the update loop // to calculate accuracy POP.score.accuracy =
(POP.score.hit / POP.score.taps) * 100; POP.score.accuracy = isNaN(POP.score.accuracy) ? 0 : ~~(POP.score.accuracy);
Lastly, we use our// a handy way to round floats
POP.Draw.text
to display
the scores in the main update function.
// and finally in the draw function POP.Draw.text
('Hit: ' + POP.score.hit, 20, 30, 14, '#fff'); POP.Draw.text
('Escaped: ' + POP.score.escaped,
20, 50, 14, '#fff'); POP.Draw.text
('Accuracy: ' + POP.score.accuracy +
'%', 20, 70, 14, '#fff');
7. Spit And Polish
There’s a common understanding that a
playable demo can be made in a couple of hours,
but a polished game takes days, week,
months or even years!
We can do a few things to improve the visual
appeal of the game.
Particle Effects
Most games boast some form of particle
effects, which are great for explosions.
What if we made a bubble explode into many
tiny bubbles when it is popped, rather than
disappear instantly?
Take a look at ourParticle
class:
POP.Particle = function(x, y,r, col) { this.x = x; this.y = y; this.r = r; this.col = col; // determines whether particle will // travel to the right of left // 50% chance of either happening this.dir =
(Math.random() * 2 > 1) ? 1 : -1; // random values so particles do not // travel at the same speeds this.vx = ~~(Math.random() * 4) * this.dir; this.vy = ~~(Math.random() * 7); this.remove = false; this.update = function() { // update coordinates this.x += this.vx; this.y += this.vy; // increase velocity so particle // accelerates off screen this.vx *= 0.99; this.vy *= 0.99; // adding this negative amount to the // y velocity exerts an upward pull on // the particle, as if drawn to the // surface this.vy -= 0.25; // off screen if (this.y < 0) { this.remove = true; } }; this.render = function() { POP.Draw.circle
(this.x, this.y, this.r, this.col); }; };
It’s fairly obvious what is going on here.Using some basic acceleration so that theparticles speed up as the reach the surfaceis a nice touch. Again, this math and physicsare beyond the scope of this article, but forthose interested,To create the particle effect, we push several
particles into ourentities
array
whenever a bubble is hit:
// modify the main update function like so: if (hit) { // spawn an explosion for (var n = 0; n < 5; n +=1 ) { POP.entities.push(new POP.Particle( POP.entities[i].x, POP.entities[i].y, 2, // random opacity to
spice it up a bit 'rgba
(255,255,255,'+Math.random()*1+')' )); } POP.score.hit += 1; }
Waves
Given the underwater theme of the game,
adding a wave effect to the top of the screen would be a nice touch. We can do this by drawing a number of overlapping circles to give the illusion of waves:
// set up our wave effect; // basically, a series of overlapping circles // across the top of screen POP.wave = { x: -25, // x coordinate of first circle y: -40, // y coordinate of first circle r: 50, // circle radius time: 0, // we'll use this in
calculating the sine wave offset: 0 // this will be the
sine wave offset }; // calculate how many circles we need to // cover the screen's width POP.wave.total =
Add the code above to theMath.ceil(POP.WIDTH / POP.wave.r) + 1;
POP.init
function.
POP.wave
has a number of values that we’ll
need to draw the waves.
Add the following to the main update function.
It uses a sine wave to adjust the position of
the waves and give the illusion of movement.
// update wave offset // feel free to play with these values for // either slower or faster waves POP.wave.time = new Date().getTime() * 0.002; POP.wave.offset =
All that’s left to be done is to draw the waves,Math.sin(POP.wave.time * 0.8) * 5;
which goes into the render function.
// display snazzy wave effect for (i = 0; i < POP.wave.total; i++) { POP.Draw.circle( POP.wave.x + POP.wave.
Here, we’ve reused our sine wave solution foroffset + (i * POP.wave.r), POP.wave.y, POP.wave.r, '#fff'); }
the bubbles to make the waves move gently to
and fro. Feeling seasick yet?
Final Thoughts
Phew! That was fun. Hope you enjoyed this short
forage into tricks and techniques for making
an HTML5 game. We’ve managed to create a very
simple game that works on most smartphones as
well as modern browsers. Here are some things
you could consider doing:
- Store high scores using local storage.
- Add a splash screen and a “Game over” screen.
- Enable power-ups.
- Add audio. Contrary to what I said at the
- beginning of this article, this isn’t
- , just a bit of a headache. One technique is
- to use audio sprites (kind of like CSS image
- sprites); Remy Sharp breaks it down 6.
- Let your imagination run wild!
If you are interested in further exploring the
possibilities of mobile HTML5 games,
I recommend test-driving a couple of frameworks
to see what works for you. Juho Vepsäläinen
offers a useful summary of most game engines.
If you’re willing to invest a little cash,
then Impact is a great starting point, with
thorough documentation and lively helpful forums.
And the impressive X-Type demonstrates what is
possible. Not bad, eh?
(al) (jc) *&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&