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

Lua视线判定:两点连线的逐格行进

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

【游戏功能】

先抛一个坑:隔着一堵墙的怪,为什么看得见你?寻路系统说路不通,仇恨系统却说你被锁定了——两套系统对"能不能看见"各说各话,根子是战场上根本没有一条统一的视线判定——判定有了,寻路和仇恨才有得对表。视线判定补的就是这一块:从你的眼到它的眼,一条线逐格走过去,撞墙即挡,全程通畅才算看得见——判定挂上之后,两套系统才有同一张表可对。这篇把整套写法拆开:配置表、逐格行进、挡格判定、畅通结论、定时器、调试按钮,一段一段照抄能跑。

一、效果演示:两点连线的逐格行进

演示场一张布了墙的格子图。点「换目标」:青色的观察眼和橙色的目标重新连线——一条光路从观察眼出发一格一格往前走:走的是绿格,路是通的;撞上灰墙的那一格变红,"视线被挡——第几格是墙"当场报数。点「换墙布」:墙重新撒一遍,光路重算。光路怎么走、走到哪被挡,全程一步一步演给你看。演示里试两个目标的对比:同一段距离,绕得开墙的畅通,绕不开的一格红。视线判定教的是行进:看得见看不见,不猜,一格一格走给系统看——走完的每一步都是证据,判挡判通都有出处。

mermaid
flowchart TD
A[起点入列] --> B[向目标推进一格]
B --> C{这一格是墙}
C -- 是 --> D[判挡 记下挡格]
C -- 否 --> E{到目标了吗}
E -- 否 --> B
E -- 是 --> F[判通]
demo
 fx-losray

二、底层原理:一次逐格行进加一道挡格判定

模块的机关是一对搭档。逐格行进:从起点到终点按直线的斜率推进——每一步决定横走还是竖走还是斜走,走过的格排成一条最贴近直线的格路,这条格路就是视线走廊。挡格判定:走廊上每一格过一遍墙表——撞墙的那一格记下序号当场判挡,走到底没撞墙判通。和距离类判定的分野在"视线认的是路上的格子不是两点距离":距离判定只量远近,中间隔十堵墙它也说通;逐格行进把中间的每一格都摊开查一遍,墙藏不住——这也是逐格行进最让策划放心的地方,判定结果永远对得上地图。

为什么用格路不用像素线?格子系统里墙是成格的,像素线要查几百个点,格路只查几十格——精度和开销都卡在格子这一层,这是格子系统该有的算账方式——像素级的精度留给渲染,格子级的精度留给判定,各管各的。

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

lua
-- @file LineOfSight.lua
-- 视线判定 —— 两点连线的逐格行进

local LineOfSight = {}

local CONST = {
    MAX_STEPS     = 64,

    COLOR_CLEAR   = "#3fb950",
    COLOR_BLOCK   = "#f85149",

    AUTOINC_BASE  = 1142000,
}

local _isWall     = nil
local _autoInc    = 0

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

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

function LineOfSight.BindWall(func)
    _isWall = func
end

function LineOfSight.BuildPath(x0, y0, x1, y1)
    local dx = math.abs(x1 - x0)
    local dy = math.abs(y1 - y0)
    local sx = x0 < x1 and 1 or -1
    local sy = y0 < y1 and 1 or -1
    local err = dx - dy
    local path = {}
    local cx, cy = x0, y0
    while true do
        path[#path + 1] = { cx, cy }
        if cx == x1 and cy == y1 then break end
        local e2 = 2 * err
        if e2 > -dy then
            err = err - dy
            cx = cx + sx
        end
        if e2 < dx then
            err = err + dx
            cy = cy + sy
        end
    end
    return path
end

四、核心代码:完整模块(下·挡格判定与畅通结论)

lua
-- 视线主入口:走廊逐格查墙,撞墙记序号
function LineOfSight.Check(x0, y0, x1, y1)
    if not _isWall then
        ShowTip("先挂墙表再查视线")
        return false, 0
    end
    local path = LineOfSight.BuildPath(x0, y0, x1, y1)
    for i, pos in ipairs(path) do
        if i > 1 and i < #path then
            if _isWall(pos[1], pos[2]) then
                return false, i
            end
        end
        if i > CONST.MAX_STEPS then break end
    end
    return true, #path
end

function LineOfSight.Cast()
    local myID = SL:GetValue("USER_ID")
    local myX = SL:GetValue("ACTOR_GRID_X", myID)
    local myY = SL:GetValue("ACTOR_GRID_Y", myID)
    local tid = SL:GetValue("MARK_TARGET_ID")
    local tX = SL:GetValue("ACTOR_GRID_X", tid)
    local tY = SL:GetValue("ACTOR_GRID_Y", tid)
    if not myX or not tX then return end
    local ok, at = LineOfSight.Check(myX, myY, tX, tY)
    if ok then
        ShowTip("视线畅通——" .. at .. "格直达")
    else
        ShowTip("视线被挡——第" .. at .. "格是墙")
    end
end

SL:ScheduleOnce(function()
    LineOfSight.BindWall(function(c, r)
        return SL:IsBlockCell(c, r)
    end)
    SL:BindDebugButton("查视线", function()
        LineOfSight.Cast()
    end)
    ShowTip("技能已加载: 视线判定 (调试按钮触发)")
end, 1.0)

function LineOfSight.Unload()
    _isWall = nil
end

return LineOfSight

五、机制问答

问:起点和终点自身是墙算挡吗?
答:不算——走廊的首尾两格跳过墙检,站在墙边缘的单位不算自己挡自己,这是口径不是漏洞。

问:斜着走的格子和直线偏差大吗?
答:偏差在半格以内——逐格行进走的是最贴近直线的格路,斜率带来的偏差比一格小,格子系统够用。

问:视线判定的开销随距离怎么涨?
答:线性涨——走廊的格数约等于两点的格距,对角线最长也就几十格,查一格一次墙表,开销可控。

问:被挡时为什么还要记挡格序号?
答:序号是调试的眼睛——报"挡了"不如报"第几格挡了",配上演示一眼就能看出是哪堵墙惹的祸。

问:视线通了就一定打得到吗?
答:不一定——视线管的是墙,射程管的是距离,两道闸各管各的,都过才算打得到。

六、调参与实战怎么用

第四笔是走廊的生成:逐格推进的斜率账是这套判定的地基——横竖斜三选一的每一步都由误差项决定,这一段照抄不要自创,自创的走线在对角处容易穿墙角。第三笔是首尾的豁免:起终点跳过墙检是口径——不豁免的版本贴墙站的角色永远判挡,豁免的口径要写进注释里让人知道是故意的。第二笔是步数的保险:最大步数封顶是死循环的刹车——斜率账写错时走廊可能原地打转,刹车让最坏情况也只是误判挡而不是卡死。第一笔是墙表的挂载:墙表以回调挂进来不写死——寻路的墙、渲染的遮挡、仇恨的视线可能各有一张墙表,一套走廊查多张表才是复用。常见坑三个:斜率账自创导致对角穿墙;首尾没豁免贴墙即挡;最大步数没封顶死循环。

实战里这条走廊不止查视线:寻路的预处理、技能的弹道预判、小地图的敌我连线,全是同一条格路换皮。组里的约定是:格子系统里的"能不能"类判定,一律先摊路再逐格过——路摊开了,什么都能查,查不出也能报出挡在哪一格。这篇的模块照抄能跑,改的就是墙表的挂载。

写完留一句给做判系的同学:视线判定卖的是"路摊开了什么都藏不住"——那条一格一格走的光路和撞墙时报出的序号,是把"看得见"三个字从玄学变成账目的第一步。

作者履历与出处

本文由 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 技术组