HTML5 2D平台游戏开发#1
在Web领域通常会用到一组sprite来展示动画,这类动画从开始到结束往往不会有用户参与,即用户很少会用控制器(例如鼠标、键盘、手柄、操作杆等输入设备)进行操作。但在游戏领域,sprite动画与控制器的操作是密不可分的。最近在写一个小游戏,涉及到很多知识点,于是打算把这些内容通过一些Demo总结出来备忘。
这是第一阶段的运行效果,用键盘A、D来控制人物左右移动,空格/K控制人物跳跃,U键冲刺:
动画帧播放器
要生成一组动画,首先需要一个能够播放各个动画帧的方法。新建一个构造函数Animation:
/** *@param frames {Array} 元数据 *@param options {Object} 可选配置 */ function Animation(frames,options) { this.frames = frames || [{ x: 0, y: 0, w: 0, h: 0, duration: 0 }]; this.options = options || { repeats:false, //是否重复播放 startFrame:0 //起始播放的位置 }; }
说明一下上面的代码,函数有两个参数,其中frames为元数据(metaData),用于标识一组sprite的坐标信息,为何需要这个数据呢,先来看一张图:
可以发现每一帧的sprite大小都不一致,特别是第二排,并不是规则的sprite,因此需要将各帧的位置大小等信息标识出来。例如这是第一排的元数据:
//人物站立时的帧动画 var idle = [ {x:0,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:2000}, {x:40,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:80,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:120,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:160,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:200,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:240,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:280,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120}, {x:320,y:0,w:40,h:41,offsetY:0,offsetX:-5,duration:120} ];
其中x,y代表所使用sprite的位置,w,h表示该sprite的宽高,offset用于修正sprite的位置,duration表示该sprite持续的时间。
题外话:如果手工处理这些sprite信息是相当繁琐的,有一款软件叫做TexturePacker专门用来生成sprite sheets。
接着新建一个AnimationPlayer来管理Animation:
//@param animation {Object} Animation的实例 function AnimationPlayer(animation) { var ani = animation || new Animation(); this.length = 0; //标记该组sprite中一共有几个动作 //当前组的sprite中正在执行的动作,例如idle[1]表示正在进行idle组中的第二个动画帧 this.frame = undefined; this.index = 0; this.elapsed = 0; //标记每帧的运行时间 this.setAnimation(ani); this.reset(); }
//重置动画 AnimationPlayer.prototype.reset = function() { this.elapsed = 0; this.index = 0; this.frame = this.animation.frames[this.index]; }; AnimationPlayer.prototype.setAnimation = function(animation) { this.animation = animation; this.length = this.animation.frames.length; }; AnimationPlayer.prototype.update = function(dt) { this.elapsed += dt; if (this.elapsed >= this.frame.duration) { this.index++; this.elapsed -= this.frame.duration; } if (this.index >= this.length) { if (this.animation.options.repeats) this.index = this.animation.options.startFrame; else this.index--; } this.frame = this.animation.frames[this.index]; };
最后在使用的时候将其实例化:
//站立 var animation = new Animation(idle, { repeats: true, startFrame: 0 }); var playerIdle = new AnimationPlayer(animation); //移动 var animation2 = new Animation(move, { repeats: true, startFrame: 0 }); var playerMove = new AnimationPlayer(animation2);
游戏循环
游戏运行的机制就是在每一次GameLoop中更新所有游戏元件的状态,例如更新元件的位置,碰撞检测,销毁元件等等。大体来说代码一般都具有以下结构:
(function render() { //清除画布 context.clearRect(0,0,canvas.width,canvas.height); //执行游戏逻辑 //将更新状态后的元件重新绘制到画布上 requestAnimationFrame(render); //进入下一次游戏循环 })();
在本Demo的GameLoop中主要执行的逻辑有:
- 计算本次GameLoop与上次间隔的时间
基于时间的运动(time-base)是保证游戏运行良好的关键,假设有两台设备,一台每1秒执行一次游戏循环,另一台每2秒执行一次,并且物体以每次5px的速度移动,那么在2秒后第一台设备中的物体移动了2X5=10px,第二台设备中的物体移动了1X5=5px。很显然,经过相同的时间但最终物体达到了不同的位置,这是不合理的。如果采用基于时间的运动,则通过公式s += vt可以发现,第一台设备在经过两秒后移动的距离为5X1+5X1=10px,第二台设备移动的距离为5X2=10px,于是两台设备达到了一致的效果。更新后的render方法代码如下:
var lastAnimationFrameTime = 0, elapsed = 0, now; (function render() { //清除画布 context.clearRect(0,0,canvas.width,canvas.height); now = +new Date; if (lastAnimationFrameTime !== 0) { elapsed = Math.min(now - lastAnimationFrameTime, 16); } lastAnimationFrameTime = now; //执行游戏逻辑 //将更新状态后的元件重新绘制到画布上 requestAnimationFrame(render); //进入下一次游戏循环 })();
- 检测输入并绘制元件
if (key[65]) { //按下A键 playerState = 'move'; direction = 0; x -= moveSpeed; } else if (key[68]) { //按下D键 playerState = 'move'; direction = 1; x += moveSpeed; } else { playerState = 'idle'; } currentMotion = motion[playerState]; currentMotion.update(elapsed); if (direction === 1) { ctx.drawImage(img, currentMotion.frame.x + currentMotion.frame.offsetX, currentMotion.frame.y, currentMotion.frame.w, currentMotion.frame.h, x, 300, currentMotion.frame.w * 1.5, currentMotion.frame.h * 1.5); } else { //图片翻转,如有需要可以复习以前总结的知识点 /*http://www.cnblogs.com/undefined000/p/flip-an-image-with-the-html5-canvas.html*/ ctx.save(); ctx.scale( - 1, 1); ctx.drawImage(img, currentMotion.frame.x + currentMotion.frame.offsetX, currentMotion.frame.y, currentMotion.frame.w, currentMotion.frame.h, -currentMotion.frame.w * 1.5 + currentMotion.frame.offsetX - x, 300, currentMotion.frame.w * 1.5, currentMotion.frame.h * 1.5); ctx.restore(); }
游戏暂停
如果在游戏运行期间窗口失去焦点,则应当暂停游戏,因为此时浏览器会以低帧率运行游戏以节省开销,这样导致的结果就是当玩家返回窗口时,deltaTime会有爆炸性的增长,从而使元件更新异常。最常见的是一些碰撞检测不能正常工作或者游戏人物高速移动。因此当窗口失去焦点时,应当暂停游戏。在主流浏览器中,可以用下面的代码标识暂停:
document.addEventListener('visibilitychange',function() { if (document.visibilityState === 'hidden') { paused = true; console.log('游戏暂停中'); } else { paused = false; console.log('游戏运行中'); } });
同时更新render方法:
(function render() { //省略部分代码以节省篇幅 if (paused) { setTimeout(function() { requestAnimationFrame(draw); },200); } else { //执行游戏逻辑 requestAnimationFrame(render); //进入下一次游戏循环 } })();
Summary
以上就是这个Demo的主要知识点,暂时先总结到这,后面有时间还会陆续更新。
更新日志
2017/4/9 更新角色跳跃
2017/4/21 更新角色冲刺
2017/5/1 更新角色状态机
2017/5/16 更新角色攻击动画