'use strict'
// ******************************** 游戏时间对象 ********************************
const Time = new class {
/** 时间戳 */
timestamp = 0
/** 时间缩放率 */
timeScale = 1
/** 已过去时间 */
elapsed = 0
/** 累计游戏时间 */
playTime = 0
/** 增量时间 */
deltaTime = 0
/** 原生增量时间 */
rawDeltaTime = 0
/** 最大增量时间 */
maxDeltaTime = 35
/** 累计帧数 */
frameCount = 0
/** 累计帧时间 */
frameTime = 0
/** 每秒游戏帧数 */
fps = 0
/** 平均每帧游戏时间 */
tpf = Infinity
// 游戏速度过渡结束后回调
_callbacks = null
// 游戏速度过渡上下文
_transition = null
/** 初始化游戏时间管理器 */
initialize() {
this.timestamp = performance.now()
}
/** 重置游戏时间管理器 */
reset() {
this.timeScale = 1
this.playTime = 0
this._callbacks = null
this._transition = null
}
/**
* 更新当前帧的时间相关参数
* @param {number} timestamp 增量时间(毫秒)
*/
update(timestamp) {
let deltaTime = timestamp - this.timestamp
// 累计帧数和所用时间
this.frameCount++
this.frameTime += deltaTime
// 每秒计算FPS
if (this.frameTime > 995) {
this.fps = Math.round(this.frameCount / (this.frameTime / 1000))
this.tpf = this.frameTime / this.frameCount
this.frameCount = 0
this.frameTime = 0
}
// 限制增量时间 - 发生跳帧时减少视觉上的落差
deltaTime = Math.min(deltaTime, this.tpf + 1, this.maxDeltaTime)
// 计算游戏速度改变时的过渡
const _transition = this._transition
if (_transition !== null) {
_transition.elapsed = Math.min(
_transition.elapsed + deltaTime,
_transition.duration,
)
const {start, end, easing, elapsed, duration} = _transition
const time = easing.map(elapsed / duration)
this.timeScale = start * (1 - time) + end * time
// 过渡结束后执行回调
if (elapsed === duration) {
this._transition = null
this.executeCallbacks()
}
}
// 更新时间属性
this.timestamp = timestamp
this.deltaTime = this.timeScale * deltaTime
this.rawDeltaTime = deltaTime
this.elapsed += this.deltaTime
this.playTime += deltaTime
}
/**
* 设置增量时间缩放比例
* @param {number} timeScale 增量时间缩放比例
* @param {string} easingId 过渡曲线ID
* @param {number} duration 持续时间(毫秒)
*/
setTimeScale(timeScale, easingId, duration) {
if (duration > 0) {
// 过渡模式
this._transition = {
start: this.timeScale,
end: timeScale,
easing: Easing.get(easingId),
elapsed: 0,
duration: duration,
}
} else {
// 立即模式
this.timeScale = timeScale
this._transition = null
this.executeCallbacks()
}
}
/**
* 解析日期时间戳
* @param {number} timestamp 时间戳
* @param {string} format 日期格式
* @returns {string}
*/
parseDateTimestamp(timestamp, format) {
const date = new Date(timestamp)
return format.replace(/\{[YMDhms]\}/g, match => {
switch (match) {
case '{Y}': return date.getFullYear()
case '{M}': return date.getMonth() + 1
case '{D}': return date.getDate()
case '{h}': return date.getHours().toString().padStart(2, '0')
case '{m}': return date.getMinutes().toString().padStart(2, '0')
case '{s}': return date.getSeconds().toString().padStart(2, '0')
}
})
}
/**
* 设置时间缩放过渡结束回调
* @param {function} callback 回调函数
*/
onTransitionEnd(callback) {
if (this._callbacks !== null) {
this._callbacks.push(callback)
} else {
this._callbacks = [callback]
}
}
/** 执行时间缩放过渡结束回调 */
executeCallbacks() {
if (this._callbacks !== null) {
for (const callback of this._callbacks) {
callback()
}
this._callbacks = null
}
}
}
// ******************************** 计时器 ********************************
class Timer {
/** 计时器当前时间
* @type {number}
*/ elapsed
/** 计时器持续时间
* @type {number}
*/ duration
/** 计时器更新函数
* @type {Function}
*/ update
/** 计时器结束回调函数
* @type {Function}
*/ callback
/**
* 计时器对象
* @param {Object} options 选项
* @param {number} options.duration 持续时间
* @param {function} [options.update] 更新回调
* @param {function} [options.callback] 结束回调
*/
constructor({duration, update, callback}) {
this.elapsed = 0
this.duration = duration
this.update = update ?? Function.empty
this.callback = callback ?? Function.empty
}
/**
* 执行周期回调函数
* @param {number} deltaTime 增量时间(毫秒)
*/
tick(deltaTime) {
this.elapsed = Math.min(this.elapsed + deltaTime, this.duration)
this.update(this)
if (this.elapsed === this.duration) {
this.callback(this)
this.remove()
}
}
/**
* 添加计时器到列表
* @returns {Timer}
*/
add() {
Timer.timers.append(this)
return this
}
/**
* 从列表中移除计时器
* @returns {Timer}
*/
remove() {
Timer.timers.remove(this)
return this
}
// 计时器列表
static timers = []
/**
* 更新计时器
* @param {number} deltaTime 增量时间(毫秒)
*/
static update(deltaTime) {
const {timers} = this
let i = timers.length
while (--i >= 0) {
timers[i].tick(deltaTime)
}
}
/**
* 等待游戏时间(未使用)
* @param {number} duration 持续时间(毫秒)
* @returns {Promise<undefined>}
*/
static wait(duration) {
return new Promise(resolve => {
new Timer({duration, callback() {resolve()}}).add()
})
}
/**
* 等待原生时间(未使用)
* @param {number} duration 持续时间(毫秒)
* @returns {Promise<undefined>}
*/
static waitRaw(duration) {
return new Promise(resolve => {
setTimeout(resolve, duration)
})
}
}
// ******************************** 过渡曲线管理器 ********************************
const Easing = new class {
// 曲线映射表刻度(精度)
scale = 10000
startPoint = {x: 0, y: 0}
endPoint = {x: 1, y: 1}
remap = {}
easingMaps = {}
linear = {map: a => a}
/** 初始化 */
initialize() {
this.remap = Data.easings.remap
}
/**
* 获取过渡曲线映射表
* @param {string} key 过渡曲线ID或键
* @returns {EasingMap}
*/
get(key) {
// 返回缓存映射表
const id = this.remap[key]
const map = this.easingMaps[id]
if (map) return map
// 创建新的映射表
const easing = Data.easings[id]
if (easing) {
return this.easingMaps[id] = new EasingMap(
this.startPoint, ...easing.points, this.endPoint,
)
}
// 返回缺省值(线性)
return this.linear
}
}
// 过渡曲线映射表类
class EasingMap extends Float32Array {
/**
* 过渡曲线映射表
* @param {...{x: number, y: number}} points 控制点列表
*/
constructor(...points) {
const scale = Easing.scale
super(scale + 1)
const length = points.length - 1
let pos = -1
// 生成过渡曲线,键值对(X,Y)写入映射表
for (let i = 0; i < length; i += 3) {
const {x: x0, y: y0} = points[i]
const {x: x1, y: y1} = points[i + 1]
const {x: x2, y: y2} = points[i + 2]
const {x: x3, y: y3} = points[i + 3]
for (let n = 0; n <= scale; n++) {
const t0 = n / scale
const t1 = 1 - t0
const n0 = t1 ** 3
const n1 = 3 * t0 * t1 ** 2
const n2 = 3 * t0 ** 2 * t1
const n3 = t0 ** 3
const x = x0 * n0 + x1 * n1 + x2 * n2 + x3 * n3
const i = Math.round(x * scale)
if (i > pos && i <= scale) {
const y = y0 * n0 + y1 * n1 + y2 * n2 + y3 * n3
this[i] = y
if (i > pos + 1) {
for (let j = pos + 1; j < i; j++) {
this[j] = this[pos] + (this[i] - this[pos]) * (j - pos) / (i - pos)
}
}
pos = i
}
}
}
this[scale] = 1
}
/**
* 映射过渡时间
* @param {number} time 原生时间
* @returns {number} 处理后的过渡时间
*/
map(time) {
return this[Math.round(Math.min(time, 1) * Easing.scale)]
}
}