让我们从一个最简单的弹幕效果开始:
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 的实现提供了保证.
由于不同设备的性能不同, 同一个游戏在不同设备上运行的物理帧数往往千差万别. 为了确保体验一致, 一切的计算应该基于逻辑帧数而非物理帧数. 因此, 我们使用当前的逻辑帧数作为钩子函数中传入的参数. 今后如果不加说明, 我们所说的"帧"均指代逻辑帧.