像素小鸟这个简单的游戏于2014年在网络上爆红,游戏上线一段时间内appleStore上的下载量一度达到5000万次,风靡一时,web
近年来移动web的普及为这样没有复杂逻辑和精致动画效果,可是趣味十足的小游戏提供了良好的环境,canvas
同时借助各大社交软件平台的传播效应,创意不断的小游戏有着良好的营销效果,获得了不少的关注。后端
此前在网上查询了不少关于这个小游戏的资料,可是大多杂乱无章,本身的结合相关教程将这个游戏的主要框架整理出来,供你们一块儿学习。浏览器
基本JavaScript基础 ,canvas 基础, 面向对象的思想;网络
首先游戏规则:鸟撞到管道上,地上要死亡,飞到屏幕外要死亡。app
其次:鸟在飞翔的过程当中,会掉落,相似落体运动,须要玩家不断点击屏幕让鸟向上飞。框架
再次就是:鸟和背景元素的相对移动的过程,鸟不动,背景左移。dom
咱们采用面向对象的思路来制做,具体的事物用构造函数来建立,方法放到构造函数的原形对象中。ide
游戏细化这个过程不是一蹴而就的,若是在没有相关指导的状况下,本身要不断的结合本身的想法去试错。函数
本人使用的方式是使用Xmind将流程以脑图的形式绘制下来,分块去作,不断细化记录本身的思路,最终呈现的效果以下:
(顺序按照图片中的序号去看 脑图、素材、及完整源码下载地址:http://pan.baidu.com/s/1c130V7M 想练习的同窗能够点这里)
脑图分为三大块:一、准备阶段 二、主函数 三、游戏优化。
如今结合脑图来逐步实现咱们的游戏。
1.设置canvas画布,准备图片数据,当图片加载完成后执行回调函数;
<canvas id="cvs" width="800" height="600"></canvas> <script> var imglist = [ { "name":"birds","src":"res/birds.png"}, { "name":"land","src":"res/land.png"}, { "name":"pipe1","src":"res/pipe1.png"}, { "name":"pipe2","src":"res/pipe2.png"}, { "name":"sky","src":"res/sky.png"} ]; var cvs = document.getElementById("cvs"); var ctx = cvs.getContext("2d"); </script>
这里这个入口函数的设置要注意,必须保证图片资源加载完成后再执行其余操做,每加载一张图片咱们让imgCount--,减到0的时候再执行主函数;
function load (source, callback ){ var imgEls={}; var imgCount=source.length; for (var i = 0; i < imgCount; i++) { var name = source[i].name; var newImg = new Image (); newImg.src = source[i].src; imgEls[name] = newImg; imgEls[name].addEventListener("load",function(){ imgCount--; if(imgCount==0){ callback(imgEls); }; }) }; };
主循环的设置:这里咱们不使用setInterval来控制循环次数,咱们使用一个叫requestAnimationFrame()的定时器
由于setInterval会产生时间偏差,setInterval只能根据时间来移动固定距离。
这对于轮播图一类几千毫秒切换一次的动做来讲并无什么关系,可是对于咱们16-18毫秒绘制一次的动画是很是不许确的;
requestAnimationFrame()这个定时器的好处是根据浏览器的性能来执行一个函数,咱们用来获取两次绘制的间隔时间;
移动距离的计算改变成速度×间隔时间的方式,来解决绘图不许确的问题。
var preTime= Date.now(); //获取当前时间 function run(){ var now = Date.now(); //获取最新时间 dt = now - preTime; //获取时间间隔 preTime = now; //更新当前时间 ctx.clearRect(0,0,800,600); //清空画布 //--------------------------------------------- 绘制代码执行区域 //----------------------------------------------- requestAnimationFrame(run); //再次执行run函数 } requestAnimationFrame(run); //首次执行run函数;
二、主函数分为两部分功能 ,简单说就是把图画上去,而后处理动态效果,再判断一下是否犯规。
2.1 小鸟的绘制:
小鸟自己有一个翅膀扇动的效果,和一个下落的过程。
翅膀扇动的过程是一张精灵图三幅画面的的切换(设置一个index属性,控制精灵图的位置),下落过程是其y坐标在画布上的移动();
因此小鸟的构造函数中应该包括(图源,x坐标,y坐标,速度,下落加速度,ctx(context画布))等参数。
这里须要注意几点:
var Bird = function (img,x,y,speed,a,ctx){ this.img = img; this.x = x; this.y = y; this.speed = speed; this.a =a ; this.ctx = ctx; this.index = 0; //用于制做小鸟扇翅膀的动做 } Bird.prototype.draw = function (){ this.ctx.drawImage( this.img,52*this.index,0,52,45, this.x,this.y,52,45 ) } var durgather=0; Bird.prototype.update = function(dur){ //小鸟翅膀扇动每100ms切换一张图片 durgather+=dur; if(durgather>100){ this.index++; if(this.index===2){ this.index=0; } durgather -= 100; } //小鸟下落动做 this.speed = this.speed + this.a *dur; this.y = this.y + this.speed * dur; }
构造一个小鸟,而且将其动做刷新函数和绘制函数放置在咱们上面提到的绘制区域,此后构造出的相似对象都是这样的操做步骤:
这里须要注意的一点是,如何让小鸟顺畅的向上飞翔,其实仍是物理知识,因为加速度的做用,咱们给小鸟一个向上的顺时速度就能够了。
load(imglist ,function(imgEls){ //建立对象 //在主函数中建立一个小鸟 var bird = new Bird(imgEls["birds"],150,100,0.0003,0.0006,ctx); //主循环 var preTime= Date.now(); function run(){ var now = Date.now(); dt = now - preTime; preTime = now; ctx.clearRect(0,0,800,600); //--------图片绘制区域------- bird.update(dt) bird.draw(); //------------------------- requestAnimationFrame(run); } requestAnimationFrame(run); //设置点击事件。给小鸟一个瞬时的向上速度 cvs.addEventListener("click",function(){ bird.speed = -0.3; } ) })
效果以下:
2.2天空的绘制:
天空的绘制比较简单了,只要使用canvas drawImage的三参数模式就能够(图源,画布上的坐标)。
这里惟一注意的一点是,无缝滚动的实现,对于800*600分辨率这种状况咱们建立两个天空对象就能够了,可是为了适配更多的状况,咱们将这个功能写活
在天空的构造函数上加一个count属性设置几个天空图片,count属性让实例经过原形中的方法访问。后面涉及到重复出现的地面和管道,都给它们添加这种考虑。
var Sky = function(img,x,speed,ctx) { this.img = img ; this.ctx = ctx; this.x = x; this.speed = speed; } Sky.prototype.draw = function(){ this.ctx.drawImage( this.img ,this.x,0 ) } Sky.prototype.setCount = function(count){ Sky.count = count; } Sky.prototype.update = function(dur){ this.x = this.x+ this.speed * dur; if(this.x<-800){ //天空图片的宽度是800 this.x = Sky.count * 800 + this.x; //当向左移动了一整张图片后马上切回第一张图片 } }
同理在主函数中建立2个天空对象,并将更新函数和绘制函数放置在主循环的绘制区域;
setcount是用来设置无缝滚动的
注意一点:绘制上的图片是有一个层级关系的,不能把鸟画到天空的下面,那固然最后画鸟了,下面涉及到的覆盖问题再也不专门提到。
这里仅插入部分相关代码
var bird = new Bird(imgEls["birds"],150,100,0.0003,0.0006,ctx); var sky1 = new Sky(imgEls["sky"],0,-0.3,ctx); var sky2 = new Sky(imgEls["sky"],800,-0.3,ctx); //主循环 var preTime= Date.now(); function run(){ var now = Date.now(); dt = now - preTime; preTime = now; ctx.clearRect(0,0,800,600); //--------图片绘制区域------- sky1.update(dt); sky1.draw() sky2.update(dt); sky2.draw() sky1.setCount(2); bird.update(dt) bird.draw(); //-------------------------
2.3 地面的绘制
和天空的绘制彻底同样,因为地面图片尺寸较小,因此咱们要多画几个
var Land = function(img,x,speed,ctx){ this.img = img ; this.x = x; this.speed = speed; this.ctx = ctx ; } Land.prototype.draw = function(){ this.ctx.drawImage ( this.img , this.x ,488 ) } Land.prototype.setCount= function(count){ Land.count = count; } Land.prototype.update = function(dur){ this.x = this.x + this.speed * dur; if (this.x <- 336){ this.x = this.x + Land.count * 336; //无缝滚动的实现 } }
//建立----放置在建立区域 var land1 = new Land(imgEls["land"],0,-0.3,ctx); var land2 = new Land(imgEls["land"],336*1,-0.3,ctx); var land3 = new Land(imgEls["land"],336*2,-0.3,ctx); var land4 = new Land(imgEls["land"],336*3,-0.3,ctx); //绘制 ----放置在绘制区域 land1.update(dt); land1.draw(); land2.update(dt); land2.draw(); land3.update(dt); land3.draw(); land4.update(dt); land4.draw(); land1.setCount(4); //设置无缝滚动
2.4绘制管道
管道的绘制有一个难点是管道高度的肯定
要点:
var Pipe = function(upImg,downImg,x,speed,ctx){ this.x = x; this.upImg = upImg ; this.downImg = downImg; this.speed = speed; this.ctx = ctx; this.r = Math.random() *200 + 100; //随机高度+固定高度 } Pipe.prototype.draw = function(){ this.ctx.drawImage( this.upImg, this.x , this.r - 420 //管道图片的长度是420 ) this.ctx.drawImage( this.downImg, this.x , this.r +150 //管道中建的留白是150px ) } Pipe.prototype.setCount = function( count,gap ){ Pipe.count = count; Pipe.gap = gap; //这里是此次绘制的特别之处,加入了间隔 } Pipe.prototype.update =function( dur ){ this.x = this.x + this.speed*dur; if(this.x <- 52){ //管道宽度52px this.x = this.x + Pipe.count * Pipe.gap; //无缝滚动 this.r = Math.random() *200 + 150; //切换后的管道必须从新设置一个高度,给用户一个新管道的错觉 } }
//建立区域 var pipe1 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],400, -0.1,ctx); var pipe2 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],600, -0.1,ctx); var pipe3 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],800, -0.1,ctx); var pipe4 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],1000,-0.1,ctx); var pipe5 = new Pipe(imgEls["pipe2"],imgEls["pipe1"],1200,-0.1,ctx); //绘制区域 pipe1.update(dt); pipe1.draw(); pipe2.update(dt); pipe2.draw(); pipe3.update(dt); pipe3.draw(); pipe4.update(dt); pipe4.draw(); pipe5.update(dt); pipe5.draw(); pipe1.setCount(5,200); //设置管道数量和间隔
到这一步咱们的主要画面就制做出来了,是否是很简单呢O(∩_∩)O~
2.5 判断游戏是否犯规
//咱们改造一下主循环,设置一个gameover为false来控制函数的执行 //任何违规都会触发gameover=true; var gameover = false; if(bird.y < 0 || bird.y > 488 -45/2 ){ //碰到天和地 gameover = true ; } if(!gameover){ //若是没有结束游戏则继续游戏 requestAnimationFrame(run); }
2. 碰到管道结束游戏
//x和y到时候咱们传入小鸟的运动轨迹,每次重绘管道都有判断 Pipe.prototype.hitTest = function(x,y){ return (x > this.x && x < this.x + 52) //在管子横向中间 &&(! (y >this.r && y < this.r +150)); //在管子竖向中间 }
var gameover = false; gameover = gameover || pipe1.hitTest(bird.x ,bird.y); gameover = gameover || pipe2.hitTest(bird.x ,bird.y); gameover = gameover || pipe3.hitTest(bird.x ,bird.y); gameover = gameover || pipe4.hitTest(bird.x ,bird.y); gameover = gameover || pipe5.hitTest(bird.x ,bird.y); //逻辑终端 if(bird.y < 0 || bird.y > 488 -45/2 ){ gameover = true ; } if(!gameover){ requestAnimationFrame(run); }
到这一步咱们的游戏完成的差很少了,剩下的就是部分数据的修正
主要须要修正的一个点是碰撞的计算,由于咱们全部的碰撞都是按照小鸟图片的左上角计算的,这样就会有不许确的问题,经过测试很容易将这个距离加减修正了
3.游戏的优化
小鸟游戏的鸟儿在上下的过程当中会随着点击,抬头飞翔,或低头冲刺,如何作到这个效果呢?
答案就是移动canvas 坐标系和选择坐标系的角度 ctx.translate()和ctx.rotate();
为了防止整个坐标系的总体旋转移动
须要在小鸟绘制函数Bird.prototype.draw里面先后端加入ctx.save() 和ctx.restore()来单独控制小鸟画布
Bird.prototype.draw = function (){ this.ctx.save(); this.ctx.translate(this.x ,this.y); //坐标移动到小鸟的中心点上 this.ctx.rotate((Math.PI /6) * this.speed / 0.3 ); //小鸟最大旋转30度,并随着速度实时改变角度 this.ctx.drawImage( this.img,52*this.index,0,52,45, -52/2,-45/2,52,45 //这里很重要的一点是,整个小鸟坐标系开始移动 ) this.ctx.restore(); }
固然最后不要忘记对管道碰撞的判断,在这里再修正一遍。
事实上若是打算加入旋转效果,上一次的修正不须要,你会发现不少重复工。
最后作出的效果以下:
主体效果和逻辑已经所有实现。更多的效果能够自行添加。
若是想本身练习一下,请点击游戏细化部分的连接下载相关素材和所有源码。