【教程】HTML5+JavaScript编写flappy bird

     作者: 风小锐      新浪微博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>




































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值