在上一篇教程中我們完成了boot、preload、menu這三個state的制作,下面我們就要進入本游戲最核心的一個state的制作了。play這個state的代碼比較多,我不會一一進行說明,只會把一些關鍵的東西挑出來說。
我們點擊游戲菜單中的開始按鈕后,首先出現的是這個畫面:
在第一部分的教程中,我們已經制作了一個游戲菜單的場景,這個頁面也跟那個差不多,只不過這個頁面去除了游戲標題和開始按鈕,而多出了Get Ready以及提示點擊屏幕來操作游戲的兩張圖片。在這個state里,我們需要啟用物理引擎,並且要能夠響應鼠標點擊事件。
game.States.play = function(){ this.create = function(){ this.bg = game.add.tileSprite(0,0,game.width,game.height,'background');//背景圖,這里先不用移動,游戲開始后再動 this.pipeGroup = game.add.group();//用於存放管道的組,后面會講到 this.pipeGroup.enableBody = true; this.ground = game.add.tileSprite(0,game.height-112,game.width,112,'ground'); //地板,這里先不用移動,游戲開始后再動 this.bird = game.add.sprite(50,150,'bird'); //鳥 this.bird.animations.add('fly');//添加動畫 this.bird.animations.play('fly',12,true);//播放動畫 this.bird.anchor.setTo(0.5, 0.5); //設置中心點 game.physics.enable(this.bird,Phaser.Physics.ARCADE); //開啟鳥的物理系統 this.bird.body.gravity.y = 0; //鳥的重力,未開始游戲,先讓重力為0,不然鳥會掉下來 game.physics.enable(this.ground,Phaser.Physics.ARCADE);//開啟地面的物理系統 this.ground.body.immovable = true; //讓地面在物理環境中固定不動 this.readyText = game.add.image(game.width/2, 40, 'ready_text'); //get ready 文字 this.playTip = game.add.image(game.width/2,300,'play_tip'); //提示點擊屏幕的圖片 this.readyText.anchor.setTo(0.5, 0); this.playTip.anchor.setTo(0.5, 0); this.hasStarted = false; //游戲是否已開始 game.time.events.loop(900, this.generatePipes, this); //利用時鍾事件來循環產生管道 game.time.events.stop(false); //先不要啟動時鍾 game.input.onDown.addOnce(this.statrGame, this); //點擊屏幕后正式開始游戲 }; }
啟用物理系統
默認的游戲中的每個對象的物理系統是關閉的,要啟用一個對象的物理系統,可以使用 game.physics.enable() 方法
enable(object, system, debug)
object : 要開啟物理系統的對象,可以是單個對象,也可以是一個包含多個對象的數組
system : 要啟用的物理系統,默認為 Phaser.Physics.ARCADE,Phaser目前支持三種物理引擎,分別是Arcade ,P2 以及 Ninja。
debug : 是否開啟調試
只有開啟了對象的物理系統,該對象才具有物理特性,開啟了物理系統后,對象的body屬性指向該對象擁有的物理系統,所有與物理相關的屬性或方法都必須在body上進行操作。
鼠標點擊事件
Phaser中的鼠標、鍵盤、觸摸等交互事件都統一由Input對象來處理。我們需要鼠標點擊屏幕后進行響應,可以使用Input對象的onDown屬性,該屬性指向一個Phaser.Signal對象,我們可以在這個對象上綁定事件,每當鼠標按鍵下,就會觸發一個onDown的信號,如果這個onDown信號對象上綁定了事件,那么這些事件就會執行。例如:
var input = game.input; //當前游戲的input對象 var signal = input.onDown; //鼠標按下時的 Signal對象 signal.add(function(){}); //給Signal 綁定事件處理函數 signal.add(function(){}); //再綁定一個 signal.addOnce(function(){}); //綁定一個只會執行一次的事件函數
時鍾對象
有時我們需要定時或者每隔一段時間就執行一段代碼,在原生js中我們可以通過setTimeout和setInterval來實現。Phaser給我們提供了功能更強大的Timer對象來實現這些功能。Timer對象主要有以下幾個方法:
loop(delay, callback, callbackContext, arguments); //以指定的時間間隔無限重復執行某一個函數,直到調用了Timer對象的stop()方法才停止 repeat(delay, repeatCount, callback, callbackContext, arguments); //讓某個函數重復執行,可以指定重復的次數
當前的Timer對象我們可以通過 game.time.events 來得到,在調用了Timer對象的loop或repeat方法后,還必須調用start方法來啟動。但是我使用的Phaser 2.0.4 版本,好像不調用start方法,loop方法就自動起作用了,不知道這是不是一個bug。如上面代碼中我們用到的:
game.time.events.loop(900, this.generatePipes, this); //利用時鍾對象來重復產生管道 game.time.events.stop(false); //先讓他停止,因為即使沒調用start方法,它也會自動啟動,這應該是一個bug
當點擊屏幕后,就可以正式開始游戲了,我們來看看點擊屏幕事件綁定的 this.startGame 函數做了什么。
this.statrGame = function(){ this.gameSpeed = 200; //游戲速度 this.gameIsOver = false; //游戲是否已結束的標志 this.hasHitGround = false; //是否已碰撞到地面的標志 this.hasStarted = true; //游戲是否已經開始的標志 this.score = 0; //初始得分 this.bg.autoScroll(-(this.gameSpeed/10),0); //讓背景開始移動 this.ground.autoScroll(-this.gameSpeed,0); //讓地面開始移動 this.bird.body.gravity.y = 1150; //給鳥設一個重力 this.readyText.destroy(); //去除 'get ready' 圖片 this.playTip.destroy(); //去除 '玩法提示 圖片 game.input.onDown.add(this.fly, this); //給鼠標按下事件綁定鳥的飛翔動作 game.time.events.start(); //啟動時鍾事件,開始制造管道 }
我們再來看下鳥的飛翔動作,它由 this.fly 函數來實現
this.fly = function(){ this.bird.body.velocity.y = -350; //飛翔,實質上就是給鳥設一個向上的速度 game.add.tween(this.bird).to({angle:-30}, 100, null, true, 0, 0, false); //上升時頭朝上的動畫 this.soundFly.play(); //播放飛翔的音效 }
重力和速度
Phaser.Physics.Arcade.Body 對象,也就是當你是用arcade物理引擎時 sprite.body 所指向的對象,擁有很多跟物理相關的屬性和方法。其中的 gravity 對象代表重力,它有x和y兩個屬性,分別代表水平方向和垂直方向的重力。我們可以使用它的 setTo(x,y)方法來同事設置兩個方向的重力。設置了重力的物體,它的運動會受到重力的影響,與真實生活中的物理現象是一致的。然后這個body它還有一個 velocity 對象,表示物體的速度,跟重力一樣,都分水平和垂直兩個方向,也可以用setTo(x,y)方法來設置。一旦給物體設置了合適的速度,它便能動了。
管道的生成
下面再來看一看管道生成函數 this.generatePipes
this.generatePipes = function(gap){ //制造一組上下的管道 gap = gap || 100; //上下管道之間的間隙寬度 var position = (505 - 320 - gap) + Math.floor((505 - 112 - 30 - gap - 505 + 320 + gap) * Math.random());//計算出一個上下管道之間的間隙的隨機位置 var topPipeY = position-360; //上方管道的位置 var bottomPipeY = position+gap; //下方管道的位置 if(this.resetPipe(topPipeY,bottomPipeY)) return; //如果有出了邊界的管道,則重置他們,不再制造新的管道了,達到循環利用的目的 var topPipe = game.add.sprite(game.width, topPipeY, 'pipe', 0, this.pipeGroup); //上方的管道 var bottomPipe = game.add.sprite(game.width, bottomPipeY, 'pipe', 1, this.pipeGroup); //下方的管道 this.pipeGroup.setAll('checkWorldBounds',true); //邊界檢測 this.pipeGroup.setAll('outOfBoundsKill',true); //出邊界后自動kill this.pipeGroup.setAll('body.velocity.x', -this.gameSpeed); //設置管道運動的速度 }
管道生成的思路:利用隨機數計算出上下管道的位置,然后檢查當前是否有管道已經出了邊界,如果有,則重置出了邊界的那組管道的位置,如果沒有,則生成一組新的管道,這樣就能避免內存浪費了。所有管道我們都把它放在一個組中,便於集中管理。這里需要掌握的是sprite對象的reset方法:
reset(x, y, health)
這個方法能重置sprite對象的位置,更重要的是,如果在一個已經被殺死了(kill)的sprite對象上執行該方法,那么該sprite的 alive, exists, visible and renderable 等屬性都會變回為true。在需要重復利用已經存在的sprite對象時,經常要使用該方法。看下我們這個游戲中是怎么使用這個方法的:
this.resetPipe = function(topPipeY,bottomPipeY){//重置出了邊界的管道,做到回收利用 var i = 0; this.pipeGroup.forEachDead(function(pipe){ //對組調用forEachDead方法來獲取那些已經出了邊界,也就是“死亡”了的對象 if(pipe.y<=0){ //是上方的管道 pipe.reset(game.width, topPipeY); //重置到初始位置 pipe.hasScored = false; //重置為未得分 }else{//是下方的管道 pipe.reset(game.width, bottomPipeY); //重置到初始位置 } pipe.body.velocity.x = -this.gameSpeed; //設置管道速度 i++; }, this); return i == 2; //如果 i==2 代表有一組管道已經出了邊界,可以回收這組管道了 }
碰撞檢測
好了,管道和鳥都已經有了,而且它們都能動了,接下來就是,實現鳥撞到管道或地面后游戲結束的功能了。
在Arcade物理引擎中,碰撞檢測主要用到兩個函數,一個是collide,還有一個是overlap。
collide方法與overlap的區別在於collide會影響兩個要檢測的對象之間的物理狀態,比如使用collide函數去檢測兩個物體,如果物體碰撞了,那么這兩個物體之間就會有力的相互作用,可能其中一個會被另一個彈開,或者兩個之間相互彈開。但如果使用overlap方法的話,則只會檢測兩個物體是否已經碰撞了,或者說已經重疊了,並不會產生物理作用,顯然,如果只需要知道兩個物體是否已經重疊了的話,overlap性能會更好。
碰撞檢測可以單個對象與單個對象進行檢測、單個對象與組進行檢測、組與組進行檢測。collide方法必須在每一幀中都進行調用,才能產生碰撞后的物理作用。
this.update = function(){ //每一幀中都要執行的代碼可以寫在update方法中 if(!this.hasStarted) return; //游戲未開始,先不執行任何東西 game.physics.arcade.collide(this.bird,this.ground, this.hitGround, null, this); //檢測與地面的碰撞 game.physics.arcade.overlap(this.bird, this.pipeGroup, this.hitPipe, null, this); //檢測與管道的碰撞 if(this.bird.angle < 90) this.bird.angle += 2.5; //下降時鳥的頭朝下的動畫 this.pipeGroup.forEachExists(this.checkScore,this); //分數檢測和更新 }
分數管理
當鳥飛過一組管道后,就得1分。飛過一組管道,是指這組管道已經在鳥的左邊的,所以可以通過管道的x坐標來判斷是否已經得分。
this.checkScore = function(pipe){//負責分數的檢測和更新,pipe表示待檢測的管道 //pipe.hasScored 屬性用來標識該管道是否已經得過分 //pipe.y<0是指一組管道中的上面那個管道,一組管道中我們只需要檢測一個就行了 //當管道的x坐標 加上管道的寬度小於鳥的x坐標的時候,就表示已經飛過了管道,可以得分了 if(!pipe.hasScored && pipe.y<=0 && pipe.x<=this.bird.x-17-54){ pipe.hasScored = true; //標識為已經得過分 this.scoreText.text = ++this.score; //更新分數的顯示 this.soundScore.play(); //得分的音效 return true; } return false; }
只需要在每一幀中對每一個管道都調用一次該函數,就可以了。
聲音的播放
在Phaser中播放一段聲音很簡單,只需要事先加載好聲音資源。然后調用play方法播放就行了。
首先使用 game.load.audio() 來加載聲音資源。我們以本游戲中得分時播放的聲音為例,在state的preload方法中預先加載聲音資源
game.load.audio('score_sound', 'assets/score.wav');//得分的音效
然后通過 game.add.sound() 來得到一個sound對象
this.soundScore = game.add.sound('score_sound');
sound對象有許多方法用來控制聲音的播放暫停等,要播放聲音,只需要調用它的play方法即可。
this.soundScore.play(); //播放聲音
好了,把這些組合起來,就能做出我們的flappy bird游戲了。我所說的這些都只是些皮毛,是想讓大家對用Pharse來做游戲有個最初步的印象,也許你還有許多不明白的地方,pharse是個功能很強大的html5游戲框架,想要掌握它,還是必須多看文檔,多看官方給出的例子,然后自己動手去實踐,一步一步一點一滴的去學習,去積累。蘋果新發布的ios8中對webgl的支持已經大大加強了,無論是在safari瀏覽器還是webView運行html5游戲,性能都相當好,這也是移動設備發展的一個趨勢,所以掌握一個html5游戲框架,無論是自娛自樂,或是對自己能力的提升,甚至是找工作,都是有一定的益處的。
第一部分教程: