canvas小遊戲——flappy bird

前言

若是說學編程就是學邏輯的話,那鍛鍊邏輯能力的最好方法就莫過於寫遊戲了。最近看了一位大神的fly bird小遊戲,感受頗有幫助。因而爲了尋求進一步的提升,我花了兩天時間本身寫了一個canvas版本的。雖然看起來原理都差很少,可是實現方法截然不同,若是有興趣的話能夠你們本身下載下來玩一玩,大概效果就像下面這樣:
遊戲效果

怎麼樣?是否是感受難度巨大?...多是由於我比較菜吧。相信高手仍是大有人在的,隨便過個幾十關也是不在話下。可是若是有和我同樣10關都過不了小菜雞的話,根本不用喪氣對吧?咱是程序員是否是?遊戲不會玩,做弊還不會嗎?咳咳,下面就是做弊的方法:javascript

首先搞清楚結構

<style>
  *{
    margin: 0;
    padding: 0;
  }
  html,body {
    height: 100%;
    width: 100%;
    overflow: hidden;
  }
  #canvas{
    display: block;
    margin: 50px auto;
  }
</style>
<canvas id="canvas" width="343" height="480"></canvas>

很簡單,就是這樣。css

注意!我要開始說了

首先咱先加載一下全部的圖片

// 圖片集合
var imgs = {
  //建立圖片
  bg: new Image(),
  grass: new Image(),
  title: new Image(),
  bird0: new Image(),
  bird1: new Image(),
  up_bird0: new Image(),
  up_bird1: new Image(),
  down_bird0: new Image(),
  down_bird1: new Image(),
  startBtn: new Image(),
  up_pipe: new Image(),
  up_mod: new Image(),
  down_pipe: new Image(),
  down_mod: new Image(),
  scroe0:new Image(),
  scroe1:new Image(),
  scroe2:new Image(),
  scroe3:new Image(),
  scroe4:new Image(),
  scroe5:new Image(),
  scroe6:new Image(),
  scroe7:new Image(),
  scroe8:new Image(),
  scroe9:new Image(),
  //加載圖片
  loadImg: function (fn) {
    this.bg.src = './img/bg.jpg';
    this.grass.src = './img/banner.jpg';
    this.title.src = './img/head.jpg';
    this.bird0.src = './img/bird0.png';
    this.bird1.src = './img/bird1.png';
    this.up_bird0.src = './img/up_bird0.png';
    this.up_bird1.src = './img/up_bird1.png';
    this.down_bird0.src = './img/down_bird0.png';
    this.down_bird1.src = './img/down_bird1.png';
    this.startBtn.src = './img/start.jpg';
    this.up_pipe.src = './img/up_pipe.png';
    this.up_mod.src = './img/up_mod.png';
    this.down_pipe.src = './img/down_pipe.png';
    this.down_mod.src = './img/down_mod.png';
    this.scroe0.src = './img/0.jpg';
    this.scroe1.src = './img/1.jpg';
    this.scroe2.src = './img/2.jpg';
    this.scroe3.src = './img/3.jpg';
    this.scroe4.src = './img/4.jpg';
    this.scroe5.src = './img/5.jpg';
    this.scroe6.src = './img/6.jpg';
    this.scroe7.src = './img/7.jpg';
    this.scroe8.src = './img/8.jpg';
    this.scroe9.src = './img/9.jpg';
    var that = this;
    //添加定時器,判斷圖片是否加載完成
    var timer = setInterval(function() {
      if (that.bg.complete&&that.grass.complete
        &&that.title.complete&&that.startBtn.complete
        &&that.bird0.complete&&that.bird1.complete
        &&that.up_bird0.complete&&that.up_bird1.complete
        &&that.down_bird0.complete&&that.down_bird1.complete
        &&that.up_pipe.complete&&that.up_mod.complete
        &&that.down_mod.complete&&that.down_pipe.complete
        &&that.scroe0.complete&&that.scroe1.complete
        &&that.scroe2.complete&&that.scroe3.complete
        &&that.scroe4.complete&&that.scroe5.complete
        &&that.scroe6.complete&&that.scroe7.complete
        &&that.scroe8.complete&&that.scroe9.complete) {
        //刪除定時器
        clearInterval(timer);
        //圖片所有加載完成後,運行此函數
        fn();
      }
    }, 50)
  }
}

...抱歉有點長,可是怕破壞代碼的結構,就所有拷下來了,上面的朋友快點下來吧,都是重複的沒啥好看的。我來給你們解釋一下,首先這是一個對象字面量,建立的時候新建了若干個圖片對象,而後它有一個函數loadImg,只要一執行,就會給全部的圖片添加路徑,而後添加一個定時器每一段時間經過查詢全部圖片的complete屬性判斷圖片是否所有加載完成。若是是,就刪除這個定時器,並執行一段回調函數,仍是很好理解的吧:),不過我感受這種方法可能有點蠢,不知道各位高人有沒有更好的方法?html

接下來,就要開始畫了

你們都知道,其實canvas就是畫圖,若是要用canvas實現動畫效果的話,就只能一遍一遍的擦了畫、畫了擦了。java

首先

先把幾個固定不動的部分的繪製方法和清空畫布的方法寫在函數裏git

//繪製背景
  function drawBg() {
    ctx.drawImage(imgs.bg,0,0);
  }
  //繪製開始按鈕
  function drawStartBtn() {
    ctx.drawImage(imgs.startBtn,130,300);
  }
    //清空畫布
  function clean() {
    ctx.clearRect(0,0,canvas.width,canvas.height);
  }

而後

把會動的部分也加上程序員

var v = 0;//草坪滾動的增量
  //繪製草坪
  function drawGrass() {
    //每次運行橫座標向左移
    ctx.drawImage(imgs.grass,3*v--,423);
    ctx.drawImage(imgs.grass,337+3*v--,423);
    if(3*v < -343){
      v=0;
    }
  }

這樣每次運行一次,草坪就會向左移一點了github

var shake = true;//標題的抖動狀態
  //標題的抖動效果
  function titleShake() {
    if (shake) {
      ctx.drawImage(imgs.title,53,97);
      ctx.drawImage(imgs.bird1,250,137);
    }else{
      ctx.drawImage(imgs.title,53,103);
      ctx.drawImage(imgs.bird0,250,143);
    }
  }

這樣經過改變shake的值,就可使標題的抖動了。
機智的各位應該已經發現了,上面兩個函數須要重複調用,才能產生動畫的效果,因此這就是我接下來要講的。編程

開始界面的定時器

開始界面

var startTimer;//開始界面定時器
var startTime = 0;//定時器運行的次數
function startLayer() {
    startTimer = setInterval(function () {
      clean();
      drawBg();
      drawStartBtn();
      drawGrass();
      titleShake();
      //定時器每運行7次改變標題位置
      if(startTime == 7){
        shake = !shake;
        startTime = 0;
      }
      //運行次數+1
      startTime++;
      //window.requestAnimationFrame(startLayer)
    }, 24);
  }

你們也能夠理解爲這就是開始界面,由於開始界面就是經過定時器一次次運行上面的函數所實現的。然而上面定義的startTimer和startTime又有什麼用呢,固然不是畫蛇添足,首先,把這個定時器賦給一個變量,是爲了在開始遊戲的時候把這個界面關掉,也就是把這個定時器取消,日後看你們就明白了:)其次,startTime是爲了記錄定時器運行的次數,由於這個定時器刷新的實現極快,只有短短的24毫秒,若是標題以這個速度抖動的話,你們的眼睛必定受不了了吧,因此我設法讓他慢下來,每運行7次抖動一次,固然你們能夠設置九、十、11使它的頻率更加緩慢(你們還能夠嘗試使用requestAnimation-
-Frame,那樣性能更佳,可是控制頻率略顯麻煩。這裏使用setInterval更容易理解)固然這個做弊沒有半毛錢關係,不過下面就是重頭戲了。canvas

主角登場!!!

var bird = {
  bird: [imgs.bird0,imgs.bird1],//正常狀態,圖片
  up_bird: [imgs.up_bird0,imgs.up_bird1],//向上飛狀態
  down_bird: [imgs.down_bird0,imgs.down_bird1],//向下掉狀態
  posX: 100,//橫座標
  posY: 200,//縱座標Y
  speed: 0,//速度
  index: 0,//翅膀揮動,切換圖片的標
  alive: true,//存活狀態
  //繪製小鳥
  draw: function (bird) {
    ctx.drawImage(bird,this.posX,this.posY);
  },
  //飛行中
  fly: function () {
    //縱座標隨速度改變
    this.posY+=this.speed;
    //加速度爲1
    this.speed++;
    //若是墜地,死亡
    if(this.posY >= 395){
      this.speed = 0;
      this.draw(this.bird[this.index]);
      this.dead();
    }
    //若是撞頂,彈回來
    if(this.posY <= 0){
      this.speed = 6;
    }
    //若是速度爲正,則向下,反之,則向上,不然水平
    if(this.speed>0){
      this.draw(this.down_bird[this.index]);
    }else if(this.speed<0){
      this.draw(this.up_bird[this.index]);
    }else{
      this.draw(this.bird[this.index]);
    }
    //確保墜落速度不會太快
    if(bird.speed > 6){
      bird.speed = 6;
    }
  },
  //煽動翅膀,切換圖片
  wingWave: function () {
    this.index++;
    if(this.index > 1){
      this.index = 0;
    }
  },
  //死亡
  dead: function() {
    this.alive = false;
  }
}

...固然這只是主角的代碼,一個對象字面量。可是它能夠操控主角的全部行爲(雖然也沒有幾個行爲...),首先就是畫出主角draw(),經過傳進不一樣的圖片繪製出主角不一樣狀況下的英姿...而後是wingWave(),經過改變index,切換上面定義的圖片數組中的圖片,也就是揮翅膀。再而後就是飛行fly(),在飛行過程當中主角會碰到各類各樣的事故,像是飛的過高撞到天花板啊,或是飛的過低,摔了個狗啃屎。再幹脆點一頭撞死在了鋼管上,可是這個函數並不在這裏,由於小鳥撞死在鋼管上究竟是小鳥的行爲,仍是鋼管的行爲呢,我還沒想明白,因此乾脆放在了全局中。數組

//判斷是否碰撞
  function isHit(oPipe){
    if(bird.posX+bird.bird[0].width>oPipe.posX&&bird.posX<oPipe.posX+oPipe.down_pipe.width){
      if(bird.posY<oPipe.up_posY||bird.posY+30>oPipe.down_posY){
        bird.dead();
      }
    }
  }

就像這樣,經過判斷小鳥和鋼管的位置判斷小鳥是否是撞在鋼管上了。反正結果仍是撞死bird.dead()。看到這裏相信不用我說,你們也明白了吧,只要將這段代碼註釋掉,咱們的小鳥不就練成的絕世鐵頭功,鋼管都捅穿給你看。或者稍稍增大一點小鳥會被碰撞到的體積,那就是凌波微步、輕功管上飄了呀。說了半天,還沒告訴你們這個水管又是哪裏來的。

鋼管

//水管類
class Pipe {
  constructor(up_pipe,up_mod,down_pipe,down_mod) {
    //構造函數
    this.up_pipe = up_pipe;//上水管頭部
    this.up_mod = up_mod;//上水管中間部分
    this.down_pipe = down_pipe;
    this.down_mod = down_mod;
    this.up_height = Math.floor(Math.random()*60);//隨機生成上管體高度
    this.down_height = (60 - this.up_height)*3;//保證全部上下水管距離相同
    this.posX = 300;//橫座標
    this.up_posY = this.up_height*3+this.up_pipe.height;//上水管縱座標
    this.down_posY = 362-this.down_height;//下水管縱座標
    this.hadSkipped = false;//是否被越過
    this.hadSkippedChange = false;//去重
  }
  //繪製水管
  drawPipe() {
    ctx.drawImage(this.up_pipe,this.posX,this.up_height*3);
    ctx.drawImage(this.down_pipe,this.posX,362-this.down_height);
  }
  //繪製管體
  drawMods() {
    for(var i=0;i<this.up_height;i++){
      ctx.drawImage(this.up_mod,this.posX,i*3)
    }
    for(var j=0;j<this.down_height;j++){
      ctx.drawImage(this.down_mod,this.posX,362-this.down_height+this.down_pipe.height+j);
    }
  }
  //水管移動
  move() {
    this.posX -= 6;
    this.drawMods();
    this.drawPipe();
  }
}

管口管體

又是一段冗長的代碼,你們不要急躁,我來給你們詳細解釋,水管分爲兩部分,一部分是固定的管口,還有一部分是爲了控制鋼管長度的管體,在上面的圖片也能夠看到,每一關的管道是分爲上下兩個的——up_pipe和down_pipe,也就是說咱們看到的鋼管是由數個相同的管體加管口構成的,這裏管體的數量是隨機的,這樣就可使管道擁有隨機的長度了。而後爲了保證上下兩個鋼管的中間距離固定,下管道的高度就是總高度減去上管道的高度,嗯,這裏須要理一理,你們也能夠直接去看個人代碼。有了上面的理論,接下來就簡單了,繪製管口drawPipe(),注意給管體預留出位置來,再繪製管體drawMods(),用一個for循環依次繪製出數個管體疊加在一塊兒的樣子。水管移動move(),就是改變水管的橫座標了。這裏能夠經過改變上下水管高度的總值,來增長上下水管之間的距離,是否是遊戲難度一下就降了不少?再有就是判斷水管是否被小鳥跨越的hadskiped屬性,往下看

//判斷是否越過水管
  function isSkipped(oPipe) {
    if(bird.posX>oPipe.posX+oPipe.down_pipe.width){
      //水管已經被越過
      oPipe.hadSkipped = true;
      //確保水管只被越過一次
      if(!oPipe.hadSkippedChange&&oPipe.hadSkipped){
        //分數+1
        scroll++;
        oPipe.hadSkippedChange = true;
      }
    }
  }

我是經過判斷水管的位置是否已經位於小鳥的後面來判斷,小鳥是否越過了水管的,若是越過了就+1分,至於沒越過就是經過前面講過到的isHit()判斷了,由於不是同一時間段發生的事情因此不能放在一塊兒。

計分表

計分表

var scroll = 0;//當前得分
var scrollImg = [imgs.scroe0,imgs.scroe1,imgs.scroe2,
              imgs.scroe3,imgs.scroe4,imgs.scroe5,
              imgs.scroe6,imgs.scroe7,imgs.scroe8,
              imgs.scroe9];//存儲數字圖片
  //繪製當前得分
  function drawScore() {
    //每繪製一位數,向右移23,繪製下一位數
    for(var i=0;i<scroll.toString().length;i++){
      ctx.drawImage(scrollImg[parseInt(scroll.toString().substr(i,1))],147+i*23,40)
    }
  }

首先,把全部分數有關的圖片放到這裏scrollImg來,方便使用。而後判斷數字的位數,也就是個十百千萬。循環並截取每一個位數,再經過相應的圖片繪製出來,而且每繪製一個位數的圖片位置向右移23,這樣數字就不會疊在一塊兒了。這裏有一種最沒意思的做弊方法,就是手動調整分數,但這只是一個數字,遊戲的樂趣果真仍是在於過程,下面...

遊戲開始!

//遊戲界面
  function gameLayer() {
    gameTimer = setInterval(function () {
      clean();
      drawBg();
      drawGrass();
      if(gameTime%5 == 0){
        if(gameTime == 30){
          createPipes();
          gameTime = 0;
        }
        bird.wingWave();
      }
      gameTime++;
      for(var i = 0;i< pipes.length;i++){
        pipes[i].move();
        isHit(pipes[i]);
        isSkipped(pipes[i]);
      }
      drawScore();
      bird.fly();
      //若是小鳥死了
      if(!bird.alive){
        gameOver();//遊戲結束
        reset();//數據重置
      }
    }, 24);
  }

...看到這裏,估計已經有人在罵我了,講了半天遊戲還沒開始...好吧,大家看,其實遊戲的界面也不過是一個定時器,將前面講到的函數和代碼,無腦的、重複的執行着。而後這裏必定要注意畫圖的順序,否則後畫的部分會把前面覆蓋掉,其次這裏的gameTimer和gameTime也和開始界面中startTimer、startTime起到相似的做用,每過一段較長的時間生成一個水管,也就是經過水管類實例化一個水管對象,具體的方法被我封裝進一個createPipes函數裏了。

var pipes = [];//用於存放水管
function createPipes() {
    var pipe = new Pipe(imgs.up_pipe,imgs.up_mod,imgs.down_pipe,imgs.down_mod);
    //添加進pipes中,若是已經有三個水管,則依次替換
    if(pipes.length<3){
      pipes.push(pipe);
    }else{
      pipes[index] = pipe;
      index++;
      if(index >= 3){
        index = 0;
      }
    }
  }

由於實現的方法沒有想象中那麼簡單,首先咱們要創造一個水管的數組,它的做用就是爲了控制水管的數量,否則咱們的定時器就會一遍一遍的創造出無數的水管,可是前面的水管早就離咱們遠去,因此我就用數組把水管裝起來,控制只有一個屏幕的水管,也就是三個。若是建立了超過三個水管,就會把最前面一個替換掉,由於它已經超出了咱們的視野。

響應事件

光有動畫也不行,只能看不能玩有個皮用啊。因此咱們固然要添加響應事件了。

//鍵盤點擊事件
  function kd(e) {
    if (e.keyCode === 32) {
      bird.speed = -10;
    }
  }
  //觸屏事件
  function ts() {
    bird.speed = -10;
  }
  //start按鈕點擊事件
  function startBtn_click(e) {
    //判斷點擊位置
    if(e.clientX>canvas.offsetLeft+canvas.width/2-imgs.startBtn.width/2
      &&e.clientX<canvas.offsetLeft+canvas.width/2+imgs.startBtn.width/2
      &&e.clientY<canvas.offsetTop+300+imgs.startBtn.height
      &&e.clientY>canvas.offsetTop+300){
      clean();
      //清除開始界面定時器
      clearInterval(startTimer);
      gameLayer();
      //添加響應事件
      window.addEventListener('keydown',kd,false)
      window.addEventListener('touchstart',ts,false)
      //刪除start按鈕響應事件
      canvas.removeEventListener('click',startBtn_click,false);
    }
  }
  canvas.addEventListener('click', startBtn_click , false);

這就是全部的響應事件了,經過按空格鍵和點擊屏幕均可以改變小鳥的速度,只要把這個速度調整到一個比較舒服的程度,遊戲難度就會大大下降。其次,由於canvas是一個總體,因此咱們沒有辦法直接監聽裏面圖片按鈕的響應事件,只能退而求其次,判斷點擊的位置是否在按鈕的位置上了,就上面那段有點長的if判斷語句。

遊戲結束

假如咱們的主角真的一個不當心如咱們所料的撞死在了鋼管上(往上翻,就在遊戲開始那裏),那就表示gameOver();

//遊戲結束
  function gameOver(){
    //清除定時器
    clearInterval(gameTimer);
    //清除窗口響應事件
    window.removeEventListener('keydown',kd,false);
    window.removeEventListener('touchstart',ts,false);
    //繪製GAME OVER
    ctx.font = "50px blod";
    ctx.fontWeight = '1000'
    ctx.fillStyle = "white";
    ctx.fillText("GAME OVER", 20, 200);
    drawStartBtn();
  }

遊戲結束

整個世界都平靜了下來,定時器關掉,響應事件移除掉,而後繪上大大的、慘白的GAME OVER,下面附帶一個遊戲開始時就出現的start按鈕。不是有一句話說的是,結束不過是新的開始嗎,你又能夠再來一局了。......好吧,這個就是我爲了偷懶隨便搞搞的。不過這還沒完,數據還得重置一下,否則怎麼從新開始。

//重置數據
  function reset(){
    bird.posY = 200;
    bird.speed = 0;
    bird.alive = true;
    pipes = [];
    scroll = 0;
    canvas.addEventListener('click', startBtn_click , false);
  }

最後再給這個start按鈕添加上點擊事件,大功告成!這就是我調整難度以後的樣子:
低難度版

嘖嘖嘖,這種閒庭信步的感受......

果真遊戲仍是有點難度纔有意思......

總結

籲...一篇又臭又長、廢話又多的文章終於寫完了,若是你們以爲有幫助,或者對這篇文章有興趣的話,就賞個贊。若是以爲個人程序有問題,或者有別的想說的,均可以在評論裏告訴我,我會看的。

個人項目地址:https://github.com/tzc123/can...

參考項目地址:http://www.jianshu.com/p/45d9...

相關文章
相關標籤/搜索