diff --git a/3dtank.com/play/static/js/unpacked/hornet_bundle.js b/3dtank.com/play/static/js/unpacked/hornet_bundle.js index 3914ae9..75b230b 100644 --- a/3dtank.com/play/static/js/unpacked/hornet_bundle.js +++ b/3dtank.com/play/static/js/unpacked/hornet_bundle.js @@ -2051,11 +2051,41 @@ return true; } + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, + // 但字段名与组件实例稳定,路径每次都能解析到当前节点) + var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] + var ScanTick = 0; + + function ResolveSrc(Src) { + var V = Src.Comp; + for (var i = 0; i < Src.Path.length; i++) { + if (!V || typeof V !== 'object') return null; + V = V[Src.Path[i]]; + } + return V; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); - function Try(V) { + // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 + ScanTick++; + var NeedRescan = (ScanTick % 300) === 0; + var Cached = NodeSrcCache.get(tank); + if (!NeedRescan && Cached && Cached.length) { + var AllOk = true; + for (var c = 0; c < Cached.length; c++) { + var V0 = ResolveSrc(Cached[c]); + if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); + } + if (AllOk) return; + } + // ② 缓存失效或首次:全量扫描并重建路径表 + var Srcs = []; + function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); + Srcs.push({ Comp: Comp, Path: Path }); } try { tank.qz1_1.s2a_1.forEach(function (VL) { @@ -2064,22 +2094,23 @@ for (var K in comp) { var V = comp[K]; if (!IsPlainObj(V)) continue; - if (IsSceneNode(V)) { Try(V); continue; } + if (IsSceneNode(V)) { Try(V, comp, [K]); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; if (!IsPlainObj(V2)) continue; - if (IsSceneNode(V2)) { Try(V2); continue; } + if (IsSceneNode(V2)) { Try(V2, comp, [K, K2]); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (IsPlainObj(V3)) Try(V3); + if (IsPlainObj(V3)) Try(V3, comp, [K, K2, K3]); } } } catch (e) {} } }); } catch (e) {} + NodeSrcCache.set(tank, Srcs); } // 结果级去重:同一 mesh 资源 + 同一节点名,只保留一份 @@ -2137,10 +2168,9 @@ }); } catch(e) {} } - // ③ 仍然为空才做组件字段扫描(不加节流:节流会导致「这帧有模型、下帧只有点」的闪烁) - if (nodes.length === 0) { - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - } + // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) + // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; diff --git a/3dtank_bundle.js b/3dtank_bundle.js index 3914ae9..75b230b 100644 --- a/3dtank_bundle.js +++ b/3dtank_bundle.js @@ -2051,11 +2051,41 @@ return true; } + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, + // 但字段名与组件实例稳定,路径每次都能解析到当前节点) + var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] + var ScanTick = 0; + + function ResolveSrc(Src) { + var V = Src.Comp; + for (var i = 0; i < Src.Path.length; i++) { + if (!V || typeof V !== 'object') return null; + V = V[Src.Path[i]]; + } + return V; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); - function Try(V) { + // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 + ScanTick++; + var NeedRescan = (ScanTick % 300) === 0; + var Cached = NodeSrcCache.get(tank); + if (!NeedRescan && Cached && Cached.length) { + var AllOk = true; + for (var c = 0; c < Cached.length; c++) { + var V0 = ResolveSrc(Cached[c]); + if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); + } + if (AllOk) return; + } + // ② 缓存失效或首次:全量扫描并重建路径表 + var Srcs = []; + function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); + Srcs.push({ Comp: Comp, Path: Path }); } try { tank.qz1_1.s2a_1.forEach(function (VL) { @@ -2064,22 +2094,23 @@ for (var K in comp) { var V = comp[K]; if (!IsPlainObj(V)) continue; - if (IsSceneNode(V)) { Try(V); continue; } + if (IsSceneNode(V)) { Try(V, comp, [K]); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; if (!IsPlainObj(V2)) continue; - if (IsSceneNode(V2)) { Try(V2); continue; } + if (IsSceneNode(V2)) { Try(V2, comp, [K, K2]); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (IsPlainObj(V3)) Try(V3); + if (IsPlainObj(V3)) Try(V3, comp, [K, K2, K3]); } } } catch (e) {} } }); } catch (e) {} + NodeSrcCache.set(tank, Srcs); } // 结果级去重:同一 mesh 资源 + 同一节点名,只保留一份 @@ -2137,10 +2168,9 @@ }); } catch(e) {} } - // ③ 仍然为空才做组件字段扫描(不加节流:节流会导致「这帧有模型、下帧只有点」的闪烁) - if (nodes.length === 0) { - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - } + // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) + // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode; diff --git a/tools/hornet_3d_wireframe.js b/tools/hornet_3d_wireframe.js index cfc49be..d549f57 100644 --- a/tools/hornet_3d_wireframe.js +++ b/tools/hornet_3d_wireframe.js @@ -526,11 +526,41 @@ return true; } + // 扫描结果按「组件字段路径」缓存(不缓存节点对象:引擎会重建节点树, + // 但字段名与组件实例稳定,路径每次都能解析到当前节点) + var NodeSrcCache = new WeakMap(); // entity -> [{ Comp, Path: [K1, K2, K3] }] + var ScanTick = 0; + + function ResolveSrc(Src) { + var V = Src.Comp; + for (var i = 0; i < Src.Path.length; i++) { + if (!V || typeof V !== 'object') return null; + V = V[Src.Path[i]]; + } + return V; + } + function ScanMeshNodes(tank, nodes, stats, Seen) { Seen = Seen || new WeakSet(); - function Try(V) { + // ① 有缓存路径:直接解析(开销极小);每 300 帧强制重扫一次,捡起后加载的部件 + ScanTick++; + var NeedRescan = (ScanTick % 300) === 0; + var Cached = NodeSrcCache.get(tank); + if (!NeedRescan && Cached && Cached.length) { + var AllOk = true; + for (var c = 0; c < Cached.length; c++) { + var V0 = ResolveSrc(Cached[c]); + if (!IsSceneNode(V0)) { AllOk = false; break; } + if (!Seen.has(V0)) CollectMeshNodes(V0, nodes, 0, 'scan', stats, Seen); + } + if (AllOk) return; + } + // ② 缓存失效或首次:全量扫描并重建路径表 + var Srcs = []; + function Try(V, Comp, Path) { if (!IsSceneNode(V) || Seen.has(V)) return; CollectMeshNodes(V, nodes, 0, 'scan', stats, Seen); + Srcs.push({ Comp: Comp, Path: Path }); } try { tank.qz1_1.s2a_1.forEach(function (VL) { @@ -539,22 +569,23 @@ for (var K in comp) { var V = comp[K]; if (!IsPlainObj(V)) continue; - if (IsSceneNode(V)) { Try(V); continue; } + if (IsSceneNode(V)) { Try(V, comp, [K]); continue; } // 深度 2/3:节点常包在皮肤对象/包装对象里(如 skin.h159_1、weaponSkin.e11p_1.a17q_1) try { for (var K2 in V) { var V2 = V[K2]; if (!IsPlainObj(V2)) continue; - if (IsSceneNode(V2)) { Try(V2); continue; } + if (IsSceneNode(V2)) { Try(V2, comp, [K, K2]); continue; } for (var K3 in V2) { var V3 = V2[K3]; - if (IsPlainObj(V3)) Try(V3); + if (IsPlainObj(V3)) Try(V3, comp, [K, K2, K3]); } } } catch (e) {} } }); } catch (e) {} + NodeSrcCache.set(tank, Srcs); } // 结果级去重:同一 mesh 资源 + 同一节点名,只保留一份 @@ -612,10 +643,9 @@ }); } catch(e) {} } - // ③ 仍然为空才做组件字段扫描(不加节流:节流会导致「这帧有模型、下帧只有点」的闪烁) - if (nodes.length === 0) { - try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} - } + // ③ 结构扫描:无条件补充漏采(车身根/部件表往往只覆盖一部分) + // 重复实例(皮肤树/场景树同 tp 同名)由 DedupNodes 收掉,不会再出现"两个身体" + try { ScanMeshNodes(tank, nodes, stats ? stats.scan : null, Seen); } catch (e) {} // ④ 炮塔(独立于车身根;重复实例由 DedupNodes 收掉) var turretNode = GetTurretNode(tank); if (stats) stats.turretComp = !!turretNode;