摘要:開始界面的定時器開始界面定時器定時器運行的次數定時器每運行次改變標題位置運行次數大家也可以理解為這就是開始界面,因為開始界面就是通過定時器一次次運行上面的函數所實現的。
前言
如果說學編程就是學邏輯的話,那鍛煉邏輯能力的最好方法就莫過于寫游戲了。最近看了一位大神的fly bird小游戲,感覺很有幫助。于是為了尋求進一步的提高,我花了兩天時間自己寫了一個canvas版本的。雖然看起來原理都差不多,但是實現方法大相徑庭,如果有興趣的話可以大家自己下載下來玩一玩,大概效果就像下面這樣:
怎么樣?是不是感覺難度巨大?...可能是因為我比較菜吧。相信高手還是大有人在的,隨便過個幾十關也是不在話下。但是如果有和我一樣10關都過不了小菜雞的話,根本不用喪氣對吧?咱是程序員是不是?游戲不會玩,作弊還不會嗎?咳咳,下面就是作弊的方法:
很簡單,就是這樣。
注意!我要開始說了 首先咱先加載一下所有的圖片// 圖片集合 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屬性判斷圖片是否全部加載完成。如果是,就刪除這個定時器,并執行一段回調函數,還是很好理解的吧:),不過我感覺這種方法可能有點蠢,不知道各位高人有沒有更好的方法?
接下來,就要開始畫了大家都知道,其實canvas就是畫圖,如果要用canvas實現動畫效果的話,就只能一遍一遍的擦了畫、畫了擦了。
首先先把幾個固定不動的部分的繪制方法和清空畫布的方法寫在函數里
//繪制背景 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; } }
這樣每次運行一次,草坪就會向左移一點了
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次抖動一次,當然大家可以設置9、10、11使它的頻率更加緩慢(大家還可以嘗試使用requestAnimation-
-Frame,那樣性能更佳,但是控制頻率略顯麻煩。這里使用setInterval更容易理解)當然這個作弊沒有半毛錢關系,不過下面就是重頭戲了。
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.posXoPipe.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
又是一段冗長的代碼,大家不要急躁,我來給大家詳細解釋,水管分為兩部分,一部分是固定的管口,還有一部分是為了控制鋼管長度的管體,在上面的圖片也可以看到,每一關的管道是分為上下兩個的——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首先,把所有分數有關的圖片放到這里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.clientXcanvas.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...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84729.html
摘要:剛剛舉行的深度學習開發者峰會上,發布了版本,這一版新增了等一系列并行算法。專注于游戲智能少兒趣味編程兩大領域。有了貝爾曼最優方程,我們就可以通過純粹貪心的策略來確定,即僅僅把最優動作的概率設置為,其他所有非最優動作的概率都設置為。 剛剛舉行的 WAVE SUMMIT 2019 深度學習開發者峰會上,PaddlePaddle 發布了 PARL 1.1 版本,這一版新增了 IMPALA、A...
閱讀 3225·2021-11-24 09:39
閱讀 3158·2021-10-21 09:38
閱讀 2396·2019-08-29 15:28
閱讀 3737·2019-08-26 12:23
閱讀 2615·2019-08-26 12:19
閱讀 1358·2019-08-23 12:44
閱讀 2125·2019-08-23 12:02
閱讀 993·2019-08-22 17:05