canvas 製做flappy bird(像素小鳥)全流程

flappy bird製做全流程:

1、前言

像素小鳥這個簡單的遊戲於2014年在網絡上爆紅,遊戲上線一段時間內appleStore上的下載量一度達到5000萬次,風靡一時,web

近年來移動web的普及爲這樣沒有複雜邏輯和精緻動畫效果,可是趣味十足的小遊戲提供了良好的環境,canvas

同時藉助各大社交軟件平臺的傳播效應,創意不斷的小遊戲有着良好的營銷效果,獲得了不少的關注。後端

此前在網上查詢了不少關於這個小遊戲的資料,可是大多雜亂無章,本身的結合相關教程將這個遊戲的主要框架整理出來,供你們一塊兒學習。瀏覽器

2、技術要點

 基本JavaScript基礎 ,canvas 基礎, 面向對象的思想;網絡

3、思路整理

整個遊戲的邏輯比較簡單:

首先遊戲規則:鳥撞到管道上,地上要死亡,飛到屏幕外要死亡。app

其次:鳥在飛翔的過程當中,會掉落,相似落體運動,須要玩家不斷點擊屏幕讓鳥向上飛。框架

再次就是:鳥和背景元素的相對移動的過程,鳥不動,背景左移。dom

將整個遊戲細化:

咱們採用面向對象的思路來製做,具體的事物用構造函數來建立,方法放到構造函數的原形對象中。ide

遊戲細化這個過程不是一蹴而就的,若是在沒有相關指導的狀況下,本身要不斷的結合本身的想法去試錯。函數

本人使用的方式是使用Xmind將流程以腦圖的形式繪製下來,分塊去作,不斷細化記錄本身的思路,最終呈現的效果以下:

(順序按照圖片中的序號去看  腦圖、素材、及完整源碼下載地址:http://pan.baidu.com/s/1c130V7M 想練習的同窗能夠點這裏)

腦圖分爲三大塊:一、準備階段 二、主函數 三、遊戲優化。

 

 

 4、遊戲實現:

如今結合腦圖來逐步實現咱們的遊戲。

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.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 判斷遊戲是否犯規

  1. 接觸到地面和天空頂部,結束遊戲
              
//咱們改造一下主循環,設置一個gameover爲false來控制函數的執行
//任何違規都會觸發gameover=true;
               var gameover = false;

                if(bird.y < 0 || bird.y > 488 -45/2 ){ //碰到天和地
                    gameover = true ;
                }
                if(!gameover){    //若是沒有結束遊戲則繼續遊戲
                    requestAnimationFrame(run);
                }
簡單判讀gameover

  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();
}
加入小鳥旋轉效果

固然最後不要忘記對管道碰撞的判斷,在這裏再修正一遍。

事實上若是打算加入旋轉效果,上一次的修正不須要,你會發現不少重複工。

最後作出的效果以下:

 主體效果和邏輯已經所有實現。更多的效果能夠自行添加。

 若是想本身練習一下,請點擊遊戲細化部分的連接下載相關素材和所有源碼。

相關文章
相關標籤/搜索