成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

canvas小游戲——flappy bird

crossea / 1365人閱讀

摘要:開始界面的定時(shí)器開始界面定時(shí)器定時(shí)器運(yùn)行的次數(shù)定時(shí)器每運(yùn)行次改變標(biāo)題位置運(yùn)行次數(shù)大家也可以理解為這就是開始界面,因?yàn)殚_始界面就是通過定時(shí)器一次次運(yùn)行上面的函數(shù)所實(shí)現(xiàn)的。

前言

如果說學(xué)編程就是學(xué)邏輯的話,那鍛煉邏輯能力的最好方法就莫過于寫游戲了。最近看了一位大神的fly bird小游戲,感覺很有幫助。于是為了尋求進(jìn)一步的提高,我花了兩天時(shí)間自己寫了一個(gè)canvas版本的。雖然看起來原理都差不多,但是實(shí)現(xiàn)方法大相徑庭,如果有興趣的話可以大家自己下載下來玩一玩,大概效果就像下面這樣:


怎么樣?是不是感覺難度巨大?...可能是因?yàn)槲冶容^菜吧。相信高手還是大有人在的,隨便過個(gè)幾十關(guān)也是不在話下。但是如果有和我一樣10關(guān)都過不了小菜雞的話,根本不用喪氣對吧?咱是程序員是不是?游戲不會玩,作弊還不會嗎?咳咳,下面就是作弊的方法:

首先搞清楚結(jié)構(gòu)

很簡單,就是這樣。

注意!我要開始說了 首先咱先加載一下所有的圖片
// 圖片集合
var imgs = {
  //創(chuàng)建圖片
  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;
    //添加定時(shí)器,判斷圖片是否加載完成
    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) {
        //刪除定時(shí)器
        clearInterval(timer);
        //圖片全部加載完成后,運(yùn)行此函數(shù)
        fn();
      }
    }, 50)
  }
}

...抱歉有點(diǎn)長,但是怕破壞代碼的結(jié)構(gòu),就全部拷下來了,上面的朋友快點(diǎn)下來吧,都是重復(fù)的沒啥好看的。我來給大家解釋一下,首先這是一個(gè)對象字面量,創(chuàng)建的時(shí)候新建了若干個(gè)圖片對象,然后它有一個(gè)函數(shù)loadImg,只要一執(zhí)行,就會給所有的圖片添加路徑,然后添加一個(gè)定時(shí)器每一段時(shí)間通過查詢所有圖片的complete屬性判斷圖片是否全部加載完成。如果是,就刪除這個(gè)定時(shí)器,并執(zhí)行一段回調(diào)函數(shù),還是很好理解的吧:),不過我感覺這種方法可能有點(diǎn)蠢,不知道各位高人有沒有更好的方法?

接下來,就要開始畫了

大家都知道,其實(shí)canvas就是畫圖,如果要用canvas實(shí)現(xiàn)動畫效果的話,就只能一遍一遍的擦了畫、畫了擦了。

首先

先把幾個(gè)固定不動的部分的繪制方法和清空畫布的方法寫在函數(shù)里

//繪制背景
  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() {
    //每次運(yùn)行橫坐標(biāo)向左移
    ctx.drawImage(imgs.grass,3*v--,423);
    ctx.drawImage(imgs.grass,337+3*v--,423);
    if(3*v < -343){
      v=0;
    }
  }

這樣每次運(yùn)行一次,草坪就會向左移一點(diǎn)了

var shake = true;//標(biāo)題的抖動狀態(tài)
  //標(biāo)題的抖動效果
  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的值,就可以使標(biāo)題的抖動了。
機(jī)智的各位應(yīng)該已經(jīng)發(fā)現(xiàn)了,上面兩個(gè)函數(shù)需要重復(fù)調(diào)用,才能產(chǎn)生動畫的效果,所以這就是我接下來要講的。

開始界面的定時(shí)器

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

大家也可以理解為這就是開始界面,因?yàn)殚_始界面就是通過定時(shí)器一次次運(yùn)行上面的函數(shù)所實(shí)現(xiàn)的。然而上面定義的startTimer和startTime又有什么用呢,當(dāng)然不是多此一舉,首先,把這個(gè)定時(shí)器賦給一個(gè)變量,是為了在開始游戲的時(shí)候把這個(gè)界面關(guān)掉,也就是把這個(gè)定時(shí)器取消,往后看大家就明白了:)其次,startTime是為了記錄定時(shí)器運(yùn)行的次數(shù),因?yàn)檫@個(gè)定時(shí)器刷新的實(shí)現(xiàn)極快,只有短短的24毫秒,如果標(biāo)題以這個(gè)速度抖動的話,大家的眼睛一定受不了了吧,所以我設(shè)法讓他慢下來,每運(yùn)行7次抖動一次,當(dāng)然大家可以設(shè)置9、10、11使它的頻率更加緩慢(大家還可以嘗試使用requestAnimation-
-Frame,那樣性能更佳,但是控制頻率略顯麻煩。這里使用setInterval更容易理解)當(dāng)然這個(gè)作弊沒有半毛錢關(guān)系,不過下面就是重頭戲了。

主角登場?。。?/b>
var bird = {
  bird: [imgs.bird0,imgs.bird1],//正常狀態(tài),圖片
  up_bird: [imgs.up_bird0,imgs.up_bird1],//向上飛狀態(tài)
  down_bird: [imgs.down_bird0,imgs.down_bird1],//向下掉狀態(tài)
  posX: 100,//橫坐標(biāo)
  posY: 200,//縱坐標(biāo)Y
  speed: 0,//速度
  index: 0,//翅膀揮動,切換圖片的標(biāo)
  alive: true,//存活狀態(tài)
  //繪制小鳥
  draw: function (bird) {
    ctx.drawImage(bird,this.posX,this.posY);
  },
  //飛行中
  fly: function () {
    //縱坐標(biāo)隨速度改變
    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;
  }
}

...當(dāng)然這只是主角的代碼,一個(gè)對象字面量。但是它可以操控主角的所有行為(雖然也沒有幾個(gè)行為...),首先就是畫出主角draw(),通過傳進(jìn)不同的圖片繪制出主角不同情況下的英姿...然后是wingWave(),通過改變index,切換上面定義的圖片數(shù)組中的圖片,也就是揮翅膀。再然后就是飛行fly(),在飛行過程中主角會碰到各種各樣的事故,像是飛的太高撞到天花板啊,或是飛的太低,摔了個(gè)狗啃屎。再干脆點(diǎn)一頭撞死在了鋼管上,但是這個(gè)函數(shù)并不在這里,因?yàn)樾▲B撞死在鋼管上到底是小鳥的行為,還是鋼管的行為呢,我還沒想明白,所以干脆放在了全局中。

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

就像這樣,通過判斷小鳥和鋼管的位置判斷小鳥是不是撞在鋼管上了。反正結(jié)果還是撞死bird.dead()??吹竭@里相信不用我說,大家也明白了吧,只要將這段代碼注釋掉,我們的小鳥不就練成的絕世鐵頭功,鋼管都捅穿給你看?;蛘呱陨栽龃笠稽c(diǎn)小鳥會被碰撞到的體積,那就是凌波微步、輕功管上飄了呀。說了半天,還沒告訴大家這個(gè)水管又是哪里來的。

鋼管
//水管類
class Pipe {
  constructor(up_pipe,up_mod,down_pipe,down_mod) {
    //構(gòu)造函數(shù)
    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);//隨機(jī)生成上管體高度
    this.down_height = (60 - this.up_height)*3;//保證所有上下水管距離相同
    this.posX = 300;//橫坐標(biāo)
    this.up_posY = this.up_height*3+this.up_pipe.height;//上水管縱坐標(biāo)
    this.down_posY = 362-this.down_height;//下水管縱坐標(biāo)
    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



又是一段冗長的代碼,大家不要急躁,我來給大家詳細(xì)解釋,水管分為兩部分,一部分是固定的管口,還有一部分是為了控制鋼管長度的管體,在上面的圖片也可以看到,每一關(guān)的管道是分為上下兩個(gè)的——up_pipe和down_pipe,也就是說我們看到的鋼管是由數(shù)個(gè)相同的管體加管口構(gòu)成的,這里管體的數(shù)量是隨機(jī)的,這樣就可以使管道擁有隨機(jī)的長度了。然后為了保證上下兩個(gè)鋼管的中間距離固定,下管道的高度就是總高度減去上管道的高度,嗯,這里需要理一理,大家也可以直接去看我的代碼。有了上面的理論,接下來就簡單了,繪制管口drawPipe(),注意給管體預(yù)留出位置來,再繪制管體drawMods(),用一個(gè)for循環(huán)依次繪制出數(shù)個(gè)管體疊加在一起的樣子。水管移動move(),就是改變水管的橫坐標(biāo)了。這里可以通過改變上下水管高度的總值,來增加上下水管之間的距離,是不是游戲難度一下就降了很多?再有就是判斷水管是否被小鳥跨越的hadskiped屬性,往下看

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

我是通過判斷水管的位置是否已經(jīng)位于小鳥的后面來判斷,小鳥是否越過了水管的,如果越過了就+1分,至于沒越過就是通過前面講過到的isHit()判斷了,因?yàn)椴皇峭粫r(shí)間段發(fā)生的事情所以不能放在一起。

計(jì)分表

var scroll = 0;//當(dāng)前得分
var scrollImg = [imgs.scroe0,imgs.scroe1,imgs.scroe2,
              imgs.scroe3,imgs.scroe4,imgs.scroe5,
              imgs.scroe6,imgs.scroe7,imgs.scroe8,
              imgs.scroe9];//存儲數(shù)字圖片
  //繪制當(dāng)前得分
  function drawScore() {
    //每繪制一位數(shù),向右移23,繪制下一位數(shù)
    for(var i=0;i

首先,把所有分?jǐn)?shù)有關(guān)的圖片放到這里scrollImg來,方便使用。然后判斷數(shù)字的位數(shù),也就是個(gè)十百千萬。循環(huán)并截取每個(gè)位數(shù),再通過相應(yīng)的圖片繪制出來,并且每繪制一個(gè)位數(shù)的圖片位置向右移23,這樣數(shù)字就不會疊在一起了。這里有一種最沒意思的作弊方法,就是手動調(diào)整分?jǐn)?shù),但這只是一個(gè)數(shù)字,游戲的樂趣果然還是在于過程,下面...

游戲開始!
//游戲界面
  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();//游戲結(jié)束
        reset();//數(shù)據(jù)重置
      }
    }, 24);
  }

...看到這里,估計(jì)已經(jīng)有人在罵我了,講了半天游戲還沒開始...好吧,你們看,其實(shí)游戲的界面也不過是一個(gè)定時(shí)器,將前面講到的函數(shù)和代碼,無腦的、重復(fù)的執(zhí)行著。然后這里一定要注意畫圖的順序,不然后畫的部分會把前面覆蓋掉,其次這里的gameTimer和gameTime也和開始界面中startTimer、startTime起到類似的作用,每過一段較長的時(shí)間生成一個(gè)水管,也就是通過水管類實(shí)例化一個(gè)水管對象,具體的方法被我封裝進(jìn)一個(gè)createPipes函數(shù)里了。

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

因?yàn)閷?shí)現(xiàn)的方法沒有想象中那么簡單,首先我們要創(chuàng)造一個(gè)水管的數(shù)組,它的作用就是為了控制水管的數(shù)量,不然我們的定時(shí)器就會一遍一遍的創(chuàng)造出無數(shù)的水管,但是前面的水管早就離我們遠(yuǎn)去,所以我就用數(shù)組把水管裝起來,控制只有一個(gè)屏幕的水管,也就是三個(gè)。如果創(chuàng)建了超過三個(gè)水管,就會把最前面一個(gè)替換掉,因?yàn)樗呀?jīng)超出了我們的視野。

響應(yīng)事件

光有動畫也不行,只能看不能玩有個(gè)皮用啊。所以我們當(dāng)然要添加響應(yīng)事件了。

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

這就是所有的響應(yīng)事件了,通過按空格鍵和點(diǎn)擊屏幕都可以改變小鳥的速度,只要把這個(gè)速度調(diào)整到一個(gè)比較舒服的程度,游戲難度就會大大降低。其次,因?yàn)閏anvas是一個(gè)整體,所以我們沒有辦法直接監(jiān)聽里面圖片按鈕的響應(yīng)事件,只能退而求其次,判斷點(diǎn)擊的位置是否在按鈕的位置上了,就上面那段有點(diǎn)長的if判斷語句。

游戲結(jié)束

假如我們的主角真的一個(gè)不小心如我們所料的撞死在了鋼管上(往上翻,就在游戲開始那里),那就表示gameOver();

  //游戲結(jié)束
  function gameOver(){
    //清除定時(shí)器
    clearInterval(gameTimer);
    //清除窗口響應(yīng)事件
    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();
  }



整個(gè)世界都平靜了下來,定時(shí)器關(guān)掉,響應(yīng)事件移除掉,然后繪上大大的、慘白的GAME OVER,下面附帶一個(gè)游戲開始時(shí)就出現(xiàn)的start按鈕。不是有一句話說的是,結(jié)束不過是新的開始嗎,你又可以再來一局了。......好吧,這個(gè)就是我為了偷懶隨便搞搞的。不過這還沒完,數(shù)據(jù)還得重置一下,不然怎么重新開始。

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

最后再給這個(gè)start按鈕添加上點(diǎn)擊事件,大功告成!這就是我調(diào)整難度之后的樣子:


嘖嘖嘖,這種閑庭信步的感覺......

果然游戲還是有點(diǎn)難度才有意思......

總結(jié)

吁...一篇又臭又長、廢話又多的文章終于寫完了,如果大家覺得有幫助,或者對這篇文章有興趣的話,就賞個(gè)贊。如果覺得我的程序有問題,或者有別的想說的,都可以在評論里告訴我,我會看的。

我的項(xiàng)目地址:https://github.com/tzc123/can...

參考項(xiàng)目地址:http://www.jianshu.com/p/45d9...

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/84729.html

相關(guān)文章

  • PaddlePaddle版Flappy-Bird—使用DQN算法實(shí)現(xiàn)游戲智能

    摘要:剛剛舉行的深度學(xué)習(xí)開發(fā)者峰會上,發(fā)布了版本,這一版新增了等一系列并行算法。專注于游戲智能少兒趣味編程兩大領(lǐng)域。有了貝爾曼最優(yōu)方程,我們就可以通過純粹貪心的策略來確定,即僅僅把最優(yōu)動作的概率設(shè)置為,其他所有非最優(yōu)動作的概率都設(shè)置為。 剛剛舉行的 WAVE SUMMIT 2019 深度學(xué)習(xí)開發(fā)者峰會上,PaddlePaddle 發(fā)布了 PARL 1.1 版本,這一版新增了 IMPALA、A...

    vpants 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<