19. 计时器,缓动,标签
3.15,周日,杀戮尖塔 2 继续。
Timer
lua
local Timer = {}
Timer.__index = Timer
-- 空函数,用作默认的回调占位符,避免产生 nil 调用错误
local function _nothing_() end
-- 更新单个定时器句柄的内部函数
local function updateTimerHandle(handle, dt)
-- handle 包含的字段说明:
-- time = <number> -- 已经过去的时间
-- after = <function> -- 计时结束后执行的回调
-- during = <function> -- 计时期间每一帧执行的回调
-- limit = <number> -- 目标延迟时间
-- count = <number> -- 循环触发的次数
-- 累加经过的时间
handle.time = handle.time + dt
-- 触发 during 回调,传入当前帧的时间 dt,以及剩余时间(不能小于0)
handle.during(dt, math.max(handle.limit - handle.time, 0))
-- 如果经过的时间达到了设定的限制,并且还有剩余的触发次数
while handle.time >= handle.limit and handle.count > 0 do
-- 执行 after 回调。如果回调明确返回 false,则提前终止循环定时器
if handle.after(handle.after) == false then
handle.count = 0
break
end
-- 扣除一个周期的时间,保持高精度(避免帧率波动导致定时器变慢)
handle.time = handle.time - handle.limit
-- 剩余次数减 1
handle.count = handle.count - 1
end
end
-- 每一帧调用的主更新函数
function Timer:update(dt)
-- 注意:定时器的回调函数内部可能会创建新的定时器或取消现有定时器。
-- 如果直接在 pairs(self.functions) 中迭代并修改它,会导致未定义行为(漏处理或报错)。
-- 所以我们需要先将当前活动的定时器拷贝到一个临时表 to_update 中。
local to_update = {}
for handle in pairs(self.functions) do
to_update[handle] = handle
end
-- 遍历临时表进行更新
for handle in pairs(to_update) do
-- 确保在迭代过程中,该定时器没有被其他逻辑取消
if self.functions[handle] then
updateTimerHandle(handle, dt)
-- 如果定时器的执行次数耗尽,将其从活动列表中移除
if handle.count == 0 then
self.functions[handle] = nil
end
end
end
end
-- 创建一个持续执行的定时器
-- delay: 持续时间
-- during: 期间每帧执行的函数
-- after: 结束时执行的函数(可选)
function Timer:during(delay, during, after)
local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
self.functions[handle] = true
return handle
end
-- 创建一个延时执行的定时器(最常用的 setTimeout)
-- delay: 延时时间
-- func: 时间到后执行的函数
function Timer:after(delay, func)
return self:during(delay, _nothing_, func)
end
-- 创建一个周期性执行的定时器(setInterval)
-- delay: 间隔时间
-- after: 每次间隔到期时执行的函数
-- count: 执行总次数(可选,默认无限次)
function Timer:every(delay, after, count)
-- 利用 Lua 中 math.huge (无穷大) 的特性:math.huge - 1 依然是 math.huge
local count = count or math.huge
local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
self.functions[handle] = true
return handle
end
-- 取消/移除指定的定时器
function Timer:cancel(handle)
self.functions[handle] = nil
end
-- 清空当前实例下的所有定时器
function Timer:clear()
self.functions = {}
end
-- 协程脚本支持:允许你用同步的方式写异步延时代码
-- 用法: Timer:script(function(wait) print("开始"); wait(1); print("1秒后") end)
function Timer:script(f)
local co = coroutine.wrap(f)
co(function(t)
-- 把唤醒协程的操作作为一个延时定时器
self:after(t, co)
-- 挂起当前协程,直到定时器将其唤醒
coroutine.yield()
end)
end
-- 缓动(Tween)模块的 metatable 设定
Timer.tween = setmetatable({
-- 辅助函数
-- out: 反转缓动函数(例如将 in-quad 变为 out-quad)
out = function(f)
return function(s, ...) return 1 - f(1 - s, ...) end
end,
-- chain: 拼接两个缓动函数(一半时间用 f1,一半时间用 f2,例如 in-out)
chain = function(f1, f2)
return function(s, ...) return (s < .5 and f1(2 * s, ...) or 1 + f2(2 * s - 1, ...)) * .5 end
end,
-- 基础的内置缓动数学公式 (s 的范围通常是 0 到 1)
linear = function(s) return s end,
quad = function(s) return s * s end,
cubic = function(s) return s * s * s end,
quart = function(s) return s * s * s * s end,
quint = function(s) return s * s * s * s * s end,
sine = function(s) return 1 - math.cos(s * math.pi / 2) end,
expo = function(s) return 2 ^ (10 * (s - 1)) end,
circ = function(s) return 1 - math.sqrt(1 - s * s) end,
-- 带反弹的缓动
back = function(s, bounciness)
bounciness = bounciness or 1.70158
return s * s * ((bounciness + 1) * s - bounciness)
end,
-- 弹跳缓动(包含魔法数字,模拟真实小球落地弹跳)
bounce = function(s)
local a, b = 7.5625, 1 / 2.75
return math.min(a * s ^ 2, a * (s - 1.5 * b) ^ 2 + .75, a * (s - 2.25 * b) ^ 2 + .9375, a * (s - 2.625 * b) ^ 2 + .984375)
end,
-- 弹性缓动(类似弹簧)
elastic = function(s, amp, period)
amp, period = amp and math.max(1, amp) or 1, period or .3
return (-amp * math.sin(2 * math.pi / period * (s - 1) - math.asin(1 / amp))) * 2 ^ (10 * (s - 1))
end,
}, {
-- 当把 Timer.tween 当作函数调用时触发:Timer.tween(持续时间, 目标对象, 属性表, 缓动方式, 结束回调)
__call = function(tween, self, len, subject, target, method, after, ...)
-- 递归收集 subject(当前对象) 和 target(目标属性表) 中共有的字段,计算好差值(delta)
-- 输出格式: { {对象引用, 属性名, 差值}, ... }
local function tween_collect_payload(subject, target, out)
for k, v in pairs(target) do
local ref = subject[k]
assert(type(v) == type(ref), 'Type mismatch in field "' .. k .. '".') -- 类型必须匹配
if type(v) == 'table' then
-- 如果是表则递归处理(支持嵌套表的缓动,例如 obj.color.r)
tween_collect_payload(ref, v, out)
else
-- 确保该属性支持数学运算(计算目标值和初始值的差值)
local ok, delta = pcall(function() return (v - ref) * 1 end)
assert(ok, 'Field "' .. k .. '" does not support arithmetic operations')
out[#out + 1] = { subject, k, delta }
end
end
return out
end
-- 获取具体的缓动方法(字符串或函数),默认是线性 'linear'
method = tween[method or 'linear']
-- 收集需要缓动的数据 payload
local payload, t, args = tween_collect_payload(subject, target, {}), 0, { ... }
local last_s = 0
-- 利用 Timer:during 创建一个持续整个缓动过程的定时器
return self:during(len, function(dt)
t = t + dt
-- 根据缓动公式计算出当前进度 s (0 到 1)
local s = method(math.min(1, t / len), unpack(args))
-- 计算这当前帧相对于上一帧的进度增量 ds
local ds = s - last_s
last_s = s
-- 将增量应用到所有被收集的属性上
for _, info in ipairs(payload) do
local ref, key, delta = unpack(info)
ref[key] = ref[key] + delta * ds
end
end, after)
end,
-- 当访问 Timer.tween 不存在的键时触发(例如 tween['out-bounce'])
-- 这里通过正则表达式动态生成组合的缓动函数
__index = function(tweens, key)
if type(key) == 'function' then return key end
assert(type(key) == 'string', 'Method must be function or string.')
-- 如果已经缓存了该方法,直接返回
if rawget(tweens, key) then return rawget(tweens, key) end
-- 根据正则表达式模式匹配(例如识别出 'out-bounce' 中的 'bounce'),动态构建函数
local function construct(pattern, f)
local method = rawget(tweens, key:match(pattern))
if method then return f(method) end
return nil
end
local out, chain = rawget(tweens, 'out'), rawget(tweens, 'chain')
-- 依次尝试匹配: in-xxx, out-xxx, in-out-xxx, out-in-xxx
-- 构建成功后返回该函数,并将其作为缓存,以供下次调用
return construct('^in%-([^-]+)$', function(...) return ... end)
or construct('^out%-([^-]+)$', out)
or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
or error('Unknown interpolation method: ' .. key)
end
})
-- Timer 实例化函数
function Timer.new()
return setmetatable({ functions = {}, tween = Timer.tween }, Timer)
end
-- 创建一个默认实例,方便作为全局单例使用
local default = Timer.new()
-- 构建模块的导出表
local module = {}
-- 将 Timer 的所有方法映射到 module 上,但内部调用的是 default 实例
-- 这样写可以直接使用 Timer.after(1, func) 而不需要手动去 new 一个对象
for k in pairs(Timer) do
if k ~= "__index" then
module[k] = function(...) return default[k](default, ...) end
end
end
-- 将 tween 方法也绑定到 module 上,并指向默认实例的 tween 逻辑
module.tween = setmetatable({}, {
__index = Timer.tween,
__newindex = function(k, v) Timer.tween[k] = v end,
__call = function(t, ...) return default:tween(...) end,
})
-- 导出模块。通过 __call 元方法,支持通过 Timer() 直接创建新的实例
return setmetatable(module, { __call = Timer.new })Enhanced Timer
基于字符串的标签系统(Tag System)和自动覆盖机制。
lua
local EnhancedTimer = object:extend()
local Timer = require 'libs/hump/timer'
function EnhancedTimer:new()
-- 实例化一个原生的 Timer 对象作为内部驱动
self.timer = Timer()
-- 核心字典:用来存储 "标签字符串" -> "原生定时器句柄" 的映射
self.tags = {}
end
-- 每一帧调用的更新函数
function EnhancedTimer:update(dt)
if self.timer then self.timer:update(dt) end
end
-- 延时执行 (重载)
function EnhancedTimer:after(tag, duration, func)
-- 如果第一个参数传入的是字符串(说明使用了标签功能)
if type(tag) == 'string' then
self:cancel(tag) -- 先尝试取消同名标签下正在运行的旧定时器
-- 创建新定时器,并将返回的句柄存入 tags 字典中
self.tags[tag] = self.timer:after(duration, func)
return self.tags[tag]
else
-- 如果没有传字符串(降级模式),则和原生 timer 一样,tag 此时其实是 duration
-- 这里的 tag 就是 duration,duration 就是 func
return self.timer:after(tag, duration, func)
end
end
-- 持续执行 (重载)
function EnhancedTimer:during(tag, duration, func, after)
if type(tag) == 'string' then
self:cancel(tag)
self.tags[tag] = self.timer:during(duration, func, after)
return self.tags[tag]
else
return self.timer:during(tag, duration, func, after)
end
end
-- 循环执行 (重载)
function EnhancedTimer:every(tag, duration, func, count)
if type(tag) == 'string' then
self:cancel(tag)
self.tags[tag] = self.timer:every(duration, func, count)
return self.tags[tag]
else
return self.timer:every(tag, duration, func, count)
end
end
-- 缓动动画 (重载)
function EnhancedTimer:tween(tag, duration, table, tween_table, tween_function, after)
if type(tag) == 'string' then
self:cancel(tag)
self.tags[tag] = self.timer:tween(duration, table, tween_table, tween_function, after)
return self.tags[tag]
else
return self.timer:tween(tag, duration, table, tween_table, tween_function, after)
end
end
-- 取消定时器 (增强版)
function EnhancedTimer:cancel(tag)
if tag then
-- 如果传入的是一个字符串标签,并且字典里存了这个标签的句柄
if self.tags[tag] then
self.timer:cancel(self.tags[tag]) -- 使用内部 timer 取消真实的句柄
self.tags[tag] = nil -- 清除记录
else
-- 如果传入的直接就是原生句柄(handle),则直接取消
self.timer:cancel(tag)
end
end
end
-- 清空所有定时器
function EnhancedTimer:clear()
self.timer:clear()
self.tags = {} -- 同步清空标签记录
end
-- 销毁定时器实例(用于垃圾回收前清理资源)
function EnhancedTimer:destroy()
self.timer:clear()
self.tags = {}
self.timer = nil
end
return EnhancedTimer