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[镜头原地不动]
B -- 否 --> D[镜头按越界量平移]
D --> E[钳制在世界边界内]
E --> A
C --> A
demo
 fx-camfollow

二、底层原理:一道死区围栏加一次越界钳制

模块的机关是一对搭档。死区围栏:镜头视野中央画一圈不动的围栏——主角在围栏里随便走,镜头一寸不挪;主角一出栏,镜头把越界的量补回来。越界钳制:镜头的平移被世界边界拦住——世界左缘右缘之外没有内容,镜头到边即停。和硬跟类写法的分野在"死区把微抖滤在了栏内":硬跟是一比一的放大器,主角坐标抖零点几像素画面就抖零点几像素;死区给抖动留了消化空间,栏内的抖动到不了镜头上——角色走路坐标里那点零点几像素的毛刺,全被围栏按在了栏内消化。

死区为什么不对称地钉在中间?居中的死区两边的跟随机会均等,偏置的死区可以让"前方"多留视野——跑酷向的地图死区偏后,探索向的地图死区居中,这一口是地图类型定的,不是写法定死的——调死区之前先问这张图想让玩家看哪儿,答案就是偏置的方向。

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

lua
-- @file CameraFollow.lua
-- 相机跟随 —— 死区围栏的视野收敛

local CameraFollow = {}

local CONST = {
    WORLD_WIDTH   = 2400,
    VIEW_WIDTH    = 660,
    DEAD_LEFT     = 230,
    DEAD_RIGHT    = 430,

    COLOR_ZONE    = "#238636",

    ZORDER_CAMERA = 200,
    AUTOINC_BASE  = 1141000,
}

local _camX       = 0
local _heroX      = 0
local _cameraFx   = nil
local _scheduler  = nil
local _autoInc    = 0

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

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

local function ClampCam(x)
    local maxX = CONST.WORLD_WIDTH - CONST.VIEW_WIDTH
    if x < 0 then return 0 end
    if x > maxX then return maxX end
    return x
end

function CameraFollow.SetHero(x)
    _heroX = x
end

function CameraFollow.CamX()
    return _camX
end

四、核心代码:完整模块(下·死区判定与钳制输出)

lua
-- 死区判定与钳制输出:每拍读主角,出栏才平移
local function StepCamera()
    local dzL = _camX + CONST.DEAD_LEFT
    local dzR = _camX + CONST.DEAD_RIGHT
    if _heroX < dzL then
        _camX = _camX - (dzL - _heroX)
    elseif _heroX > dzR then
        _camX = _camX + (_heroX - dzR)
    end
    _camX = ClampCam(_camX)
    if _cameraFx and not GUI:Widget_IsNull(_cameraFx) then
        GUI:setPosition(_cameraFx, math.floor(-_camX), 0)
    end
end

if not _scheduler then
    _scheduler = SL:Schedule(function(dt)
        StepCamera()
    end, 0.02)
end

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

function CameraFollow.Bind(node)
    SafeRemove(_cameraFx)
    _cameraFx = node
end

SL:ScheduleOnce(function()
    CameraFollow.Bind(GUI:GetSceneRoot())
    ShowTip("相机跟随已挂载——主角出栏才见镜头动")
end, 1.0)

function CameraFollow.Unload()
    if _scheduler then SL:Unschedule(_scheduler) end
    SafeRemove(_cameraFx)
    _scheduler = nil
end

return CameraFollow

五、机制问答

问:死区一定居中吗?
答:不一定——跑酷图死区偏后给前方多留视野,解谜图死区居中偏稳,死区是设计参数不是公式。

问:主角瞬移会怎样?
答:镜头一步跟到位——死区补的是越界量,瞬移多远补多远,想要滑动跟就瞬移后自己插值。

问:世界比视野还小怎么办?
答:钳制兜底——边界钳制会把镜头钉在零,世界小于视野时钳制值恒零,镜头等于关闭。

问:死区里主角的抖动全都不上屏吗?
答:全不上——栏内的位移只累积不输出,这正是滤抖的原理,也是死区存在的全部意义。

问:纵向地图能套用吗?
答:能——把横轴的死区换成纵轴,公式原样竖过来,上下出栏才纵向平移。

六、调参与实战怎么用

第四笔是标记的自清:死区判定的每拍都要读实时坐标——那场帕金森事故的另一层教训是坐标读数不能缓存,缓存一拍抖动就漏一拍到屏幕上。第三笔是边界的钳制:世界边界必须先算好再钳——边界值每拍现算的话,边界外的世界闪一下白屏,钳制值要随地图尺寸初始化一次。第二笔是死区的宽窄:两百步的栏宽是"漫步不晃、疾跑不顿"的折中——栏太窄退化成硬跟,太宽主角快出屏了镜头才动,栏宽要拿主角的最高移速试出来。第一笔是输出的取整:镜头坐标落屏前取整——半像素的位移在低配屏上就是一排锯齿的抖,取整是最便宜的抗抖。常见坑三个:每帧硬跟放大角色坐标抖动的帕金森事故;世界边界值每拍现算导致闪屏;镜头取整前做了缩放导致半像素抖动。

实战里这道死区不止跟镜头:小地图的视框、拖拽列表的回弹、摇杆的行程圈,全是同一道围栏换皮。组里的约定是:凡是要"稳"的跟随一律加死区,凡是要"弹"的跟随才走弹簧——稳和弹是两条路,别在一套公式里打架。这篇的模块照抄能跑,改的就是 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 技术组