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{是出口}
D -- 是 --> E[栈即通路 结束]
D -- 否 --> A
B -- 无 --> F[出栈回溯一格]
F --> A
demo
 fx-dfsmaze

二、底层原理:一次标记前进加一次出栈回溯

模块的机关是一对搭档。标记前进:每到一个格子先盖"来过"的章,再从四个方向里挑没去过的走——走一步入一层栈,栈里存的就是脚下这条路。出栈回溯:四向全是死路的那一拍出栈退一格——退回岔口换一条没走过的方向接着探,格子上的灰印就是回溯的尸检报告——灰印越多的迷宫,死胡同越多。和递归版的分野在"栈自己管深度":递归版的栈是调用栈,深度超限就爆;显式栈是自己建的表,一百层一千层都只是表长——那次爆栈事故换来的就是这一版。

栈为什么恰好就是通路?栈里每一层都是"从起点走到这里的一步"——回溯只出栈不改栈里剩下的层,探到出口那一刻,从栈底到栈顶读出来就是一条完整的路。这不是巧合,是深度优先的定义本身——栈有多深,路就有多长,栈空了,路就断在半途。

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

lua
-- @file DfsMaze.lua
-- 深度优先 —— 标记回溯的迷宫探索

local DfsMaze = {}

local CONST = {
    GRID_W        = 15,
    GRID_H        = 9,
    STEP_MAX      = 400,

    AUTOINC_BASE  = 1144000,
}

local _walls     = {}
local _visited   = {}
local _stack     = {}
local _done      = false
local _steps     = 0
local _exitC     = 0
local _exitR     = 0

local function KeyOf(c, r)
    return c .. "," .. r
end

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

function DfsMaze.SetMaze(wallTable, exitC, exitR)
    _walls = wallTable or {}
    _exitC = exitC
    _exitR = exitR
end

function DfsMaze.Reset(startC, startR)
    _visited = {}
    _stack = {}
    _done = false
    _steps = 0
    _visited[KeyOf(startC, startR)] = true
    _stack[#_stack + 1] = { startC, startR }
end

四、核心代码:完整模块(下·试探回溯与栈即通路)

lua
-- 试探回溯:单步推进,撞墙出栈
local DIRS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }

function DfsMaze.Step()
    if _done or _steps >= CONST.STEP_MAX then
        return false
    end
    local cur = _stack[#_stack]
    if not cur then
        _done = true
        ShowTip("走投无路——回溯到头也没出口")
        return false
    end
    local c, r = cur[1], cur[2]
    if c == _exitC and r == _exitR then
        _done = true
        ShowTip("到出口了——栈就是走通的路")
        return true
    end
    _steps = _steps + 1
    for _, dir in ipairs(DIRS) do
        local nc = c + dir[1]
        local nr = r + dir[2]
        if nc >= 0 and nr >= 0
            and nc < CONST.GRID_W and nr < CONST.GRID_H
            and not _walls[KeyOf(nc, nr)]
            and not _visited[KeyOf(nc, nr)] then
            _visited[KeyOf(nc, nr)] = true
            _stack[#_stack + 1] = { nc, nr }
            return true
        end
    end
    _stack[#_stack] = nil
    return true
end

function DfsMaze.Path()
    return _stack
end

SL:ScheduleOnce(function()
    SL:BindDebugButton("走一步", function()
        DfsMaze.Step()
    end)
    ShowTip("技能已加载: 深度优先 (调试按钮触发)")
end, 1.0)

function DfsMaze.Unload()
    _walls = {}
    _visited = {}
    _stack = {}
    _done = false
end

return DfsMaze

五、机制问答

问:栈版为什么不会爆栈?
答:栈是自己建的表——递归版的深度压在调用栈上,超限即挂;显式栈只是表长,千层迷宫也只是千个元素。

问:栈里的内容为什么恰好是通路?
答:回溯只出栈不重排——留下的每一层都是起点到脚下的必经步,探到出口从栈底读到栈顶就是答案。

问:灰色的回溯格还能再走吗?
答:能——回溯格盖着"来过"的章,不重复试探,但换一条路经过它时照样踩过去记进栈。

问:深度优先找到的是最短路吗?
答:不是——它找的是"第一条走通的路",要最短得换广度优先,两个朋友的分工从来不同。

问:最坏要走多少步?
答:格数封顶——每格至多进栈出栈一回,步数封顶的保险再兜一层,最坏也就是全迷宫刷一遍色。

六、调参与实战怎么用

第四笔是爆栈的教训:千层递归改显式栈是那场事故的整改——递归写法留着看,上线的版本一律用栈版,这条是组里的军规。第三笔是邻居的次序:四向的试探次序决定路的形状——先右后下找出的路贴边走,先下后右找出的路贴底走,次序一变路就变,但没有对错。第二笔是步数的刹车:四百步封顶是死循环的闸——标记漏打的迷宫能让探索者永远打转,刹车让最坏情况也只是提前收摊。第一笔是标记的口径:来过就盖章不看出栈与否——回溯过的格子保留绿印,这是防止转圈的根,也是灰印能留作尸检报告的前提。常见坑三个:递归版深层的爆栈事故;标记漏打导致原地转圈;出口判定漏了起点即终点的情况。

实战里深度优先不止走迷宫:任务的依赖试探、表格的连通检查、界面的焦点链遍历,全是"一条道走到黑再回头"的同一套骨架。组里的约定是:探路类逻辑先问深度上不封顶吗——上不封顶就显式栈,封了顶才配递归。这篇的模块照抄能跑,改的就是迷宫的墙表和出口。

写完留一句给做探索系的同学:深度优先卖的是"一条道走到黑的底气"——那一格格染绿又染灰的试探和最后一条金色的栈路,是把"走不通就回头"做成账本的一根探条。

作者履历与出处

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