闲扯游戏编程之html5篇--山寨版《flappy bird》源码
新年新气象,最近事情不多,继续闲暇学习记点随笔,欢迎拍砖。之前的〈简单游戏学编程语言python篇〉写的比较幼稚和粗糙,且告一段落。开启新的一篇关于javascript+html5的从零开始的学习。仍然以咱们有兴趣写的小游戏开始,〈flappy bird〉最近真是火的离谱,我也是昨天才开始找这个游戏试玩一下,果然难度不小,只能玩到33分了 ,哈哈。这游戏的评论网上已经铺天盖地了,这里不做过多评论,毕竟个人属于这个移动游戏圈子之外的。不过还是忍不住说一下,这游戏创意已经不算新颖,像素级的入门游戏精美度更是差上很多,开发难度也就是入门级的水平(相对来说)。不过作为菜鸟的门外汉来说,这游戏的设计思路和开发细节还是比较值得新手去研究和作为练手的案例研究一下。于是找到网上牛人放出的山寨版《flappy bird》之clumsy-bird,来简单研究一下源码吧,顺便从零学习一下canvas和Js一些东西,作为记录。
clumsy-bird的github地址为:https://github.com/ellisonleao/clumsy-bird
在线试玩地址:http://ellisonleao.github.io/clumsy-bird/(保证你的浏览器支持html5哟)
一、源码框架介绍
这个游戏呢,采用开源的html5游戏引擎melonJS作为框架,这个引擎比较轻量级,比较简单易懂。了解完源码整体框架就明白了整个引擎的框架了。
首先看一下游戏入口这里(game.js):大部分是框架相关的,非框架代码则是data的补充定义,用户按键事件绑定这些。
var game = { data : { score : 0, timer: 0, start: false }, "onload" : function () { if (!me.video.init("screen", 900, 600, true, 'auto')) { alert("Your browser does not support HTML5 canvas."); return; } me.audio.init("mp3,ogg"); me.loader.onload = this.loaded.bind(this); me.loader.preload(game.resources); me.state.change(me.state.LOADING); }, "loaded" : function () { me.state.set(me.state.MENU, new game.TitleScreen()); me.state.set(me.state.PLAY, new game.PlayScreen()); me.state.set(me.state.GAME_OVER, new game.GameOverScreen()); me.state.transition("fade", "#000", 100); me.input.bindKey(me.input.KEY.SPACE, "fly", true); me.input.bindTouch(me.input.KEY.SPACE); me.state.change(me.state.MENU); } };
onload 预加载的game.resources主要是图片如下的一些素材。
从界面加载完"loaded"函数看起,有三个状态 Menu 对应game.TitleScreen()是我们的标题界面处理,PLAY是我们的game.PlayScreen(),这个就是游戏开始的相关部分,即我们主要研究的部分--screens/play.js
这里面主要继承重写了ScreenObject的init初始化函数onResetEvent状态改变函数 及刷新界面函数update。
init定义了主要的变量管道长度this.pipeHoleSize = 1240;左右相邻管道出现的间隔时间this.pipeFrequency = 92;
update函数中处理逻辑即每隔pipeFrequency生成上下两个管道和碰撞体(这个实际并不渲染,后面代码中实体的alpha渲染做透明出现,只作为碰撞检测用),两个管道的位置简单画一下应该可求出(pipe1是下管道,保证中间距离是100,且最短管道要保证有100)
if (this.generate++ % this.pipeFrequency == 0){ var posY = this.getRandomInt( me.video.getHeight() - 100, 200 ); var posY2 = posY - me.video.getHeight() - this.pipeHoleSize; var pipe1 = new me.entityPool.newInstanceOf("pipe", this.posX, posY); var pipe2 = new me.entityPool.newInstanceOf("pipe", this.posX, posY2); var hitPos = posY - 100; var hit = new me.entityPool.newInstanceOf("hit", this.posX, hitPos); pipe1.renderable.flipY(); me.game.add(pipe1, 10); me.game.add(pipe2, 10); me.game.add(hit, 11); }
接下来是游戏界面状态处理函数onResetEvent
1 me.input.bindKey(me.input.KEY.SPACE, "fly", true); 2 //this.start = false; 3 game.data.score = 0; 4 game.data.timer = 0; 5 game.data.start = false; 6 7 me.game.add(new BackgroundLayer('bg', 1)); 8 9 var groundImage = me.loader.getImage('ground'); 10 11 this.ground = new me.SpriteObject( 12 0, 13 me.video.getHeight() - groundImage.height, 14 groundImage 15 ); 16 me.game.add(this.ground, 11); 17 18 this.HUD = new game.HUD.Container(); 19 me.game.world.addChild(this.HUD); 20 21 me.entityPool.add("clumsy", BirdEntity); 22 me.entityPool.add("pipe", PipeEntity, true); 23 me.entityPool.add("hit", HitEntity, true); 24 25 this.bird = me.entityPool.newInstanceOf("clumsy", 60, 26 me.game.viewport.height/2 - 100); 27 me.game.add(this.bird, 10); 28 this.posX = me.game.viewport.width; 29 30 //inputs 31 me.input.bindMouse(me.input.mouse.LEFT, me.input.KEY.SPACE); 32 me.state.transition("fade", "#fff", 100); 33 34 this.getReady = new me.SpriteObject( 35 me.video.getWidth()/2 - 200, 36 me.video.getHeight()/2 - 100, 37 me.loader.getImage('getready') 38 ); 39 me.game.add(this.getReady, 11); 40 var popOut = new me.Tween(this.getReady.pos).to({y: -132}, 2000) 41 .easing(me.Tween.Easing.Linear.None) 42 .onComplete(function(){ game.data.start = true;}).start(); 43 },
这里面主要完成界面背景层的加载,HUd作为分数显示,及重要游戏对象生成。
me.entityPool.add("clumsy", BirdEntity); 小鸟实体类
me.entityPool.add("pipe", PipeEntity, true); 管道实体类
me.entityPool.add("hit", HitEntity, true); 碰撞体类
this.bird = me.entityPool.newInstanceOf("clumsy", 60,
me.game.viewport.height/2 - 100);
me.game.add(this.bird, 10); 首先只有小鸟新实例对象生成,游戏正式开始才有管道等。EntityPool 就是作为引擎管理游戏中实例化对象而存在的。
二、游戏对象类的实现
这块者重介绍主重要的上面提到的那三个游戏对象类。(entities.js)
实现还是比较简单的,重写ObjectEntity重要的几个函数就行了。就是 init 和 update这两个函数,分别完成对象初始化和每帧的刷新。主要学习的是update里面逻辑的处理。这里主要介绍小鸟的处理,那两个基本上没多少代码处理。
1 var BirdEntity = me.ObjectEntity.extend({ 2 init: function(x, y){ 3 var settings = {}; 4 settings.image = me.loader.getImage('clumsy'); 5 settings.spritewidth = 85; 6 settings.spriteheight= 60; 7 8 this.parent(x, y, settings); 9 this.alwaysUpdate = true; 10 this.gravity = 0.2; 11 this.gravityForce = 0.01; 12 this.maxAngleRotation = Number.prototype.degToRad(30); 13 this.maxAngleRotationDown = Number.prototype.degToRad(90); 14 this.renderable.addAnimation("flying", [0, 1, 2]); 15 this.renderable.addAnimation("idle", [0]); 16 this.renderable.setCurrentAnimation("flying"); 17 this.animationController = 0; 18 this.updateColRect(10, 70, 2, 58); 19 }, 20 21 update: function(x, y){ 22 // mechanics 23 if (game.data.start) { 24 if (me.input.isKeyPressed('fly')){ 25 this.gravityForce = 0.01; 26 27 var currentPos = this.pos.y; 28 tween = new me.Tween(this.pos).to({y: currentPos - 72}, 100); 29 tween.easing(me.Tween.Easing.Exponential.InOut); 30 tween.start(); 31 32 this.renderable.angle = -this.maxAngleRotation; 33 }else{ 34 this.renderable.setCurrentAnimation("flying"); 35 this.gravityForce += 0.2; 36 this.pos.add(new me.Vector2d(0, me.timer.tick * this.gravityForce)); 37 this.renderable.angle += Number.prototype.degToRad(3) * me.timer.tick; 38 if (this.renderable.angle > this.maxAngleRotationDown) 39 this.renderable.angle = this.maxAngleRotationDown; 40 } 41 } 42 //manual animation 43 var actual = this.renderable.getCurrentAnimationFrame(); 44 if (this.animationController++ % 2){ 45 actual++; 46 this.renderable.setAnimationFrame(actual); 47 } 48 49 res = this.collide(); 50 var hitGround = me.game.viewport.height - (96 + 60); 51 var hitSky = -80; // bird height + 20px 52 if (res) { 53 if (res.obj.type != 'hit'){ 54 me.state.change(me.state.GAME_OVER); 55 return false; 56 } 57 me.game.remove(res.obj); 58 game.data.timer++; 59 return true; 60 }else if (this.pos.y >= hitGround || this.pos.y <= hitSky){ 61 me.state.change(me.state.GAME_OVER); 62 return false; 63 } 64 65 var updated = (this.vel.x != 0 || this.vel.y != 0); 66 if (updated){ 67 this.parent(); 68 return true; 69 } 70 return false; 71 }, 72 73 });
在小鸟初始化函数完成了动画帧的加载 三个序列动画[0,1,2]作为一次动画过程,
this.renderable.addAnimation("flying", [0, 1, 2]);this.renderable.addAnimation("idle", [0]);this.renderable.setCurrentAnimation("flying");
this.gravity = 0.2;this.gravityForce = 0.01;完成重力设定。
update函数中小鸟动画实现为
//manual animation
var actual = this.renderable.getCurrentAnimationFrame();
if (this.animationController++ % 2){
actual++;
this.renderable.setAnimationFrame(actual);
} 如上变实现小鸟的动画。
update函数主要功能除了上面,还主要负责完成用户按键处理和碰撞检测等处理逻辑:
当按下space键或者点击鼠标执行如下动作 (好像是利用tween增强了物理效果,具体没深究这里。)
this.gravityForce = 0.01;var currentPos = this.pos.y;
tween = new me.Tween(this.pos).to({y: currentPos - 72}, 100);
tween.easing(me.Tween.Easing.Exponential.InOut);
tween.start();
this.renderable.angle = -this.maxAngleRotation;
当没有按下相应键处理为
this.renderable.setCurrentAnimation("flying");
this.gravityForce += 0.2;
this.pos.add(new me.Vector2d(0, me.timer.tick * this.gravityForce));
this.renderable.angle += Number.prototype.degToRad(3) * me.timer.tick; 每帧增加0.2重力加速度值,并且y值每秒增加g*t位移,角度加大,就是实现小鸟下坠效果。
1 res = this.collide(); 2 var hitGround = me.game.viewport.height - (96 + 60); 3 var hitSky = -80; // bird height + 20px 4 if (res) { 5 if (res.obj.type != 'hit'){ 6 me.state.change(me.state.GAME_OVER); 7 return false; 8 } 9 me.game.remove(res.obj); 10 game.data.timer++; 11 return true; 12 }else if (this.pos.y >= hitGround || this.pos.y <= hitSky){ 13 me.state.change(me.state.GAME_OVER); 14 return false; 15 }
判断当前发生碰撞的对象是不是‘hit’,当不是之前的"hit"类型时说明发生碰撞对象改变,即小鸟撞管子上了 ,报游戏结束,并清理对象。
上面就是主要的代码逻辑了,感兴趣的话可以从git上fork一份自己研究一下,这里不做过多探讨了,闲扯之余顺便学习一下Js和html5也是收获。
以上素材及代码均来自网络,仅供学习研究,转载请注明出处,谢谢支持。