H5小游戏(2D)的一些总结

h5game

24 Jun 2019

Author:wuguanxi


简介

H5小游戏,基本上都是利用canvas画布及其API绘制而成,较少用到DOM。优点是全部都是JS,移植性较好,比用DOM要流畅。缺点是缺少CSS的支持,布局比较蛋疼。

游戏框架的选择

当然,你可以不用任何库或者框架,但是开发效率会比较低。框架上分2D和3D的框架,3D最出名的是Three.js,2D比较出名的有PIXI.js,Phaser,CreateJs,Egret,Laya等

具体介绍可以参考凹凸实验室的H5游戏开发:游戏引擎入门推荐

本文只探讨2D的H5小游戏,而我个人比较常用的2D框架是CreateJs,对于互动营销的小游戏来说是足够的了,不过他们更新是比较佛系的,下面出现的例子都会是以CreateJs为例。推荐浏览官网https://www.createjs.com

CreateJs的简单介绍

CreateJs由四部分组成,分别是绘图相关的EaselJS、加载器PreloadJS、动画库TweenJS、音频处理SoundJS。

一些套路流程

要完成一个简单的H5小游戏,其实是有一些固有套路可以应用的。一个完整的游戏大概都会有以下流程:

加载资源 -> 设置游戏参数 -> 初始化舞台 -> 渲染场景 -> 初始化Timeline -> 初始化Ticker -> 绑定事件 -> 开始游戏 -> 游戏结束

加载资源

一个游戏通常包含很多静态资源,如静态图片、音频、精灵图等等。框架都有自带或者推荐配套使用的资源加载器,帮助加载和管理静态资源,CreateJs默认的就是PreloadJs

const queue = new createjs.LoadQueue(false,'','Anonymous');
queue.installPlugin(createjs.Sound); // 加载声音的插件
queue.loadManifest([
  {id:"car",src: "images/car.png"},
  {id:"sound", src:"sound.mp3"},
]);

设置游戏参数

游戏参数这里包含几个类别:

回调函数:游戏结束的回调函数,游戏过程一些特殊事件的回调函数...

初始属性:生命值、分数、是否静音、游戏难度...

分支选项:是否显示教程、进入第几个关卡...

挂靠的DOM:canvas

其实H5游戏也是前端UI展示的一部分,不应该有状态的储存,所以会通过参数的方式传递。

初始化舞台

创建一个舞台(Stage),一个最基础的容器,确定其宽高并绑定页面上的canvas。

const stageDom = document.getElementById(stageId);
const stage = new createjs.Stage(this.stageDom);

渲染场景

在Stage上我们可以使用 图像(Bitmap)、图形(Shape) 和 图形的填充(Graphics)、雪碧的位图动画(Sprite、SpriteSheet)、简单文本(Text)、容器(Container)。

create

canvas的绘画API很强大,但是它只定义了各种画画的方法,缺少图层的概念。而CreateJs的EaselJS则抽象出类似图层的显示基类(DisplayObject),DisplayObject定义了x、y、alpha等基础属性,Bitmap、Shape、Sprite、Text、Container都是其子类。

图像(Bitmap):Bitmap是一块专门将加载好的静态图片添加到其中的一块图层,除了图片还可以是另一个canvas。

const id = "car";
const bitmap = new createjs.Bitmap(queue.getResult(id));

形状(Shape) 和 图形的数据(Graphics):Shape和Graphics一般成对出现,Shape创建一个图形的图层,Graphics则是定义了这块图层上画的是什么形状(矩形、圆形、线段、描边等的大小、位置、颜色)。

const rect = new createjs.Shape();
const x = 0;
const y = 0;
const width = 100;
const height = 100;
const color = "rgba(255,188,5,0.8)";
rect.graphics.beginFill(color).drawRect(x, y, width, height);

雪碧的位图与动画(Sprite、SpriteSheet):Sprite和SpriteSheet也是成对出现。SpriteSheet定义一张精灵图上不同帧的位置,和帧动画的序列,Sprite这是其容器图层。

// 定义帧动画结构
const spriteSheet = new createjs.SpriteSheet({
  images: [someImg], // 雪碧图片
  frames: someFrames, // 图片帧划分
  animations: {
    play: { // 动画名字
      frames: [0, 1, 2, 3, 4, 5, 6, 7], // 用到哪些帧及顺序 如果只有一帧就是静态的图
      speed: 0.16, // 播放速度
      next:null
    },
  },
});
// 创建 sprite 实例播放帧动画
const sprite = new createjs.Sprite(spriteSheet);

文本(Text):同样的,Text定义其图层上的文字内容、大小、颜色等等...

const text = new createjs.Text("some text",'36px Arial', "#FFFFFF");

容器(Container):可以把Bitmap、Shape、Sprite、Text、Container集合起来。Stage也是一种容器,根容器。

const container =  new createjs.Container();
container.addChild(rect,text);

我们把创建的Bitmap、Shape、Sprite、Text、Container都添加到Stage中,就组成一棵树形结构的DisplayObject Tree。

stage.addChild(bitmap,container,spriteSheet);

当我们使用Satge.update()时会遍历把Bitmap、Shape、Sprite、Text都渲染到canvas中(深度优先)。

stage.update();

Ticker

Ticker(心跳器)其实是一个定时器,它可以设置不同的速率(FTP)。

一般我们会将其设置为RAF(requestAnimationFrame)。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。

同时Ticker可以监听“tick”事件,每一tick时都会执行。Ticker是游戏的核心,基本上所有的动画、行为都会在“tick”里进行计算并渲染完成。

“tick”事件的回调函数接受参数e,包含一个paused属性,可以通过paused属性操作控制整个游戏循环的进行与暂停。

createjs.Ticker.timingMode = createjs.Ticker.RAF;  // 使用requestAnimationFrame模式
createjs.Ticker.addEventListener("tick", tickHandle); // 监听事件
createjs.Ticker.paused = true; // 游戏还没开始,paused === true
function tickHandle(e){
  if(!e.paused) {  // 相当于全局的paused
    doSometing();  // 游戏逻辑
    stage.update();  // 将stage.update()包含在(!e.paused)条件下;
  } 
}

Timeline

Timeline与Ticker有点相似,都是与时间有关的一种表示。不过Ticker是永远向前的,Timeline则有回退机制(playBack)。Timeline会在后面动画部分,继续详细说明。

绑定事件

前面已经说过了Ticker的“tick”事件,这个是游戏的核心部分。

CreateJs还有强大的事件系统,每一个DisplayObject都可以监听click,dblclick,mousedown,pressmove,pressup等事件。

其原理是在stage这个根容器挂靠的canvas上监听DOM事件“dblclick”、“mousedown”,监听window上“mouseup”,“mousemove”事件,再通过换算转化为click,dblclick,mousedown,pressmove,pressup等,再广播到 DisplayObject Tree中。对应的方法是stage.enableDOMEvents。

stage 初始化时,enableDOMEvents是默认开启的 stage.enableDOMEvents(true)。所以如果中途将stage挂载的canvas替换调(例如Vue的if),所有事件都会失效。

// 正确的替换方法
  stage.enableDOMEvents(false); // 卸载事件
  stage.canvas = otherStageDom; // satge的canvas挂载新的DOM
  stage.enableDOMEvents(true); // 绑定新DOM的事件

开始游戏

游戏开始,将Ticker的paused置为false,开始游戏具体逻辑。

  createjs.Ticker.paused = false; // 将ticker的paused至为false
  function tickHandle(e){
    if(!e.paused) {  // 这里开始执行
      doSometing();  // 游戏逻辑
      stage.update();  // 渲染
    } 
  }

游戏结束

每个游戏都会设置一个结束条件,可能是时间或者生命值。一旦触发这个game over的条件,就可以将createjs.Ticker.paused设为true,卸载事件参数恢复默认值,并执行预先传入的game over回调函数。

  if (gameOver) {
    createjs.Ticker.paused = true; // 将ticker的paused至为false
    clear() // 卸载事件参数恢复默认值
    gameOverCallback(something); // 执行预先传入的game over回调函数
  }

至此,整个游戏流程结束,下面在说一些细节。

动画

游戏里会有大量涉及动画的处理。

其实电脑动画的本质都是一样,它们都没有连续性动起来,而是就像楼梯层级那样一级一级不断的跃迁,当两级之间的时间足够少(1/16秒),人的大脑形成错觉,误以为是连续的运动。

这里我根据动画来源分3种形式,分别是tick动画、SpriteSheet帧动画、Tween动画。

Tween动画(补间动画)

补间动画是指定一个属性的初始值、结束的值、时间间隔、缓动曲线,而进行改变的一种动画,中间数据都是通过计算得出的。一般是用来改变DisplayObject对显示有效的属性如alpha、scaleX、scaleY、rotation、x、y等等。

CreateJs中专门建了一个TweenJS的库来处理补间动画。

TweenJS十分强大,采用类似JQuery的链式调用API。

createjs.Tween
  .get(target,{loop:false})  // 选取一个 DisplayObject
  .to({alpha:1,y:-20 /* 变化的属性 */},300/* 动画时长 */)  // 定义一段补间动画
  .wait(300)  // 在上一个指令后面,定义一段等待时间
  .to({alpha:0,y:-40},300,createjs.Ease.bounceInOut/* 动画缓动效果 */)  // 在上一个指令后面,再定义一段补间动画
  .call(callback);  // 在上一个指令后面,定义回调函数

SpriteSheet帧动画

帧动画是指根据不同帧率切换不同位图而达到动画效果。

EaselJS中专门有SpriteSheet类来处理帧动画。

tick动画

tick动画是值在每一个“tick”事件里对游戏中的某个显示物体作出改动而成的动画。其实Tween动画和SpriteSheet帧动画都可以在tick动画里实现,但因为它们都拥有固定的模式而抽象出来。

tick动画适合没有固定模式,随时会根据游戏发展而变化的动画,如赛跑游戏里,背景开始是匀速运动的,当用户触发加深道具时会突然加速。

tick动画还适合Graphics类的图形数据改变而成的动画。

tick动画要注意的一点是,因为每一tick的触发间隔不是一个确定值,RAF模式下稳定时是1/60秒,但如果页面有卡顿,这个时间会增大,所以不能在每一次tick时间里增加一个定值,而是要根据具体的时间间隔乘以一个单位量。

const unit = 0.01; // 0.01每ms
function tickHand(e){
  const {delta,paused} = e;  // delta 是离上一次tick的时间差单位ms
  if (!paused) {
    DisplayObject.y += delta * unit;
    stage.update();
  }
}

音频

音效在游戏里也是不可或缺的,CreateJs自带音频工具SoundJS,可以很方便对音频进行操作。

createjs.Sound.play("sound");  // sound是前面PreloadJs预先加载好的声音文件

下面的内容都是createJs缺少的,或者说现版本还没有的,但是会经常用到

Timeline

注:TweenJS也有Timeline,但我这里的定义和它不一样,而且未来版本TweenJS会移除Timeline

所有动画,都可以看成是在时间轴上的变化,可以看成是播放器的进度条,可以播放(play)、暂停(pause)、停止(stop)、回播(playBack),每个动画可以有独立的时间轴,又或者共用一条时间轴。这样就可以令动画一起播放或者暂停,或者回退了。

Timeline应用Demo

碰撞检查

CreateJs没有碰撞检测,它的hitTest只是检测点击是否在图形上。 鉴于游戏机制,有时候可能并不需要真正的碰撞检测,只对齐x、y坐标也会实现效果。 要实现碰撞检测的话,需要自己写一套逻辑。

碰撞检查的向量实现

更多的CreateJs参考

参考文章:

canvas 动画库 CreateJs 之 EaselJS(上篇)

canvas 动画库 CreateJs 之 EaselJS(下篇)

createJS实战深入解析