摘要:開始界面的定時(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)都過不了小菜雞的話,根本不用喪氣對吧?咱是程序員是不是?游戲不會玩,作弊還不會嗎?咳咳,下面就是作弊的方法:
很簡單,就是這樣。
注意!我要開始說了 首先咱先加載一下所有的圖片// 圖片集合 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)生動畫的效果,所以這就是我接下來要講的。
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)系,不過下面就是重頭戲了。
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)整難度之后的樣子:
總結(jié)
嘖嘖嘖,這種閑庭信步的感覺......
果然游戲還是有點(diǎn)難度才有意思......吁...一篇又臭又長、廢話又多的文章終于寫完了,如果大家覺得有幫助,或者對這篇文章有興趣的話,就賞個(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
摘要:剛剛舉行的深度學(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...
閱讀 3240·2021-11-24 09:39
閱讀 3179·2021-10-21 09:38
閱讀 2406·2019-08-29 15:28
閱讀 3748·2019-08-26 12:23
閱讀 2623·2019-08-26 12:19
閱讀 1368·2019-08-23 12:44
閱讀 2134·2019-08-23 12:02
閱讀 1006·2019-08-22 17:05