作者: 风小锐 新浪微博ID:永远de风小锐 QQ:547953539 转载请注明出处
PS:新修复了两个bug,已下载代码的同学请查看一下
大学立即要毕业了。未来的公司为我们制定了在校学习计划。希望我们能在毕业之前掌握一些技术,当中有一项就是使用HTML5+JavaScript编写flappy bird这个小游戏。
相信大家和我一样,对这个小游戏还是非常熟悉的,控制小鸟跳过高矮不一的水管,并记录下每局得到的分数,对于亲手编写这个小游戏非常感兴趣,立即開始着手開始编写。
学习JavaScript的时间并不久,看了《JavaScript语言精粹》和《HTML5游戏开发》这两本书。感觉都还不错,推荐给想要学习HTML游戏开发的朋友。
游戏的编写基本上用了两个晚上,进度还是比較快的,这里附上几张完毕后的截图。从左到右依次是開始时、进行时、结束后的截图。
闲话说完了,以下送上游戏的制作流程。给初学JavaScript的同学一个參考。
我将整个游戏的制作分为以下几步:
一、游戏总体框架的搭建
这一部分包含html一些标签的设定,游戏循环框架的编写。
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我这里找到的地面图片是这个样子的 。因此想要绘制以下的完整地须要先计算出多少条能将下部填满,使用了
for(i=0;i就绘制出了下方地面的一帧图像,想要让地面动起来,我选择每一帧都让绘制的图片向左移动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这里的凝视比較具体,我简单解释一下,推断会先看鸟在x轴上是否与某一管道有重合,假设有则再检測y轴上是否有重合,两项都符合则游戏结束。地面则较为简单。backgroundheight-pipeoncanvas[0][1]) gamestate=2; //游戏结束 } //推断第二组管道 //假设鸟在x轴上与第二组管道重合 else if(birdx+birdwidth>pipeoncanvas[1][0]&&birdx+birdwidth backgroundheight-pipeoncanvas[1][1]) gamestate=2; //游戏结束 } //推断是否碰撞地面 if(birdy+birdheight>backgroundheight) gamestate=2; //游戏结束}
五、加入键盘和鼠标控制
想要在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); //鼠标是否在又一次開始button上 if(mx>boardx+14&&mx这里相比键盘多了鼠标位置获取和位置推断,目的是在游戏结束后推断是否点击了又一次開始button。boardy+boardheight-40&&my
至此,我们实现了这个游戏的基本逻辑,已经能窥见游戏的雏形了。这时的效果如flapp_1.html。
六、添加计分,加入開始提示和结束积分板
计分的实现比較简单,使用一全局变量就可以,在每次通过管道时分数加1,并依据全局变量的值将分数绘制在画布上。
var highscore=0; //得到过的最高分var score=0 //眼下得到的分数 //通过了一根管道加一分 if(birdx+birdwidth>pipeoncanvas[0][0]-movespeed/2&&birdx+birdwidth在绘制文本之前须要先指定字体和颜色pipeoncanvas[1][0]-movespeed/2&&birdx+birdwidth
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标签,这样在使用中就不会出现故障。
然后将定义的变量与标签绑定:
//各种音效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仓库中
地址
点击页面右边的download zipbutton就可以下载。
因为刚接触JavaScript不久,难免经验不足,对于代码中的缺陷与不足,欢迎大家批评和指正。
我的新浪微博ID ,期待与大家讨论各种问题。
PS:今天发生了灵异事件。我编辑好的文章发表后后半段变成了全是代码,希望不要再出问题。
。
。。
最后附上终于版完整的源码方便大家查看:
My flappy bird