作者: 風小銳 新浪微博ID:永遠de風小銳 QQ:547953539 轉載請註明出處
PS:新修復了兩個bug,已下載代碼的同學請查看一下
大學馬上要畢業了,未來的公司爲我們制定了在校學習計劃,希望我們能在畢業之前掌握一些技術,其中有一項就是使用HTML5+JavaScript編寫flappy bird這個小遊戲。
相信大家和我一樣,對這個小遊戲還是非常熟悉的,控制小鳥跳過高矮不一的水管,並記錄下每局得到的分數,對於親手編寫這個小遊戲很感興趣,馬上開始着手開始編寫。
學習JavaScript的時間並不久,看了《JavaScript語言精粹》和《HTML5遊戲開發》這兩本書,感覺都還不錯,推薦給想要學習HTML遊戲開發的朋友。
遊戲的編寫基本上用了兩個晚上,進度還是比較快的,這裏附上幾張完成後的截圖,從左到右依次是開始時、進行時、結束後的截圖。
閒話說完了,下面送上游戲的製作流程,給初學JavaScript的同學一個參考。
我將整個遊戲的製作分爲以下幾步:
一、遊戲整體框架的搭建
這一部分包括html一些標籤的設定,遊戲循環框架的編寫。
<body onLoad="init();"> <canvas id="canvas" width="384" height="512" style="margin-top: 8px;"> Your browser doesn't support the HTML5 element canvas. </canvas> </body>
canvas標籤的設定,用於繪製圖像。
var fps=30; //遊戲的幀數,推薦在30~60之間
function init(){
ctx=document.getElementById('canvas').getContext('2d');
ctx.lineWidth=2;
canvas=document.getElementById("canvas");
setInterval(run,1000/fps);
}
遊戲主邏輯run()將會以每秒fps幀的速度運行,這和後面繪製移動物體有關。還有一些全局變量的設置,具體含義後面還會提到
var boxx=0;
var boxy=0;
var boxwidth=384;
var boxheight=512;
var backgroundwidth=384;
var backgroundheight=448;
var groundwidth=18.5;
var groundheight=64;
var birdwidth=46;
var birdheight=32;
var birdx=192-birdwidth;
var birdy=224-birdheight;
var birdvy=0; //鳥初始的y軸速度
var gravity=1; //重力加速度
var jumpvelocity=11; //跳躍時獲得的向上速度
var pipewidth=69; //管道的寬度
var blankwidth=126; //上下管道之間的間隔
var pipeinterval=pipewidth+120; //兩個管道之間的間隔
var birdstate;
var upbackground;
var bottombackground;
var birdimage;
var pipeupimage;
var pipedownimage;
var pipenumber=0; //當前已經讀取管道高度的個數
var fps=30; //遊戲的幀數,推薦在30~60之間
var gamestate=1; //遊戲狀態:0--未開始,1--已開始,2--已結束
var times;
var canvas;
var ctx;
var i;
var bottomstate;
var pipeheight=[];
var pipeoncanvas=[ //要顯示在Canvas上的管道的location和height
[0,0],
[0,0],
[0,0]];
遊戲中的基本場景包括上方靜止的背景,下方移動地面的繪製,以及管道的繪製。
首先是靜止的圖片,只要用Image對象保存圖片地址後使用drawImage指定位置和大小就行了。
var backgroundwidth=384;
var backgroundheight=448;
var upbackground;
function init(){
upbackground=new Image();
upbackground.src="images/background.png";
ctx.drawImage(upbackground,0,0,backgroundwidth,backgroundheight);
}
下方動態的地面較爲複雜,先貼出代碼
//繪製下方的動態背景
function drawmovingscene(){
if(bottomstate==1){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*i,backgroundheight,groundwidth,groundheight);
bottomstate=2;
}
else if(bottomstate==2){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.25),backgroundheight,groundwidth,groundheight);
bottomstate=3;
}
else if(bottomstate==3){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.5),backgroundheight,groundwidth,groundheight);
bottomstate=4;
}
else if(bottomstate==4){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.75),backgroundheight,groundwidth,groundheight);
bottomstate=1;
}
}
我這裏找到的地面圖片是這個樣子的,因此想要繪製下面的完整地需要先計算出多少條能將下部填滿,使用了 for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*i,backgroundheight,groundwidth,groundheight);
就繪製出了下方地面的一幀圖像,想要讓地面動起來,我選擇每一幀都讓繪製的圖片向左移動1/4寬度,這樣就可以在遊戲運行時顯示地面在移動,這裏使用了一個bottomstate狀態量,以此來記錄當前地面的繪製狀態,每次加1,到4後下一幀變爲1。
然後是移動的管道。對於管道的繪製首先需要隨機生成若干個管道的高度,並將其並存放在一個pipeheight數組中待用
//隨機生成管道高度數據
function initPipe(){
for(i=0;i<200;i++)
pipeheight[i]=Math.ceil(Math.random()*216)+56;//高度範圍從56~272
for(i=0;i<3;i++){
pipeoncanvas[i][0]=boxwidth+i*pipeinterval;
pipeoncanvas[i][1]=pipeheight[pipenumber];
pipenumber++;
}
}
鑑於管道在畫面中不會同時出現4個,因此我首先取三個管道高度數據放入pipecanvas數組,並根據畫面的寬度和管道的間隔生成管道位置,爲繪製管道作準備,這是pipecanvas的結構
var pipeoncanvas=[ //要顯示在Canvas上的管道的location和height
[0,0],
[0,0],
[0,0]];
下面就要對管道進行繪製了,先實現一根管道上下兩部分的繪製
//使用給定的高度和位置繪製上下兩根管道
function drawPipe(location,height){
//繪製下方的管道
ctx.drawImage(pipeupimage,0,0,pipewidth*2,height*2,location,boxheight-(height+groundheight),pipewidth,height);
//繪製上方的管道
ctx.drawImage(pipedownimage,0,793-(backgroundheight-height-blankwidth)*2,pipewidth*2,
(backgroundheight-height-blankwidth)*2,location,0,pipewidth,backgroundheight-height-blankwidth);
}
函數比較簡單不再贅述,在run函數中加入drawAllPipe函數,來繪製要顯示的三根管道
//繪製需要顯示的管道
function drawAllPipe(){
for(i=0;i<3;i++){
pipeoncanvas[i][0]=pipeoncanvas[i][0]-4.625;
}
if(pipeoncanvas[0][0]<=-pipewidth){
pipeoncanvas[0][0]=pipeoncanvas[1][0];
pipeoncanvas[0][1]=pipeoncanvas[1][1];
pipeoncanvas[1][0]=pipeoncanvas[2][0];
pipeoncanvas[1][1]=pipeoncanvas[2][1];
pipeoncanvas[2][0]=pipeoncanvas[2][0]+pipeinterval;
pipeoncanvas[2][1]=pipeheight[pipenumber];
pipenumber++;
}
for(i=0;i<3;i++){
drawPipe(pipeoncanvas[i][0],pipeoncanvas[i][1]);
}
}
這裏會先判斷第一根管道是否已經移出畫布,如果移出了畫布則後面的管道數據向前順延,並將新的管道高度讀入第三根管道,處理完後按順序意思繪製三根管道。
基本場景繪製結束。
三、鳥的繪製
這裏的鳥有一個扇翅膀的動作,我拿到的圖片是這個樣子的,因此需要對圖片進行裁剪,每次使用1/3,用狀態量需要記錄下鳥當前的翅膀狀態,並根據狀態決定下一幀的繪製,代碼如下:
function drawBird(){
birdy=birdy+birdvy;
if(birdstate==1||birdstate==2||birdstate==3){
ctx.drawImage(birdimage,0,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
}
else if(birdstate==4||birdstate==5||birdstate==6){
ctx.drawImage(birdimage,92,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
}
else if(birdstate==7||birdstate==8||birdstate==9){
ctx.drawImage(birdimage,184,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
if(birdstate==9) birdstate=1;
}
//context.drawImage(img,0,0,swidth,sheight,x,y,width,height);
}
在反覆嘗試後,這裏我選擇3幀改變一次翅膀的位置,每幀狀態量加1。
這裏有必要說一下drawImage這個函數,在使用9個參數的時候,第2-5個參數可以指定位置和寬高對圖片進行裁剪,有興趣的同學可以去查一下相關的資料。
遊戲開始時需要設定鳥的初始位置。要讓鳥移動起來,還要給鳥添加縱向的速度值,在遊戲開始時這個值會是0。
birdy=birdy+birdvy;
birdvy=birdvy+gravity;
每一幀鳥的位置都是由上一幀的位置加上速度決定的,在運行過程中每一幀速度都會減去重力值(由我設定的),在檢測到用戶輸入會賦給鳥一個固定的速度(後面會提到),形成了跳躍的動作。
至此,我們在一幀中已經繪製了基本場景和鳥,下面是碰撞檢測。
四、碰撞檢測
這裏我們需要依次檢測鳥是否與管道以及地面發生碰撞。
function checkBird(){
//先判斷第一組管道
//如果鳥在x軸上與第一組管道重合
if(birdx+birdwidth>pipeoncanvas[0][0]&&birdx+birdwidth<pipeoncanvas[0][0]+pipewidth+birdwidth){
//如果鳥在y軸上與第一組管道上部或下部重合
if(birdy<backgroundheight-pipeoncanvas[0][1]-blankwidth||birdy+birdheight>backgroundheight-pipeoncanvas[0][1])
gamestate=2; //遊戲結束
}
//判斷第二組管道
//如果鳥在x軸上與第二組管道重合
else if(birdx+birdwidth>pipeoncanvas[1][0]&&birdx+birdwidth<pipeoncanvas[1][0]+pipewidth+birdwidth){
//如果鳥在y軸上與第二組管道上部或下部重合
if(birdy<backgroundheight-pipeoncanvas[1][1]-blankwidth||birdy+birdheight>backgroundheight-pipeoncanvas[1][1])
gamestate=2; //遊戲結束
}
//判斷是否碰撞地面
if(birdy+birdheight>backgroundheight)
gamestate=2; //遊戲結束
}
這裏的註釋比較詳細,我簡單解釋一下,判斷會先看鳥在x軸上是否與某一管道有重合,如果有則再檢測y軸上是否有重合,兩項都符合則遊戲結束。地面則較爲簡單。
五、添加鍵盤和鼠標控制
想要在HTML中讀取用戶輸入,需要在init中增加監聽事件
canvas.addEventListener("mousedown",mouseDown,false);
window.addEventListener("keydown",keyDown,false);
mousedow字段監聽鼠標按下事件並調用mouseDown函數,keydown字段監聽按鍵事件並調用keyDown函數。
這兩個函數定義如下
//處理鍵盤事件
function keyDown(){
if(gamestate==0){
playSound(swooshingsound,"sounds/swooshing.mp3");
birdvy=-jumpvelocity;
gamestate=1;
}
else if(gamestate==1){
playSound(flysound,"sounds/wing.mp3");
birdvy=-jumpvelocity;
}
}
鍵盤不區分按下的鍵,會給將鳥的速度變爲一個設定的值(jumpvelocity)function mouseDown(ev){
var mx; //存儲鼠標橫座標
var my; //存儲鼠標縱座標
if ( ev.layerX || ev.layerX == 0) { // Firefox
mx= ev.layerX;
my = ev.layerY;
} else if (ev.offsetX || ev.offsetX == 0) { // Opera
mx = ev.offsetX;
my = ev.offsetY;
}
if(gamestate==0){
playSound(swooshingsound,"sounds/swooshing.mp3");
birdvy=-jumpvelocity;
gamestate=1;
}
else if(gamestate==1){
playSound(flysound,"sounds/wing.mp3");
birdvy=-jumpvelocity;
}
//遊戲結束後判斷是否點擊了重新開始
else if(gamestate==2){
//ctx.fillRect(boardx+14,boardy+boardheight-40,75,40);
//鼠標是否在重新開始按鈕上
if(mx>boardx+14&&mx<boardx+89&&my>boardy+boardheight-40&&my<boardy+boardheight){
playSound(swooshingsound,"sounds/swooshing.mp3");
restart();
}
}
}
這裏相比鍵盤多了鼠標位置獲取和位置判斷,目的是在遊戲結束後判斷是否點擊了重新開始按鈕。
至此,我們實現了這個遊戲的基本邏輯,已經能窺見遊戲的雛形了,這時的效果如flapp_1.html。
六、增加計分,添加開始提示和結束積分板
計分的實現比較簡單,使用一全局變量即可,在每次通過管道時分數加1,並根據全局變量的值將分數繪製在畫布上。
var highscore=0; //得到過的最高分
var score=0 //目前得到的分數
//通過了一根管道加一分
if(birdx+birdwidth>pipeoncanvas[0][0]-movespeed/2&&birdx+birdwidth<pipeoncanvas[0][0]+movespeed/2
||birdx+birdwidth>pipeoncanvas[1][0]-movespeed/2&&birdx+birdwidth<pipeoncanvas[1][0]+movespeed/2){
playSound(scoresound,"sounds/point.mp3");
score++;
}
function drawScore(){
ctx.fillText(score,boxwidth/2-2,120);
}
在繪製文本之前需要先指定字體和顏色
ctx.font="bold 40px HarlemNights"; //設置繪製分數的字體
ctx.fillStyle="#FFFFFF";
開始時的提示和結束的計分板都是普通的圖片,計分板上用兩個文本繪製了當前分數和得到的最高分數
function drawTip(){
ctx.drawImage(tipimage,birdx-57,birdy+birdheight+10,tipwidth,tipheight);
}
//繪製分數板
function drawScoreBoard(){
//繪製分數板
ctx.drawImage(boardimage,boardx,boardy,boardwidth,boardheight);
//繪製當前的得分
ctx.fillText(score,boardx+140,boardheight/2+boardy-8);//132
//繪製最高分
ctx.fillText(highscore,boardx+140,boardheight/2+boardy+44);//184
}
這裏的最高分highscroe會在每次遊戲結束時更新
//刷新最好成績
function updateScore(){
if(score>highscore)
highscore=score;
}
這時的遊戲已經比較完整了,運行效果如flappybird_2.html版本,但和原版比還是覺得差了什麼,所以有了下一步
七、給鳥添加俯仰動作,添加音效
在完成了第二個版本後,我察覺到鳥的動作還不是非常豐富,有必要給鳥添加上仰、俯衝的動作使其更富動感。代碼如下:
function drawBird(){
birdy=birdy+birdvy;
if(gamestate==0){
drawMovingBird();
}
//根據鳥的y軸速度來判斷鳥的朝向,只在遊戲進行階段生效
else if(gamestate==1){
ctx.save();
if(birdvy<=8){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(-Math.PI/6);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>8&&birdvy<=12){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/6);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>12&&birdvy<=16){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/3);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>16){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/2);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
drawMovingBird();
ctx.restore();
}
//遊戲結束後鳥頭向下並停止活動
else if(gamestate==2){
ctx.save();
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/2);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
ctx.drawImage(birdimage,0,0,92,64,birdx,birdy,birdwidth,birdheight);
ctx.restore();
}
}
這裏使用了圖片的旋轉操作。過程是保存繪畫狀態,將繪畫原點移到鳥的中心,旋轉一定的角度,將原點移回原位(防止影響其他物體的繪製),恢復繪畫狀態:
ctx.save();
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(-Math.PI/6);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
ctx.restore();
旋轉的角度我根據鳥當前的速度來判斷,birdvy<=8時向上旋轉30度,8<birdvy<=12時向下旋轉30度,12<birdvy<=16時向下旋轉60度,birdvy>16時向下旋轉90度,在確定了旋轉角度後再使用之前的方法進行鳥的繪製,這樣就同時實現了鳥的俯仰和扇動翅膀。在開始和結束階段繪製方法並不一樣,感興趣的同學可以仔細看一看。
現在看看我們還差什麼?
一個遊戲怎麼能缺少聲音能,優秀的音樂和音效能爲遊戲增加許多的樂趣,提高玩家的代入感。
關於在HTML中使用音效,我查閱了許多資料,經過反覆試驗後,排除了許多效果不佳的方法,最終選擇使用audio這個HTML標籤來實現音效的播放。
要使用audio標籤,首先要在HTML的body部分定義之
<audio id="flysound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="scoresound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="hitsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="deadsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="swooshingsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
爲了時播放音效時不發生衝突,我爲每個音效定義了一個audio標籤,這樣在使用中就不會出現問題。
然後將定義的變量與標籤綁定:
//各種音效
var flysound; //飛翔的聲音
var scoresound; //得分的聲音
var hitsound; //撞到管道的聲音
var deadsound; //死亡的聲音
var swooshingsound; //切換界面時的聲音
function init(){
flysound = document.getElementById('flysound');
scoresound = document.getElementById('scoresound');
hitsound = document.getElementById('hitsound');
deadsound = document.getElementById('deadsound');
swooshingsound = document.getElementById('swooshingsound');
}
再定義用來播放音效的函數
function playSound(sound,src){
if(src!='' && typeof src!=undefined){
sound.src = src;
}
}
函數的兩個參數分別指定了要使用的標籤和聲音文件的路徑,接下來只要在需要播放音效的地方調用這個函數並指定聲音文件就行了,比如
else if(gamestate==1){
playSound(flysound,"sounds/wing.mp3");
birdvy=-jumpvelocity;
}
這裏在點擊鍵盤按鍵且遊戲正在運行的時候使鳥跳躍,並播放扇動翅膀的音效。使用的地方很多,這裏不一一提到,用到的五種音效分別是界面切換、扇動翅膀、撞上管道、鳥死亡、得分。
至此,整個遊戲已經全部完成,達到了flappybird_3.html的效果(如果可能的話還可以將計分的數字由文本改爲圖片,這裏由於資源不足沒有做這件事)。
八、源代碼資源和感悟
在整個遊戲的製作過程中,我學到了很多技術,積累了一些經驗,掌握了一些基本的設計方法。
整個項目的源代碼和資源我放在github倉庫中
地址https://github.com/fengxiaorui/My-flappy-bird
點擊頁面右邊的download zip按鈕即可下載。
由於剛接觸JavaScript不久,難免經驗不足,對於代碼中的缺陷與不足,歡迎大家批評和指正。
我的新浪微博ID 永遠de風小銳,期待與大家討論各種問題。
PS:今天發生了靈異事件,我編輯好的文章發表後後半段變成了全是代碼,希望不要再出問題。。。。
最後附上最終版完整的源代碼方便大家查看:
<html>
<head>
<title>My flappy bird</title>
<script>
//====================================================
// Name: flappybird_3.html
// Des: flappy bird 的最終版本,在第二版的基礎上加入了鳥的上下俯仰動作,
// 添加了飛翔得分碰撞等音效,重構了部分代碼
// 2014年 4月26日 Create by 風小銳
// 2015年 4月30日 modify by 風小銳
// 1.修改了checkBird方法中關於得分的判斷,現在不會在撞上管道左邊的情況下得分了。
// 2.將checkBird方法中關於得分的判斷放在了碰撞檢測之前,現在不會出現最高分比當前得分高一分的情況了。
//====================================================
var boxx=0;
var boxy=0;
var boxwidth=384;
var boxheight=512;
var backgroundwidth=384;
var backgroundheight=448;
var groundwidth=18.5;
var groundheight=64;
var birdwidth=46;
var birdheight=32;
var birdx=192-birdwidth;
var birdy=224-birdheight;
var birdvy=0; //鳥初始的y軸速度
var birdimage;
var gravity=1; //重力加速度
var jumpvelocity=11; //跳躍時獲得的向上速度
var birdstate;
var upbackground;
var bottombackground;
var bottomstate;
var pipeupimage;
var pipedownimage;
var pipewidth=69; //管道的寬度
var blankwidth=126; //上下管道之間的間隔
var pipeinterval=pipewidth+120; //兩個管道之間的間隔
var pipenumber=0; //當前已經讀取管道高度的個數
var fps=30; //遊戲的幀數,推薦在30~60之間
var gamestate=0; //遊戲狀態:0--未開始,1--已開始,2--已結束
var times; //地板圖片的條數 Math.ceil(boxwidth/groundwidth)+1;
var highscore=0; //得到過的最高分
var score=0 //目前得到的分數
var movespeed=groundwidth/4; //場景向左移動的速度,爲底部場景的寬度的1/4
var tipimage; //開始的提示圖片
var tipwidth=168;
var tipheight=136;
var boardimage; //分數板的圖片
var boardx;
var boardy=140;
var boardwidth=282;
var boardheight=245;
var canvas;
var ctx;
var i;
var pipeheight=[];
//各種音效
var flysound; //飛翔的聲音
var scoresound; //得分的聲音
var hitsound; //撞到管道的聲音
var deadsound; //死亡的聲音
var swooshingsound; //切換界面時的聲音
var pipeoncanvas=[ //要顯示在Canvas上的管道的location和height
[0,0],
[0,0],
[0,0]];
function init(){
ctx=document.getElementById('canvas').getContext('2d');
flysound = document.getElementById('flysound');
scoresound = document.getElementById('scoresound');
hitsound = document.getElementById('hitsound');
deadsound = document.getElementById('deadsound');
swooshingsound = document.getElementById('swooshingsound');
ctx.lineWidth=2;
//ctx.font="bold 40px HarlemNights"; //設置繪製分數的字體 Quartz Regular \HarlemNights
ctx.font="bold 40px HirakakuProN-W6"; //繪製字體還原
ctx.fillStyle="#FFFFFF";
upbackground=new Image();
upbackground.src="images/background.png";
bottombackground=new Image();
bottombackground.src="images/ground.png";
bottomstate=1;
birdimage=new Image();
birdimage.src="images/bird.png";
birdstate=1;
tipimage=new Image();
tipimage.src="images/space_tip.png";
boardimage=new Image();
boardimage.src="images/scoreboard.png";
boardx=(backgroundwidth-boardwidth)/2;
///////////////////
pipeupimage=new Image();
pipeupimage.src="images/pipeup.png";
pipedownimage=new Image();
pipedownimage.src="images/pipedown.png";
/////////////////////
times=Math.ceil(boxwidth/groundwidth)+1;
initPipe();
canvas=document.getElementById("canvas");
canvas.addEventListener("mousedown",mouseDown,false);
window.addEventListener("keydown",keyDown,false);
//window.addEventListener("keydown",getkeyAndMove,false);
setInterval(run,1000/fps);
}
//隨機生成管道高度數據
function initPipe(){
for(i=0;i<200;i++)
pipeheight[i]=Math.ceil(Math.random()*216)+56;//高度範圍從56~272
for(i=0;i<3;i++){
pipeoncanvas[i][0]=boxwidth+i*pipeinterval;
pipeoncanvas[i][1]=pipeheight[pipenumber];
pipenumber++;
}
}
//遊戲的主要邏輯及繪製
function run(){
//遊戲未開始
if(gamestate==0){
drawBeginScene(); //繪製開始場景
drawBird(); //繪製鳥
drawTip(); //繪製提示
}
//遊戲進行中
if(gamestate==1){
birdvy=birdvy+gravity;
drawScene(); //繪製場景
drawBird(); //繪製鳥
drawScore(); //繪製分數
checkBird(); //檢測鳥是否與物體發生碰撞
}
//遊戲結束
if(gamestate==2){
if(birdy+birdheight<backgroundheight) //如果鳥沒有落地
birdvy=birdvy+gravity;
else {
birdvy=0;
birdy=backgroundheight-birdheight;
}
drawEndScene(); //繪製結束場景
drawBird(); //繪製鳥
drawScoreBoard(); //繪製分數板
//ctx.fillRect(boardx+14,boardy+boardheight-40,75,40); // 測試重新開始按鈕的位置
}
}
function drawTip(){
ctx.drawImage(tipimage,birdx-57,birdy+birdheight+10,tipwidth,tipheight);
}
//繪製分數板
function drawScoreBoard(){
//繪製分數板
ctx.drawImage(boardimage,boardx,boardy,boardwidth,boardheight);
//繪製當前的得分
ctx.fillText(score,boardx+140,boardheight/2+boardy-8);//132
//繪製最高分
ctx.fillText(highscore,boardx+140,boardheight/2+boardy+44);//184
}
//繪製開始場景(不包括管道)
function drawBeginScene(){
//清理畫布上上一楨的畫面
ctx.clearRect(boxx,boxy,boxwidth,boxheight);
//繪製上方靜態背景
ctx.drawImage(upbackground,0,0,backgroundwidth,backgroundheight);
//繪製下方的動態背景
drawmovingscene();
//繪製邊框線
ctx.strokeRect(boxx+1,boxy+1,boxwidth-2,boxheight-2);
}
//繪製場景
function drawScene(){
ctx.clearRect(boxx,boxy,boxwidth,boxheight); //清理畫布上上一楨的畫面
ctx.drawImage(upbackground,0,0,backgroundwidth,backgroundheight); //繪製上方靜態背景
drawmovingscene(); //繪製下方的動態背景
drawAllPipe(); //繪製管道
ctx.strokeRect(boxx+1,boxy+1,boxwidth-2,boxheight-2); //繪製邊框線
}
//繪製結束場景(不包括管道)
function drawEndScene(){
ctx.clearRect(boxx,boxy,boxwidth,boxheight); //清理畫布上上一楨的畫面
ctx.drawImage(upbackground,0,0,backgroundwidth,backgroundheight); //繪製上方靜態背景
//繪製下方的靜態背景,根據bottomstate來判斷如何繪製靜態地面
switch(bottomstate){
case 1:
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.75),backgroundheight,groundwidth,groundheight);
break;
case 2:
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*i,backgroundheight,groundwidth,groundheight);
break;
case 3:
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.25),backgroundheight,groundwidth,groundheight);
break;
case 4:
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.5),backgroundheight,groundwidth,groundheight);
}
//繪製當前的柱子
for(i=0;i<3;i++){
drawPipe(pipeoncanvas[i][0],pipeoncanvas[i][1]);
}
ctx.strokeRect(boxx+1,boxy+1,boxwidth-2,boxheight-2); //繪製邊框線
}
//繪製下方的動態背景
function drawmovingscene(){
if(bottomstate==1){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*i,backgroundheight,groundwidth,groundheight);
bottomstate=2;
}
else if(bottomstate==2){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.25),backgroundheight,groundwidth,groundheight);
bottomstate=3;
}
else if(bottomstate==3){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.5),backgroundheight,groundwidth,groundheight);
bottomstate=4;
}
else if(bottomstate==4){
for(i=0;i<times;i++)
ctx.drawImage(bottombackground,groundwidth*(i-0.75),backgroundheight,groundwidth,groundheight);
bottomstate=1;
}
}
//使用給定的高度和位置繪製上下兩根管道
function drawPipe(location,height){
//繪製下方的管道
ctx.drawImage(pipeupimage,0,0,pipewidth*2,height*2,location,boxheight-(height+groundheight),pipewidth,height);
//繪製上方的管道
ctx.drawImage(pipedownimage,0,793-(backgroundheight-height-blankwidth)*2,pipewidth*2,
(backgroundheight-height-blankwidth)*2,location,0,pipewidth,backgroundheight-height-blankwidth);
}
//繪製需要顯示的管道
function drawAllPipe(){
for(i=0;i<3;i++){
pipeoncanvas[i][0]=pipeoncanvas[i][0]-movespeed;
}
if(pipeoncanvas[0][0]<=-pipewidth){
pipeoncanvas[0][0]=pipeoncanvas[1][0];
pipeoncanvas[0][1]=pipeoncanvas[1][1];
pipeoncanvas[1][0]=pipeoncanvas[2][0];
pipeoncanvas[1][1]=pipeoncanvas[2][1];
pipeoncanvas[2][0]=pipeoncanvas[2][0]+pipeinterval;
pipeoncanvas[2][1]=pipeheight[pipenumber];
pipenumber++;
}
for(i=0;i<3;i++){
drawPipe(pipeoncanvas[i][0],pipeoncanvas[i][1]);
}
}
function drawBird(){
birdy=birdy+birdvy;
if(gamestate==0){
drawMovingBird();
}
//根據鳥的y軸速度來判斷鳥的朝向,只在遊戲進行階段生效
else if(gamestate==1){
ctx.save();
if(birdvy<=8){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(-Math.PI/6);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>8&&birdvy<=12){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/6);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>12&&birdvy<=16){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/3);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
if(birdvy>16){
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/2);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
}
drawMovingBird();
ctx.restore();
}
//遊戲結束後鳥頭向下並停止活動
else if(gamestate==2){
ctx.save();
ctx.translate(birdx+birdwidth/2,birdy+birdheight/2);
ctx.rotate(Math.PI/2);
ctx.translate(-birdx-birdwidth/2,-birdy-birdheight/2);
ctx.drawImage(birdimage,0,0,92,64,birdx,birdy,birdwidth,birdheight);
ctx.restore();
}
}
//繪製扇動翅膀的鳥
function drawMovingBird(){
if(birdstate==1||birdstate==2||birdstate==3){
ctx.drawImage(birdimage,0,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
}
else if(birdstate==4||birdstate==5||birdstate==6){
ctx.drawImage(birdimage,92,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
}
else if(birdstate==7||birdstate==8||birdstate==9){
ctx.drawImage(birdimage,184,0,92,64,birdx,birdy,birdwidth,birdheight);
birdstate++;
if(birdstate==9) birdstate=1;
}
}
function drawScore(){
ctx.fillText(score,boxwidth/2-2,120);
}
//檢查鳥是否與管道產生碰撞(不可能與第三組管道重合),以及鳥是否碰撞地面
function checkBird(){
//通過了一根管道加一分
if(birdx>pipeoncanvas[0][0]&&birdx<pipeoncanvas[0][0]+movespeed
||birdx>pipeoncanvas[1][0]&&birdx<pipeoncanvas[1][0]+movespeed){
playSound(scoresound,"sounds/point.mp3");
score++;
}
//先判斷第一組管道
//如果鳥在x軸上與第一組管道重合
if(birdx+birdwidth>pipeoncanvas[0][0]&&birdx+birdwidth<pipeoncanvas[0][0]+pipewidth+birdwidth){
//如果鳥在y軸上與第一組管道上部或下部重合
if(birdy<backgroundheight-pipeoncanvas[0][1]-blankwidth||birdy+birdheight>backgroundheight-pipeoncanvas[0][1]){
hitPipe();
}
}
//判斷第二組管道
//如果鳥在x軸上與第二組管道重合
//這裏我原本使用else if出現了問題,但第一版中卻沒有問題,對比代碼後發現原因是上方第一個if後沒有加大括號,
//這裏的else無法區分對應哪一個if,加上大括號後問題解決,建議將if後的內容都加上大括號,養成良好的變成習慣
else if(birdx+birdwidth>pipeoncanvas[1][0]&&birdx+birdwidth<pipeoncanvas[1][0]+pipewidth+birdwidth){
//如果鳥在y軸上與第二組管道上部或下部重合
if(birdy<backgroundheight-pipeoncanvas[1][1]-blankwidth||birdy+birdheight>backgroundheight-pipeoncanvas[1][1]){
hitPipe();
}
}
//判斷是否碰撞地面
else if(birdy+birdheight>backgroundheight){
hitPipe();
}
}
//撞擊到管道或地面後的一些操作
function hitPipe(){
ctx.font="bold 40px HirakakuProN-W6";
//ctx.font="bold 35px HarlemNights";
ctx.fillStyle="#000000";
playSound(hitsound,"sounds/hit.mp3");
playSound(deadsound,"sounds/die.mp3");
updateScore();
gamestate=2; //遊戲結束
}
//刷新最好成績
function updateScore(){
if(score>highscore)
highscore=score;
}
//處理鍵盤事件
function keyDown(){
if(gamestate==0){
playSound(swooshingsound,"sounds/swooshing.mp3");
birdvy=-jumpvelocity;
gamestate=1;
}
else if(gamestate==1){
playSound(flysound,"sounds/wing.mp3");
birdvy=-jumpvelocity;
}
}
//處理鼠標點擊事件,相比鍵盤多了位置判斷
function mouseDown(ev){
var mx; //存儲鼠標橫座標
var my; //存儲鼠標縱座標
if ( ev.layerX || ev.layerX == 0) { // Firefox
mx= ev.layerX;
my = ev.layerY;
} else if (ev.offsetX || ev.offsetX == 0) { // Opera
mx = ev.offsetX;
my = ev.offsetY;
}
if(gamestate==0){
playSound(swooshingsound,"sounds/swooshing.mp3");
birdvy=-jumpvelocity;
gamestate=1;
}
else if(gamestate==1){
playSound(flysound,"sounds/wing.mp3");
birdvy=-jumpvelocity;
}
//遊戲結束後判斷是否點擊了重新開始
else if(gamestate==2){
//ctx.fillRect(boardx+14,boardy+boardheight-40,75,40);
//鼠標是否在重新開始按鈕上
if(mx>boardx+14&&mx<boardx+89&&my>boardy+boardheight-40&&my<boardy+boardheight){
playSound(swooshingsound,"sounds/swooshing.mp3");
restart();
}
}
}
function restart(){
gamestate=0; //回到未開始狀態
//ctx.font="bold 40px HarlemNights"; //繪製字體還原
ctx.font="bold 40px HirakakuProN-W6"; //繪製字體還原
ctx.fillStyle="#FFFFFF";
score=0; //當前分數清零
pipenumber=0; //讀取的管道數清零
initPipe(); //重新初始化水管高度
birdx=192-birdwidth; //鳥的位置和速度回到初始值
birdy=224-birdheight;
birdvy=0;
}
function playSound(sound,src){
if(src!='' && typeof src!=undefined){
sound.src = src;
}
}
</script>
</head>
<body onLoad="init();">
<audio id="flysound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="scoresound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="hitsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="deadsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<audio id="swooshingsound" playcount="1" autoplay="true" src="">
Your browser doesn't support the HTML5 element audio.
</audio>
<canvas id="canvas" width="384" height="512" style="margin-top: 8px;">
Your browser doesn't support the HTML5 element canvas.
</canvas>
</body>
</html>