This commit is contained in:
2026-10-12 19:28:14 +08:00
parent c1c5f05d3f
commit 3ee5643e82
16 changed files with 1259 additions and 93 deletions
+56 -11
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Spine.Unity;
using UnityEngine;
@@ -591,9 +592,11 @@ public class FlyTurretData : TurretData
}
// ==================== 入场飞机预热(进房加载收尾时由面板调用,卡顿被进度条遮住) ====================
// XJMH 进入房间时"命中"的那架完整飞机(骨架 + 尾迹/护盾十多个粒子系统 + 贴图首次上传)首帧初始化很重;
// 全部堆在"加载到100%、进度条已隐藏"那一帧做,玩家会看到明显卡顿(其他玩法没有这套 XJMH 独有资产)。
// 预热 = 在进度条还在走时把飞机临时挂到激活的父节点下、缩到 0 隐藏,激活一两帧完成初始化,再原样还原。
// XJMH 进入房间时"命中"的那架完整飞机 PW_Plane(骨架 + 41 个粒子系统、2.2MB 预制体、52 个节点):
// 整根激活会让 41 套粒子在同一帧全部首初始化(分配缓冲/上传网格贴图)——单帧几百 ms 主线程尖峰。
// 这就是"只有星际迷航进房卡、其它玩法不卡"的原因(其他玩法没有这套 XJMH 独有资产)。
// 预热 = 进度条还在走时把飞机临时挂到激活父节点下、缩到 0 隐藏;骨架 1 帧 + 粒子按每帧 8ms 预算
// 逐帧点亮完成初始化,再原样还原(玩家全程不可见)。
private Coroutine _prewarmRoutine;
private Transform _prewarmPlane; // 预热中的飞机(被打断时还原用)
@@ -603,6 +606,8 @@ public class FlyTurretData : TurretData
private Quaternion _prewarmOrigRot;
private Vector3 _prewarmOrigScale;
private bool _prewarmOrigActive;
/// <summary>粒子分帧点亮的帧内计时(Stopwatch 不受 Time.realtimeSinceStartup 帧内不刷新影响)</summary>
private static readonly System.Diagnostics.Stopwatch _prewarmFrameSw = new System.Diagnostics.Stopwatch();
/// <summary>入场飞机预热(面板在加载收尾阶段调用;座位/炮台需已激活)。幂等:重复调用只跑一次。</summary>
public void PrewarmEnterPlane()
@@ -625,27 +630,67 @@ public class FlyTurretData : TurretData
_prewarmOrigScale = plane.localScale;
_prewarmOrigActive = plane.gameObject.activeSelf;
// 只开入场要用的 Body、关掉免费三类——避免把五十来个粒子系统全部初始化(反而更重);
// ⭐ 护盾粒子拆到下一帧再开:和 Body 骨架/粒子挤同一帧 = 单帧尖峰(90% 收尾窗口卡顿的来源之一)
// 容器显隐(与入场动画 EnterActionRoutine 一致):开 Body/护盾、关免费三类
SetPlaneChildActive(plane, "Body", true);
SetPlaneChildActive(plane, "PW_Effect_Defend", true);
SetPlaneChildActive(plane, "FreeEffect", false);
SetPlaneChildActive(plane, "FreeEffect_Crit", false);
SetPlaneChildActive(plane, "FreeEffect_Speedup", false);
// ⭐ 关键修复("只有 XJMH 进房卡主线程"的根因):
// 整根 SetActive(true) 会让 41 个粒子系统在同一帧全部首初始化(单帧几百 ms)。
// 先把"会被激活的"粒子节点逐个关掉(飞机根此刻未激活,只是置标志、不做初始化),
// 等飞机激活(这一帧只初始化骨架等非粒子部分)后,按"每帧约 8ms 预算"逐帧点亮——
// 设备快一帧多开几个、设备慢一帧只开一个,单帧尖峰摊平到十几帧,不再顶住主线程。
// ⚠️ 只动"容器开着"的粒子(Body/护盾);免费三类容器已关、默认关着的粒子都不动,保持原状。
var psNodes = new List<GameObject>(64);
foreach (var ps in plane.GetComponentsInChildren<ParticleSystem>(true))
{
if (ps == null || ps.gameObject == plane.gameObject) continue;
if (!IsActiveUnder(ps.transform, plane)) continue; // 容器关着的(免费三类等)不预热
ps.gameObject.SetActive(false);
psNodes.Add(ps.gameObject);
}
// 临时挂到根节点(激活中)并缩到 0:既能触发初始化,又完全不可见
Transform holder = transform.root;
if (holder != null && holder != _prewarmOrigParent)
plane.SetParent(holder, true);
plane.localScale = Vector3.zero;
plane.gameObject.SetActive(true);
yield return null; // Body 骨架首帧网格
SetPlaneChildActive(plane, "PW_Effect_Defend", true); // 护盾粒子单独一帧开
yield return null; // 护盾首帧模拟
yield return null; // 再等一帧让首帧网格上传铺开
LoadingStallWatchdog.Phase = "预热·飞机骨架激活帧";
plane.gameObject.SetActive(true); // 这一帧只初始化骨架等非粒子部分
yield return null; // 骨架首帧网格
// ⭐ 逐帧分批点亮粒子(帧内时间预算自适应:快机多开、慢机少开)
LoadingStallWatchdog.Phase = "预热·飞机粒子分帧";
const double frameBudgetMs = 8.0;
_prewarmFrameSw.Restart();
for (int i = 0; i < psNodes.Count; i++)
{
if (psNodes[i] == null) continue;
psNodes[i].SetActive(true);
if (_prewarmFrameSw.Elapsed.TotalMilliseconds >= frameBudgetMs)
{
_prewarmFrameSw.Restart();
yield return null;
}
}
yield return null; // 最后一批首帧模拟
yield return null; // 首帧网格上传铺开
FinishPrewarmRestore();
Debug.Log($"[XJMH入场] 飞机预热完成({plane.name})");
Debug.Log($"[XJMH入场] 飞机预热完成({plane.name},粒子分帧点亮 {psNodes.Count} 个)");
_prewarmRoutine = null;
// 诊断:预热结束,把卡顿监测阶段还给加载段(后续卡顿归因不错位到预热)
LoadingStallWatchdog.Phase = "60~78 音频加载";
}
/// <summary>节点在"飞机根若激活"的前提下是否会被激活(检查自身到飞机根之间每级的 activeSelf)</summary>
private static bool IsActiveUnder(Transform node, Transform stopRoot)
{
for (var t = node; t != null && t != stopRoot; t = t.parent)
if (!t.gameObject.activeSelf) return false;
return true;
}
/// <summary>预热还原:飞机隐藏并恢复到预热前的挂载/变换/激活状态(协程正常收尾或中途被打断都用它)</summary>