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的主要知识点,暂时先总结到这,后面有时间还会陆续更新。

 

更新日志

Demo

  2017/4/9  更新角色跳跃

  2017/4/21  更新角色冲刺

  2017/5/1  更新角色状态机

  2017/5/16    更新角色攻击动画

posted @ 2017-03-29 08:15  逐影  阅读(1802)  评论(4编辑  收藏  举报