游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

举例阐述制作HTML5手机游戏的7个步骤

发布时间:2013-07-09 16:19:46 Tags:,,,,,

作者:Eoin McGrath

想用HTML5制作跨平台的手机游戏?不必理会Java或Objective-C?也不用管应用商店?听起来真不可思议!

现在有许多游戏开发者都在挖掘手机HTML手机游戏的潜能。如《Nutmeg》和《Lunch Bug》就是优秀的案例。HTML5游戏的优势在于,使用相同的代码就能让游戏在手机和电脑上运行得一样好。这是否意味着HTML5能够让游戏代码编写成为一件一劳永逸的事?

准备

在你开始编写你自己的“神庙逃亡”或“愤怒的小鸟”以前,以下几点可能会浇灭你的创作热情:

表现:

一般说来,手机浏览器的JavaScript引擎表现并不出众。尽管从iOS 6和Chrome的Android测试版的情况上看,它进步得很快。

分辨率:

Android设备的分辨率已经五花八门了,更别说iPhone 4和iPad 3的分辨率和像素密度也在不断提高。

声音:

但愿你不介意没有声音的游戏——手机浏览器的声音支持很差。延迟是主要问题,大部分设备只支持一种声道。iOS甚至要等到用户主动开启才会加载声音。

现在,作为网页开发者的你已经习惯于处理浏览器的这些缺陷。所以,一些技术问题是难不倒你的,对吧?另外,这些表现和声音问题都是暂时的。毕竟手机浏览器进步飞快,这些问题很快就会变成历史。

在本教程中,你将通过制作一款比较简单的游戏来了解这些基本问题以及解决办法。

iphone(from smashingmagazine)

iphone(from smashingmagazine)

这是一款相当简单的游戏:玩家要做的就是点击从屏幕底部浮起来的白色圆形,不要让它们通过。你可以把这些白色圆形想象成漂浮上升的泡泡,你要在它们飞上天以前刺破它们。所以,我把这款小游戏叫作《POP》。

我们的制作过程可以分成如下7个步骤:

1、设置视图,以适合大多数手机屏幕的尺寸;

2、使用canvas API在屏幕上绘制图形;

3、捕捉触击事件;

4、制作基本的游戏循环;

5、引入游戏“实体”;

6、添加碰撞检测和一些简单的数学计算;

7、修饰外观,即添加少量特效。

1、设置视图

背景故事就随便了。

正如前面提到的,不同的设备具有不同的分辨率和像素密度。这意味着我们必须调整画布以适应不同的视图。这就可能损失游戏的外观质量,但我们有一个小技巧,也就是先使用小画布,然后按比例放大,这么做后的画面效果就好多了。

我们先写一段基本的HTML代码:

<!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>

这个meta视图标签的作用是,命令浏览器禁止用户缩放画面,以及按完全尺寸渲染,而不是收缩页面。随后的带apple-前缀的meta标签允许游戏被加入书签。在iPhone上,加入书签的应用不会显示页面底部的在工具条上,因此节省了大片空间。

看看以下代码:

// 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);

首先,我们要给游戏创建一个命名空间“POP”。优秀的开发者不会污染整个命名空间的。比较好的做法是在程序的开头部分就声明所有变量。大部分变量是很容易理解的:canvas表示HTML中的canvas元素;ctx使我们可以通过JavaScript canvas API来访问它。

在POP.init,我们获得canvas元素的引用,调整canvas元素的尺寸为480 × 320。resize函数会在调整大小和加载事件时启用,从而按比例调整canvas的style属性(游戏邦注:高度和宽度)。实际上,canvas仍然是相同的尺寸,只是已经通过CSS放大了。试一试调整你的浏览器大小,看看canvas的缩放效果。

如果你在你的手机上实验,你会发现地址栏仍然可见。解决这个问题的做法是:对文件添加额外像素,然后向下滚动以隐藏地址栏,如下:

// 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;

以上代码搜索userAgent,如果存在则标记。在调用POP.resize()以前,把它添加到POP.init后面。

然后,在resize函数中,如果android或ios为true,我们就把文件的高度再增加50像素——这就足以隐藏地址栏了。

// 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’;
}

注意,我们的做法只适用于Android和iOS设备;否则,讨厌的滚动条就会出现。另外,我们必须延迟scrollTo,以确保Safari设备不会忽略它。

2、在画布上绘制

我们已经根据视图调整好画布了,接下来我们该在上面画点什么了。

注:在本教程中,我们只使用基本的几何形状。iOS 5和Chrome的Android测试版可以用很高的帧率处理大量子画面。在Android 3.2或以下版本中实验一下,你会发现前者的帧率确实大大提高了。幸运地是,绘制圆形并不需要占用太多内存,所以我们的游戏中可以大量使用圆形,即使是在老设备上,表现也不会太差。

以下,我们已经添加了一个基本的Draw对象,使我们可以清除屏幕,绘制矩形和圆形,然后添加文本。

// 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);
}

};

我们的Draw对象有清除屏幕和绘制矩形、圆形及文本的方法。抽象这些操作的好处是,我们不必记忆确切的canvas API调用,而且绘制圆形的代码简单到只有一句。

代码如下:

// 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′);

把上述代码放在POP.init函数之后。你应该可以看到画布上绘制出许多图形。

3、触击事件

与click事件一样,手机浏览器有捕捉触击事件的方法。

以下代码的重点是touchstart、touchmove和touchend事件。对于标准的click事件,我们可以从e.pageX的e.pageY中获得座标。触击事件则稍有不同,它们有一个touches集合,其中的各个元素都包含触击座标和其他数据。我们只想要第一次触击,所以我们要设置一个e.touches[0]。

注:只有版本4以后, Android才支持访问多次触击动作的JavaScript。

当禁用滚动、缩放和其他会中断游戏的活动时,我们还要调用e.preventDefault(); 。

添加以下代码到POP.init函数:

// 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);

你可能已注意到,以上代码把事件数据传输给Input对象。但我们现在要先定义一下它:

// + 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’);
}

};

现在,测试一下。圆形没有出现。这是为什么?有了!因为我们已经缩放画布了,当映射触击到屏幕的位置时,我们必须考虑到这一点。

首先,我们必须从座标中扣除偏移值。

var offsetTop = POP.canvas.offsetTop,
offsetLeft = POP.canvas.offsetLeft;

this.x = data.pageX – offsetLeft;
this.y = data.pageY – offsetTop;

offset_diagram(from smashingmagazine)

offset_diagram(from smashingmagazine)

然后,考虑到画布已经缩放过了,我们得计算一下实际画布(游戏邦注:仍然是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;

scaled_canvas_diagram(from smashingmagazine)

scaled_canvas_diagram(from smashingmagazine)

你开始觉得头疼了吧?那我就给你举个例子。想象一下玩家轻击500 × 750的画布上的座标400,400。我们必须调整这个座标,因为画布的实际尺寸是480 × 320。所以,真正的X座标是400除以比例,即400 ÷ 1.56 = 320.5。

我们当然不是在每一个触击事件发生时计算,而是在调整完画布尺寸后计算座标。在程序的开头部分添加如下代码,以及其他变量声明:

// 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},

在我们的调整大小函数中,调整画布的宽高后,我们要记录一下当前的尺寸和偏移量:

// add this to the resize function.
POP.scale = POP.currentWidth / POP.WIDTH;
POP.offset.top = POP.canvas.offsetTop;
POP.offset.left = POP.canvas.offsetLeft;

现在,我们可以在POP.Input类的set方法中使用它们了:

this.x = (data.pageX – POP.offset.left) / POP.scale;
this.y = (data.pageY – POP.offset.top) / POP.scale;

4、循环

典型的游戏循环如下:

1、用户输入,

2、更新和处理碰撞,

3、在屏幕上渲染,

4、重复。

我们当然可以使用setInterval,但在requestAnimationFrame中有一个新玩意儿。它能保证动画更流畅,并且能节省电池量。不幸地是,并非所有浏览器都支持它。但Paul Irish已经想到一个方便的解决办法。

我们也可以借鉴他的办法,代码如下:

// 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(callback, 1000 / 60);
};
})();

接着我们来制作初步的游戏循环:

// 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之后调用这个循环。POP.loop接着调用我们的POP.update和POP.render方法。requestAnimFrame保证这个循环被再次调用,最好是以60帧每秒的速度。注意,我们不必担心查看循环中的输入,因为我们已经在注意通过POP.Input类可以访问到的触击和点击事件。

现在的问题是,我们的最后一次触击在屏幕上消失得太快了。我们必须想办法让屏幕更好地记忆和显示触击位置。

5、触击

首先,我们添加一个实体集合。这个集合包含游戏中出现的所有触击点、泡泡、粒子和其他动态物品。

// put this at start of program
entities: [],

我们来做一个Touch类,它将在接触点处绘制一个圆点,这个圆点之后会慢慢消褪。

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(255,0,0,’+this.opacity+’)');
};

};

Touch类具有一系列属性。x和y座标是参数,半径this.r为5像素,初始不透明度为1,触击点消裉速率为0.05,remove标记告诉主游戏循环是否将触击圆点从该实体集合中移除。

关键是,这个类有两个主要方法:update和render。我们将从游戏循环的相应部分调用它们。

我们先在游戏循环中刷出一个新的Touch实例,再通过update方法移除:

// 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.length; 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是true,那么我们就添加一个新的POP.Touch实例到我们的实体集合。循环这个实体集合,即调用各个实体的update方法。最后,如果实体被标记为移除,它就会从该集合中删除。

接着,我们在POP.render函数中渲染它们。

// 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.entities.length; i += 1) {
POP.entities[i].render();
}

},

类似于update函数,循环实体和调用它们的render方法,在屏幕上绘制它们。

到目前为止,一切进展顺利。现在我们要添加Bubble类,它的作用是生产漂浮上升的泡泡。

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(this.x, this.y, this.r, ‘rgba(255,255,255,1)’);
};

};

POP.Bubble类非常接近于Touch类,主要的区别是它并不是像触击点一样消褪,而是向上移动。这个活动是通过在update函数中改变y位置即 this.y实现的。这里,我们也要查看泡泡是否离开屏幕;如果是,我们就要把它标记为移除。

注:我们已经制作了基本Entity类,Touch和Bubble都包含在内。但是,我现在不想比较JavaScript原型的继承和类。

// 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 = ( Math.random() * 100 ) + 100;
}

以上,我们已经为游戏循环添加了随机计时器。游戏循环会在随机位置刷出Bubble实例。在游戏开始时,我们设置nextBubble(下一个泡泡)的出现间隔为100,即当100减少到0时,游戏就会刷出新泡泡,并重置nextBubble计数器。

6、整合

首先,我们还没使用到任何碰撞检测。我们可以用简单地函数实现它。

// 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.x, 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);
}
}

现在的泡泡比较无趣,移动速度和轨迹都一样。我们可以通过下面这段简单的代码来随机化泡泡的运动:

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 + (Math.random() * 100) + 100;

this.remove = false;

this.update = function() {

this.y -= this.speed;

// the rest of the class is unchanged

我们要让泡泡左右摆,使玩家更难触击到它们:

// 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

我们使用一些基本的几何学知识就能达到这个效果,也就是正弦波。做游戏不一定要精通数学,基本的知识就非常够用了。

游戏屏幕上还应该显示计数。为此,我们要追踪游戏过程的各种活动。

将以下代码与所有其他变量声明一起放在程序的开头部分:

// this goes at the start of the program
// to track players’s progress
POP.score = {
taps: 0,
hit: 0,
escaped: 0,
accuracy: 0
},

现在,在Bubble类,当泡泡离开屏幕,我们可以用POP.score.escaped记录。

// 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:

// in the update loop
if (POP.entities[i].type === ‘bubble’ && checkCollision) {
hit = POP.collides(POP.entities[i],
{x: POP.Input.x, y: POP.Input.y, r: 7});
if (hit) {
POP.score.hit += 1;
}

POP.entities[i].remove = hit;
}

为了得出命中率,我们必须记录玩家的所有触击动作:

// and record all taps
if (POP.Input.tapped) {
// keep track of taps; needed to
// calculate accuracy
POP.score.taps += 1;

命中率的算法就是,触击数乘上100。注意,~~(POP.score.accuracy)把约数变成整数。

// 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); // a handy way to round floats

最后,我们使用POP.Draw.text来显示得分。

// 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、修饰

我们都知道,制作一个可玩的demo只需要若干小时,但一款漂亮的游戏却要耗费数天、数月甚至数年!

我们可以通过以下做法增加这款小游戏的视觉吸引力。

颗粒效果

大多数游戏都会使用颗粒效果,特别是对于爆炸。当玩家触击泡泡时,泡泡就碎成若干小泡泡,而不是立即消失,效果会不会更好呢?

看看我们的Particle类:

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);
};

};

以上代码的作用显而易见。当泡泡被击中时,它会碎成若干加速上升到水面的颗粒。不过,本文不会探讨这个效果的算术和物理学。

为了制作颗粒效果,我们我们要在entities集合中添加若干泡泡被击中时会出现的颗粒:

// 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;
}

水波

考虑到游戏发生在水下,有必要在屏幕顶部添加水波效果。我们可以通过绘制大量重叠的圆形来制造水波的错觉:

// 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 = Math.ceil(POP.WIDTH / POP.wave.r) + 1;

把以上代码添加到POP.init函数前面。POP.wave有许多值。

添加以下代码到主要更新函数中。它作用正统波来调整水波的位置,从而产生水面运动的错觉。

// 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 = Math.sin(POP.wave.time * 0.8) * 5;

最后要做的就是让render函数绘制水波:

// display snazzy wave effect
for (i = 0; i < POP.wave.total; i++) {

POP.Draw.circle(
POP.wave.x + POP.wave.offset +  (i * POP.wave.r),
POP.wave.y,
POP.wave.r,
‘#fff’);
}

这里,我们对泡泡重复使用正弦波,使水波活动更加温和。

结语

终于完工了。希望你通过这个粗糙的教程能学习到一些制作HTML5游戏的技巧。我们已经制作了一款非常简单的游戏,它可以在大多数智能手机和浏览器上运行。你还可以考虑从以下几个方面进一步改进游戏:

1、使用本地存储器保存最高得分。

2、添加载入画面和结束画面。

3、添加增益道具。

4、添加声音。与我在本文开头部分所说的相反,这并非不可能,只是会有一点麻烦。技术之一是使用声音精灵(相当于CSS中的图像精灵)。

5、尽情发挥你的想像力!

如果你有兴趣进一步探索手机HTML5游戏的潜力,我建议你多多测试框架,看看什么对你有用。如果你愿意花一点钱,Impact引擎是一个好起点,它附带详尽的说明文件和实用的论坛。《X-Type》效果不错吧?(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

How To Design A Mobile Game With HTML5

By Eoin McGrath

Care to make a cross-platform mobile game with HTML5? No need to dabble in Java or Objective-C? Bypass the app stores? Sounds like an instant win!

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.

Resolution

A veritable cornucopia of Android devices sport a wide range of resolutions. Not to mention the increased resolution and pixel density of the iPhone 4 and iPad 3.

Audio

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:

Play the demo.

Download the demo (ZIP).

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.

1. Setting The Stage

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 for our 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, replete with examples for drawing to the canvas.

// 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 the click 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 can get 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 at 400,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 actual x 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:

// 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},

In our resize function, after adjusting the canvas’ width and height, we make note of the current scale and offset:

// add this to the resize function.
POP.scale = POP.currentWidth / POP.WIDTH;
POP.offset.top = POP.canvas.offsetTop;
POP.offset.left = POP.canvas.offsetLeft;

Now, we can use them in the set method of our POP.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, use setInterval, but there’s a shiny new toy in town named requestAnimationFrame. It promises smoother animation and is more battery-efficient. The bad news is that it’s not supported consistently across browsers. But Paul Irish has 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(callback, 1000 / 60);
};
})();
And let’s create a rudimentary game loop:
// 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();
}

We call the loop at the end of POP.init. The POP.loop function in turn calls our POP.update and POP.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 our POP.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.

// put this at start of program
entities: [],
Let’s create a 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(255,0,0,’+this.opacity+’)');
};

};

The 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 a remove flag that tells the main game loop whether to remove this from the entities array.

Crucially, the class has two main methods: update and render. We will call these from the corresponding part of our game loop.

We can then spawn a new instance of Touch 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.length; 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);
}
}

},

Basically, if POP.Input.tapped is true, then we add a new instance of POP.Touch to our entities array. We then cycle through the entities array, calling the update method for each entity. Finally, if the entity is flagged for removal, we delete it from the array.

Next, we render them in the POP.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.entities.length; i += 1) {
POP.entities[i].render();
}

},

Similar to our update function, we cycle through the entities and call their render method to draw them to the screen.

So far, so good. Now we’ll add a Bubble 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(this.x, this.y, this.r, ‘rgba(255,255,255,1)’);
};

};

The 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 the y 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 base Entity class that both Touch and Bubble 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 = ( Math.random() * 100 ) + 100;
}

Above, we have added a random timer to our game loop that will spawn an instance of Bubble at a random position. At the start of the game, we set nextBubble with a value of 100. This is subtracted on each game tick and, when it reaches 0, we spawn a bubble and reset the nextBubble counter.

6. Putting It Together

First of all, there is not yet any notion of collision detection. We can add that with a simple function. The math behind this is basic geometry, which you can brush up on at Wolfram MathWorld.

// 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.x, 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);
}
}

The bubbles are rather boring; they all 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 + (Math.random() * 100) + 100;

this.remove = false;

this.update = function() {

this.y -= this.speed;

// the rest of the class is unchanged

And let’s make them oscillate from side to 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 to achieve this effect; in this case, a sine wave. While you don’t need to be a math whiz to make games, basic understanding goes a long way. The article “

A Quick Look Into the Math of Animations 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.

// this goes at the start of the program
// to track players’s progress
POP.score = {
taps: 0,
hit: 0,
escaped: 0,
accuracy: 0
},
Now, in the Bubble class we can keep track of POP.score.escaped when a bubble goes off screen.
// 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;
}
In the main update loop, we increase 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.Input.y, r: 7});
if (hit) {
POP.score.hit += 1;
}

POP.entities[i].remove = hit;
}
In order for the statistics to be accurate, we need to record all of the taps the user makes:
// and record all taps
if (POP.Input.tapped) {
// keep track of taps; needed to
// calculate accuracy
POP.score.taps += 1;

Accuracy is obtained by dividing the number 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); // a handy way to round floats
Lastly, we use our 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 our Particle 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 the particles speed up as the reach the surface is a nice touch. Again, this math and physics are beyond the scope of this article, but for those interested, Skookum Digital Works explains it in depth.

To create the particle effect, we push several particles into our entities 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 = Math.ceil(POP.WIDTH / POP.wave.r) + 1;

Add the code above to the 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 = Math.sin(POP.wave.time * 0.8) * 5;
All that’s left to be done is to draw the waves, 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.offset +  (i * POP.wave.r),
POP.wave.y,
POP.wave.r,
‘#fff’);
}

Here, we’ve reused our sine wave solution for 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 impossible, just a bit of a headache. One technique is to use audio sprites (kind of like CSS image sprites); Remy

Sharp breaks it down.

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?(source:mobile.smashingmagazine)


上一篇:

下一篇: