canvas小游戲——flappy_bird

前言

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

游戲效果
游戲效果


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

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

<style>a
  *{
    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>

很簡(jiǎn)單,就是這樣。

注意!我要開(kāi)始說(shuō)了

首先咱先加載一下所有的圖片

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

接下來(lái),就要開(kāi)始畫(huà)了

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

首先

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

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

然后

把會(huì)動(dòng)的部分也加上

var v = 0;//草坪滾動(dòng)的增量
  //繪制草坪
  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)行一次,草坪就會(huì)向左移一點(diǎn)了

var shake = true;//標(biāo)題的抖動(dòng)狀態(tài)
  //標(biāo)題的抖動(dòng)效果
  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);
    }
  }

這樣通過(guò)改變shake的值,就可以使標(biāo)題的抖動(dòng)了。
機(jī)智的各位應(yīng)該已經(jīng)發(fā)現(xiàn)了,上面兩個(gè)函數(shù)需要重復(fù)調(diào)用,才能產(chǎn)生動(dòng)畫(huà)的效果,所以這就是我接下來(lái)要講的。

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

開(kāi)始界面
開(kāi)始界面
var startTimer;//開(kāi)始界面定時(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);
  }

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

主角登場(chǎng)?。?!

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,//翅膀揮動(dòng),切換圖片的標(biāo)
  alive: true,//存活狀態(tài)
  //繪制小鳥(niǎo)
  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();
    }
    //如果撞頂,彈回來(lái)
    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]);
    }
    //確保墜落速度不會(huì)太快
    if(bird.speed > 6){
      bird.speed = 6;
    }
  },
  //煽動(dòng)翅膀,切換圖片
  wingWave: function () {
    this.index++;
    if(this.index > 1){
      this.index = 0;
    }
  },
  //死亡
  dead: function() {
    this.alive = false;
  }
}

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

  //判斷是否碰撞
  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();
      }
    }
  }

就像這樣,通過(guò)判斷小鳥(niǎo)和鋼管的位置判斷小鳥(niǎo)是不是撞在鋼管上了。反正結(jié)果還是撞死bird.dead()。看到這里相信不用我說(shuō),大家也明白了吧,只要將這段代碼注釋掉,我們的小鳥(niǎo)不就練成的絕世鐵頭功,鋼管都捅穿給你看。或者稍稍增大一點(diǎn)小鳥(niǎo)會(huì)被碰撞到的體積,那就是凌波微步、輕功管上飄了呀。說(shuō)了半天,還沒(méi)告訴大家這個(gè)水管又是哪里來(lái)的。

鋼管

//水管類
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;//是否被越過(guò)
    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);
    }
  }
  //水管移動(dòng)
  move() {
    this.posX -= 6;
    this.drawMods();
    this.drawPipe();
  }
}

管口
管口
管體
管體


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

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

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

計(jì)分表

計(jì)分表
計(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];//存儲(chǔ)數(shù)字圖片
  //繪制當(dāng)前得分
  function drawScore() {
    //每繪制一位數(shù),向右移23,繪制下一位數(shù)
    for(var i=0;i<scroll.toString().length;i++){
      ctx.drawImage(scrollImg[parseInt(scroll.toString().substr(i,1))],147+i*23,40)
    }
  }

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

游戲開(kāi)始!

//游戲界面
  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();
      //如果小鳥(niǎo)死了
      if(!bird.alive){
        gameOver();//游戲結(jié)束
        reset();//數(shù)據(jù)重置
      }
    }, 24);
  }

...看到這里,估計(jì)已經(jīng)有人在罵我了,講了半天游戲還沒(méi)開(kāi)始...好吧,你們看,其實(shí)游戲的界面也不過(guò)是一個(gè)定時(shí)器,將前面講到的函數(shù)和代碼,無(wú)腦的、重復(fù)的執(zhí)行著。然后這里一定要注意畫(huà)圖的順序,不然后畫(huà)的部分會(huì)把前面覆蓋掉,其次這里的gameTimer和gameTime也和開(kāi)始界面中startTimer、startTime起到類似的作用,每過(guò)一段較長(zhǎng)的時(shí)間生成一個(gè)水管,也就是通過(guò)水管類實(shí)例化一個(gè)水管對(duì)象,具體的方法被我封裝進(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)的方法沒(méi)有想象中那么簡(jiǎn)單,首先我們要?jiǎng)?chuàng)造一個(gè)水管的數(shù)組,它的作用就是為了控制水管的數(shù)量,不然我們的定時(shí)器就會(huì)一遍一遍的創(chuàng)造出無(wú)數(shù)的水管,但是前面的水管早就離我們遠(yuǎn)去,所以我就用數(shù)組把水管裝起來(lái),控制只有一個(gè)屏幕的水管,也就是三個(gè)。如果創(chuàng)建了超過(guò)三個(gè)水管,就會(huì)把最前面一個(gè)替換掉,因?yàn)樗呀?jīng)超出了我們的視野。

響應(yīng)事件

光有動(dòng)畫(huà)也不行,只能看不能玩有個(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.clientX<canvas.offsetLeft+canvas.width/2+imgs.startBtn.width/2
      &&e.clientY<canvas.offsetTop+300+imgs.startBtn.height
      &&e.clientY>canvas.offsetTop+300){
      clean();
      //清除開(kāi)始界面定時(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)事件了,通過(guò)按空格鍵和點(diǎn)擊屏幕都可以改變小鳥(niǎo)的速度,只要把這個(gè)速度調(diào)整到一個(gè)比較舒服的程度,游戲難度就會(huì)大大降低。其次,因?yàn)閏anvas是一個(gè)整體,所以我們沒(méi)有辦法直接監(jiān)聽(tīng)里面圖片按鈕的響應(yīng)事件,只能退而求其次,判斷點(diǎn)擊的位置是否在按鈕的位置上了,就上面那段有點(diǎn)長(zhǎng)的if判斷語(yǔ)句。

游戲結(jié)束

假如我們的主角真的一個(gè)不小心如我們所料的撞死在了鋼管上(往上翻,就在游戲開(kāi)始那里),那就表示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();
  }

游戲結(jié)束
游戲結(jié)束


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

  //重置數(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)整難度之后的樣子:

低難度版
低難度版


嘖嘖嘖,這種閑庭信步的感覺(jué)......

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

總結(jié)

吁...一篇又臭又長(zhǎng)、廢話又多的文章終于寫(xiě)完了,如果大家覺(jué)得有幫助,或者對(duì)這篇文章有興趣的話,就賞個(gè)贊。如果覺(jué)得我的程序有問(wèn)題,或者有別的想說(shuō)的,都可以在評(píng)論里告訴我,我會(huì)看的。

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

參考項(xiàng)目地址:http://www.itdecent.cn/p/45d994d04a25

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 由于很多小伙伴要demo我就不一一發(fā)了,直接丟在github上自己下載吧:https://github.com/s...
    FKSky閱讀 23,986評(píng)論 27 99
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,048評(píng)論 25 709
  • 原文發(fā)表于2017-03-09 的淘寶技術(shù)微信公眾號(hào) https://mp.weixin.qq.com/s?__b...
    leonliu2閱讀 5,724評(píng)論 0 4
  • 一:canvas簡(jiǎn)介 1.1什么是canvas? ①:canvas是HTML5提供的一種新標(biāo)簽 ②:HTML5 ...
    GreenHand1閱讀 4,881評(píng)論 2 32
  • 6月22日,今天被領(lǐng)導(dǎo)罵了一頓,事情的發(fā)展超出我的預(yù)料,有什么問(wèn)題要及時(shí)的和領(lǐng)導(dǎo)溝通,雖然有件事有點(diǎn)超出自己的能力...
    i學(xué)思閱讀 241評(píng)論 0 0

友情鏈接更多精彩內(nèi)容