【语法算法】
fi=(fi+1)%N——先拆这一行。帧号加一取模总帧数:播到末帧归零重新开始,一行就把循环模式的骨架写完了。帧动画机的全部机关就这一行加两道开关:序列帧按帧率逐张翻,翻到末尾看模式——循环的归零重来,单次的停在末帧,一页之差两种活法。这篇把整套写法拆开:配置表、计帧推进、模式判定、帧条输出、定时器、调试按钮,一段一段照抄能跑。
一、效果演示:序列帧的播放与循环
演示场一个火柴人跑者——六张手绘的帧轮着播。点「播放/暂停」:火柴人迈腿摆臂跑起来,底部帧条一格格点亮;点「速度」:帧率在四、八、十六之间轮换——十六帧的火柴人跑得像飞,四帧的像慢动作回放。点「单次/循环」:单次模式播到末帧定住,像定格在冲线的瞬间;循环模式末帧一过归零重来。左上角实时报当前帧号——帧号走到哪,火柴人的腿就迈到哪。演示里试两笔账:八帧和十六帧的顺滑度——帧率翻倍,动作的成本也翻倍,顺滑是要花钱的——钱花在重要动作上,待机动作省着花。帧动画机教的是翻页:动画没有魔法,就是翻页翻得够快——快到眼睛追不上单页,连续的动作就出来了。
flowchart TD
A[计帧累加] --> B{攒满一帧时长}
B -- 否 --> A
B -- 是 --> C[帧号加一取模总帧数]
C --> D{单次模式且到末帧}
D -- 是 --> E[停 在末帧定住]
D -- 否 --> F[按帧号取图上屏]
F --> A
E --> A
fx-frameanim
二、底层原理:一次计帧推进加一次模式判定
模块的机关是一对搭档。计帧推进:每一拍给帧计时器加时长,攒满一帧的时长帧号进一——帧率就是这一格时长的倒数,帧率的档位换的是这一格的长短。模式判定:帧号到末尾的那一拍看模式——循环取模归零接着播,单次停在末帧不再推进。和逐帧手写的分野在"帧动画机把翻页和内容拆开":手写的版本每帧一段代码写死在哪,帧动画机只管翻页,帧里画什么随便换——翻页器是通用的,内容是随时的。
单次模式为什么停在末帧而不是归零?归零的版本单次播完突然回到第一帧,动作像被掐断;停末帧的版本动作有个收势,接后续逻辑也方便——停在最后一格是动画对故事的礼貌——挥砍停在刀势尽处,跳跃停在落地的半空,定格的位置就是台词。
三、核心代码:完整模块(上·骨架)
-- @file FrameAnim.lua
-- 帧动画机 —— 序列帧的播放与循环
local FrameAnim = {}
local CONST = {
FRAME_COUNT = 6,
FPS = 8,
LOOP = true,
AUTOINC_BASE = 1153000,
}
local _frame = 0
local _frameT = 0
local _playing = true
local _fps = CONST.FPS
local _loop = CONST.LOOP
local _onFrame = nil
local _scheduler = nil
local _autoInc = 0
local function GenID(prefix)
_autoInc = _autoInc + 1
return string.format("%s_%d", prefix or "FAM",
CONST.AUTOINC_BASE + _autoInc)
end
local function ShowTip(msg)
if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end
function FrameAnim.OnFrame(func)
_onFrame = func
end
function FrameAnim.Frame()
return _frame
end
function FrameAnim.TogglePlay()
_playing = not _playing
end
function FrameAnim.SetSpeed(fps)
_fps = fps
end
function FrameAnim.SetLoop(loop)
_loop = loop
end
四、核心代码:完整模块(下·计帧推进与模式判定)
-- 计帧推进与模式判定:攒满时长翻一页
if not _scheduler then
_scheduler = SL:Schedule(function(dt)
if not _playing then return end
_frameT = _frameT + dt
if _frameT < 1 / _fps then return end
_frameT = _frameT - 1 / _fps
_frame = _frame + 1
if _frame >= CONST.FRAME_COUNT then
if _loop then
_frame = 0
else
_frame = CONST.FRAME_COUNT - 1
_playing = false
ShowTip("单次播完——停在末帧")
end
end
if _onFrame then
_onFrame(_frame)
end
end, 0.02)
end
function FrameAnim.Play(frames, fps, loop)
if frames then CONST.FRAME_COUNT = #frames end
if fps then _fps = fps end
if loop ~= nil then _loop = loop end
_frame = 0
_frameT = 0
_playing = true
end
function FrameAnim.Unload()
if _scheduler then SL:Unschedule(_scheduler) end
_playing = false
_frame = 0
end
return FrameAnim
五、机制问答
问:帧率调到十六还会丢帧吗?
答:不丢——计帧是攒时长的账,帧率再高也一帧一帧翻,翻不过来就攒着,一帧都不漏。
问:单次播完再点播放会怎样?
答:从头播——暂停和播完是两个状态,播完的单次再按播放归零重来,定格的末帧只是临时的收势。
问:帧率中途切换单前帧会跳吗?
答:不跳——帧计时器留着余量,切帧率只改每一格的长短,当前帧原地不动,下一格按新节奏翻。
问:一个动画机能播两个火柴人吗?
答:分两个实例——机是通用的壳,一壳一动画,两个火柴人各挂各的机互不串帧。
问:帧号取模和判断归零写法一样吗?
答:结果一样取模更省——一行取模替代判断加归零两步,循环模式的最简写法就是它。
六、调参与实战怎么用
第四笔是模式的分野:循环管待机、单次管施法——待机动作循环播,出手动作单次播完接回调,两把模式配对用才是完整的动作系统。第三笔是帧率的档位:八帧是待机动作的基准——动作类十六帧起、待机类四到八帧,帧率是钱,动作的重要程度决定花多少。第二笔是计帧的余量:攒的时长要减去而不是清零——清零的版本帧率高时会丢帧频,减去余量的版本帧率再乱也不丢帧。第一笔是回调的挂点:每翻一页吐一次帧号——特效、音效、逻辑全挂在帧号上,帧动画机只管翻页不管发声。常见坑三个:计帧清零代替减余量在高帧率丢帧频;单次播完的暂停态和手动暂停混在一个标志里;帧条的高亮跟随慢半拍。
实战里帧动画机是所有序列帧的通用壳:待机的呼吸、出手的挥砍、爆点的炸裂,全是同一台翻页器换内容。组里的约定是:序列帧的播放一律过帧动画机,不许散落在业务里手写计帧——翻页器只有一台,戏可以随便换着唱——机写得通用,谁用谁省事,组里每个人都少写一遍计帧——省下来的时间够多调两套动作参数——省下来的就是赚下来的。这篇的模块照抄能跑,改的就是 CONST 那张表。
写完留一句给做动效系的同学:帧动画机卖的是"动画没有魔法"——那一条一行取模的翻页循环和帧条上逐格点亮的光,是把序列帧的翻页做成通用件的一台放映机。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…
【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…
【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…
【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…
【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…
【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…