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

Lua弹簧跟随:速度累积的阻尼收敛

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

【语法算法】

上一版留了个只有低配机才看得见的隐蔽报错:跟随图标永远差半拍——测试机丝般顺滑,玩家的旧手机上图标一抖一抖,掉帧的时候拖出一条重影。追到底发现跟随用的是百分比插值,插值系数写死按每帧算——帧率一掉,收敛的速度跟着掉,抖动全从这来。修法是把跟随换成弹簧模型:速度累积加阻尼,按时间算账不按帧算账。这篇把弹簧跟随整套写法拆开:配置表、速度累积、阻尼收敛、跟画输出、定时器、调试按钮,一段一段照抄能跑。

一、效果演示:速度累积的阻尼收敛

演示场一个橙色的目标点和一个蓝色的跟随者。点「猛拉」:目标点跳到远处——跟随者不是匀速爬过去,而是先加速冲、冲过头、再回弹、慢慢贴住,完全是一根弹簧的手感。点「换个劲」:劲度在软、中、硬三档轮换——软档慢悠悠地荡过去,硬档嗖一下贴住还带一点过冲。左上角实时显示当前速度,速度归零就是贴稳了。演示里试两种劲度对着追同一个远目标——软硬之间的手感差异,一眼就懂。弹簧跟随教的是收敛:不是到了就停,是带着劲道停——停得稳不稳,全看最后一寸的阻尼给不给面子。

mermaid
flowchart TD
A[每一拍读取目标点] --> B[力差 = 目标减当前]
B --> C[速度 = 速度加力差乘劲度]
C --> D[速度乘阻尼衰减]
D --> E[位置加速度推进]
E --> A
demo
 fx-springfollow

二、底层原理:一次力差采样加一阶阻尼收敛

模块的机关是一对搭档。力差采样:每一拍读一次目标和跟随者的距离差——差值乘劲度系数就是这一拍要加的速度,距离越远加得越快,这就是弹簧的拉力。阻尼收敛:速度每拍乘一个零到一之间的阻尼系数——没有阻尼的弹簧会永远荡下去,阻尼把多余的劲一点点吃掉,速度和距离同时归零才算贴稳。和百分比插值的分野在"弹簧带速度有惯性":插值是每一拍走剩余距离的固定比例,永远不会超调;弹簧的速度是攒出来的,劲大了会冲过头再荡回来——插值像跟班,弹簧像吊在橡皮筋上,手感完全两样。

为什么必须按时间算账不按帧算账?帧率相关的系数在掉帧时收敛变慢、高帧率时收敛变快——那场"低配机差半拍"的报错根源就在这;所有系数先乘每帧步长再参与运算,六十帧和三十帧收敛得一样快,这是所有运动类写法的地基。

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

lua
-- @file SpringFollow.lua
-- 弹簧跟随 —— 速度累积的阻尼收敛

local SpringFollow = {}

local CONST = {
    STIFFNESS     = 0.08,
    DAMPING       = 0.90,
    STEP_SCALE    = 60,
    SNAP_DIST     = 0.5,

    COLOR_FOLLOW  = "#58a6ff",
    COLOR_TARGET  = "#f0883e",

    ZORDER_FOLLOW = 140,
    AUTOINC_BASE  = 1140000,
}

local _tx, _ty     = 0, 0
local _x, _y       = 0, 0
local _vx, _vy     = 0, 0
local _followFx    = nil
local _scheduler   = nil
local _autoInc     = 0

local function GenID(prefix)
    _autoInc = _autoInc + 1
    return string.format("%s_%d", prefix or "SPF",
        CONST.AUTOINC_BASE + _autoInc)
end

local function SafeRemove(node)
    if node and not GUI:Widget_IsNull(node) then
        GUI:removeFromParent(node)
    end
end

local function ShowTip(msg)
    if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end

function SpringFollow.SetTarget(x, y)
    _tx, _ty = x, y
end

function SpringFollow.SetPos(x, y)
    _x, _y = x, y
    _vx, _vy = 0, 0
end

四、核心代码:完整模块(下·速度累积与阻尼收敛)

lua
-- 速度累积与阻尼收敛:每拍按时间步长推进
local function StepSpring(dt)
    local step = dt * CONST.STEP_SCALE
    _vx = (_vx + (_tx - _x) * CONST.STIFFNESS * step)
        * math.pow(CONST.DAMPING, step)
    _vy = (_vy + (_ty - _y) * CONST.STIFFNESS * step)
        * math.pow(CONST.DAMPING, step)
    _x = _x + _vx * step
    _y = _y + _vy * step
    local dist = math.sqrt((_tx - _x) ^ 2 + (_ty - _y) ^ 2)
    if dist < CONST.SNAP_DIST
        and math.abs(_vx) + math.abs(_vy) < CONST.SNAP_DIST then
        _x, _y = _tx, _ty
        _vx, _vy = 0, 0
    end
end

function SpringFollow.Bind(node)
    SafeRemove(_followFx)
    _followFx = node
end

if not _scheduler then
    _scheduler = SL:Schedule(function(dt)
        StepSpring(dt)
        if _followFx and not GUI:Widget_IsNull(_followFx) then
            GUI:setPosition(_followFx,
                math.floor(_x), math.floor(_y))
        end
    end, 0.02)
end

SL:ScheduleOnce(function()
    local root = GUI:GetRootWindow()
    SpringFollow.SetPos(300, 200)
    SpringFollow.SetTarget(300, 200)
    SpringFollow.Bind(root)
    ShowTip("弹簧跟随已挂载——改目标看收敛")
end, 1.0)

function SpringFollow.Unload()
    if _scheduler then SL:Unschedule(_scheduler) end
    SafeRemove(_followFx)
    _scheduler = nil
end

return SpringFollow

五、机制问答

问:为什么用速度累积不用直接插值?
答:手感——插值永远走直线不超调,弹簧带惯性会冲过头再荡回来,跟班类的东西要的就是这点活气。

问:阻尼系数调大一舍得吗?
答:看需求——阻尼越大收得越死板、越像插值,越小荡得越久,零点九附近是最像弹簧的手感位。

问:掉帧时弹簧会变形吗?
答:不会——所有系数都先乘时间步长,帧率掉了步长变大,收敛的速度按真实时间算,这是那场低配报错换来的规矩。

问:什么时候算贴稳?
答:距离和速度双双过阈值——只看距离不看速度,高速掠过目标的一拍会被误判成贴稳,两道闸一起查。

问:弹簧能跟旋转吗?
答:能——把位置换成角度值,同一套公式照跑,转头的弹性跟随就是这套公式的第二次利用。

六、调参与实战怎么用

第四笔是劲度的档位:零点零八是"跟得上但不黏"的手感位——劲度太小跟班永远吊在半路,太大弹簧退化成瞬移,零点零八前后各试一档再定。第三笔是阻尼的取舍:零点九是把"荡"压到两三个来回的值——阻尼一改动整个手感,动之前先把旧值记在注释里。第二笔是贴稳的双闸:距离和速度都过阈值才吸附——单闸吸附会在高速掠过时闪现跳变,双闸是稳的底线。第一笔是步长的统一:所有系数乘时间步长是运动写法的地基——这一行是低配机报错换来的,任何运动类模块都先写它。常见坑三个:系数按帧算的帧率依赖;弹簧跟随的目标是界面节点时节点销毁没解绑;速度累积没有封顶导致拉极远时第一帧冲出屏幕。

实战里这套公式不止跟图标:跟随镜头、跟随血条、飘字的回弹、抽屉的开合,全是同一根弹簧换皮。组里的约定是:凡是要"带一点弹性"的跟随,一律走弹簧不走向插值——手感统一了,调参的口诀也统一了。这篇的模块照抄能跑,改的就是 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 技术组