【游戏功能】
先抛一个坑:隔着一堵墙的怪,为什么看得见你?寻路系统说路不通,仇恨系统却说你被锁定了——两套系统对"能不能看见"各说各话,根子是战场上根本没有一条统一的视线判定——判定有了,寻路和仇恨才有得对表。视线判定补的就是这一块:从你的眼到它的眼,一条线逐格走过去,撞墙即挡,全程通畅才算看得见——判定挂上之后,两套系统才有同一张表可对。这篇把整套写法拆开:配置表、逐格行进、挡格判定、畅通结论、定时器、调试按钮,一段一段照抄能跑。
一、效果演示:两点连线的逐格行进
演示场一张布了墙的格子图。点「换目标」:青色的观察眼和橙色的目标重新连线——一条光路从观察眼出发一格一格往前走:走的是绿格,路是通的;撞上灰墙的那一格变红,"视线被挡——第几格是墙"当场报数。点「换墙布」:墙重新撒一遍,光路重算。光路怎么走、走到哪被挡,全程一步一步演给你看。演示里试两个目标的对比:同一段距离,绕得开墙的畅通,绕不开的一格红。视线判定教的是行进:看得见看不见,不猜,一格一格走给系统看——走完的每一步都是证据,判挡判通都有出处。
flowchart TD
A[起点入列] --> B[向目标推进一格]
B --> C{这一格是墙}
C -- 是 --> D[判挡 记下挡格]
C -- 否 --> E{到目标了吗}
E -- 否 --> B
E -- 是 --> F[判通]
fx-losray
二、底层原理:一次逐格行进加一道挡格判定
模块的机关是一对搭档。逐格行进:从起点到终点按直线的斜率推进——每一步决定横走还是竖走还是斜走,走过的格排成一条最贴近直线的格路,这条格路就是视线走廊。挡格判定:走廊上每一格过一遍墙表——撞墙的那一格记下序号当场判挡,走到底没撞墙判通。和距离类判定的分野在"视线认的是路上的格子不是两点距离":距离判定只量远近,中间隔十堵墙它也说通;逐格行进把中间的每一格都摊开查一遍,墙藏不住——这也是逐格行进最让策划放心的地方,判定结果永远对得上地图。
为什么用格路不用像素线?格子系统里墙是成格的,像素线要查几百个点,格路只查几十格——精度和开销都卡在格子这一层,这是格子系统该有的算账方式——像素级的精度留给渲染,格子级的精度留给判定,各管各的。
三、核心代码:完整模块(上·骨架)
-- @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
四、核心代码:完整模块(下·挡格判定与畅通结论)
-- 视线主入口:走廊逐格查墙,撞墙记序号
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 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【语法算法】 上一版的炮台索敌逻辑有个隐蔽报错:炮台永远打同一个怪——即使那只怪已经死了,索敌的游标也不挪窝。追到底索敌的游…
【语法算法】 先抛一个坑:打不完的火系怪怎么办?火抗怪火打不动,换冰系技能要切装备要换面板——切完黄花菜都凉了。元素转换的答…
【语法算法】 上个月的事故复盘会上有个数字被念了三遍:四成——策划写的是"同伴陪疼四成",代码落下去成了"陪疼四十点",两只…
【语法算法】 单行代码拆解:弹射初速=-420——弹射的全部动力就这一行的负初速。负号朝上、四百二十是弹射的初速大小——踩上…
【语法算法】 上一版的减速类模块全按"乘以零点五"来写,帧率无关的版本照搬了这套写法——结果高帧率机上减速效果好,低帧率机上…
【语法算法】 先抛一个坑:怎么把散在四处的怪聚到一起打?逐个拉是笨办法,一个范围技又只能打一片——引力球的答案是一个会动的吸…