想用HTML5制作跨平台的手机游戏?不必理会Java或Objective-C?也不用管应用商店?听起来真不可思议!
现在有许多游戏开发者都在挖掘手机HTML手机游戏的潜能。如《Nutmeg》和《Lunch Bug》就是优秀的案例。HTML5游戏的优势在于,使用相同的代码就能让游戏在手机和电脑上运行得一样好。这是否意味着HTML5能够让游戏代码编写成为一件一劳永逸的事?
准备
在你开始编写你自己的“神庙逃亡”或“愤怒的小鸟”以前,以下几点可能会浇灭你的创作热情:
表现:
一般说来,手机浏览器的JavaScript引擎表现并不出众。尽管从iOS 6和Chrome的Android测试版的情况上看,它进步得很快。
分辨率:
Android设备的分辨率已经五花八门了,更别说iPhone 4和iPad 3的分辨率和像素密度也在不断提高。
声音:
但愿你不介意没有声音的游戏——手机浏览器的声音支持很差。延迟是主要问题,大部分设备只支持一种声道。iOS甚至要等到用户主动开启才会加载声音。
现在,作为网页开发者的你已经习惯于处理浏览器的这些缺陷。所以,一些技术问题是难不倒你的,对吧?另外,这些表现和声音问题都是暂时的。毕竟手机浏览器进步飞快,这些问题很快就会变成历史。
在本教程中,你将通过制作一款比较简单的游戏来了解这些基本问题以及解决办法。
这是一款相当简单的游戏:玩家要做的就是点击从屏幕底部浮起来的白色圆形,不要让它们通过。你可以把这些白色圆形想象成漂浮上升的泡泡,你要在它们飞上天以前刺破它们。所以,我把这款小游戏叫作《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
- POPPOP.RATIO = POP.WIDTH / POP.HEIGHT;
- // these will change when the screen is resized
- POPPOP.currentWidth = POP.WIDTH;
- POPPOP.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
- POPPOP.canvas.width = POP.WIDTH;
- POPPOP.canvas.height = POP.HEIGHT;
- // the canvas context enables us to
- // interact with the canvas api
- POPPOP.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
- POPPOP.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
- POPPOP.canvas.style.width = POP.currentWidth + ‘px’;
- POPPOP.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();
- POPPOP.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;
然后,考虑到画布已经缩放过了,我们得计算一下实际画布(游戏邦注:仍然是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;
你开始觉得头疼了吧?那我就给你举个例子。想象一下玩家轻击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.
- POPPOP.scale = POP.currentWidth / POP.WIDTH;
- POPPOP.offset.top = POP.canvas.offsetTop;
- POPPOP.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;
别走开,下页内容更精彩
#p#
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
- thisthis.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
- thisthis.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》效果不错吧?