专栏名称: __ihhu
前段
目录
相关文章推荐
前端早读课  ·  【开源】TinyEngine开启新篇章,服务 ... ·  2 天前  
前端大全  ·  真的建议所有前端立即拿下软考(红利期) ·  5 天前  
前端大全  ·  React+AI 技术栈(2025 版) ·  4 天前  
商务河北  ·  经开区“美•强•优”三重奏 ·  4 天前  
51好读  ›  专栏  ›  __ihhu

H5游戏开发:套圈圈

__ihhu  · 掘金  · 前端  · 2018-01-09 03:02

正文

前言

虽然本文标题为介绍一个水压套圈h5游戏,但是窃以为仅仅如此对读者是没什么帮助的,毕竟读者们的工作生活很少会再写一个类似的游戏,更多的是面对需求的挑战。我更希望能举一反三,给大家在编写h5游戏上带来一些启发,无论是从整体流程的把控,对游戏框架、物理引擎的熟悉程度还是在某一个小难点上的思路突破等。因此本文将很少详细列举实现代码,取而代之的是以伪代码展现思路为主。

游戏 demo 地址: 游戏页面

希望能给诸位读者带来的启发

  1. 技术选型
  2. 整体代码布局
  3. 难点及解决思路
  4. 优化点

技术选型

一个项目用什么技术来实现,权衡的因素有许多。其中时间是必须优先考虑的,毕竟效果可以减,但上线时间是死的。

本项目预研时间一周,真正排期时间只有两周。虽然由项目特点来看比较适合走 3D 方案,但时间明显是不够的。最后保守起见,决定采用 2D 方案尽量逼近真实立体的游戏效果。

从游戏复杂度来考虑,无须用到 Egret 或 Cocos 这些“牛刀”,而轻量、易上手、团队内部也有深厚沉淀的 CreateJS 则成为了渲染框架的首选。

另外需要考虑的是是否需要引入物理引擎,这点需要从游戏的特点去考虑。本游戏涉及重力、碰撞、施力等因素,引入物理引擎对开发效率的提高要大于学习使用物理引擎的成本。因此权衡再三,我引入了同事们已经玩得挺溜的 Matter.js 。( Matter.js 文档清晰、案例丰富,是切入学习 web 游戏引擎的一个不错的框架)

整体代码布局

在代码组织上,我选择了面向对象的手法,对整个游戏做一个封装,抛出一些控制接口给其他逻辑层调用。

伪代码:

<!-- index.html -->

<!-- 游戏入口 canvas -->
<canvas id="waterfulGameCanvas" width="660" height="570"></canvas>
// game.js

/**
* 游戏对象
*/
class Waterful {
  // 初始化函数
  init () {}

  // CreateJS Tick,游戏操作等事件的绑定放到游戏对象内
  eventBinding () {}

  // 暴露的一些方法
  score () {}

  restart () {}

  pause () {}

  resume () {}

  // 技能
  skillX () {}
}


/**
* 环对象
*/
class Ring {
  // 于每一个 CreateJS Tick 都调用环自身的 update 函数
  update () {}

  // 进针后的逻辑
  afterCollision () {}
}
// main.js

// 根据业务逻辑初始化游戏,调用游戏的各种接口
const waterful = new Waterful()
waterful.init({...})

初始化

游戏的初始化接口主要做了4件事情:

  1. 参数初始化
  2. CreateJS 显示元素(display object)的布局
  3. Matter.js 刚体(rigid body)的布局
  4. 事件的绑定

下面主要聊聊游戏场景里各种元素的创建与布局,即第二、第三点。

一、CreateJS 结合 Matter.js

阅读 Matter.js 的 demo 案例,都是用其自带的渲染引擎 Matter.Render。但是由于某些原因(后面会说到),我们需要使用 CreateJS 去渲染每个环的贴图。

不像 Laya 配有和 Matter.js 自身用法一致的 Render,CreateJS 需要单独创建一个贴图层,然后在每个 Tick 里把贴图层的坐标同步为 Matter.js 刚体的当前坐标。

伪代码:

createjs.Ticker.addEventListener('tick', e => {
  环贴图的坐标 = 环刚体的坐标
})

使用 CreateJS 去渲染后,要单独调试 Matter.js 的刚体是非常不便的。建议写一个调试模式专门使用 Matter.js 的 Render 去渲染,以便跟踪刚体的运动轨迹。

二、环

本游戏的难点是要以 2D 去模拟 3D,环是一点,进针的效果是一点,先说环。

环由一个圆形的刚体,和半径稍大一些的贴图层所组成。如下图,蓝色部分为刚体:

伪代码:

class Ring {
  constructor () {
    // 贴图
    this.texture = new createjs.Sprite(...)
    // 刚体
    this.body = Matter.Bodies.circle(...)
  }
}

三、刚体

为什么把刚体半径做得稍小呢,这也是受这篇文章 推金币 里金币的做法所启发。推金币游戏中,为了达到金币间的堆叠效果,作者很聪明地把刚体做得比贴图小,这样当刚体挤在一起时,贴图间就会层叠起来。所以这样做是为了使环之间稍微有点重叠效果,更重要的也是当两个紧贴的环不会因翻转角度太接近而显得留白太多。如图:

为了模拟环在水中运动的效果,可以选择给环加一些空气摩擦力。另外在实物游戏里,环是塑料做成的,碰撞后动能消耗较大,因此可以把环的 restitution 值调得稍微小一些。

需要注意 Matter.js 中因为各种物理参数都是没有单位的,一些物理公式很可能用不上,只能基于其默认值慢慢进行微调。下面的 frictionAir 和 restitution 值就是我慢慢凭感觉调整出来的:

this.body = Matter.Bodies.circle(x, y, r, {
  frictionAir: 0.02,
  restitution: 0.15
})

四、贴图

环在现实世界中的旋转是三维的,而 CreateJS 只能控制元素在二维平面上的旋转。对于一个环来说,二维平面的旋转是没有任何意义的,无论如何旋转,都只会是同一个样子。

想要达到环绕 x 轴旋转的效果,一开始想到的是使用 rotation + scaleY。虽然这样能在视觉上达到目的,但是 scaleY 会导致环有被压扁的感觉,图片会失真:


显然这样的效果是不能接受的,最后我采取了逐帧图的方式,最接近地还原了环的旋转姿态:

注意在每个 Tick 里需要去判断环是否静止,若非静止则继续播放,并将贴图的 rotation 值赋值为刚体的旋转角度。如果是停止状态,则暂停逐帧图的播放:

// 贴图与刚体位置的小数点后几位有点不一样,需要降低精度
const x1 = Math.round(texture.x)
const x2 = Math.round(body.position.x)
const y1 = Math.round(texture.y)
const y2 = Math.round(body.position.y)

if (x1 !== x2 || y1 !== y2) {
  texture.paused && texture.play()
  texture.rotation = body.angle * 180 / Math.PI
} else {
  !texture.paused && texture.stop()
}
texture.x = body.position.x
texture.y = body.position.y

五、舞台

舞台需要主要由物理世界、背景图,墙壁,针所组成。

1. 物理世界

为了模拟真实世界环在水中的向下加速度,可以把 y 方向的 g 值调小:

engine.world.gravity.y = 0.2

左右重力感应对环的加速度影响同样可以通过改变 x 方向的 g 值达到:

// 最大倾斜角度为 70 度,让用户不需要过分倾斜手机
// 0.4 为灵敏度值,根据具体情况调整
window.addEventListener('deviceorientation', e => {
  let gamma = e.gamma
  if (gamma < -70) gamma = -70
  if (gamma > 70) gamma = 70
  this.engine.world.gravity.x = (e.gamma / 70) * 0.4
})

2. 背景图

本游戏布景为游戏机及海底世界,两者可以作为父容器的背景图,把 canvas 的位置定位到游戏机内即可。canvas 覆盖范围为下图的蓝色蒙层:

3. 墙壁

因为环的刚体半径比贴图半径小,因此墙壁刚体需要有一些提前位移,环贴图才不会溢出,位移量为 R - r(下图红线为墙壁刚体的一部分):

4. 针

为了模拟针的边缘轮廓,针的刚体由一个矩形与一个圆形所组成。下图红线描绘了针的刚体:

为什么针边缘没有像墙壁一样有一些提前量呢?这是因为进针效果要求针顶的平台区域尽量地窄。作为补偿,可以把环刚体的半径尽可能地调得更大,这样在视觉上环与针的重叠也就不那么明显了。

进针

进针是整个游戏的核心部分,也是最难模拟的地方。

进针后

两个二维平面的物体交错是不能产生“穿过”效果的:

除非把环分成前后两部分,这样层级关系才能得到解决。但是由于环贴图是逐帧图,分两部分的做法并不合适。

最后找到的解决办法是利用视觉错位来达到“穿过”效果:

具体做法是,当环被判定成功进针时,把环刚体去掉,环的逐帧图逐渐播放到 平放 的那一帧,rotation 值也逐渐变为 0。同时利用 CreateJS 的 Tween 动画把环平移到针底。

进针后需要去掉环刚体,平移环贴图,这就是上文为什么环的贴图必须由 CreateJS 负责渲染的答案。

伪代码:

// Object Ring
afterCollision (waterful) {
  // 平移到针底部
  createjs.Tween.get(this.texture)
    .to({y: y}, duration)

  // 消去刚体
  Matter.World.remove(waterful.engine.world, this.body)
  this.body = null

  // 接下来每一 Tick 的更新逻辑改变如下
  this.update = function () {
    const texture = this.texture

    if 当前环贴图就是第 0 帧 (环平放的那一帧) {
      texture.gotoAndStop(0)
    } else {
      每 5 个 Tick 往前播放一帧 (相隔多少 Tick 切换一帧可以凭感觉调整, 主要是为了使切换到平放状态的过程不显得太突兀)
    }

    // 使针大概在环中央位置穿过
    if (texture.x < 200) ++texture.x
    if (texture.x > 213 && texture.x < 300) --texture.x
    if (texture.x > 462) --texture.x
    if (texture.x > 400 && texture.x < 448) ++texture.x

    // 把环贴图尽快旋转到水平状态
    let rotation = Math.round(texture.rotation) % 180
    if (rotation < 0) rotation += 180

    if (rotation > 0 && rotation <= 90) {
      texture.rotation = rotation - 1
    } else if (rotation > 90 && rotation < 180) {
      texture.rotation = rotation + 1
    } else if (frame === 0) {
      this.update = function () {}
    }
  }

  // 调用得分回调函数
  waterful.score()
}

进针判断

进针条件:

1. 到达针顶

到达针顶是环进针成功的必要条件。

2. 动画帧

环必须垂直于针才能被顺利穿过,水平于针时应该是与针相碰后弹开。

当然条件可以相对放宽一些,不需要完全垂直,下图红框内的6帧都被规定为符合条件:

为了降低游戏难度,我规定超过针一半高度时,只循环播放前6帧:

this.texture.on('animationend', e => {
  if (e.target.y < 400) {
    e.target.gotoAndPlay('short')
  } else {
    e.target.gotoAndPlay('normal')
  }
})

3. rotation 值

同理,为了使得环与针相垂直,rotation 值不能太接近 90 度。经试验后规定 0 <= rotation <= 65 或 115 <= rotation <= 180 是进针的必要条件。

下图这种过大的倾角逻辑上是不能进针成功的:

初探:

一开始我想的是把三维的进针做成二维的“圆球进桶”,进针的判断也就归到物理事件上面去,不需要再去考虑。

具体做法如下图,红线为针壁,当环刚体(蓝球)掉入桶内且与 Sensor (绿线)相碰,则判断进针成功。为了使游戏难度不至于太大,环刚体必须设置得较小,而且针壁间距离要比环刚体直径稍大。

这种模拟其实已经能达到不错的效果了,但是一个技能打破了这种思路的可能性。

产品那边想做一个放大技能,当用户使用此技能时环会放大,更容易套中。但是在桶口直径不变的情况下,只是环贴图变大并不能降低游戏难度。如果把环刚体变小,的确容易进了,但相近的环之间的贴图重叠范围会很大,这就显得很不合理了。

改进:

“进桶”的思路走不通是因为不兼容放大技能,而放大技能改变的是环的直径。因此需要找到一种进针判断方法在环直径小时,进针难度大,直径大时,进针难度小。

下面两图分别为普通环和放大环,其中红色虚线表示水平方向的内环直径:

在针顶设置一小段探测线(下图红色虚线),当内环的水平直径与探测线相交时,证明进针成功,然后走进针后的逻辑。在环放大时,内环的水平直径变长,也就更容易与探测线相交。







请到「今天看啥」查看全文