flappy bird制作全流程:

一、前言
像素小鳥這個簡單的游戲於2014年在網絡上爆紅,游戲上線一段時間內appleStore上的下載量一度達到5000萬次,風靡一時,
近年來移動web的普及為這樣沒有復雜邏輯和精致動畫效果,但是趣味十足的小游戲提供了良好的環境,
同時借助各大社交軟件平台的傳播效應,創意不斷的小游戲有着良好的營銷效果,得到了很多的關注。
此前在網上查詢了很多關於這個小游戲的資料,但是大多雜亂無章,自己的結合相關教程將這個游戲的主要框架整理出來,供大家一起學習。
二、技術要點
基本JavaScript基礎 ,canvas 基礎, 面向對象的思想;
三、思路整理
整個游戲的邏輯比較簡單:
首先游戲規則:鳥撞到管道上,地上要死亡,飛到屏幕外要死亡。
其次:鳥在飛翔的過程中,會掉落,類似落體運動,需要玩家不斷點擊屏幕讓鳥向上飛。
再次就是:鳥和背景元素的相對移動的過程,鳥不動,背景左移。
將整個游戲細化:
我們采用面向對象的思路來制作,具體的事物用構造函數來創建,方法放到構造函數的原形對象中。
游戲細化這個過程不是一蹴而就的,如果在沒有相關指導的情況下,自己要不斷的結合自己的想法去試錯。
本人使用的方式是使用Xmind將流程以腦圖的形式繪制下來,分塊去做,不斷細化記錄自己的思路,最終呈現的效果如下:
(順序按照圖片中的序號去看 腦圖、素材、及完整源碼下載地址:http://pan.baidu.com/s/1c130V7M 想練習的同學可以點這里)
腦圖分為三大塊:1、准備階段 2、主函數 3、游戲優化。


四、游戲實現:
現在結合腦圖來逐步實現我們的游戲。
1.設置canvas畫布,准備圖片數據,當圖片加載完成后執行回調函數;
<canvas id="cvs" width="800" height="600"></canvas>
<script>
var imglist = [
{ "name":"birds","src":"res/birds.png"},
{ "name":"land","src":"res/land.png"},
{ "name":"pipe1","src":"res/pipe1.png"},
{ "name":"pipe2","src":"res/pipe2.png"},
{ "name":"sky","src":"res/sky.png"}
];
var cvs = document.getElementById("cvs");
var ctx = cvs.getContext("2d");
</script>
這里這個入口函數的設置要注意,必須保證圖片資源加載完成后再執行其他操作,每加載一張圖片我們讓imgCount--,減到0的時候再執行主函數;
function load (source, callback ){ var imgEls={}; var imgCount=source.length; for (var i = 0; i < imgCount; i++) { var name = source[i].name; var newImg = new Image (); newImg.src = source[i].src; imgEls[name] = newImg; imgEls[name].addEventListener("load",function(){ imgCount--; if(imgCount==0){ callback(imgEls); }; }) }; };
主循環的設置:這里我們不使用setInterval來控制循環次數,我們使用一個叫requestAnimationFrame()的定時器
因為setInterval會產生時間誤差,setInterval只能根據時間來移動固定距離。
這對於輪播圖一類幾千毫秒切換一次的動作來說並沒有什么關系,但是對於我們16-18毫秒繪制一次的動畫是非常不准確的;
requestAnimationFrame()這個定時器的好處是根據瀏覽器的性能來執行一個函數,我們用來獲取兩次繪制的間隔時間;
移動距離的計算改變成速度×間隔時間的方式,來解決繪圖不准確的問題。
var preTime= Date.now(); //獲取當前時間 function run(){ var now = Date.now(); //獲取最新時間 dt = now - preTime; //獲取時間間隔 preTime = now; //更新當前時間 ctx.clearRect(0,0,800,600); //清空畫布 //--------------------------------------------- 繪制代碼執行區域 //----------------------------------------------- requestAnimationFrame(run); //再次執行run函數 } requestAnimationFrame(run); //首次執行run函數;
2、主函數分為兩部分功能 ,簡單說就是把圖畫上去,然后處理動態效果,再判斷一下是否犯規。
2.1 小鳥的繪制:
小鳥本身有一個翅膀扇動的效果,和一個下落的過程。
翅膀扇動的過程是一張精靈圖三幅畫面的的切換(設置一個index屬性,控制精靈圖的位置),下落過程是其y坐標在畫布上的移動();
所以小鳥的構造函數中應該包括(圖源,x坐標,y坐標,速度,下落加速度,ctx(context畫布))等參數。
這里需要注意幾點:
- 小鳥的繪制采用canvas drawImage的九參數模式(分別是圖片,原圖的裁切起點,原圖的寬高,貼到畫布上的位置,貼到畫布上的寬高);
- 小鳥的翅膀扇動不能太快,所以我們設置一個閥門函數,當累計計時超過100ms的時候切換一下圖片,然后在讓累計計時減去100ms;
- 小鳥的下落需要用到一定物理知識,但是都很簡單啦。 我們都是通過速度×時間來實現;
var Bird = function (img,x,y,speed,a,ctx){ this.img = img; this.x = x; this.y = y; this.speed = speed; this.a =a ; this.ctx = ctx; this.index = 0; //用於制作小鳥扇翅膀的動作 } Bird.prototype.draw = function (){ this.ctx.drawImage( this.img,52*this.index,0,52,45, this.x,this.y,52,45 ) } var durgather=0; Bird.prototype.update = function(dur){ //小鳥翅膀扇動每100ms切換一張圖片 durgather+=dur; if(durgather>100){ this.index++; if(this.index===2){ this.index=0; } durgather -= 100; } //小鳥下落動作 this.speed = this.speed + this.a *dur; this.y = this.y + this.speed * dur; }
構造一個小鳥,並且將其動作刷新函數和繪制函數放置在我們上面提到的繪制區域,此后構造出的類似對象都是這樣的操作步驟:
這里需要注意的一點是,如何讓小鳥順暢的向上飛翔,其實還是物理知識,由於加速度的作用,我們給小鳥一個向上的順時速度就可以了。
load(imglist ,function(imgEls){ //創建對象 //在主函數中創建一個小鳥 var bird = new Bird(imgEls["birds"],150,100,0.0003,0.0006,ctx); //主循環 var preTime= Date.now(); function run(){ var now = Date.now(); dt = now - preTime; preTime = now; ctx.clearRect(0,0,800,600); //--------圖片繪制區域------- bird.update(dt) bird.draw(); //------------------------- requestAnimationFrame(run); } requestAnimationFrame(run); //設置點擊事件。給小鳥一個瞬時的向上速度 cvs.addEventListener("click",function(){ bird.speed = -0.3; } ) })
效果如下:

2.2天空的繪制:
天空的繪制比較簡單了,只要使用canvas drawImage的三參數模式就可以(圖源,畫布上的坐標)。
這里唯一注意的一點是,無縫滾動的實現,對於800*600分辨率這種情況我們創建兩個天空對象就可以了,但是為了適配更多的情況,我們將這個功能寫活
在天空的構造函數上加一個count屬性設置幾個天空圖片,count屬性讓實例通過原形中的方法訪問。后面涉及到重復出現的地面和管道,都給它們添加這種考慮。
var Sky = function(img,x,speed,ctx) { this.img = img ; this.ctx = ctx; this.x = x; this.speed = speed; } Sky.prototype.draw = function(){ this.ctx.drawImage( this.img ,this.x,0 ) } Sky.prototype.setCount = function(count){ Sky.count = count; } Sky.prototype.update = function(dur){ this.x = this.x+ this.speed * dur; if(this.x<-800){ //天空圖片的寬度是800 this.x = Sky.count * 800 + this.x; //當向左移動了一整張圖片后立刻切回第一張圖片 } }
同理在主函數中創建2個天空對象,並將更新函數和繪制函數放置在主循環的繪制區域;
setcount是用來設置無縫滾動的
注意一點:繪制上的圖片是有一個層級關系的,不能把鳥畫到天空的下面,那當然最后畫鳥了,下面涉及到的覆蓋問題不再專門提到。
這里僅插入部分相關代碼
var bird = new Bird(imgEls["birds"],150,100,0.0003,0.0006,ctx); var sky1 = new Sky(imgEls["sky"],0,-0.3,ctx); var sky2 = new Sky(imgEls["sky"],800,-0.3,ctx); //主循環 var preTime= Date.now(); function run(){ var now = Date.now(); dt = now - preTime; preTime = now; ctx.clearRect(0,0,800,600); //--------圖片繪制區域------- sky1.update(dt); sky1.draw() sky2.update(dt); sky2.draw() sky1.setCount(2); bird.update(dt) bird.draw(); //-------------------------
2.3 地面的繪制
和天空的繪制完全一樣,由於地面圖片尺寸較小,所以我們要多畫幾個
var Land = function(img,x,speed,ctx){ this.img = img ; this.x = x; this.speed = speed; this.ctx = ctx ; } Land.prototype.draw = function(){ this.ctx.drawImage ( this.img , this.x ,488 ) } Land.prototype.setCount= function(count){ Land.count = count; } Land.prototype.update = function(dur){ this.x = this.x + this.speed * dur; if (this.x <- 336){ this.x = this.x + Land.count * 336; //無縫滾動的實現 } }
//創建----放置在創建區域 var land1 = new Land(imgEls["land"],0,-0.3,ctx); var land2 = new Land(imgEls["land"],336*1,-0.3,ctx); var land3 = new Land(imgEls["land"],336*2,-0.3,ctx); var land4 = new Land(imgEls["land"],336*3,-0.3,ctx); //繪制 ----放置在繪制區域 land1.update(dt); land1.draw(); land2.update(dt); land2.draw(); land3.update(dt); land3.draw(); land4.update(dt); land4.draw(); land1.setCount(4); //設置無縫滾動
2.4繪制管道
管道的繪制有一個難點是管道高度的確定
要點:
- 為了保障游戲可玩性,管道必須有一個固定高度+一個隨機高度,且上下管道之間的留白是固定的寬度。
- 管道不是連續的,兩個相鄰的管道之間有間隔
- 注意管道在無縫播放,抽回后必須付給一個新的隨機高度,給用戶一種錯覺,以為又一個管道飄了過來。
var Pipe = function(upImg,downImg,x,speed,ctx){ this.x = x; this.upImg = upImg ; this.downImg = downImg; this.speed = speed; this.ctx = ctx; this.r = Math.random() *200 + 100; //隨機高度+固定高度 } Pipe.prototype.draw = function(){ this.ctx.drawImage( this.upImg, this.x , this.r - 420 //管道圖片的長度是420 ) this.ctx.drawImage( this.downImg, this.x , this.r +150 //管道中建的留白是150px ) } Pipe.prototype.setCount = function( count,gap ){ Pipe.count = count; Pipe.gap = gap; //這里是這次繪制的特別之處,加入了間隔 } Pipe.prototype.update =function( dur ){ this.x = this.x + this.speed*dur; if(this.x <- 52){ //管道寬度52px this.x = this.x + Pipe.count * Pipe.gap; //無縫滾動 this.r = Math.random() *200 + 150; //切換后的管道必須重新設置一個高度,給用戶一個新管道的錯覺 } }
//創建區域 var pipe1 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],400, -0.1,ctx); var pipe2 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],600, -0.1,ctx); var pipe3 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],800, -0.1,ctx); var pipe4 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],1000,-0.1,ctx); var pipe5 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],1200,-0.1,ctx); //繪制區域 pipe1.update(dt); pipe1.draw(); pipe2.update(dt); pipe2.draw(); pipe3.update(dt); pipe3.draw(); pipe4.update(dt); pipe4.draw(); pipe5.update(dt); pipe5.draw(); pipe1.setCount(5,200); //設置管道數量和間隔
到這一步我們的主要畫面就制作出來了,是不是很簡單呢O(∩_∩)O~
2.5 判斷游戲是否犯規
- 接觸到地面和天空頂部,結束游戲
//我們改造一下主循環,設置一個gameover為false來控制函數的執行 //任何違規都會觸發gameover=true; var gameover = false; if(bird.y < 0 || bird.y > 488 -45/2 ){ //碰到天和地 gameover = true ; } if(!gameover){ //如果沒有結束游戲則繼續游戲 requestAnimationFrame(run); }
2. 碰到管道結束游戲
//x和y到時候我們傳入小鳥的運動軌跡,每次重繪管道都有判斷 Pipe.prototype.hitTest = function(x,y){ return (x > this.x && x < this.x + 52) //在管子橫向中間 &&(! (y >this.r && y < this.r +150)); //在管子豎向中間 }
var gameover = false; gameover = gameover || pipe1.hitTest(bird.x ,bird.y); gameover = gameover || pipe2.hitTest(bird.x ,bird.y); gameover = gameover || pipe3.hitTest(bird.x ,bird.y); gameover = gameover || pipe4.hitTest(bird.x ,bird.y); gameover = gameover || pipe5.hitTest(bird.x ,bird.y); //邏輯終端 if(bird.y < 0 || bird.y > 488 -45/2 ){ gameover = true ; } if(!gameover){ requestAnimationFrame(run); }

到這一步我們的游戲完成的差不多了,剩下的就是部分數據的修正
主要需要修正的一個點是碰撞的計算,因為我們所有的碰撞都是按照小鳥圖片的左上角計算的,這樣就會有不准確的問題,通過測試很容易將這個距離加減修正了
3.游戲的優化
小鳥游戲的鳥兒在上下的過程中會隨着點擊,抬頭飛翔,或低頭沖刺,如何做到這個效果呢?
答案就是移動canvas 坐標系和選擇坐標系的角度 ctx.translate()和ctx.rotate();
為了防止整個坐標系的整體旋轉移動
需要在小鳥繪制函數Bird.prototype.draw里面前后端加入ctx.save() 和ctx.restore()來單獨控制小鳥畫布
Bird.prototype.draw = function (){ this.ctx.save(); this.ctx.translate(this.x ,this.y); //坐標移動到小鳥的中心點上 this.ctx.rotate((Math.PI /6) * this.speed / 0.3 ); //小鳥最大旋轉30度,並隨着速度實時改變角度 this.ctx.drawImage( this.img,52*this.index,0,52,45, -52/2,-45/2,52,45 //這里很重要的一點是,整個小鳥坐標系開始移動 ) this.ctx.restore(); }
當然最后不要忘記對管道碰撞的判斷,在這里再修正一遍。
事實上如果打算加入旋轉效果,上一次的修正不需要,你會發現很多重復工。
最后做出的效果如下:

主體效果和邏輯已經全部實現。更多的效果可以自行添加。
如果想自己練習一下,請點擊游戲細化部分的鏈接下載相關素材和全部源碼。
