作者: 风小锐      新浪微博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="data: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~272for(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) { // Firefoxmx= ev.layerX;my = ev.layerY;} else if (ev.offsetX || ev.offsetX == 0) { // Operamx = 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); //鼠标是否在又一次開始button上if(mx>boardx+14&&mx<boardx+89&&my>boardy+boardheight-40&&my<boardy+boardheight){playSound(swooshingsound,"sounds/swooshing.mp3");restart();}}
}

这里相比键盘多了鼠标位置获取和位置推断,目的是在游戏结束后推断是否点击了又一次開始button。

至此,我们实现了这个游戏的基本逻辑,已经能窥见游戏的雏形了。这时的效果如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 zipbutton就可以下载。

因为刚接触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/4var 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 \HarlemNightsctx.font="bold 40px HirakakuProN-W6";    //绘制字体还原ctx.fillStyle="#FFFFFF";upbackground=new Image();upbackground.src="data:images/background.png";bottombackground=new Image();bottombackground.src="data:images/ground.png";bottomstate=1;birdimage=new Image();birdimage.src="data:images/bird.png";birdstate=1;tipimage=new Image();tipimage.src="data:images/space_tip.png";boardimage=new Image();boardimage.src="data:images/scoreboard.png";boardx=(backgroundwidth-boardwidth)/2;///pipeupimage=new Image();pipeupimage.src="data:images/pipeup.png";pipedownimage=new Image();pipedownimage.src="data: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~272for(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); // 測试又一次開始button的位置}
}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) { // Firefoxmx= ev.layerX;my = ev.layerY;} else if (ev.offsetX || ev.offsetX == 0) { // Operamx = 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); //鼠标是否在又一次開始button上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>

【教程】HTML5+JavaScript编写flappy bird相关推荐

  1. 【教程】HTML5+JavaScript编写转动的水彩五环

         作者: 风小锐      新浪微博ID:永远de风小锐      QQ:547953539      转载请注明出处 今天浏览CSDN博客,发现了whqet编写的<CreateJS奥运 ...

  2. html5怎么修改图片大小,HTML5 javascript修改canvas的大小

    方法1: 设定固定的值,这种方式跟在html中设定canvas的值没有什么区别: window.onload = function(){ canvas.height = 100; canvas.wid ...

  3. flappy bird游戏源代码揭秘和下载后续---移植到html5网页浏览器

    前言:      我们分析了flappy bird的代码思路(flappy bird游戏源代码揭秘和下载),也移植到了android平台(flappy bird游戏源代码揭秘和下载后续---移植到an ...

  4. HTML5版Flappy Bird游戏源码下载

    Flappy Bird相信大家都很熟悉了,2014年最热门的手机游戏之一.Flappy Bird这款游戏是一位来自越南河内的独立游戏开发者阮哈东开发,形式简易但难度极高的休闲游戏,很容易让人上瘾.今天 ...

  5. 详解用65行javascript代码做Flappy Bird

    点击查看特效 JavaScript做Flappy Bird游戏,代码仅仅65行 资源包括: javascript源码:phaser.min.js:main.js:index.html 素材:两张图片! ...

  6. flappy bird游戏源代码揭秘和下载后续---移植到android真机上

    前言:         上一篇博客 flappy bird游戏源代码揭秘和下载,源码是运行在window或者mac系统上的,现在我们需要把代码移植到android真机上,让小鸟在手机里飞起来! ps: ...

  7. flappy bird游戏源代码揭秘和下载

    背景: 最近火爆全球的游戏flappy bird让笔者叹为观止,于是花了一天的时间山寨了一个一模一样的游戏,现在把游戏的思路和源码分享出来,代码是基于javascript语言,cocos2d-x游戏引 ...

  8. 【源码+图片素材+详细教程】Java游戏开发_Java开发经典游戏飞翔的小鸟_飞扬的小鸟_Java游戏项目Flappy Bird像素鸟游戏_Java课程设计项目

    课程目标: 1.通过本课程的学习巩固Java的相关基础知识,例如循环判断,数组和集合的使用,对象的继承,接口的实现,窗口的创建,事件监听,图形绘制. 2.完成小鸟的移动,管道自动生成.碰撞死亡,计分系 ...

  9. 教你用 HTML5 制作Flappy Bird(下)

    本文由 伯乐在线 -杨帅 翻译自 lessmilk.未经许可,禁止转载! 欢迎加入:技术翻译小组,或分享原创到伯乐头条. 在上一篇HTML5教程中,我们做了一个简化版的Flappy Bird.虽然可以 ...

最新文章

  1. response.setContentType()方法浅析
  2. Unable to lock JVM Memory: error=12--elasticsearch
  3. [Codevs] 1004 四子连棋
  4. 山东计算机类好的民办大学,2021年山东所有民办大学名单及排名(教育部)
  5. c++ 表达式必须包含指向类的指针类型_C++:18const关键字(附常量指针、指针常量、常量指针常量)...
  6. 基于Zookeeper实现简易版服务的注册与发现机制
  7. MySQL—内连接和外连接区别
  8. 计算机组成及工作原理课件,计算机组成与工作原理电子教案课件.ppt
  9. 开发计算机软件的基本流程
  10. python 购物车总额_python之购物车
  11. matlab的基本语法规则_MATLAB语法规则
  12. 唐太宗灵州受降【会盟】的意义
  13. 从今往后要认真记录自己的成长啦
  14. uni-app的video禁止快进及seek()上的小坑
  15. 2018/12/22
  16. 什么产品适合做海外众筹
  17. 使用Windows10搭建服务器 ——一次虚拟机实验记录
  18. CodeChef - COVERING 高维前后缀和 + 容斥原理
  19. Ubnt EdgeMax 3322 ddns更新方法
  20. Linux入门合集(入门一篇就够了!)

热门文章

  1. java中构造方法和方法全面解析
  2. Android面试题集合
  3. RxJava 教程第一部分:入门之 关键的类
  4. mysql双主数据一致性_mysql双主复制的主备数据一致性知多少
  5. 小手取红色球C语言程序,C语言程序设计例精编.doc
  6. Java编程思想 第十三章:字符串
  7. 北京交通大学2018计算机硕士录取公示,2017年北京交通大学研究生录取名单!!!...
  8. python实战项目_11 个实战项目,掌握 Python 数据可视化
  9. iOS之Block总结以及内存管理
  10. Linux中chown和chmod的区别和用法(转)