【语法算法】
上一版留了个只有低配机才看得见的隐蔽报错:跟随图标永远差半拍——测试机丝般顺滑,玩家的旧手机上图标一抖一抖,掉帧的时候拖出一条重影。追到底发现跟随用的是百分比插值,插值系数写死按每帧算——帧率一掉,收敛的速度跟着掉,抖动全从这来。修法是把跟随换成弹簧模型:速度累积加阻尼,按时间算账不按帧算账。这篇把弹簧跟随整套写法拆开:配置表、速度累积、阻尼收敛、跟画输出、定时器、调试按钮,一段一段照抄能跑。
一、效果演示:速度累积的阻尼收敛
演示场一个橙色的目标点和一个蓝色的跟随者。点「猛拉」:目标点跳到远处——跟随者不是匀速爬过去,而是先加速冲、冲过头、再回弹、慢慢贴住,完全是一根弹簧的手感。点「换个劲」:劲度在软、中、硬三档轮换——软档慢悠悠地荡过去,硬档嗖一下贴住还带一点过冲。左上角实时显示当前速度,速度归零就是贴稳了。演示里试两种劲度对着追同一个远目标——软硬之间的手感差异,一眼就懂。弹簧跟随教的是收敛:不是到了就停,是带着劲道停——停得稳不稳,全看最后一寸的阻尼给不给面子。
flowchart TD
A[每一拍读取目标点] --> B[力差 = 目标减当前]
B --> C[速度 = 速度加力差乘劲度]
C --> D[速度乘阻尼衰减]
D --> E[位置加速度推进]
E --> A
fx-springfollow
二、底层原理:一次力差采样加一阶阻尼收敛
模块的机关是一对搭档。力差采样:每一拍读一次目标和跟随者的距离差——差值乘劲度系数就是这一拍要加的速度,距离越远加得越快,这就是弹簧的拉力。阻尼收敛:速度每拍乘一个零到一之间的阻尼系数——没有阻尼的弹簧会永远荡下去,阻尼把多余的劲一点点吃掉,速度和距离同时归零才算贴稳。和百分比插值的分野在"弹簧带速度有惯性":插值是每一拍走剩余距离的固定比例,永远不会超调;弹簧的速度是攒出来的,劲大了会冲过头再荡回来——插值像跟班,弹簧像吊在橡皮筋上,手感完全两样。
为什么必须按时间算账不按帧算账?帧率相关的系数在掉帧时收敛变慢、高帧率时收敛变快——那场"低配机差半拍"的报错根源就在这;所有系数先乘每帧步长再参与运算,六十帧和三十帧收敛得一样快,这是所有运动类写法的地基。
三、核心代码:完整模块(上·骨架)
-- @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
四、核心代码:完整模块(下·速度累积与阻尼收敛)
-- 速度累积与阻尼收敛:每拍按时间步长推进
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 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…
【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…
【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…
【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…
【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…
【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…