舉例闡述制作HTML5手機(jī)游戲的7個(gè)步驟
想用HTML5制作跨平臺(tái)的手機(jī)游戲?不必理會(huì)Java或Objective-C?也不用管應(yīng)用商店?聽(tīng)起來(lái)真不可思議!
現(xiàn)在有許多游戲開(kāi)發(fā)者都在挖掘手機(jī)HTML手機(jī)游戲的潛能。如《Nutmeg》和《Lunch Bug》就是優(yōu)秀的案例。HTML5游戲的優(yōu)勢(shì)在于,使用相同的代碼就能讓游戲在手機(jī)和電腦上運(yùn)行得一樣好。這是否意味著HTML5能夠讓游戲代碼編寫成為一件一勞永逸的事?
準(zhǔn)備
在你開(kāi)始編寫你自己的“神廟逃亡”或“憤怒的小鳥(niǎo)”以前,以下幾點(diǎn)可能會(huì)澆滅你的創(chuàng)作熱情:
表現(xiàn):
一般說(shuō)來(lái),手機(jī)瀏覽器的JavaScript引擎表現(xiàn)并不出眾。盡管從iOS 6和Chrome的Android測(cè)試版的情況上看,它進(jìn)步得很快。
分辨率:
Android設(shè)備的分辨率已經(jīng)五花八門了,更別說(shuō)iPhone 4和iPad 3的分辨率和像素密度也在不斷提高。
聲音:
但愿你不介意沒(méi)有聲音的游戲——手機(jī)瀏覽器的聲音支持很差。延遲是主要問(wèn)題,大部分設(shè)備只支持一種聲道。iOS甚至要等到用戶主動(dòng)開(kāi)啟才會(huì)加載聲音。
現(xiàn)在,作為網(wǎng)頁(yè)開(kāi)發(fā)者的你已經(jīng)習(xí)慣于處理瀏覽器的這些缺陷。所以,一些技術(shù)問(wèn)題是難不倒你的,對(duì)吧?另外,這些表現(xiàn)和聲音問(wèn)題都是暫時(shí)的。畢竟手機(jī)瀏覽器進(jìn)步飛快,這些問(wèn)題很快就會(huì)變成歷史。
在本教程中,你將通過(guò)制作一款比較簡(jiǎn)單的游戲來(lái)了解這些基本問(wèn)題以及解決辦法。
iphone(from smashingmagazine)
這是一款相當(dāng)簡(jiǎn)單的游戲:玩家要做的就是點(diǎn)擊從屏幕底部浮起來(lái)的白色圓形,不要讓它們通過(guò)。你可以把這些白色圓形想象成漂浮上升的泡泡,你要在它們飛上天以前刺破它們。所以,我把這款小游戲叫作《POP》。
我們的制作過(guò)程可以分成如下7個(gè)步驟:
1、設(shè)置視圖,以適合大多數(shù)手機(jī)屏幕的尺寸;
2、使用canvas API在屏幕上繪制圖形;
3、捕捉觸擊事件;
4、制作基本的游戲循環(huán);
5、引入游戲“實(shí)體”;
6、添加碰撞檢測(cè)和一些簡(jiǎn)單的數(shù)學(xué)計(jì)算;
7、修飾外觀,即添加少量特效。
1、設(shè)置視圖
背景故事就隨便了。
正如前面提到的,不同的設(shè)備具有不同的分辨率和像素密度。這意味著我們必須調(diào)整畫布以適應(yīng)不同的視圖。這就可能損失游戲的外觀質(zhì)量,但我們有一個(gè)小技巧,也就是先使用小畫布,然后按比例放大,這么做后的畫面效果就好多了。
我們先寫一段基本的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>
這個(gè)meta視圖標(biāo)簽的作用是,命令瀏覽器禁止用戶縮放畫面,以及按完全尺寸渲染,而不是收縮頁(yè)面。隨后的帶apple-前綴的meta標(biāo)簽允許游戲被加入書簽。在iPhone上,加入書簽的應(yīng)用不會(huì)顯示頁(yè)面底部的在工具條上,因此節(jié)省了大片空間。
看看以下代碼:
- // 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);
首先,我們要給游戲創(chuàng)建一個(gè)命名空間“POP”。優(yōu)秀的開(kāi)發(fā)者不會(huì)污染整個(gè)命名空間的。比較好的做法是在程序的開(kāi)頭部分就聲明所有變量。大部分變量 是很容易理解的:canvas表示HTML中的canvas元素;ctx使我們可以通過(guò)JavaScript canvas API來(lái)訪問(wèn)它。
在POP.init,我們獲得canvas元素的引用,調(diào)整canvas元素的尺寸為480 × 320。resize函數(shù)會(huì)在調(diào)整大小和加載事件時(shí)啟用,從而按比例調(diào)整canvas的style屬性(游戲邦注:高度和寬度)。實(shí)際上,canvas仍 然是相同的尺寸,只是已經(jīng)通過(guò)CSS放大了。試一試調(diào)整你的瀏覽器大小,看看canvas的縮放效果。
如果你在你的手機(jī)上實(shí)驗(yàn),你會(huì)發(fā)現(xiàn)地址欄仍然可見(jiàn)。解決這個(gè)問(wèn)題的做法是:對(duì)文件添加額外像素,然后向下滾動(dòng)以隱藏地址欄,如下:
- // 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,如果存在則標(biāo)記。在調(diào)用POP.resize()以前,把它添加到POP.init后面。
然后,在resize函數(shù)中,如果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設(shè)備;否則,討厭的滾動(dòng)條就會(huì)出現(xiàn)。另外,我們必須延遲scrollTo,以確保Safari設(shè)備不會(huì)忽略它。
2、在畫布上繪制
我們已經(jīng)根據(jù)視圖調(diào)整好畫布了,接下來(lái)我們?cè)撛谏厦娈孅c(diǎn)什么了。
注:在本教程中,我們只使用基本的幾何形狀。iOS 5和Chrome的Android測(cè)試版可以用很高的幀率處理大量子畫面。在Android 3.2或以下版本中實(shí)驗(yàn)一下,你會(huì)發(fā)現(xiàn)前者的幀率確實(shí)大大提高了。幸運(yùn)地是,繪制圓形并不需要占用太多內(nèi)存,所以我們的游戲中可以大量使用圓形,即使是在 老設(shè)備上,表現(xiàn)也不會(huì)太差。
以下,我們已經(jīng)添加了一個(gè)基本的Draw對(duì)象,使我們可以清除屏幕,繪制矩形和圓形,然后添加文本。
- // 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對(duì)象有清除屏幕和繪制矩形、圓形及文本的方法。抽象這些操作的好處是,我們不必記憶確切的canvas API調(diào)用,而且繪制圓形的代碼簡(jiǎn)單到只有一句。
代碼如下:
- // 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函數(shù)之后。你應(yīng)該可以看到畫布上繪制出許多圖形。
3、觸擊事件
與click事件一樣,手機(jī)瀏覽器有捕捉觸擊事件的方法。
以下代碼的重點(diǎn)是touchstart、touchmove和touchend事件。對(duì)于標(biāo)準(zhǔn)的click事件,我們可以從e.pageX的 e.pageY中獲得座標(biāo)。觸擊事件則稍有不同,它們有一個(gè)touches集合,其中的各個(gè)元素都包含觸擊座標(biāo)和其他數(shù)據(jù)。我們只想要第一次觸擊,所以我 們要設(shè)置一個(gè)e.touches[0]。
注:只有版本4以后, Android才支持訪問(wèn)多次觸擊動(dòng)作的JavaScript。
當(dāng)禁用滾動(dòng)、縮放和其他會(huì)中斷游戲的活動(dòng)時(shí),我們還要調(diào)用e.preventDefault(); 。
添加以下代碼到POP.init函數(shù):
- // 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);
- 你可能已注意到,以上代碼把事件數(shù)據(jù)傳輸給Input對(duì)象。但我們現(xiàn)在要先定義一下它:
- // + 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’);
- }
- };
現(xiàn)在,測(cè)試一下。圓形沒(méi)有出現(xiàn)。這是為什么?有了!因?yàn)槲覀円呀?jīng)縮放畫布了,當(dāng)映射觸擊到屏幕的位置時(shí),我們必須考慮到這一點(diǎn)。
首先,我們必須從座標(biāo)中扣除偏移值。
- var offsetTop = POP.canvas.offsetTop,
- offsetLeft = POP.canvas.offsetLeft;
- this.x = data.pageX – offsetLeft;
- this.y = data.pageY – offsetTop;
offset_diagram(from smashingmagazine)
然后,考慮到畫布已經(jīng)縮放過(guò)了,我們得計(jì)算一下實(shí)際畫布(游戲邦注:仍然是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)
你開(kāi)始覺(jué)得頭疼了吧?那我就給你舉個(gè)例子。想象一下玩家輕擊500 × 750的畫布上的座標(biāo)400,400。我們必須調(diào)整這個(gè)座標(biāo),因?yàn)楫嫴嫉膶?shí)際尺寸是480 × 320。所以,真正的X座標(biāo)是400除以比例,即400 ÷ 1.56 = 320.5。
我們當(dāng)然不是在每一個(gè)觸擊事件發(fā)生時(shí)計(jì)算,而是在調(diào)整完畫布尺寸后計(jì)算座標(biāo)。在程序的開(kāi)頭部分添加如下代碼,以及其他變量聲明:
- // 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},
在我們的調(diào)整大小函數(shù)中,調(diào)整畫布的寬高后,我們要記錄一下當(dāng)前的尺寸和偏移量:
- // add this to the resize function.
- POPPOP.scale = POP.currentWidth / POP.WIDTH;
- POPPOP.offset.top = POP.canvas.offsetTop;
- POPPOP.offset.left = POP.canvas.offsetLeft;
現(xiàn)在,我們可以在POP.Input類的set方法中使用它們了:
- this.x = (data.pageX – POP.offset.left) / POP.scale;
- this.y = (data.pageY – POP.offset.top) / POP.scale;
別走開(kāi),下頁(yè)內(nèi)容更精彩
#p#
4、循環(huán)
典型的游戲循環(huán)如下:
1、用戶輸入,
2、更新和處理碰撞,
3、在屏幕上渲染,
4、重復(fù)。
我們當(dāng)然可以使用setInterval,但在requestAnimationFrame中有一個(gè)新玩意兒。它能保證動(dòng)畫更流暢,并且能節(jié)省電池量。不幸地是,并非所有瀏覽器都支持它。但Paul Irish已經(jīng)想到一個(gè)方便的解決辦法。
我們也可以借鑒他的辦法,代碼如下:
- // 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);
- };
- })();
接著我們來(lái)制作初步的游戲循環(huán):
- // 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();
- }
我們?cè)赑OP.init之后調(diào)用這個(gè)循環(huán)。POP.loop接著調(diào)用我們的POP.update和POP.render方法。 requestAnimFrame保證這個(gè)循環(huán)被再次調(diào)用,最好是以60幀每秒的速度。注意,我們不必?fù)?dān)心查看循環(huán)中的輸入,因?yàn)槲覀円呀?jīng)在注意通過(guò) POP.Input類可以訪問(wèn)到的觸擊和點(diǎn)擊事件。
現(xiàn)在的問(wèn)題是,我們的最后一次觸擊在屏幕上消失得太快了。我們必須想辦法讓屏幕更好地記憶和顯示觸擊位置。
5、觸擊
首先,我們添加一個(gè)實(shí)體集合。這個(gè)集合包含游戲中出現(xiàn)的所有觸擊點(diǎn)、泡泡、粒子和其他動(dòng)態(tài)物品。
// put this at start of program
entities: [],
我們來(lái)做一個(gè)Touch類,它將在接觸點(diǎn)處繪制一個(gè)圓點(diǎn),這個(gè)圓點(diǎn)之后會(huì)慢慢消褪。
- 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座標(biāo)是參數(shù),半徑this.r為5像素,初始不透明度為1,觸擊點(diǎn)消裉速率為0.05,remove標(biāo)記告訴主游戲循環(huán)是否將觸擊圓點(diǎn)從該實(shí)體集合中移除。
關(guān)鍵是,這個(gè)類有兩個(gè)主要方法:update和render。我們將從游戲循環(huán)的相應(yīng)部分調(diào)用它們。
我們先在游戲循環(huán)中刷出一個(gè)新的Touch實(shí)例,再通過(guò)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,那么我們就添加一個(gè)新的POP.Touch實(shí)例到我們的實(shí)體集合。循環(huán)這個(gè)實(shí)體集合,即調(diào)用各個(gè)實(shí)體的update方法。最后,如果實(shí)體被標(biāo)記為移除,它就會(huì)從該集合中刪除。
接著,我們?cè)赑OP.render函數(shù)中渲染它們。
- // 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函數(shù),循環(huán)實(shí)體和調(diào)用它們的render方法,在屏幕上繪制它們。
到目前為止,一切進(jìn)展順利?,F(xiàn)在我們要添加Bubble類,它的作用是生產(chǎn)漂浮上升的泡泡。
- 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類,主要的區(qū)別是它并不是像觸擊點(diǎn)一樣消褪,而是向上移動(dòng)。這個(gè)活動(dòng)是通過(guò)在update函數(shù)中改變y位置即 this.y實(shí)現(xiàn)的。這里,我們也要查看泡泡是否離開(kāi)屏幕;如果是,我們就要把它標(biāo)記為移除。
注:我們已經(jīng)制作了基本Entity類,Touch和Bubble都包含在內(nèi)。但是,我現(xiàn)在不想比較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;
- }
以上,我們已經(jīng)為游戲循環(huán)添加了隨機(jī)計(jì)時(shí)器。游戲循環(huán)會(huì)在隨機(jī)位置刷出Bubble實(shí)例。在游戲開(kāi)始時(shí),我們?cè)O(shè)置nextBubble(下一個(gè)泡泡)的出現(xiàn)間隔為100,即當(dāng)100減少到0時(shí),游戲就會(huì)刷出新泡泡,并重置nextBubble計(jì)數(shù)器。
6、整合
首先,我們還沒(méi)使用到任何碰撞檢測(cè)。我們可以用簡(jiǎn)單地函數(shù)實(shí)現(xiàn)它。
- // 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);
- }
- }
現(xiàn)在的泡泡比較無(wú)趣,移動(dòng)速度和軌跡都一樣。我們可以通過(guò)下面這段簡(jiǎn)單的代碼來(lái)隨機(jī)化泡泡的運(yùn)動(dòng):
- 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
我們使用一些基本的幾何學(xué)知識(shí)就能達(dá)到這個(gè)效果,也就是正弦波。做游戲不一定要精通數(shù)學(xué),基本的知識(shí)就非常夠用了。
游戲屏幕上還應(yīng)該顯示計(jì)數(shù)。為此,我們要追蹤游戲過(guò)程的各種活動(dòng)。
將以下代碼與所有其他變量聲明一起放在程序的開(kāi)頭部分:
- // this goes at the start of the program
- // to track players’s progress
- POP.score = {
- taps: 0,
- hit: 0,
- escaped: 0,
- accuracy: 0
- },
現(xiàn)在,在Bubble類,當(dāng)泡泡離開(kāi)屏幕,我們可以用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;
- }
在主要更新循環(huán)中,我們相應(yīng)地增加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;
- }
為了得出命中率,我們必須記錄玩家的所有觸擊動(dòng)作:
- // and record all taps
- if (POP.Input.tapped) {
- // keep track of taps; needed to
- // calculate accuracy
- POP.score.taps += 1;
命中率的算法就是,觸擊數(shù)乘上100。注意,~~(POP.score.accuracy)把約數(shù)變成整數(shù)。
- // 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來(lái)顯示得分。
- // 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、修飾
我們都知道,制作一個(gè)可玩的demo只需要若干小時(shí),但一款漂亮的游戲卻要耗費(fèi)數(shù)天、數(shù)月甚至數(shù)年!
我們可以通過(guò)以下做法增加這款小游戲的視覺(jué)吸引力。
顆粒效果
大多數(shù)游戲都會(huì)使用顆粒效果,特別是對(duì)于爆炸。當(dāng)玩家觸擊泡泡時(shí),泡泡就碎成若干小泡泡,而不是立即消失,效果會(huì)不會(huì)更好呢?
看看我們的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);
- };
- };
以上代碼的作用顯而易見(jiàn)。當(dāng)泡泡被擊中時(shí),它會(huì)碎成若干加速上升到水面的顆粒。不過(guò),本文不會(huì)探討這個(gè)效果的算術(shù)和物理學(xué)。
為了制作顆粒效果,我們我們要在entities集合中添加若干泡泡被擊中時(shí)會(huì)出現(xiàn)的顆粒:
- // 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;
- }
水波
考慮到游戲發(fā)生在水下,有必要在屏幕頂部添加水波效果。我們可以通過(guò)繪制大量重疊的圓形來(lái)制造水波的錯(cuò)覺(jué):
- // 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函數(shù)前面。POP.wave有許多值。
添加以下代碼到主要更新函數(shù)中。它作用正統(tǒng)波來(lái)調(diào)整水波的位置,從而產(chǎn)生水面運(yùn)動(dòng)的錯(cuò)覺(jué)。
- // 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函數(shù)繪制水波:
- // 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’);
- }
這里,我們對(duì)泡泡重復(fù)使用正弦波,使水波活動(dòng)更加溫和。
結(jié)語(yǔ)
終于完工了。希望你通過(guò)這個(gè)粗糙的教程能學(xué)習(xí)到一些制作HTML5游戲的技巧。我們已經(jīng)制作了一款非常簡(jiǎn)單的游戲,它可以在大多數(shù)智能手機(jī)和瀏覽器上運(yùn)行。你還可以考慮從以下幾個(gè)方面進(jìn)一步改進(jìn)游戲:
1、使用本地存儲(chǔ)器保存最高得分。
2、添加載入畫面和結(jié)束畫面。
3、添加增益道具。
4、添加聲音。與我在本文開(kāi)頭部分所說(shuō)的相反,這并非不可能,只是會(huì)有一點(diǎn)麻煩。技術(shù)之一是使用聲音精靈(相當(dāng)于CSS中的圖像精靈)。
5、盡情發(fā)揮你的想像力!
如果你有興趣進(jìn)一步探索手機(jī)HTML5游戲的潛力,我建議你多多測(cè)試框架,看看什么對(duì)你有用。如果你愿意花一點(diǎn)錢,Impact引擎是一個(gè)好起點(diǎn),它附帶詳盡的說(shuō)明文件和實(shí)用的論壇?!禭-Type》效果不錯(cuò)吧?