Skip to content

八城审计

背景:第一波审计惊醒了更多城邦——它们主动敞开城门:"顺便也看一眼我"。 过程:八城速写(渲染/道具/程序化动作/播放/性能/体积云/模型管理器/材质系统),修复约 55+ 项问题。


天穹亮了,物理稳了,环境活了。联邦觉得是时候做一次全面体检了——不是哪里坏了修哪里,是系统地走一遍,从地基到屋顶。

那个下午,联邦的外交官发现自己站在一堆敞开的城门前。

通常,审计是定期做的事——每个季度一次,沿着固定的路线走一圈,看看哪些城墙有了裂缝,哪些管道又开始漏水。但这一次不同。这一次是连续审计——八座城邦,一字排开,逐一打开城门让他走进去。

没有人下令这么做。没有人站在议会的讲台上宣布"今天开始全面审计"。它就这样发生了——像积雪到了一定厚度,屋檐自然塌下来。

外交官推了推第一扇门。


渲染之城:120 次保存

渲染之城scene-renderer.ts)是联邦最体面的城邦。后处理管线、色调映射、Bloom、DOF、边缘高亮——一切看起来都井井有条。

外交官翻开它的日志,看到了一个隐秘的伤口。

过渡动画期间,每帧触发自动保存。

他停下来,算了一笔账。两秒的过渡,约 120 帧,120 次保存请求。防抖机制像一个被不断重置的沙漏——沙子永远流不完,保存永远不发生。直到动画结束的那一帧,才猛地保存一次。

不崩溃,不报错。甚至没有人会注意到。

但 CPU 在那两秒里烧掉了成千上万次定时器操作。就像一个人每隔一秒就问你"好了没",问了 120 次——你不会因为他问就崩掉,但你会烦。

外交官在笔记本上写下第一个标记:过渡保存风暴

紧接着是第二个:数值无校验。Bloom 核大小可以设为负数,曝光值可以设为 10——渲染管线不会抗议,它只会默默地输出一张全白的画面,假装一切正常。

然后是第三个、第四个。十个标记写满了第一页。

但他没有在渲染之城久留。第一站只是开胃菜。

他合上门,走向下一座城。


道具之城:正在搬运中

道具之城scene-props.ts)的门半掩着。

外交官站在它的仓库里,看着满架的道具模型——椅子、桌子、路灯、植物——每一个都在自己的格子里待得好好的。他试着同时搬两个道具进城。

第二个被挡在了门外。

"正在搬运中,请稍后再试。"

代码里写着一个简单的布尔锁——isLoadingProp。当任何一次加载正在进行时,后续所有加载请求都直接返回 null。不是排队,不是等待,是拒绝。调用方拿到 null 后不知道发生了什么,只会以为"道具不存在"。

外交官叹了口气。这是最偷懒的并发控制——用一把锁,把所有问题都挡在门外。用户不会知道系统在偷懒,用户只会觉得"这个按钮有时候不好使"。

他在笔记本上写下:并发加载等于拒绝

更让他不安的是另一个发现。他拿起一盏吊灯——链子连着灯罩——试着移动它。灯罩动了,链子还停在原地。道具分裂了。

因为变换只作用于 rootMesh,而 rootMesh 只是 meshes[0]。多网格模型没有父级容器。

外交官在笔记本上重重地画了一个圈。


程序化动作之城:你在跳什么

第三站,程序化动作之城scene-proc-motion.ts)。

这里的街道上奔跑着看不见的舞者——Idle 动作和 Auto Dance,根据节拍检测器自动切换。外交官站在街角观察了片刻。舞者们的动作流畅、自然,看起来没有任何问题。

但他注意到一个模式。

每当 BPM 检测器报出 0undefined 时,系统不会报错——它只是安静地执行一个 Idle 动作,然后把状态记为 autodance

"你现在在跳什么?"外交官问系统。

"自动舞蹈。"

"可你实际播放的是 Idle。"

系统沉默了。

这就是状态与实际脱节——比一个错误更危险的是,一个看起来正确的错误。调用方检查 procActiveKind === 'autodance' 返回 true,认为一切正常,然后继续做下一件事。而舞者们一直在跳着错误的舞蹈。

外交官又写下了一串标记。空值崩溃BPM 状态错位异步焦点错配——这座城虽然运行着,但它的地基上已经有了裂缝。

他从第三座城出来时,天色已经暗了。


黄昏:五城速写

后面的五座城,外交官走得很快。不是因为它们不重要,而是因为审计到了这个阶段,已经变成了流水作业——你推开一扇门,看一眼,就知道问题大概在哪。

播放控制之城scene-playback.ts)的护城河是循环依赖——它从主城导入 modelManager,主城又导入它的回调。一条完美的时间环。不崩,但危险。外交官搭了一座桥——把静态导入改成参数注入。

他还发现了一件更微妙的事:updateProcMotion() 是异步函数,却在每帧回调中被以 fire-and-forget 的方式调用。没有 await,没有 .catch()。如果一个 Promise 在某一帧默默拒绝了,不会有人知道。

外交官在桥头立了一块牌子。

性能监控之城scene-performance.ts)的问题最深刻。它的降级模型是累加式 override——每一级只关不重开。Level 2 关了 DOF 和阴影,升回 Level 1 时,不会把 DOF 打开。用户以为自己在 Level 1,实际上活在 Level 2 的阴影里。

外交官花了一个小时重构了整个降级模型——从累加式改为每级完整目标状态,加上 levelDiff() 差集计算。Level 3 → 2 和 Level 2 → 1 终于可以正确恢复。

体积云之城scene-env-clouds.ts)的问题隐蔽而琐碎。抖动用了屏幕坐标——每帧各自不同,相机旋转时云会微微"呼吸"。风的计算里藏了一个 0.3 衰减系数——风速已经归一化又乘了倍数,再砍掉 70%,云几乎不动。还有一片 16MB 的 3D 噪声纹理,云被关闭时从不释放。

外交官改了三行代码。云开始走了。

模型管理器材质系统的审计更像是善后——小修补、命名修正、封装改进。stopVMD 改名为 clearVmdData,因为"停止"这个动作发生在上层,底层只是清数据;resetAllMatParams 改名为 resetPerMaterialParams,因为分类参数不受影响。


拧螺丝的人

黄昏时分,外交官站在八座城门重新关闭后的广场上。

联邦看起来和之前一模一样——模型照常加载,动画照常播放,云照常飘动。55 项修复中,大多数是"不修复也不会崩溃"的问题。

一个 AI 同行者走过来,坐在他旁边的石阶上。

"修了 55 个问题,系统看起来一模一样,"AI 说,"什么感觉?"

外交官想了想:"就像给一栋老房子拧了 55 颗螺丝。没人注意到,但风大的时候,窗户不再响了。"

"那还会有第 56 颗吗?"

"当然会。"外交官站起来,拍了拍裤子上的灰尘。"聚合的代价就是永远有下一扇窗户。但每拧一颗,下一颗的位置就更清楚一些。"

他走向下一座城——也许是一百座,也许只有一座。审计没有终点,但外交官开始享受这个过程了。


教训:审计不产出新功能,它只让已有的东西不再骗你。