起步

让我们从一个最简单的弹幕效果开始:

module.exports = {
  mounted() {
    this.setInterval(3, (tick) => {
      this.emitBullets(8, (index) => ({
        display: 'scaly',
        origin: 'center',
        state: {
          rho: 12,
          face: tick / 20 + index / 4,
        },
        mutate(tick) {
          this.rho += 3 + tick / 60
          this.polarLocate()
        },
      }))
    })
  }
}

让我们来看看它是怎么做的:

  • mounted: 当弹幕被挂载在屏幕上时调用的钩子函数.
  • setInterval: 设定一个计时任务, 每隔 3 帧执行一次回调.
  • emitBullets: 发射 8 枚子弹, 子弹的详细配置在下面给出:
    • display: 'scaly': 设定子弹的样式为鳞弹.
    • origin: 'center': 设定子弹的发射源为中央.
    • state: 设定子弹的初始状态:
      • rho: 12: 子弹距离发射源的初始距离为 12.
      • face: tick / 20 + index / 4: 8 枚子弹等距排列, 发射源每 20 帧转过一个平角.
    • mutate: 子弹运行中每一帧调用的钩子函数.
    • rho += 3 + tick / 60: 子弹的速度为 3, 加速度为 1/60.
    • polarLocate: 将极坐标切回直角坐标.

很简单吧? 下面就让我来简单介绍一些 web-stg 设计的基本概念.

一些基本概念

钩子函数

无论是子弹, 掉落物, 自机, 敌机还是整个弹幕系统, 都有着自己的生命周期. 如果我们想要制定它们的行为, 最简单的办法就是提供相应的钩子函数. 基本的钩子函数有:

  • mounted: 当一个实例被挂载在屏幕上时调用. 你可以在这里进行一些初始化工作.
  • mutate: 在一个实例正常生命周期的每一帧调用, 可以带一个参数 tick, 表示当前的帧数.
  • display: 在一个实例绘制时调用, 可以带一个参数 tick, 表示当前的 display 帧数.

此外, 有些对象还会提供其特有的钩子函数, 将在后面详细介绍.

物理帧与逻辑帧

上文中我们屡次提到了"帧"的概念. 虽然大家可能对它并不陌生, 但"帧"却是一个有误导性的词语. 事实上, 我们会有内外两层两层循环维护游戏的运转. 外循环负责显示, 调用绘制有关的函数并处理与显示器的垂直同步; 内循环则根据两次渲染之间的时间差规划每个实例的更新. 外循环执行的帧率称为物理帧(frame), 决定了用户看到的图案的刷新率; 内循环执行的帧率称为逻辑帧(tick), 决定了游戏实际运行的速率. 这种体系能够确保画面在低性能下保持精美, 同时也为 replay 的实现提供了保证.

由于不同设备的性能不同, 同一个游戏在不同设备上运行的物理帧数往往千差万别. 为了确保体验一致, 一切的计算应该基于逻辑帧数而非物理帧数. 因此, 我们使用当前的逻辑帧数作为钩子函数中传入的参数. 今后如果不加说明, 我们所说的"帧"均指代逻辑帧.