跳转至

新游戏没重写旧引擎:直接 import 了 2D 测向模块

算法实验室 TSP 复盘 · 第 5 篇。低迹勘测是一款 3D FPV 侦察游戏,它的射频暴露模型没有重写——直接 import 了静默监听站(2D 网格游戏)的测向解算。这一篇记录一次被记录在案的判断反转,以及"机制级复用"和普通代码复用差在哪。

现象

低迹勘测的核心拉扯是"图传开关即生死开关":图传开着才看得见目标,但每次发射都是射频暴露,敌方测向站据此算方位线、滑窗交会出误差圆。这套暴露模型,静默监听站已经做完、验证过、平衡过。

问题是两个游戏连时空模型都不同:静默监听站是离散网格、tick 驱动;低迹勘测是连续坐标的方形世界(mapSize 1000)、秒级子步、Three.js 渲染。直觉说重写。

实际落点(low-trace-fpv/engine/simulate.ts):

import { tryBearingLine, updateEstimate, inErrorCircle } from '../../silent-listener/engine/direction-finding';

误差圆估计处的注释:"复用监听站估计器:方位线时效/滑窗/交会"。

我一开始的错误判断

这一篇的"错误判断"有白纸黑字的记录,而且相当打脸:我最初判断 3D 款根本不该进这个壳。 评审文档的结论原文是——等高线反读和低迹勘测"不进本框架,独立立项",理由包括"2 款 3D 款不适合塞进本壳"和"需要 Web 3D(Three.js)超出本壳定位"。

后来的路线图推翻了它:低迹勘测以"Three.js 入本框架"立项,并明确写下"射频测向解算复用监听站 engine(机制级联动)";评审文档在 FPV 条目里其实早已埋了伏笔——"值得做但要等:依赖监听站验证后的射频暴露模型"。也就是说,复用不是实现阶段的将就,是立项阶段的计划;真正被反转的是"3D 塞不进本壳"这个前提。

另外两条被否掉的路也值得记录:重写一套"3D 化"的测向模型,意味着暴露模型要做两次平衡——静默监听站那轮 12-seed 的平衡验证全部重跑,两游戏的暴露手感还会渐渐漂移;抽公共库,意味着在只有两个调用方、还看不见第三个的时候设计抽象。都不划算。

排查与关键证据

先分清要复用的是代码还是机制。 要复用的不是某个函数,是暴露模型的三条机制:方位线有时效、窗口内两两交会、交会收敛成误差圆。这三条与坐标系无关——2D 网格只是它们当时的宿主。

把隐含假设显式化成映射层。 low-trace-fpv/engine/types.ts 里专门有一段:

/* --- 射频(复用监听站测向解算的参数映射) --- */

下面是一组 rf 前缀参数:命中基率、距离衰减、误差除数、方位线时效、滑窗长度、误差圆半径上下限。每个旧参数在新语境里重命名、重标定——映射是显式的一层,而不是散落在调用点的适配。

桥接点收窄到一处。 3D 侧只负责一件事:把"我在哪、何时发射"喂给旧解算器。simulate 的子步循环里,"图传开启时 1Hz 尝试"调用测向解算(代码注释原话),其余飞行、地形、巡逻都是新世界自己的事。目视通道则是新游戏自己的模型——连续注视若干秒才注入一条高精度方位线,与射频通道并行。

引擎纯度没破。 新游戏 engine 照旧不碰渲染层,simulate.test.ts 正常单测;两游戏的测向行为由同一份代码保证一致——静默监听站里验证过的暴露手感,在 3D 世界里原样成立。

补测实验:同一解算器、两套映射参数的行为对比(成稿时补做,可复跑:npx vitest run src/evidence-sweep.test.ts)。 "机制一致"原来只是代码事实(同一份 import);成稿时补了一个实验把"一致"变成可测:同一几何场景(两座测向站 + 玩家直线移动、逐 tick 尝试捕获),分别按两套参数喂给共享解算器——静默监听站默认参数(世界尺度 16 格)与 dfParams(FPV_DEFAULT_PARAMS)(世界尺度 1000 m,按 FPV 典型交战距离约 400 m 布设),逐项数据落盘 data/df-mapping-check.json:

  • 归一化定位误差几乎相同:最终位置误差占世界尺度的比例,监听站 1.64% vs FPV 1.63%。单位制不同、结论同构——这是"映射层重标定成功"最直接的证据。
  • 角度噪声保真:典型距离上的方位线角度 σ,监听站 1.73° vs FPV 1.66°(σ = sigmaBase + sigmaPowerGap×(1−powerMult) + d/sigmaDistDiv;映射把 sigmaDistDiv 从 28(格)换到 350(m),角误差量级没变)。
  • 差异是设计出来的,不是漂移:命中基率(0.171 vs 0.058 每次)与估计形成率(90% vs 62.5% 的 tick)不同——rfHitBase 与距离衰减本来就被有意调低(图传是持续暴露源,不是间歇发射);误差圆半径各自收敛到各自的 estimateRadiusMin 下限(0.8 格 / 12 m)。

根因

复用的单位是"机制",不是"函数"。 直接复制代码,机制会随两个副本各自演化而漂移;重写则连"机制一致"都保证不了。唯一让"修一处、两处生效"成立的方式,就是共享同一份实现——代价是把旧引擎的隐含假设(参数值域、tick 语义)全部显式化到映射层,否则复用会在调参阶段变成耦合 bug 的温床。

"3D 塞不进本壳"的误判错在哪。 当时把"壳的定位"理解成了渲染技术栈(2D Canvas),但壳真正的定位是分层约束:engine 纯 TS、可单测、Panel 只做渲染。这条约束对 Three.js 完全成立——3D 引擎照样可以不碰 React 照样单测。评审文档里那个"等"字("依赖监听站验证后的射频暴露模型")说明障碍从来不是渲染,是复用的前置条件还没就绪。

约束与取舍

  • 放弃重写。 换来机制级联动与开发量缩减;代价是接受旧参数语义与新语境不完全贴合,靠映射层补偿。
  • 放弃抽公共库。 代价是目录上留着一个跨模块 import,架构洁癖上不舒服;换来最小改动。判据:如果第三款游戏还要用同一机制,再抽不迟。
  • 映射层的历史踩坑无法补测。 有没有哪个参数照搬后行为不对,是历史事实,实验只能验证现状、无法回溯过程——这一条仍是记忆题(见文末核对节)。若想不起来,就按现状写:补测实验显示映射后的行为同构(归一化误差 1.64% vs 1.63%),"映射一次到位"是有证据支撑的表述。

可复用检查清单

  1. 复用前先列旧引擎的隐含假设清单(时空模型、参数值域、状态语义),逐条声明如何映射。
  2. 映射独立成层(重命名 + 重标定集中一处),禁止在调用点散落 if 适配。
  3. 复用模块保持引擎纯度:新世界只喂数据,不把渲染/输入状态传进旧引擎。
  4. 两游戏共用同一机制时,修复与调参一次生效两处——把这条写进联动文档,防漂移。
  5. 判断一个"壳"能不能装下新技术栈时,分清约束是渲染技术还是分层纪律——多数时候是后者。

记录核对情况(发布前删除本节)

已按仓库记录落实:

  • "不进本框架,独立立项"与"2 款 3D 款不适合塞进本壳"的原始判断:docs/surveyor-games-review.md §一、§二.6/7。
  • 判断反转:同文档路线图第六批"低迹勘测 FPV(Three.js 入本框架,射频测向解算复用监听站 engine)";"依赖监听站验证后的射频暴露模型"为 §二.6 原文。
  • import 关系与映射层:low-trace-fpv/engine/simulate.ts、types.ts。
  • 补测实验 D(映射参数行为对比、归一化误差 1.64% vs 1.63%、角 σ 1.73° vs 1.66°):src/evidence-sweep.test.ts、docs/articles/data/df-mapping-check.json。注意:这是"现状验证",不能证明"历史上没踩过坑"。

仍属重建、无记录支撑(发布前请按真实经历改写或删除):

  • "重写 / 抽公共库"两条路的成本权衡(两轮平衡验证、抽象时机)是按项目状态推演的,规划文档只记录了"复用"的决定,未记录被否方案的讨论过程。
  • 映射层是否有历史踩坑实例:记忆题(见取舍一节),补测无法替代。
  • "误判错在哪"一节的归因(渲染技术栈 vs 分层纪律)是重建,请按真实想法核对。

引用出处

  • src/algorithms/low-trace-fpv/engine/simulate.ts(import、子步注释、dfParams 映射函数)
  • src/algorithms/low-trace-fpv/engine/types.ts(参数映射段)
  • docs/surveyor-games-review.md(§一、§二.6/7、路线图第六批)
  • src/evidence-sweep.test.ts(补测实验 D)
  • docs/articles/data/df-mapping-check.json(实验数据 + 复现命令)