NOTE
逆向方法与数据支撑:本文基于对 32 位客户端 Torchlight2.exe(ImageBase 0x400000,文内所有 sub_XXXXXX 与 0xXXXXXX 均为绝对虚拟地址)寻路模块 0x462F60–0x464400 的反编译体与汇编分析,并对 10 处关键常量与指令完成 PE 字节级核对(每步移动代价、结点大小、提前收敛阈值、迭代上限字段、吸附半径、哨兵、后继槽数)。该模块共 24 个函数,当前已逆向其中 8 个核心函数。配套数据涵盖官方 7 张地图 .mpp 走格的连通性普查,以及静态道具阻挡与走格对账分析。所有尚未完全闭环的边界均如实标注。
核心工程结论:原版寻路采用 8 邻域 A* 算法,但代价模型与启发函数具有特殊设计——正交与斜向每步代价恒定为 1.0,而启发函数为欧氏距离的平方。由于 $g$(单位:格)与 $h$(单位:格²)量纲不一致,启发函数不可采纳,算法行为偏向贪心最佳优先搜索,无法保证生成最短路径;怪物绕远与贴墙转向等表象系搜索算法特性导致。此外,“可行走站立”与“路径规划通行”为两套判定系统(前者单点无碰撞半径,后者具备净空约束,单格缝隙无法规划通过);TL2 客户端不基于 3D 碰撞体裁决通行,场景装饰物无实体碰撞,通行性完全由 .mpp 走格数据决定。
0. 核心结论速览
- 模块与入口:寻路模块位于
0x462F60–0x464400,共 24 个函数(已分析 8 个核心函数)。移动逻辑sub_5425A0调用sub_464010(unit+112 /*世界X*/, unit+120 /*世界Z*/, goalX, goalZ)触发寻路。 - 8 邻域调用确认:
sub_463940中对sub_4637B0的call指令共有 8 处,整段.text对sub_4637B0的引用亦仅此 8 处。结点结构中分配了 8 个后继槽位(cmp ecx, 8@0x463881),对应 8 邻域展开。 - ★★★ 每步代价恒 1.0,启发函数为距离平方:每步代价固定增加 1.0(
fadd dword ptr [0x213C5DC],内存值为float 1.0),斜向移动亦为 1.0;启发函数 $h = (dx)^2 + (dz)^2$(单位为格²)。由于启发不可采纳,原版无法保证最短路径。辅助证明:sub_463650为向已闭合结点回传并更新更优 g 值的专用例程。 - 起终点 ±16 格吸附:目标吸附
sub_463270在[cx−16, cx+16] × [cz−16, cz+16](33×33 方格范围)内选取世界坐标欧氏距离最近的可走格,哨兵距离 9999.0;起点吸附sub_4633F0同样扫描 ±16 范围,但候选格额外要求周围具备净空。16 格对应 6.4 世界单位。 - A 内核结构*:结点通过
calloc(1, 0x40)分配(64 字节);Open 表ctx+12与 Closed 表ctx+16均为单链表,sub_463220按 f 值升序插入,主循环取表头;按索引查找结点采用单链表线性遍历($O(n)$)。 - 迭代上限与提前收敛退出:主循环存在迭代次数限制(
if (i > *(DWORD*)(ctx+68)) break;),超出上限时以当前 h 最小结点作为局部路径返回,并清零完整路径标志(ctx+66 = 0)。若目标曾被吸附修正,只要当前最佳结点 $h < 2.0$(格²,约 1.41 格距离,常量地址[0x2143F64])即提前终止搜索。 - ★★ 可行走判定与路径规划判定分离:
CLevel::isWalkable(sub_5DB690)为单点无半径查表,判定单位能否在此处站立;寻路例程sub_463940具备净空要求(正交移动要求侧向有空格,斜向要求两个正交邻格均可走),判定能否规划路径通过。因此单格宽(0.4u)缝隙无法规划穿行,地图通道设计应至少预留 2 格宽。 - 双层格网与阻挡极性:
ctx+48为主格网(必查),ctx+52受ctx+65门控(仅为 false 时检查)。阻挡判定条件为有符号字节 $> 0$;255 解释为 $-1$ 视为可行走,与isWalkable(有符号 $\le 0$ 可走)保持一致。 - 地图连通分量分布:7 张官方测试地图走格并非单块整体,包含 3 至 67 个独立连通分量,主分量占比在 39.7% 至 92.1% 之间。寻路系统无“最大连通分量”概念,吸附格不可达即寻路失败。
- ★★★ 静态模型碰撞脱节(幽灵墙现象):7 张地图中 4,943 个标记为 OBSTACLE 的装饰道具中,有 639 个(约 13%)其坐标所在格子在
.mpp中为可行走。TL2 原版不使用 3D 碰撞体裁剪移动,阻挡完全由.mpp走格决定,因此穿透部分装饰模型属于原版设计行为。 - 未闭环事项:净空规则总开关
ctx+72的写入条件、双层格网的具体职责分工、迭代上限配置值以及是否存在视线平滑(string pulling)例程尚未定位。
1. 模块位置、入口与函数清单
寻路模块是 .text 里一段连续的区间:0x462F60–0x464400,ALLFUNCS/_realfuncs.json 落在该区间的条目共 24 个。本文已分析 8 个核心例程,职责如下:
| 函数 | 体积(字节) | 职责 |
|---|---|---|
sub_464010 | 656 | 世界坐标 → 格坐标、起终点吸附、派发搜索 |
sub_463270 | 371 | 目标吸附:±16 格方块内取世界欧氏最近的空格 |
sub_4633F0 | 604 | 起点吸附:同样 ±16,候选格另要求周围有净空 |
sub_463EA0 | 360 | A* 主循环 |
sub_463940 | 1367 | 邻域展开,正好 8 次 call sub_4637B0 |
sub_4637B0 | 387 | 松弛一个邻居:g = parent.g + 1.0,h = 距离²,f = g + h |
sub_463220 | 65 | 开表按 f 升序插入 |
sub_463650 | 266 | 把改小了的 g 回传给已闭合结点 |
其余未分析函数:sub_462F60 / sub_463030 / sub_4630A0 / sub_463130 / … / sub_4642A0 / sub_464360 等。部分高级机制(如平滑拉线)仍需结合剩余函数进一步排查。
入口:移动侧 sub_5425A0 调 sub_464010(unit+112, unit+120, goalX, goalZ),其中 +112 / +120 是单位结构体里的世界 X / Z。
搜索方向:sub_464010 调的是 sub_463EA0(goalX, goalZ, startX, startZ, 1)——根结点是起点、终止条件是走到终点;最后那个 1 让它把 parent 链就地反转,结果读出来是「起点 → 终点」。起终点落在同一格时不跑搜索,直接置 ctx+67 = 1(=「已经到了」)。
MOD 开发结论:寻路系统为独立硬编码算法,不解析单位 DAT 配置,无法通过单位属性修改搜索行为;地图通行性仅由对应关卡的 .mpp 走格数据决定。
2. 走格取值:格子换算、两层格网、阻挡极性
2.1 世界坐标到格坐标
cell = floor((world − origin) × (1.0 / cellSize))寻路上下文(下文写作 ctx)里与格网相关的字段:
ctx 偏移 | 含义 |
|---|---|
+32 | Z 方向格数 |
+36 | X 方向格数 |
+40 | cellSize(一格 = 0.4 世界单位) |
+48 | 第一层格网(永远查) |
+52 | 第二层格网(仅 ctx+65 为假时查) |
+56 | origin X |
+60 | origin Z |
2.2 两层格网
ctx+48 永远查;ctx+52 只在 ctx+65 为假时查,两层的判定完全同形。
WARNING
哪一层对应静态地形、哪一层对应动态阻挡占位,尚需结合动态写入点确认;ctx+65 置位源头未分析。
2.3 阻挡极性:有符号 byte > 0
阻挡判定条件为有符号 byte > 0。这与 CLevel::isWalkable(sub_5DB690)的 setle(有符号 ≤ 0 视为可走)保持一致:数值 255 在两处均被解析为 −1(可行走)。
该判定与走格导出工具的解析规则 walk = (cells == 0) 相互印证——发行 .mpp 中的原始数据仅包含 {0, 1, 255},255 在装载后进入内存不会被视为阻挡。
MOD 开发结论:走格数据中可行走判定采用区间模型(有符号 ≤ 0)。数值 255(−1)通常代表地图边界外或未定义瓦片,引擎将其视为可行走。编写走格编辑工具时切忌将 255 误判为实心阻挡。
3. 起点与目标的吸附:±16 格
3.1 目标吸附 sub_463270
目标格越界或被挡时,扫 [cx−16, cx+16] × [cz−16, cz+16] 这个 33×33 方块,把每个空格换算回世界坐标求欧氏距离,取最小。哨兵是 9999.0;一个空格都没有就原样返回。命中时把 ctx+64 置 1(=「目标被挪过」)。
字节层:lea ebx, [esi − 0x10] @ 0x4632DD、lea eax, [esi + 0x10] @ 0x4632E4;哨兵 float 9999.0 在 0x214882C。
3.2 起点吸附 sub_4633F0
同样的 ±16 方块、同样的欧氏最近,但候选格额外要求周围有净空(见 §5)。
3.3 吸附机制的影响
玩家点击阻挡区域角色移动至边缘的现象,并非前端表现层插值,而是寻路规划层吸附逻辑的直接结果。
MOD 开发结论:
- 16 格对应 6.4 世界单位。目标点距最近可行走格超过 16 格时寻路直接判定失败,单位不会尝试部分趋近。
- 吸附计算取世界坐标欧氏最近距离,非简单的曼哈顿或切比雪夫逐圈扩展,斜向边界处判定更为严格。
- 目标若触发吸附(
ctx+64置 1),将激活 §4.5 中的提前终止条件(最佳结点 $h < 2.0$ 即退出),可能导致单位在距离吸附目标约 1.41 格处停止移动。
4. A* 内核:结点、开闭表、代价、启发
4.1 结点:64 字节
主循环用 calloc(1, 0x40) 分配结点(push 0x40 @ 0x463ED7),布局按 DWORD 下标:
| 下标 | 字节偏移 | 含义 |
|---|---|---|
[0] | +0 | f |
[1] | +4 | h |
[2] | +8 | g |
[3] | +12 | x |
[4] | +16 | z |
[5] | +20 | index = x + z × width |
[6] | +24 | parent |
[7..14] | +28..+56 | 最多 8 个后继回指针(cmp ecx, 8 @ 0x463881) |
[15] | +60 | 链表 next |
4.2 开表与闭表:单链表 + 线性扫
开表 ctx+12、闭表 ctx+16,都是单链表(next 在结点 +60)。sub_463220(65 字节)按 f 升序插入,主循环每轮摘表头 ⇒ 等价于优先队列。
但按 index 找结点是线性扫链(while (node[5] != idx)),O(n)。这正是它需要迭代上限的原因。
4.3 ★★★ 代价恒 1.0,启发是距离的平方
sub_4637B0 松弛一个邻居:
g = parent.g + 1.0 // fadd dword ptr [0x213C5DC],该四字节现读 = float 1.0
h = (dx)² + (dz)² // 单位是格²
f = g + h两项关键特征:
- 每步代价恒定 1.0:正交与斜向移动代价相同,度量标准为切比雪夫度量,而非欧几里得度量。
- 启发函数为欧氏距离平方:$g$ 的度量单位为格,而 $h$ 的单位为格²。量纲不匹配导致 $h$ 远距离时呈平方级放大,启发函数不可采纳(Admissible 条件失效)。算法整体偏向贪心最佳优先搜索,无法保证生成最短路径。
代码佐证:sub_463650(266 字节)为专门负责向已闭合结点回传更优 g 值的逻辑。在经典 A* 中若启发函数严格可采纳,闭合结点的 g 值即为最优,无需回传;当启发不可采纳导致闭表结点可能被更优路径重新松弛时,方需此逻辑进行修正。
MOD 开发结论:怪物绕行、贴墙转弯或舍近求远等寻路表现,由底层启发函数的数学特性决定,并非 AI 决策层逻辑缺陷或碰撞半径配置错误。
4.4 迭代上限与部分路径
if (i > *(DWORD*)(ctx+68)) break; // cmp ebp, [esi+0x44] @ 0x463F8A迭代计数超过上限后,搜索直接将 ctx+8(历史 h 最小结点)作为局部终点返回,并将 ctx+66 设为 0(标记非完整路径)。
WARNING
迭代上限字段 ctx+68 的配置初值尚未通过动态调试确认。
MOD 开发结论:超大开阔场景中超远距离点击可能导致单位中途停步,系触发迭代步数上限所返回的截断路径。
4.5 提前终止退出:h < 2.0
当目标曾触发吸附(ctx+64 置位)时,若当前搜索最优结点的启发值 $h < 2.0$(格²,即实际距离小于约 1.41 格),寻路立即提前终止并退出,同时标记 ctx+66 = 0。浮点常量 2.0 读取自 [0x2143F64](fld dword ptr [0x2143F64] @ 0x463F76)。
注意触发前提:该逻辑仅在目标曾被吸附修正时生效。若目标初始即落在可走格上,搜索将严格推进至终点。
5. 8 邻域展开与净空规则分析
5.1 8 邻域调用点静态核对
sub_463940(1367 字节)负责邻域展开,汇编中调用 sub_4637B0 的 call 指令共有 8 处:
sub_463940内部包含 8 处call sub_4637B0;- 整个
.text代码段中对sub_4637B0的交叉引用亦严格仅有 8 处; _realfuncs.json统计显示sub_4637B0仅被单个函数调用。
结点数据结构中偏移 [7..14] 预留了 8 个后继结点槽位(cmp ecx, 8 @ 0x463881),在结构层面印证了 8 邻域模型。
NOTE
寻路邻域展开方式经汇编调用计数与结点槽位验证,确认为 8 邻域展开模型,而非 4 邻域。
5.2 净空与拐角约束
| 情形 | 条件约束 |
|---|---|
| 斜向移动 | 两个相邻的正交格均必须可行走(拐角规则,禁止贴墙切角移动) |
| 正交移动 | 目标格所在行必须包含至少一个可通行的横向相邻格 |
起点吸附(sub_4633F0) | 候选落脚格周围必须具备净空 |
正交移动约束表明:单格宽度(0.4u)的狭缝无法被规划算法选为通路。即便狭缝内各格子 isWalkable 均返回可通行,由于缺乏侧向净空格,寻路算法依然会拒绝规划穿行。
CAUTION
上述净空判定均以 ctx+72 为真作为前置开关。ctx+72 的赋值逻辑与开启条件当前尚未逆向闭环。
5.3 ★★ 可行走判定与路径规划判定的分离机制
两者在引擎中具有明确分工:
| 判定范畴 | 对应函数 | 判定准则 |
|---|---|---|
| 站立可行走性 | CLevel::isWalkable(sub_5DB690) | 单点查表,有符号 ≤ 0 即可,不考虑碰撞体积半径 |
| 路径规划通行性 | 寻路例程 sub_463940 / sub_4633F0 | 包含净空与拐角约束(§5.2),具备隐式宽度要求 |
二者互不冲突:“单位可以被放置于单格宽缝隙中站立”与“寻路规划器拒绝将单位规划穿过单格宽缝隙”同时成立。
MOD 开发结论:
- 场景通路设计须保持至少 2 格宽度(0.8u)。单格宽度的通道即使在
.mpp中刷为可走,寻路算法亦无法生成穿越路径。 - 摆放装饰物与绘制走格时,通道余量应依据规划净空规则评估,而非仅以单点可站立为准。
6. 字节锚点表
下表列出本文所有关键结论的 PE 字节级锚点。每一条都可以在 Torchlight2.exe 里按虚地址直接核对。
| 锚点 | 虚地址 | 字节 | 长度 | 说明 |
|---|---|---|---|---|
step_cost_fadd | 0x4637BF | D8 05 DC C5 13 02 | 6 | sub_4637B0:fadd dword ptr [0x213C5DC]——每步代价的加数 |
step_cost_value | 0x213C5DC | 00 00 80 3F | 4 | 上一条指向的四字节 = float 1.0 |
node_is_64_bytes | 0x463ED7 | 6A 40 | 2 | sub_463EA0:push 0x40——calloc 的结点大小 |
early_accept_fld | 0x463F76 | D9 05 64 3F 14 02 | 6 | sub_463EA0:fld dword ptr [0x2143F64] |
early_accept_value | 0x2143F64 | 00 00 00 40 | 4 | 上一条指向的四字节 = float 2.0(单位是格²) |
iteration_cap_field | 0x463F8A | 3B 6E 44 | 3 | sub_463EA0:cmp ebp, [esi+0x44]——0x44 = 68 = 迭代上限字段 |
snap_radius_minus_16 | 0x4632DD | 8D 5E F0 | 3 | sub_463270:lea ebx, [esi − 0x10] |
snap_radius_plus_16 | 0x4632E4 | 8D 46 10 | 3 | sub_463270:lea eax, [esi + 0x10] |
snap_sentinel_value | 0x214882C | 00 3C 1C 46 | 4 | sub_463270 的哨兵 = float 9999.0 |
eight_child_slots | 0x463881 | 83 F9 08 | 3 | sub_4637B0:cmp ecx, 8——结点最多 8 个后继 |
7. 发行地图走格的连通分量普查
用 tools/f2_walkgrid_reach.py 离线读七张验收图导出的走格(四邻域洪泛),分量阈值 200 格(≥ 200 算「主要分量」,否则算碎屑)。「最近可走格」一列以复刻实现侧的合成出生点 (132.0, 44.0) 为基准。
| 场景 | 格网 | 可走格 | 分量数 | 主要分量(格) | 主分量占比 | 碎屑(块 / 格) | 最近可走格落在主分量上? |
|---|---|---|---|---|---|---|---|
act1_town | 301 × 350 | 20,003 | 9 | 16,572 / 3,166 | 82.8% | 7 / 265 | 是 |
clockwork_boss | 401 × 226 | 18,310 | 3 | 15,715 / 2,594 | 85.8% | 1 / 1 | 否(落在 2,594) |
echopass_jd | 601 × 1050 | 86,611 | 29 | 34,413 / 28,058 / 13,465 / 8,322 / 880 / 555 / 430 | 39.7% | 22 / 488 | 否(落在 28,058) |
echopass_jt | 650 × 900 | 80,496 | 29 | 56,734 / 13,467 / 8,356 / 898 / 429 | 70.5% | 24 / 612 | 是 |
echopass_lm | 925 × 900 | 87,941 | 23 | 64,156 / 13,465 / 8,322 / 430 / 411 / 358 / 281 | 73.0% | 16 / 518 | 是 |
echopass_pb | 625 × 1125 | 99,834 | 28 | 53,709 / 23,451 / 13,465 / 8,322 / 430 | 53.8% | 23 / 457 | 否(落在 129 格的碎屑上) |
sundered_zone | 1328 × 1352 | 506,886 | 67 | 466,800 / 37,023 / 2,148 / 330 | 92.1% | 63 / 585 | 是 |
七张里 3 张的「最近可走格」不在主分量上。echopass_pb 最极端:主分量 53,709 格,最近可走格却落在一块 129 格(0.1%)的碎屑上。
NOTE
4 张 Echo Pass 地图变体中重复出现相同的 13,465 / 8,322 格连通区域,系共用地形瓦片导致的真实阻隔(如水体/悬崖),而非数据导出异常。普查工具仅统计拓扑分布,不裁决设计合理性。
MOD 开发结论:
- 引擎内部不存在“最大连通分量”的概念。目标吸附仅在 ±16 格范围内寻找欧氏距离最近的可行空格,不预先验证该空格与起点是否处于同一连通块;若不可达则寻路直接失败。
- 地图全局“可行走”不代表“任意两点可互相到达”。拼装地图中被水体、悬崖物理阻隔划分的孤岛属于正常地形;但若将出生点、传送门或关键 NPC 放置在孤立碎屑连通分量中,将导致逻辑死锁。
8. 场景物理碰撞与走格阻挡解耦分析(幽灵墙现象)
8.1 现象复现与微观抖动
在 echopass_jd(seed 1234)场景测试中,移动状态机出现持续卡顿:无法移动至目标点,解卡逻辑持续触发(849 次),坐标在 $x = 45.3$ 与 $45.7$ 之间往复振荡,振幅严格等于单次解卡步长(0.4u)。
8.2 阻挡源排查定位链路
- 启用碰撞体检测,获取产生阻挡接触的具体碰撞体名称:
col_Room_Piece0_clone723428_85,法线方向为 (0.96, 0.00, 0.29),呈现朝向 +X 的竖直阻挡面。 - 前方每 0.4u 发射垂直射线检测地面高度剖面:0.00 / 0.00 / 2.00 / 1.72 / 0.79 / 0.00 …,证实前方存在约 2.0u 的高度突变,台阶攀爬判定正确拒绝(最大攀爬高度限制为 0.6u)。
- 将走格通行性与物理几何模型映射至同一网格比对:走格整行全部显示为可行走,而几何模型上呈现 1.1–4.0u 的隆起带。
- 追溯场景模型资源节点:对应 Mesh 路径为
props/z1tundra_props/rock_06.res(独立石头模型)。
8.3 通行阻挡裁决机制定性
早期移植管线根据模型资源命名包含 rock 将其归入 OBSTACLE_KEYS 并挂载了实心 3D 碰撞体。然而发行 .mpp 走格数据中该位置标记为可行走。
原版走格具有唯一裁决权:Torchlight II 客户端在角色常规通行中不依赖 3D Mesh 物理碰撞体,能否通行完全由 CLevel::isWalkable(sub_5DB690)单点查表决定。场景装饰物(如石头、路障模型)在原版中不具备实体碰撞阻挡效果,角色穿透模型通过属于符合原版设计的行为。
8.4 官方场景资源普查统计
通过工具扫描官方场景瓦片,比对模型分类与底层走格的可通行性:
| 场景 | 节点数 | OBSTACLE | GROUND | 不建碰撞 | 幽灵墙(模型标记阻挡但走格可通行) | 占 OBSTACLE 比 |
|---|---|---|---|---|---|---|
act1_town | 1,800 | 123 | 116 | 1,561 | 36 | 29.3% |
clockwork_boss | 706 | 169 | 63 | 474 | 4 | 2.4% |
echopass_jd | 4,184 | 327 | 445 | 3,412 | 75 | 22.9% |
echopass_jt | 5,598 | 470 | 442 | 4,686 | 115 | 24.5% |
echopass_lm | 4,524 | 240 | 457 | 3,827 | 53 | 22.1% |
echopass_pb | 6,473 | 514 | 530 | 5,429 | 112 | 21.8% |
sundered_zone | 21,012 | 3,100 | 5,024 | 12,888 | 244 | 7.9% |
| 合计 | — | 4,943 | — | — | 639 | 13% |
echopass_jd 典型样例如下:
| 节点 | Mesh 路径 | XZ 坐标 |
|---|---|---|
Room_Piece0_clone12282319_12 | props/Z1Tundra_props/rock_05.res | (38.2, 68.9) |
Room_Piece0_clone12093446_34 | props/z1tundra_props/rock_06.res | (37.9, 70.6) |
Room_Piece0_clone12282230_61 | props/Z1Tundra_props/rock_05.res | (54.9, 67.6) |
Room_Piece0_clone12227765_65 | props/z1tundra_props/rock_06.res | (75.9, 25.7) |
Room_Piece3_clone12281951_75 | props/Z1Tundra_props/rock_01.res | (69.7, 48.8) |
WARNING
统计数值 639 属于保守下界(仅根据节点原点坐标落点判定,未叠加模型包围盒占地面积)。由于走格为 2D 投影,局部桥梁分层区域可能存在投影重叠。
8.5 调试过程中的常见误区与工具校验陷阱
- 碰撞射线误判落脚面:排查初期将模型高度抬升误认为桥面落脚点,反复调整高度解卡逻辑无果。后经核查发现测试射线未配置
collision_mask,射线击中 OBSTACLE 层而非 GROUND 真实可行走地形。 - 正则边界导致普查漏检:早期普查脚本对瓦片节点末尾
groups=["tl2_room_piece"]中的闭合括号解析错误,导致含组属性的房间瓦片被全部过滤。后引入端到端基准对照机制(以实测卡顿节点作为强制测试样本),消除工具解析偏差。
8.6 MOD 制作与关卡设计准则
- ★★★ 装饰道具不产生通行阻挡:通行性完全由
.mpp走格数据决定。单纯摆放石头、木箱等装饰模型无法阻挡角色;若需形成阻挡,必须在走格数据中将对应格子刷为阻挡。 - ★ 避免空气墙事故:若在
.mpp中刷为阻挡但视觉上无任何模型覆盖,将导致玩家遭遇“空气墙”。 - ★ 重制与移植建议:若基于资源名称规则自动生成 3D 碰撞体,必须与
.mpp走格数据进行双向对账校验,防止生成阻挡原本可行走的幽灵墙。
9. 移植与复刻实现口径对照
本节对照重制与移植开发中的算法参数与行为差异:
| 机制项 | 原版客户端引擎 | 移植参考实现(Godot AStarGrid2D 等) | 行为差异与影响 | 对齐建议 |
|---|---|---|---|---|
| 邻域拓扑 | 8 邻域展开 | 8 邻域展开 | 一致 | — |
| 拐角规则 | 斜向移动要求两个正交邻格均可行走 | 启用斜向阻挡检测 | 一致 | — |
| 目标吸附半径 | 16 格(方块区域取欧氏最近) | 16 格(切比雪夫由内向外逐圈扩散) | 斜角方向存在 1 格差异 | 对齐欧氏距离扫描公式 |
| 起终点重合 | 跳过搜索直接成功 | 跳过搜索直接成功 | 一致 | — |
| 代价与启发模型 | 每步代价 1.0 + 欧氏距离平方启发(偏贪心) | OCTILE 启发 + $\sqrt{2}$ 斜向代价(最优路径) | 移植版路径通常比原版更短更直 | 如需完全复刻原版走位手感,需重载自定义启发函数 |
| 正交净空约束 | 目标行需具备横向可行走邻格 | 无额外净空约束 | 移植版允许穿行 1 格宽狭缝 | 引入净空后处理或自定义展开准则 |
| 迭代上限 | 存在上限(超出返回局部最优路径) | 通常无内置上限 | 极端远距离可能引起搜索帧耗时波动 | 结合连通分量预检提前拦截不可达目标 |
| 双层格网 | ctx+52 动态层(受门控控制) | 单层静态格网 | 动态单位阻挡处理差异 | 待明确动态格网写入机制后补齐 |
| 视线平滑(string pulling) | 未完全确定(当前分析函数未见拉线逻辑) | 包含拐点平滑过滤 | 移植版折线拐点较少 | 当前逆向范围尚未覆盖,不代表原版完全缺失 |
| 最大连通分量 | 无全局概念(局部吸附失败即退出) | 生成期约束出生点落在最大分量 | 保证动态出生点全局可达 | 原版关卡出生点为静态指定,无需动态计算 |
NOTE
吸附半径参数从 12 调整至 16 系规范对齐;由于实测样本中目标离最近可行走格通常不超过 12 格,该调整对小范围测试集的命中率无显著影响。
幽灵墙临时工程处理方案:在物理层发生卡顿时,临时从碰撞检测掩码中剔除装饰碰撞层,待移动指定位移或超时后恢复;关卡生成期根据走格数据过滤重合碰撞体方为根本解决方案。
CAUTION
寻路模块与原版的等价性验证最终须依赖原版运行时的逐帧轨迹(Trace Oracle)进行严格判定,单元测试通过仅表明算法自洽。
10. 未闭环事项清单
| 待验证项 | 当前状态 |
|---|---|
ctx+72(净空/拐角规则总开关)的写入调用点与激活条件 | 尚未完全定位,当前净空结论均基于该开关于运行时为真 |
双层格网 ctx+48 / ctx+52 的具体职责分工及 ctx+65 触发条件 | 尚未闭环,需排查动态单位占位逻辑 |
迭代步数上限 ctx+68 的默认初始化数值 | 需结合动态调试读取 |
| 引擎是否存在视线平滑(string pulling)机制 | 模块 24 个函数中尚有 16 个未逐行逆向,平滑例程尚未排除 |
| 寻路模块未读函数功能梳理 | sub_462F60、sub_463030、sub_4630A0、sub_463130 等例程待分析 |
| 装饰物碰撞与走格重叠的生成期清洗规则 | 需在瓦片导出阶段增加走格比对过滤 |
| 走格不可走但物理层缺失碰撞体的反向普查 | 尚未统计 |
附录 A:核心符号与虚拟地址表(Torchlight2.exe,ImageBase 0x400000)
| 功能描述 | 虚拟地址 / 符号特征 |
|---|---|
| 寻路模块连续区间 | 0x462F60–0x464400(共 24 个例程) |
| 移动触发调用入口 | sub_5425A0 → sub_464010(unit+112, unit+120, goalX, goalZ) |
| 坐标转换与搜索派发 | sub_464010(656 字节) |
| 目标点吸附(±16 格,欧氏最近) | sub_463270(371 字节;lea @ 0x4632DD / 0x4632E4;哨兵 0x214882C) |
| 起点吸附(±16 格 + 净空约束) | sub_4633F0(604 字节) |
| A* 主搜索循环 | sub_463EA0(360 字节;push 0x40 @ 0x463ED7;fld [0x2143F64] @ 0x463F76;cmp ebp, [esi+0x44] @ 0x463F8A) |
| 邻域展开(8 处调用) | sub_463940(1367 字节) |
| 邻居结点松弛(g + 1.0,h = 距离²) | sub_4637B0(387 字节;fadd [0x213C5DC] @ 0x4637BF;cmp ecx, 8 @ 0x463881) |
| Open 表按 f 升序插入 | sub_463220(65 字节) |
| 已闭合结点 g 值回传更新 | sub_463650(266 字节) |
| 每步代价浮点常量 1.0 | 0x213C5DC |
| 提前终止阈值浮点常量 2.0 | 0x2143F64 |
| 吸附距离初值哨兵 9999.0 | 0x214882C |
基础可行走判定 CLevel::isWalkable | sub_5DB690(单点查表,setle) |
附录 B:支撑源材料与逆向结论映射
- 逆向验证项:
- 走格寻路:8 邻域展开模型、切比雪夫移动代价与平方启发函数;可行走与路径规划判定分离;
- 碰撞解耦:静态装饰道具碰撞与
.mpp走格阻挡解耦机制、场景幽灵墙普查分析。
- 数据结构与静态配置清单:
walkgrid_pathfinding.json、phantom_walls.json。 - 分析工具:
tools/f2_walkgrid.py(走格导出)、tools/f2_walkgrid_reach.py(连通分量普查)、tools/f2_phantom_walls.py(装饰物与走格重合普查)。