CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
进阶实战游戏功能996引擎

Lua帧动画机:序列帧的播放与循环

2026-10-02 02:28 作者:996 技术组 996引擎Lua教程传奇脚本进阶实战游戏功能996引擎

【语法算法】

fi=(fi+1)%N——先拆这一行。帧号加一取模总帧数:播到末帧归零重新开始,一行就把循环模式的骨架写完了。帧动画机的全部机关就这一行加两道开关:序列帧按帧率逐张翻,翻到末尾看模式——循环的归零重来,单次的停在末帧,一页之差两种活法。这篇把整套写法拆开:配置表、计帧推进、模式判定、帧条输出、定时器、调试按钮,一段一段照抄能跑。

一、效果演示:序列帧的播放与循环

演示场一个火柴人跑者——六张手绘的帧轮着播。点「播放/暂停」:火柴人迈腿摆臂跑起来,底部帧条一格格点亮;点「速度」:帧率在四、八、十六之间轮换——十六帧的火柴人跑得像飞,四帧的像慢动作回放。点「单次/循环」:单次模式播到末帧定住,像定格在冲线的瞬间;循环模式末帧一过归零重来。左上角实时报当前帧号——帧号走到哪,火柴人的腿就迈到哪。演示里试两笔账:八帧和十六帧的顺滑度——帧率翻倍,动作的成本也翻倍,顺滑是要花钱的——钱花在重要动作上,待机动作省着花。帧动画机教的是翻页:动画没有魔法,就是翻页翻得够快——快到眼睛追不上单页,连续的动作就出来了。

mermaid
flowchart TD
A[计帧累加] --> B{攒满一帧时长}
B -- 否 --> A
B -- 是 --> C[帧号加一取模总帧数]
C --> D{单次模式且到末帧}
D -- 是 --> E[停 在末帧定住]
D -- 否 --> F[按帧号取图上屏]
F --> A
E --> A
demo
 fx-frameanim

二、底层原理:一次计帧推进加一次模式判定

模块的机关是一对搭档。计帧推进:每一拍给帧计时器加时长,攒满一帧的时长帧号进一——帧率就是这一格时长的倒数,帧率的档位换的是这一格的长短。模式判定:帧号到末尾的那一拍看模式——循环取模归零接着播,单次停在末帧不再推进。和逐帧手写的分野在"帧动画机把翻页和内容拆开":手写的版本每帧一段代码写死在哪,帧动画机只管翻页,帧里画什么随便换——翻页器是通用的,内容是随时的。

单次模式为什么停在末帧而不是归零?归零的版本单次播完突然回到第一帧,动作像被掐断;停末帧的版本动作有个收势,接后续逻辑也方便——停在最后一格是动画对故事的礼貌——挥砍停在刀势尽处,跳跃停在落地的半空,定格的位置就是台词。

三、核心代码:完整模块(上·骨架)

lua
-- @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

四、核心代码:完整模块(下·计帧推进与模式判定)

lua
-- 计帧推进与模式判定:攒满时长翻一页
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 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。

← 返回文章地图返回研学路径

幂尔框架 · 实战干货 · 接口调用

LATEST ARTICLES

全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

Lua自动炮台:架一个自动索敌开火

【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua元素转换:火伤转冰伤附带减速

【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua连锁引燃:一个着火引燃下一个

【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua弹射地垫:踩上去弹飞到空中

【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua时间减缓:流速减半的局部时差

【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…

2026-10-02 04:50 996 技术组
进阶实战游戏功能

Lua引力球:丢出去把怪吸过来

【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…

2026-10-02 04:50 996 技术组