【语法算法】
先抛一个坑:列表一松手就急刹,顺滑去哪了?拖的时候跟手,一松手指列表当场钉死——玩家嘴里不说,心里给这界面打了个不及格。惯性滚动补的就是松手之后的那口气:松手不是停,是带着最后一点速度继续滑,速度逐拍衰减,滑到劲道耗尽才稳稳站住——站的位置每一次都不一样,这就是惯性的签名。这篇把整套写法拆开:配置表、初速注入、衰减推进、边界回弹、定时器、调试按钮,一段一段照抄能跑。
一、效果演示:甩一下,滑多远停多稳
演示场二十行条目的长列表。点「甩一下」:列表获得一笔随机初速——条目呼啦啦往上滑,速度一拍一拍变小,滑动越来越慢,最后近乎蠕行,稳稳停在半路,停的位置每一次都不一样,这就是惯性的签名。点「到顶」「到底」直接回两端,边界处速度清零不越界——不越界是清单的规矩,弹回是另一种玩法。左上角实时播报偏移和速度——速度掉到个位数的那几拍,就是"缓停"的手感来源。演示里试两笔账:甩大力和甩小力的滑行距离——初速差一倍,滑程差得更多,衰减是按比例走的——比例的世界里,起点就是命。惯性滚动教的是余劲:停不停不由手指说了算,由最后的速度说了算——这一口余劲,就是拖拽和点按之间最大的手感差。
flowchart TD
A[松手 读末段速度] --> B[偏移加速度推进]
B --> C[速度乘衰减系数]
C --> D{速度近乎零}
D -- 否 --> E{越过边界}
E -- 是 --> F[贴边 速度清零]
E -- 否 --> B
F --> B
D -- 是 --> G[缓停 定住]
fx-inertiascroll
二、底层原理:一笔初速注入加一阶速度衰减
模块的机关是一对搭档。初速注入:松手的那一拍把拖拽末段的位移折算成速度——甩得越狠初速越大,这一笔是惯性的源头——初速为零的版本松手即停,那不叫惯性叫急刹。速度衰减:每一拍速度乘一个小于一的系数——衰减按比例走,速度大掉得快、速度小掉得慢,滑行自然收出一条缓停的曲线——这条曲线玩家未必说得出名字,但手感骗不了人。和分页翻屏的分野在"惯性把停的时机交给物理":翻屏是手指抬起点一下跳一页,惯性是松手后世界自己往前滑——同样是从 A 到 B,一个听指挥,一个有性子。
衰减为什么用指数不用匀减?匀减的滑行是匀减速直线运动,停得很突兀;指数衰减永远走剩余速度的比例,最后一段是温柔地蠕行——缓停的"缓"字就写在指数的曲线上——前段大步流星,后段寸寸挪动,同一个系数管出两种步态。
三、核心代码:完整模块(上·骨架)
-- @file InertiaScroll.lua
-- 惯性滚动 —— 速度衰减的缓停列表
local InertiaScroll = {}
local CONST = {
VIEW_HEIGHT = 240,
ROW_HEIGHT = 40,
ROW_COUNT = 20,
FLING_SPEED = 600,
DECAY = 0.12,
STOP_SPEED = 1,
COLOR_ROW_A = "#1c2128",
COLOR_ROW_B = "#21262d",
AUTOINC_BASE = 1150000,
}
local _offset = 0
local _velocity = 0
local _scheduler = nil
local _autoInc = 0
local function GenID(prefix)
_autoInc = _autoInc + 1
return string.format("%s_%d", prefix or "INS",
CONST.AUTOINC_BASE + _autoInc)
end
local function ShowTip(msg)
if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end
local function MaxOffset()
return CONST.ROW_COUNT * CONST.ROW_HEIGHT
- CONST.VIEW_HEIGHT
end
function InertiaScroll.Offset()
return _offset
end
function InertiaScroll.Fling(vel)
_velocity = vel or CONST.FLING_SPEED
ShowTip("甩出去了——看速度怎么衰减")
end
四、核心代码:完整模块(下·衰减推进与边界回弹)
-- 衰减推进与边界回弹:每一拍按速度滑
if not _scheduler then
_scheduler = SL:Schedule(function(dt)
if math.abs(_velocity) <= CONST.STOP_SPEED then
return
end
_offset = _offset + _velocity * dt
_velocity = _velocity
* math.pow(CONST.DECAY, dt)
local maxOff = MaxOffset()
if _offset < 0 then
_offset = 0
_velocity = 0
elseif _offset > maxOff then
_offset = maxOff
_velocity = 0
end
end, 0.02)
end
SL:ScheduleOnce(function()
SL:BindDebugButton("甩一下", function()
InertiaScroll.Fling(
-(600 + math.random() * 500))
end)
ShowTip("技能已加载: 惯性滚动 (调试按钮触发)")
end, 1.0)
function InertiaScroll.Unload()
if _scheduler then SL:Unschedule(_scheduler) end
_offset = 0
_velocity = 0
end
return InertiaScroll
五、机制问答
问:衰减系数越小越跟手吗?
答:反过来——系数越小速度掉得越快,滑程越短,跟手但不顺滑;要顺滑就把系数放大,代价是收得慢。
问:速度小到多少算停?
答:阈值定一——低于一的速度每拍挪不了半个像素,继续算只会白耗拍子,到阈值直接归零定住。
问:边界处为什么要清速度?
答:不清会抖——顶到底还带着速度,下一拍又往回算,画面在边上嗡嗡震,清零是边界唯一的礼貌。
问:惯性滚动的速度从哪来?
答:松手前最后几拍的位移折算——拖拽模块把末段位移除以时间喂给惯性,两段要共用一个坐标系。
问:滚动的条目要全画吗?
答:不用——只画视口内首尾几行,滚出视口的跳过,惯性感来自偏移的推进,不来自画了多少行。
六、调参与实战怎么用
第四笔是衰减的档位:零点一二的系数是"甩一下滑半屏"的手感——系数太大滑不到头就停像断电,太小一甩滑出三条街收不住,档位要拿最常见的甩动力度校。第三笔是初速的封顶:甩得再狠也六百起——不封顶的版本狂甩一次飞到表尾,初速封顶保住"滑得远但不离谱"。第二笔是停阈的位置:一是"半像素都懒得挪"的收笔线——阈值调大停得干脆但末端发顿,调小尾段绵长,一是两头的平衡位。第一笔是边界的清零:越界即贴边清速是唯一的边界礼节——弹回效果的版本另算,不弹回的清单清零是最稳的收法。常见坑三个:衰减按帧算的帧率依赖;边界清零漏了负方向;滚动偏移喂给渲染前没取整导致条目边缘发虚。
实战里这套衰减不止滚列表:轮盘的转停、摇杆的回中、抽屉的滑收,全是"初速加衰减"同一副骨架。组里的约定是:凡是松手后还要自己动一段的界面,一律走惯性不走瞬移——顺滑是白捡的体面,玩家未必夸,但急刹一定被骂。这篇的模块照抄能跑,改的就是 CONST 那张表。
写完留一句给做手感系的同学:惯性滚动卖的是"松手之后的余韵"——那一段越滑越慢的蠕行和速度归零时的稳稳站住,是把急刹的生硬折成缓停的一口气。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…
【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…
【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…
【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…
【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…
【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…