前言
如果說學(xué)編程就是學(xué)邏輯的話家妆,那鍛煉邏輯能力的最好方法就莫過于寫游戲了。最近看了一位大神的fly bird小游戲冕茅,感覺很有幫助伤极。于是為了尋求進(jìn)一步的提高蛹找,我花了兩天時間自己寫了一個canvas版本的。雖然看起來原理都差不多哨坪,但是實現(xiàn)方法大相徑庭庸疾,如果有興趣的話可以大家自己下載下來玩一玩,大概效果就像下面這樣:
怎么樣当编?是不是感覺難度巨大届慈?...可能是因為我比較菜吧。相信高手還是大有人在的忿偷,隨便過個幾十關(guān)也是不在話下金顿。但是如果有和我一樣10關(guān)都過不了小菜雞的話,根本不用喪氣對吧鲤桥?咱是程序員是不是揍拆?游戲不會玩,作弊還不會嗎茶凳?咳咳礁凡,下面就是作弊的方法:
首先搞清楚結(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>
很簡單,就是這樣慧妄。
注意顷牌!我要開始說了
首先咱先加載一下所有的圖片
// 圖片集合
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;
//添加定時器,判斷圖片是否加載完成
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);
//圖片全部加載完成后塞淹,運(yùn)行此函數(shù)
fn();
}
}, 50)
}
}
...抱歉有點(diǎn)長窟蓝,但是怕破壞代碼的結(jié)構(gòu),就全部拷下來了饱普,上面的朋友快點(diǎn)下來吧运挫,都是重復(fù)的沒啥好看的。我來給大家解釋一下套耕,首先這是一個對象字面量谁帕,創(chuàng)建的時候新建了若干個圖片對象,然后它有一個函數(shù)loadImg冯袍,只要一執(zhí)行匈挖,就會給所有的圖片添加路徑,然后添加一個定時器每一段時間通過查詢所有圖片的complete屬性判斷圖片是否全部加載完成康愤。如果是儡循,就刪除這個定時器,并執(zhí)行一段回調(diào)函數(shù)征冷,還是很好理解的吧:)择膝,不過我感覺這種方法可能有點(diǎn)蠢,不知道各位高人有沒有更好的方法检激?
接下來肴捉,就要開始畫了
大家都知道腹侣,其實canvas就是畫圖,如果要用canvas實現(xiàn)動畫效果的話齿穗,就只能一遍一遍的擦了畫谱醇、畫了擦了京景。
首先
先把幾個固定不動的部分的繪制方法和清空畫布的方法寫在函數(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)了腮出,上面兩個函數(shù)需要重復(fù)調(diào)用,才能產(chǎn)生動畫的效果芝薇,所以這就是我接下來要講的胚嘲。
開始界面的定時器
var startTimer;//開始界面定時器
var startTime = 0;//定時器運(yùn)行的次數(shù)
function startLayer() {
startTimer = setInterval(function () {
clean();
drawBg();
drawStartBtn();
drawGrass();
titleShake();
//定時器每運(yùn)行7次改變標(biāo)題位置
if(startTime == 7){
shake = !shake;
startTime = 0;
}
//運(yùn)行次數(shù)+1
startTime++;
//window.requestAnimationFrame(startLayer)
}, 24);
}
大家也可以理解為這就是開始界面,因為開始界面就是通過定時器一次次運(yùn)行上面的函數(shù)所實現(xiàn)的洛二。然而上面定義的startTimer和startTime又有什么用呢馋劈,當(dāng)然不是多此一舉,首先晾嘶,把這個定時器賦給一個變量妓雾,是為了在開始游戲的時候把這個界面關(guān)掉,也就是把這個定時器取消垒迂,往后看大家就明白了:)其次械姻,startTime是為了記錄定時器運(yùn)行的次數(shù),因為這個定時器刷新的實現(xiàn)極快机断,只有短短的24毫秒楷拳,如果標(biāo)題以這個速度抖動的話,大家的眼睛一定受不了了吧吏奸,所以我設(shè)法讓他慢下來欢揖,每運(yùn)行7次抖動一次,當(dāng)然大家可以設(shè)置9奋蔚、10她混、11使它的頻率更加緩慢(大家還可以嘗試使用requestAnimation-
-Frame,那樣性能更佳,但是控制頻率略顯麻煩泊碑。這里使用setInterval更容易理解)當(dāng)然這個作弊沒有半毛錢關(guān)系产上,不過下面就是重頭戲了。
主角登場6旯贰=痢!
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)然這只是主角的代碼,一個對象字面量瘤运。但是它可以操控主角的所有行為(雖然也沒有幾個行為...)窍霞,首先就是畫出主角draw(),通過傳進(jìn)不同的圖片繪制出主角不同情況下的英姿...然后是wingWave()拯坟,通過改變index,切換上面定義的圖片數(shù)組中的圖片但金,也就是揮翅膀。再然后就是飛行fly(),在飛行過程中主角會碰到各種各樣的事故郁季,像是飛的太高撞到天花板啊冷溃,或是飛的太低,摔了個狗啃屎梦裂。再干脆點(diǎn)一頭撞死在了鋼管上似枕,但是這個函數(shù)并不在這里,因為小鳥撞死在鋼管上到底是小鳥的行為年柠,還是鋼管的行為呢菠净,我還沒想明白,所以干脆放在了全局中彪杉。
//判斷是否碰撞
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();
}
}
}
就像這樣毅往,通過判斷小鳥和鋼管的位置判斷小鳥是不是撞在鋼管上了。反正結(jié)果還是撞死bird.dead()派近∨饰ǎ看到這里相信不用我說,大家也明白了吧渴丸,只要將這段代碼注釋掉侯嘀,我們的小鳥不就練成的絕世鐵頭功,鋼管都捅穿給你看谱轨〗溽#或者稍稍增大一點(diǎn)小鳥會被碰撞到的體積,那就是凌波微步土童、輕功管上飄了呀诗茎。說了半天,還沒告訴大家這個水管又是哪里來的献汗。
鋼管
//水管類
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<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();
}
}
又是一段冗長的代碼敢订,大家不要急躁王污,我來給大家詳細(xì)解釋,水管分為兩部分楚午,一部分是固定的管口昭齐,還有一部分是為了控制鋼管長度的管體,在上面的圖片也可以看到矾柜,每一關(guān)的管道是分為上下兩個的——up_pipe和down_pipe阱驾,也就是說我們看到的鋼管是由數(shù)個相同的管體加管口構(gòu)成的,這里管體的數(shù)量是隨機(jī)的怪蔑,這樣就可以使管道擁有隨機(jī)的長度了里覆。然后為了保證上下兩個鋼管的中間距離固定,下管道的高度就是總高度減去上管道的高度饮睬,嗯租谈,這里需要理一理篮奄,大家也可以直接去看我的代碼捆愁。有了上面的理論,接下來就簡單了窟却,繪制管口drawPipe()昼丑,注意給管體預(yù)留出位置來,再繪制管體drawMods(),用一個for循環(huán)依次繪制出數(shù)個管體疊加在一起的樣子夸赫。水管移動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()判斷了悴品,因為不是同一時間段發(fā)生的事情所以不能放在一起禀综。
計分表
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<scroll.toString().length;i++){
ctx.drawImage(scrollImg[parseInt(scroll.toString().substr(i,1))],147+i*23,40)
}
}
首先定枷,把所有分?jǐn)?shù)有關(guān)的圖片放到這里scrollImg來,方便使用届氢。然后判斷數(shù)字的位數(shù)欠窒,也就是個十百千萬。循環(huán)并截取每個位數(shù)退子,再通過相應(yīng)的圖片繪制出來贱迟,并且每繪制一個位數(shù)的圖片位置向右移23姐扮,這樣數(shù)字就不會疊在一起了。這里有一種最沒意思的作弊方法衣吠,就是手動調(diào)整分?jǐn)?shù)茶敏,但這只是一個數(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īng)有人在罵我了忧换,講了半天游戲還沒開始...好吧恬惯,你們看,其實游戲的界面也不過是一個定時器亚茬,將前面講到的函數(shù)和代碼酪耳,無腦的、重復(fù)的執(zhí)行著刹缝。然后這里一定要注意畫圖的順序碗暗,不然后畫的部分會把前面覆蓋掉,其次這里的gameTimer和gameTime也和開始界面中startTimer梢夯、startTime起到類似的作用言疗,每過一段較長的時間生成一個水管,也就是通過水管類實例化一個水管對象颂砸,具體的方法被我封裝進(jìn)一個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)有三個水管人乓,則依次替換
if(pipes.length<3){
pipes.push(pipe);
}else{
pipes[index] = pipe;
index++;
if(index >= 3){
index = 0;
}
}
}
因為實現(xiàn)的方法沒有想象中那么簡單勤篮,首先我們要創(chuàng)造一個水管的數(shù)組,它的作用就是為了控制水管的數(shù)量色罚,不然我們的定時器就會一遍一遍的創(chuàng)造出無數(shù)的水管碰缔,但是前面的水管早就離我們遠(yuǎn)去,所以我就用數(shù)組把水管裝起來保屯,控制只有一個屏幕的水管手负,也就是三個。如果創(chuàng)建了超過三個水管姑尺,就會把最前面一個替換掉竟终,因為它已經(jīng)超出了我們的視野。
響應(yīng)事件
光有動畫也不行切蟋,只能看不能玩有個皮用啊统捶。所以我們當(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();
//清除開始界面定時器
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)擊屏幕都可以改變小鳥的速度喘鸟,只要把這個速度調(diào)整到一個比較舒服的程度匆绣,游戲難度就會大大降低。其次什黑,因為canvas是一個整體崎淳,所以我們沒有辦法直接監(jiān)聽里面圖片按鈕的響應(yīng)事件,只能退而求其次愕把,判斷點(diǎn)擊的位置是否在按鈕的位置上了拣凹,就上面那段有點(diǎn)長的if判斷語句。
游戲結(jié)束
假如我們的主角真的一個不小心如我們所料的撞死在了鋼管上(往上翻恨豁,就在游戲開始那里)嚣镜,那就表示gameOver();
//游戲結(jié)束
function gameOver(){
//清除定時器
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();
}
整個世界都平靜了下來,定時器關(guān)掉橘蜜,響應(yīng)事件移除掉菊匿,然后繪上大大的、慘白的GAME OVER,下面附帶一個游戲開始時就出現(xiàn)的start按鈕计福。不是有一句話說的是跌捆,結(jié)束不過是新的開始嗎,你又可以再來一局了棒搜。......好吧疹蛉,這個就是我為了偷懶隨便搞搞的活箕。不過這還沒完力麸,數(shù)據(jù)還得重置一下,不然怎么重新開始育韩。
//重置數(shù)據(jù)
function reset(){
bird.posY = 200;
bird.speed = 0;
bird.alive = true;
pipes = [];
scroll = 0;
canvas.addEventListener('click', startBtn_click , false);
}
最后再給這個start按鈕添加上點(diǎn)擊事件克蚂,大功告成!這就是我調(diào)整難度之后的樣子:
嘖嘖嘖筋讨,這種閑庭信步的感覺......
果然游戲還是有點(diǎn)難度才有意思......
總結(jié)
吁...一篇又臭又長埃叭、廢話又多的文章終于寫完了,如果大家覺得有幫助悉罕,或者對這篇文章有興趣的話赤屋,就賞個贊。如果覺得我的程序有問題壁袄,或者有別的想說的类早,都可以在評論里告訴我,我會看的嗜逻。
我的項目地址:https://github.com/tzc123/canvas_game
參考項目地址:http://www.reibang.com/p/45d994d04a25