From 8e475e6ab2c9addc087a466141062e3bba990e84 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com>
Date: Tue, 4 Aug 2026 21:20:56 +0800
Subject: [PATCH] 1
---
Assets/Editor/ProfilerDataExporter.cs | 389 ++++++++++++++++++
Assets/Editor/ProfilerDataExporter.cs.meta | 11 +
Assets/Framework/Pool/EffectPool.cs | 27 +-
.../Scripts/Manager/CreateFishManager.cs | 364 ++++++++++------
Assets/MyGame/Scripts/Net/NetLog.cs | 33 ++
Assets/MyGame/Scripts/Net/NetLog.cs.meta | 11 +
.../Scripts/Net/WebSocketManager_Message.cs | 137 ++++--
.../Scripts/Net/WebSocketReponseList.cs | 2 +-
.../Scripts/Net/WebSocketSendList_Fish.cs | 5 +-
.../AnimEvent/ScoreNumScrollEvent.cs | 3 +-
.../DefaultDieEffect/AnimEvent/coinEvent.cs | 3 +-
.../TeShuBJ/BaoJiangTSSmallFish.cs | 3 +-
.../HHWK/MonsterTSY/SDY/HHWK_SDY_DieEvent.cs | 4 +-
.../HHWK/MonsterTSY/ZDX/HHWK_ZDX_DieEvent.cs | 2 +-
14 files changed, 823 insertions(+), 171 deletions(-)
create mode 100644 Assets/Editor/ProfilerDataExporter.cs
create mode 100644 Assets/Editor/ProfilerDataExporter.cs.meta
create mode 100644 Assets/MyGame/Scripts/Net/NetLog.cs
create mode 100644 Assets/MyGame/Scripts/Net/NetLog.cs.meta
diff --git a/Assets/Editor/ProfilerDataExporter.cs b/Assets/Editor/ProfilerDataExporter.cs
new file mode 100644
index 000000000..a3d7253cd
--- /dev/null
+++ b/Assets/Editor/ProfilerDataExporter.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using Unity.Profiling;
+using UnityEditor;
+using UnityEditor.Profiling;
+using UnityEditorInternal;
+using UnityEngine;
+using UnityEngine.Profiling;
+
+///
+/// Profiler .data 文件导出器
+/// 使用 ProfilerDriver.GetHierarchyFrameDataView API 提取函数级 GC Alloc / CPU 耗时 / 调用次数
+/// 菜单: Tools > Profiler Data Exporter
+///
+public class ProfilerDataExporter : EditorWindow
+{
+ [MenuItem("Tools/Profiler Data Exporter")]
+ public static void ShowWindow()
+ {
+ GetWindow("Profiler Exporter");
+ }
+
+ private string dataFilePath = "";
+ private string outputDir = "";
+ private bool includeAllThreads = false;
+
+ void OnEnable()
+ {
+ if (string.IsNullOrEmpty(outputDir))
+ outputDir = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ }
+
+ void OnGUI()
+ {
+ GUILayout.Label("Profiler Data Exporter", EditorStyles.boldLabel);
+ GUILayout.Label("用 ProfilerDriver API 提取函数级 GC/CPU 数据", EditorStyles.miniLabel);
+
+ GUILayout.Space(10);
+
+ // 数据文件选择
+ GUILayout.Label("1. 选择 .data 文件:");
+ GUILayout.BeginHorizontal();
+ dataFilePath = EditorGUILayout.TextField(dataFilePath);
+ if (GUILayout.Button("浏览", GUILayout.Width(60)))
+ {
+ string picked = EditorUtility.OpenFilePanel("选择 Profiler .data 文件", "", "data");
+ if (!string.IsNullOrEmpty(picked))
+ dataFilePath = picked;
+ }
+ GUILayout.EndHorizontal();
+
+ GUILayout.Space(5);
+
+ // 输出目录
+ GUILayout.Label("2. 输出目录:");
+ GUILayout.BeginHorizontal();
+ outputDir = EditorGUILayout.TextField(outputDir);
+ if (GUILayout.Button("浏览", GUILayout.Width(60)))
+ {
+ string picked = EditorUtility.OpenFolderPanel("选择输出目录", outputDir, "");
+ if (!string.IsNullOrEmpty(picked))
+ outputDir = picked;
+ }
+ GUILayout.EndHorizontal();
+
+ GUILayout.Space(5);
+ includeAllThreads = EditorGUILayout.Toggle("包含所有线程(否则仅主线程)", includeAllThreads);
+
+ GUILayout.Space(15);
+
+ if (GUILayout.Button("导出函数级数据 (GC + CPU)", GUILayout.Height(35)))
+ {
+ ExportFunctionLevelData();
+ }
+
+ GUILayout.Space(5);
+
+ if (GUILayout.Button("导出逐帧摘要 (帧时间 + GC)", GUILayout.Height(35)))
+ {
+ ExportPerFrameSummary();
+ }
+
+ GUILayout.Space(5);
+
+ if (GUILayout.Button("一键全量导出 (两个CSV)", GUILayout.Height(35)))
+ {
+ ExportFunctionLevelData();
+ ExportPerFrameSummary();
+ }
+ }
+
+ // ================================================================
+ // 加载 .data 并等待帧就绪(LoadProfile 是异步的,首次调用 firstFrameIndex 可能为 -1)
+ // 返回 false 表示加载失败
+ // ================================================================
+ bool LoadAndValidate(out int firstFrame, out int lastFrame)
+ {
+ firstFrame = -1;
+ lastFrame = -1;
+
+ ProfilerDriver.LoadProfile(dataFilePath, false);
+
+ // LoadProfile 异步,轮询等待帧索引就绪(最多 10 秒)
+ DateTime deadline = DateTime.Now.AddSeconds(10);
+ int retry = 0;
+ while (DateTime.Now < deadline)
+ {
+ firstFrame = ProfilerDriver.firstFrameIndex;
+ lastFrame = ProfilerDriver.lastFrameIndex;
+ if (firstFrame >= 0 && lastFrame >= 0 && lastFrame >= firstFrame)
+ return true;
+
+ retry++;
+ if (EditorUtility.DisplayCancelableProgressBar("加载 Profiler 数据",
+ $"等待帧数据就绪... ({retry})", 0f))
+ {
+ EditorUtility.ClearProgressBar();
+ return false;
+ }
+
+ // 让编辑器处理一帧,使 LoadProfile 继续推进
+ System.Threading.Thread.Sleep(50);
+ }
+
+ EditorUtility.ClearProgressBar();
+ Debug.LogError($"[ProfilerExport] 加载超时 firstFrame={firstFrame} lastFrame={lastFrame}");
+ return false;
+ }
+
+ // ================================================================
+ // 函数级导出:每个函数的 GC Alloc / Self Time / Total Time / Calls
+ // ================================================================
+ void ExportFunctionLevelData()
+ {
+ if (!ValidateFile()) return;
+
+ string baseName = Path.GetFileNameWithoutExtension(dataFilePath);
+ string csvPath = Path.Combine(outputDir, baseName + "_functions.csv");
+
+ if (!LoadAndValidate(out int firstFrame, out int lastFrame))
+ {
+ EditorUtility.DisplayDialog("错误", "数据文件加载失败或没有帧数据", "OK");
+ return;
+ }
+
+ // 聚合:函数名 -> 统计
+ var stats = new Dictionary();
+
+ int totalFrames = lastFrame - firstFrame + 1;
+ int processed = 0;
+ int threadCount = includeAllThreads ? 16 : 1; // 最多扫16个线程
+
+ for (int frame = firstFrame; frame <= lastFrame; frame++)
+ {
+ if (EditorUtility.DisplayCancelableProgressBar("导出函数级数据",
+ $"帧 {frame} ({processed}/{totalFrames})",
+ (float)processed / totalFrames))
+ break;
+
+ for (int thread = 0; thread < threadCount; thread++)
+ {
+ using (var frameData = ProfilerDriver.GetHierarchyFrameDataView(
+ frame, thread,
+ HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName,
+ HierarchyFrameDataView.columnGcMemory, false))
+ {
+ if (!frameData.valid) break; // 线程不存在,跳出
+
+ int rootId = frameData.GetRootItemID();
+ ProcessItemRecursive(frameData, rootId, stats);
+ }
+ }
+
+ processed++;
+ }
+
+ EditorUtility.ClearProgressBar();
+
+ // 按 GC Alloc 降序排列
+ var sorted = new List(stats.Values);
+ sorted.Sort((a, b) => b.totalGcAlloc.CompareTo(a.totalGcAlloc));
+
+ using (var writer = new StreamWriter(csvPath, false, Encoding.UTF8))
+ {
+ writer.WriteLine("FunctionName,TotalGcAllocBytes,AvgGcPerCall,TotalCalls,SelfTimeMs,TotalTimeMs,FrameCount");
+ foreach (var s in sorted)
+ {
+ long avgGc = s.totalCalls > 0 ? s.totalGcAlloc / s.totalCalls : 0;
+ writer.WriteLine($"{EscapeCsv(s.name)},{s.totalGcAlloc},{avgGc},{s.totalCalls},{s.selfTimeMs:F4},{s.totalTimeMs:F4},{s.frameCount}");
+ }
+ }
+
+ // 打印 Top 20 GC 分配函数
+ Debug.Log($"[ProfilerExport] 函数级数据已导出: {csvPath}");
+ Debug.Log($"[ProfilerExport] 共 {sorted.Count} 个函数,处理 {processed}/{totalFrames} 帧");
+ Debug.Log("[ProfilerExport] ====== GC Alloc Top 20 ======");
+ int topN = Math.Min(20, sorted.Count);
+ for (int i = 0; i < topN; i++)
+ {
+ var s = sorted[i];
+ Debug.Log($" {i + 1}. {s.name} | GC={s.totalGcAlloc:N0}B | Calls={s.totalCalls} | Self={s.selfTimeMs:F2}ms");
+ }
+
+ EditorUtility.DisplayDialog("完成",
+ $"导出 {sorted.Count} 个函数到:\n{csvPath}\n\nTop 20 GC 函数已打印到 Console", "OK");
+ }
+
+ // ================================================================
+ // 逐帧摘要:每帧的总时间、GC、DrawCall 等
+ // ================================================================
+ void ExportPerFrameSummary()
+ {
+ if (!ValidateFile()) return;
+
+ string baseName = Path.GetFileNameWithoutExtension(dataFilePath);
+ string csvPath = Path.Combine(outputDir, baseName + "_frames.csv");
+
+ if (!LoadAndValidate(out int firstFrame, out int lastFrame))
+ {
+ EditorUtility.DisplayDialog("错误", "数据文件加载失败或没有帧数据", "OK");
+ return;
+ }
+
+ int totalFrames = lastFrame - firstFrame + 1;
+ int processed = 0;
+
+ using (var writer = new StreamWriter(csvPath, false, Encoding.UTF8))
+ {
+ writer.WriteLine("FrameIndex,FrameTimeMs,Fps,TotalGcAllocBytes,DrawCalls,Batches,SetPassCalls,Triangles,Vertices");
+
+ for (int frame = firstFrame; frame <= lastFrame; frame++)
+ {
+ if (EditorUtility.DisplayCancelableProgressBar("导出逐帧摘要",
+ $"帧 {frame} ({processed}/{totalFrames})",
+ (float)processed / totalFrames))
+ break;
+
+ float frameTimeMs = 0f;
+ long frameGc = 0;
+ int drawCalls = 0, batches = 0, setPass = 0, triangles = 0, vertices = 0;
+
+ using (var frameData = ProfilerDriver.GetHierarchyFrameDataView(
+ frame, 0,
+ HierarchyFrameDataView.ViewModes.MergeSamplesWithTheSameName,
+ HierarchyFrameDataView.columnGcMemory, false))
+ {
+ if (frameData.valid)
+ {
+ frameTimeMs = frameData.frameTimeMs;
+ // 遍历根节点找 GC Alloc 总量和渲染统计
+ int rootId = frameData.GetRootItemID();
+ var children = new List();
+ frameData.GetItemChildren(rootId, children);
+ foreach (int childId in children)
+ {
+ string name = frameData.GetItemName(childId);
+ float gc = frameData.GetItemColumnDataAsFloat(childId, HierarchyFrameDataView.columnGcMemory);
+ frameGc += (long)gc;
+
+ // 渲染相关
+ if (name == "Render")
+ {
+ var renderChildren = new List();
+ frameData.GetItemChildren(childId, renderChildren);
+ foreach (int rid in renderChildren)
+ {
+ string rname = frameData.GetItemName(rid);
+ float val = frameData.GetItemColumnDataAsFloat(rid, HierarchyFrameDataView.columnTotalTime);
+ // 渲染统计需要用 ProfilerRecorder 获取
+ }
+ }
+ }
+ }
+ }
+
+ // 用 ProfilerRecorder 获取渲染数据
+ using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count", 1))
+ drawCalls = (int)rec.LastValue;
+ using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Batches Count", 1))
+ batches = (int)rec.LastValue;
+ using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "SetPass Calls Count", 1))
+ setPass = (int)rec.LastValue;
+ using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Triangles Count", 1))
+ triangles = (int)rec.LastValue;
+ using (var rec = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Vertices Count", 1))
+ vertices = (int)rec.LastValue;
+
+ float fps = frameTimeMs > 0 ? 1000f / frameTimeMs : 0;
+ writer.WriteLine($"{frame},{frameTimeMs:F2},{fps:F1},{frameGc},{drawCalls},{batches},{setPass},{triangles},{vertices}");
+
+ processed++;
+ }
+ }
+
+ EditorUtility.ClearProgressBar();
+ Debug.Log($"[ProfilerExport] 逐帧摘要已导出: {csvPath}");
+ EditorUtility.DisplayDialog("完成", $"逐帧摘要已导出到:\n{csvPath}", "OK");
+ }
+
+ // ================================================================
+ // 递归遍历 hierarchy,累加每个函数的统计
+ // 每层递归用独立 List,避免 GetItemChildren 修改正在枚举的集合
+ // ================================================================
+ void ProcessItemRecursive(HierarchyFrameDataView frameData, int itemId,
+ Dictionary stats)
+ {
+ ProcessItem(frameData, itemId, stats);
+
+ // 递归子节点:每层新建 list,避免递归间共享集合被修改导致枚举异常
+ var children = new List();
+ frameData.GetItemChildren(itemId, children);
+ foreach (int childId in children)
+ {
+ ProcessItemRecursive(frameData, childId, stats);
+ }
+ }
+
+ void ProcessItem(HierarchyFrameDataView frameData, int itemId, Dictionary stats)
+ {
+ string name = frameData.GetItemName(itemId);
+ if (string.IsNullOrEmpty(name)) return;
+
+ float totalTime = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnTotalTime);
+ float selfTime = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnSelfTime);
+ float gcAlloc = frameData.GetItemColumnDataAsFloat(itemId, HierarchyFrameDataView.columnGcMemory);
+ int calls = itemId >= 0 ? 1 : 0; // calls 列可能不直接可用,用出现次数代替
+
+ // 尝试获取 calls 列(如果该列存在)
+ try
+ {
+ // 某些 Unity 版本支持 GetItemColumnDataAsInt
+ var method = typeof(HierarchyFrameDataView).GetMethod("GetItemColumnDataAsInt",
+ System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
+ if (method != null)
+ {
+ calls = (int)method.Invoke(frameData, new object[] { itemId, HierarchyFrameDataView.columnCalls });
+ }
+ }
+ catch { /* 忽略,用默认值 */ }
+
+ if (!stats.TryGetValue(name, out var s))
+ {
+ s = new FuncStats { name = name };
+ stats[name] = s;
+ }
+ s.totalTimeMs += totalTime;
+ s.selfTimeMs += selfTime;
+ s.totalGcAlloc += (long)gcAlloc;
+ s.totalCalls += calls;
+ s.frameCount++;
+ }
+
+ // ================================================================
+ // 工具方法
+ // ================================================================
+ bool ValidateFile()
+ {
+ if (string.IsNullOrEmpty(dataFilePath) || !File.Exists(dataFilePath))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的 .data 文件", "OK");
+ return false;
+ }
+ if (string.IsNullOrEmpty(outputDir) || !Directory.Exists(outputDir))
+ {
+ EditorUtility.DisplayDialog("错误", "请选择有效的输出目录", "OK");
+ return false;
+ }
+ return true;
+ }
+
+ string EscapeCsv(string field)
+ {
+ if (field.Contains(",") || field.Contains("\"") || field.Contains("\n"))
+ return "\"" + field.Replace("\"", "\"\"") + "\"";
+ return field;
+ }
+
+ class FuncStats
+ {
+ public string name;
+ public float totalTimeMs;
+ public float selfTimeMs;
+ public long totalGcAlloc;
+ public int totalCalls;
+ public int frameCount;
+ }
+}
diff --git a/Assets/Editor/ProfilerDataExporter.cs.meta b/Assets/Editor/ProfilerDataExporter.cs.meta
new file mode 100644
index 000000000..9b9f69c6e
--- /dev/null
+++ b/Assets/Editor/ProfilerDataExporter.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 36fb1e0e209a62f478f5019e12f38d9d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Framework/Pool/EffectPool.cs b/Assets/Framework/Pool/EffectPool.cs
index 3c8df4453..f086370d0 100644
--- a/Assets/Framework/Pool/EffectPool.cs
+++ b/Assets/Framework/Pool/EffectPool.cs
@@ -23,6 +23,16 @@ public class EffectPool : Singleton
/// 从池中取一个特效(池空则 Instantiate),取出后自动播放并在播完时回池
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent)
+ {
+ return Get(prefab, position, rotation, parent, autoReturn: true);
+ }
+
+ ///
+ /// 从池中取一个特效。
+ /// autoReturn=true: 粒子特效,EffectAutoReturn 检测粒子播完自动回池
+ /// autoReturn=false: 动画/UI 驱动特效(如 BJBase/DieEffect_TSY),由动画事件手动调用 Return()
+ ///
+ public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent, bool autoReturn)
{
if (prefab == null) return null;
@@ -55,12 +65,17 @@ public class EffectPool : Singleton
activeMap[obj] = prefabID;
- // 挂上自动回收组件(已挂则复用)
- var autoReturn = obj.GetComponent();
- if (autoReturn == null)
- autoReturn = obj.AddComponent();
- autoReturn.prefabID = prefabID;
- autoReturn.Play();
+ if (autoReturn)
+ {
+ // 粒子特效:挂上自动回收组件(已挂则复用)
+ var auto = obj.GetComponent();
+ if (auto == null)
+ auto = obj.AddComponent();
+ auto.prefabID = prefabID;
+ auto.Play();
+ }
+ // autoReturn=false: 动画/UI 驱动特效,不挂自动回收组件
+ // 由动画事件(coinEvent/ScoreNumScrollEvent 等)调用 EffectPool.Instance.Return(gameObject) 回池
return obj;
}
diff --git a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
index 31715d023..ca49b50fe 100644
--- a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
+++ b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs
@@ -642,65 +642,137 @@ public class CreateFishManager : MonoSingleton
if (data == null || data.fish == null || data.fish.Count == 0) return;
if (isFrozen) return; // 冰冻中不创建鱼
if (spawnDic == null) return; // 场景卸载后忽略
-
+
+ // ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
+ // 避免一次出大量普通鱼导致单帧卡顿
+ var batchedQueue = new List(data.fish.Count);
+
foreach (var item in data.fish)
{
if (item == null) continue;
- //BeijingTimeManager.Instance.SyncBeijingTime(() =>
- //{
- // Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
- // Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
- // Debug.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
- //});
- Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
- Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
- Debug.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
- // ⭐ 鱼阵时 fishNumber 为空,用 fishArrayNumber 作为配置 key;
- // 单体鱼时 fishNumber 非空,直接用 fishNumber
- string cfgKey = string.IsNullOrEmpty(item.fishNumber) ? item.fishArrayNumber : item.fishNumber;
- if (string.IsNullOrEmpty(cfgKey)) continue;
- // guid 用服务端 fishId
- string guid = item.fishId.ToString();
+ // 判断是否需要分帧:普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧
+ bool shouldBatch = ShouldBatchSpawn(item);
- // guid 去重:已有此 guid 的鱼跳过
- // ⭐ 鱼阵鱼的 guid 是 baseGuid_pathId_i 格式,字典里没有 baseGuid 本身,
- // 所以要额外检查是否有以 baseGuid+"_" 开头的 key,防止 sync 模式重复出鱼
- // 导致 guid 冲突覆盖(旧鱼变野鱼无法销毁)
- if (!string.IsNullOrEmpty(guid) &&
- fishDatas_hashCodekey != null &&
- (fishDatas_hashCodekey.ContainsKey(guid) ||
- fishDatas_hashCodekey.Keys.Any(k => k.StartsWith(guid + "_"))))
- continue;
-
- // 构造 FrameData_CreateFish 复用 SpawnFishMainThread
- var createFishData = new FrameData_CreateFish
+ if (shouldBatch)
{
- fishNumber = cfgKey,
- guid = guid,
- moveLineId = item.routeId.ToString()
- };
-
- // ⭐ 根据 type 决定 moveTime:
- // - spawn/init:新鱼,传当前时间 → moveTime≈0,鱼从曲线起点开始游
- // (避免服务端/客户端时间差导致 moveTime>0,鱼瞬间出现在曲线中间)
- // - sync:同步已有鱼,传 createdAt → moveTime=时间差,鱼出现在路径正确位置
- long spawnTime;
- //if (data.type == "sync")
- spawnTime = item.createdAt;
- //else
- // spawnTime = BeijingTimeManager.Instance.CurrentTimestampMs;
- SpawnFishMainThread(createFishData, spawnTime);
-
- // ⭐ expireAt 过期自动移除暂不启用:
- // - 走路线的鱼:OnMoveUpdata 里 moveTime>=moveLine.moveTime 时自动 Id_DieFish_NotFX
- // - 原点不动的鱼(Boss):正常播放结束动画后销毁
- // 后续若需要服务端强制过期销毁,再启用此协程
- // if (item.remainingSeconds > 0)
- // StartCoroutine(ExpireFishCoroutine(guid, item.remainingSeconds));
+ batchedQueue.Add(item);
+ }
+ else
+ {
+ // Boss/特殊鱼/鱼阵等:立即生成
+ TrySpawnSingleFish(item);
+ }
}
- Debug.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}");
+ // 分帧组:启动协程逐帧生成
+ if (batchedQueue.Count > 0)
+ {
+ StartCoroutine(SpawnFishBatchedCoroutine(batchedQueue));
+ }
+
+ NetLog.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}, batched={batchedQueue.Count}");
+ }
+
+ ///
+ /// 判断该鱼是否需要分帧生成。
+ /// 普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧;其他(Boss/特殊鱼/特殊Boss/鱼阵) → 立即出。
+ ///
+ private bool ShouldBatchSpawn(OnFishSpawnFishItem item)
+ {
+ // 鱼阵一次性全出(fishArrayNumber 非空表示鱼阵)
+ if (!string.IsNullOrEmpty(item.fishArrayNumber)) return false;
+
+ // 通过 fishNumber 查 FishTypeConfig 判断鱼类型
+ if (string.IsNullOrEmpty(item.fishNumber)) return false;
+ if (FishDatas != null && FishDatas.TryGetValue(item.fishNumber, out var cfg))
+ {
+ int ft = cfg.fishType;
+ // 普通鱼=1, 黄金鱼=3 → 分帧
+ return ft == (int)FishTypeEnum.普通鱼 || ft == (int)FishTypeEnum.黄金鱼;
+ }
+ // 查不到配置,默认分帧(保守处理,避免未知类型一次大量生成)
+ return true;
+ }
+
+ ///
+ /// 分帧出鱼协程:每帧生成 batchSize 条,避免单帧卡顿。
+ ///
+ private IEnumerator SpawnFishBatchedCoroutine(List queue)
+ {
+ const int batchSize = 5; // 每帧生成5条
+ int index = 0;
+ while (index < queue.Count)
+ {
+ // 冰冻中持续等待解冻,避免冰冻期间继续生成鱼
+ while (isFrozen) yield return null;
+
+ int end = Mathf.Min(index + batchSize, queue.Count);
+ for (int i = index; i < end; i++)
+ {
+ TrySpawnSingleFish(queue[i]);
+ }
+ index = end;
+ yield return null; // 等待下一帧
+ }
+ }
+
+ ///
+ /// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
+ ///
+ private bool TrySpawnSingleFish(OnFishSpawnFishItem item)
+ {
+ if (item == null) return false;
+
+ NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
+ NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
+ NetLog.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
+
+ // ⭐ 鱼阵时 fishNumber 为空,用 fishArrayNumber 作为配置 key;
+ // 单体鱼时 fishNumber 非空,直接用 fishNumber
+ string cfgKey = string.IsNullOrEmpty(item.fishNumber) ? item.fishArrayNumber : item.fishNumber;
+ if (string.IsNullOrEmpty(cfgKey)) return false;
+ // guid 用服务端 fishId
+ string guid = item.fishId.ToString();
+
+ // guid 去重:已有此 guid 的鱼跳过
+ // ⭐ 鱼阵鱼的 guid 是 baseGuid_pathId_i 格式,字典里没有 baseGuid 本身,
+ // 所以要额外检查是否有以 baseGuid+"_" 开头的 key,防止 sync 模式重复出鱼
+ // 导致 guid 冲突覆盖(旧鱼变野鱼无法销毁)
+ // ⭐ 优化:用 foreach 代替 LINQ Any(StartsWith),消除 Lambda 闭包和迭代器 GC
+ if (!string.IsNullOrEmpty(guid) && fishDatas_hashCodekey != null)
+ {
+ if (fishDatas_hashCodekey.ContainsKey(guid)) return false;
+
+ string guidPrefix = guid + "_";
+ bool hasPrefixMatch = false;
+ foreach (var key in fishDatas_hashCodekey.Keys)
+ {
+ if (key != null && key.StartsWith(guidPrefix))
+ {
+ hasPrefixMatch = true;
+ break;
+ }
+ }
+ if (hasPrefixMatch) return false;
+ }
+
+ // 构造 FrameData_CreateFish 复用 SpawnFishMainThread
+ var createFishData = new FrameData_CreateFish
+ {
+ fishNumber = cfgKey,
+ guid = guid,
+ moveLineId = item.routeId.ToString()
+ };
+
+ // ⭐ 根据 type 决定 moveTime:
+ // - spawn/init:新鱼,传当前时间 → moveTime≈0,鱼从曲线起点开始游
+ // (避免服务端/客户端时间差导致 moveTime>0,鱼瞬间出现在曲线中间)
+ // - sync:同步已有鱼,传 createdAt → moveTime=时间差,鱼出现在路径正确位置
+ long spawnTime = item.createdAt;
+ SpawnFishMainThread(createFishData, spawnTime);
+
+ return true;
}
///
@@ -1203,6 +1275,8 @@ public class CreateFishManager : MonoSingleton
Destroy(bjEffectRoot.gameObject);
bjEffectRoot = null;
}
+ // ⭐ 清理特效池:bjEffectRoot 销毁后池内对象失效,清空避免复用已销毁对象
+ EffectPool.Instance.ClearAll();
//for (int i = fishDatas_hashCodekey.childCount - 1; i >= 0; i--)
// Destroy(fishRoot.GetChild(i).gameObject);
}
@@ -1221,50 +1295,77 @@ public class CreateFishManager : MonoSingleton
// 随机工具方法
// ================================================================
+ // ⭐ 静态复用 Random 实例,避免每次调用 new System.Random() 产生 GC
+ private static readonly System.Random _sharedRand = new System.Random();
+ // ⭐ 预分配结果 List,避免调用方反复 new List
+ private static readonly List _reusableIntResult = new List(64);
+
///
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
+ /// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
+ /// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
///
public static List RandomUniqueNumbers(int count, int min, int max)
{
+ _reusableIntResult.Clear();
if (min >= max)
{
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
- return new List();
+ return _reusableIntResult;
}
count = Mathf.Min(count, max - min); // 不能超过范围
- var result = new HashSet();
- var rand = new System.Random();
+ // ⭐ 用 HashSet 去重,但复用静态实例避免每次 new
+ _reusableIntSet.Clear();
+ while (_reusableIntSet.Count < count)
+ _reusableIntSet.Add(_sharedRand.Next(min, max));
+ foreach (var v in _reusableIntSet)
+ _reusableIntResult.Add(v);
+ return _reusableIntResult;
+ }
+ private static readonly HashSet _reusableIntSet = new HashSet(64);
- while (result.Count < count)
- result.Add(rand.Next(min, max));
-
- return result.ToList();
+ // ⭐ 预分配 string 结果 List,避免每次调用 new List
+ private static readonly List _reusableStrResult = new List(64);
+ // ⭐ 预分配 key 缓冲,避免 keyCollection.ToList() 产生 GC
+ private static readonly List _reusableKeyBuf = new List(128);
+ // ⭐ 预分配索引 HashSet,避免每次 new HashSet
+ private static readonly HashSet _reusableIdxSet = new HashSet(64);
+ // ⭐ 缓存 uiRoot.Canvas,避免每次杀鱼特效都 GetComponent