若是說學編程就是學邏輯的話,那鍛鍊邏輯能力的最好方法就莫過於寫遊戲了。最近看了一位大神的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...