canvas 制作flappy bird(像素小鸟)全流程
flappy bird制作全流程:
一、前言
像素小鸟这个简单的游戏于2014年在网络上爆红,游戏上线一段时间内appleStore上的下载量一度达到5000万次,风靡一时,
近年来移动web的普及为这样没有复杂逻辑和精致动画效果,但是趣味十足的小游戏提供了良好的环境,
同时借助各大社交软件平台的传播效应,创意不断的小游戏有着良好的营销效果,得到了很多的关注。
此前在网上查询了很多关于这个小游戏的资料,但是大多杂乱无章,自己的结合相关教程将这个游戏的主要框架整理出来,供大家一起学习。
二、技术要点
基本JavaScript基础 ,canvas 基础, 面向对象的思想;
三、思路整理
整个游戏的逻辑比较简单:
首先游戏规则:鸟撞到管道上,地上要死亡,飞到屏幕外要死亡。
其次:鸟在飞翔的过程中,会掉落,类似落体运动,需要玩家不断点击屏幕让鸟向上飞。
再次就是:鸟和背景元素的相对移动的过程,鸟不动,背景左移。
将整个游戏细化:
我们采用面向对象的思路来制作,具体的事物用构造函数来创建,方法放到构造函数的原形对象中。
游戏细化这个过程不是一蹴而就的,如果在没有相关指导的情况下,自己要不断的结合自己的想法去试错。
本人使用的方式是使用Xmind将流程以脑图的形式绘制下来,分块去做,不断细化记录自己的思路,最终呈现的效果如下:
(顺序按照图片中的序号去看 脑图、素材、及完整源码下载地址:http://pan.baidu.com/s/1c130V7M 想练习的同学可以点这里)
脑图分为三大块:1、准备阶段 2、主函数 3、游戏优化。
四、游戏实现:
现在结合脑图来逐步实现我们的游戏。
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、主函数分为两部分功能 ,简单说就是把图画上去,然后处理动态效果,再判断一下是否犯规。
2.1 小鸟的绘制:
小鸟本身有一个翅膀扇动的效果,和一个下落的过程。
翅膀扇动的过程是一张精灵图三幅画面的的切换(设置一个index属性,控制精灵图的位置),下落过程是其y坐标在画布上的移动();
所以小鸟的构造函数中应该包括(图源,x坐标,y坐标,速度,下落加速度,ctx(context画布))等参数。
这里需要注意几点:
- 小鸟的绘制采用canvas drawImage的九参数模式(分别是图片,原图的裁切起点,原图的宽高,贴到画布上的位置,贴到画布上的宽高);
- 小鸟的翅膀扇动不能太快,所以我们设置一个阀门函数,当累计计时超过100ms的时候切换一下图片,然后在让累计计时减去100ms;
- 小鸟的下落需要用到一定物理知识,但是都很简单啦。 我们都是通过速度×时间来实现;
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);
}
简单判读gameover
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();
}
加入小鸟旋转效果
当然最后不要忘记对管道碰撞的判断,在这里再修正一遍。
事实上如果打算加入旋转效果,上一次的修正不需要,你会发现很多重复工。
最后做出的效果如下:
主体效果和逻辑已经全部实现。更多的效果可以自行添加。
如果想自己练习一下,请点击游戏细化部分的链接下载相关素材和全部源码。
canvas 制作flappy bird(像素小鸟)全流程的更多相关文章
- 【Unity3D基础教程】给初学者看的Unity教程(四):通过制作Flappy Bird了解Native 2D中的RigidBody2D和Collider2D
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 在第一篇文章[Unity3D基础教程] ...
- 【转】通过制作Flappy Bird了解Native 2D中的RigidBody2D和Collider
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 在第一篇文章[Unity3D基础教程] ...
- 通通制作Html5小游戏——第二弹(仿flappy bird像素鸟)
亲爱的博友们,我又回来啦~因为我们技术宅的思想只有技术宅懂得,好不容易写了点好玩的东西发QQ空间,结果只有11的UV,0回复....10分钟ps一个女神的素描效果发QQ空间朋友圈,一大堆回复加赞,作为 ...
- 【Unity3D基础教程】给初学者看的Unity教程(三):通过制作Flappy Bird了解Native 2D中的Sprite,Animation
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 上一次我们讲了MonoBehaviou ...
- 【转】通过制作Flappy Bird了解Native 2D中的Sprite,Animation
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 引子 上一次我们讲了MonoBehaviou ...
- windows下如何制作和应用数字签名证书 全流程
目前我们在发布应用程序时,有时用户下载后会被360杀毒当做木马直接隔离.为应用程序可执行文件打上数字签名可以让360杀毒放宽检测规则.下文是讲述如何制作数字签名证书的过程. 需要准备的工具:makec ...
- 自己动手写游戏:Flappy Bird
START:最近闲来无事,看了看一下<C#开发Flappy Bird游戏>的教程,自己也试着做了一下,实现了一个超级简单版(十分简陋)的Flappy Bird,使用的语言是C#,技术采用了 ...
- 用Phaser来制作一个html5游戏——flappy bird (一)
Phaser是一个简单易用且功能强大的html5游戏框架,利用它可以很轻松的开发出一个html5游戏.在这篇文章中我就教大家如何用Phaser来制作一个前段时间很火爆的游戏:Flappy Bird,希 ...
- 用Phaser来制作一个html5游戏——flappy bird (二)
在上一篇教程中我们完成了boot.preload.menu这三个state的制作,下面我们就要进入本游戏最核心的一个state的制作了.play这个state的代码比较多,我不会一一进行说明,只会把一 ...
随机推荐
- 一步步开发自己的博客 .NET版(3、注册登录功能)
前言 这次开发的博客主要功能或特点: 第一:可以兼容各终端,特别是手机端. 第二:到时会用到大量html5,炫啊. 第三:导入博客园的精华文章,并做分类.(不要封我) 第四:做 ...
- MyBatis4:动态SQL
什么是动态SQL MyBatis的一个强大特性之一通常是它的动态SQL能力.如果你有使用JDBC或其他相似框架的经验,你就明白条件串联SQL字符串在一起是多么地痛苦,确保不能忘了空格或者在列表的最后的 ...
- direction和unicode-bidi
在做多语言页面,接触过阿利伯语.希伯来语的同学肯定了解书写方向的重要性,包括我们五四运动前的书写顺序也是从右到左的.css中 unicode-bidi和direction属性决定了HTML或XML文字 ...
- 提高(Android)开发效率的工具与网站
Dev_Tools 写这篇不是为了"水",是因为有些工具确实不错,但是换个电脑要找到或者在某个时候你想用但是你只知道存在了书签里.然后就只能下载chrome然后登录账户同步书签了. ...
- WCF学习之旅—WCF服务的批量寄宿(十三)
上接 WCF学习之旅—WCF服务部署到IIS7.5(九) WCF学习之旅—WCF服务部署到应用程序(十) WCF学习之旅—WCF服务的Windows 服务程序寄宿(十一) WCF学习之旅—WCF ...
- js中的事件委托
原文 http://itindex.net/detail/48661-js-事件 1,什么是事件委托:通俗的讲,事件就是onclick,onmouseover,onmouseout,等就是事件,委托呢 ...
- Android自定义控件之自定义属性
前言: 上篇介绍了自定义控件的基本要求以及绘制的基本原理,本篇文章主要介绍如何给自定义控件自定义一些属性.本篇文章将继续以上篇文章自定义圆形百分比为例进行讲解.有关原理知识请参考Android自定义控 ...
- 设计模式(一):“穿越火线”中的“策略模式”(Strategy Pattern)
在前段时间呢陆陆续续的更新了一系列关于重构的文章.在重构我们既有的代码时,往往会用到设计模式.在之前重构系列的博客中,我们在重构时用到了“工厂模式”.“策略模式”.“状态模式”等.当然在重构时,有的地 ...
- JQuery 加载 CSS、JS 文件
JS 方式加载 CSS.JS 文件: //加载 css 文件 function includeCss(filename) { var head = document.getElementsByTagN ...
- Objective-C runtime的常见应用
用Objective-C等面向对象语言编程时,"对象"(object)就是"基本构造单元"(building block).开发者可以通过对象来存储并传递数据. ...