跳转至内容

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

Functions