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()(5处复用) + private static Canvas _cachedUiCanvas; + private static Canvas GetUiCanvas() + { + if (_cachedUiCanvas == null && UIManager.Instance != null && UIManager.Instance.uiRoot != null) + _cachedUiCanvas = UIManager.Instance.uiRoot.GetComponent(); + return _cachedUiCanvas; } /// /// 从 KeyCollection 中随机取 count 个不重复的 string key。 + /// ⭐ 优化:复用静态 Random + 预分配缓冲,零 GC 分配。 + /// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。 /// public static List RandomUniqueNumbers(int count, int min, int max, Dictionary>.KeyCollection keyCollection) { - if (min >= max) return new List(); + _reusableStrResult.Clear(); + if (min >= max) return _reusableStrResult; count = Mathf.Min(count, max - min); - var keyList = keyCollection.ToList(); - var indices = new HashSet(); - var rand = new System.Random(); - var result = new List(count); + // ⭐ 用 foreach 拷贝到复用缓冲,避免 keyCollection.ToList() 的 GC + _reusableKeyBuf.Clear(); + foreach (var k in keyCollection) _reusableKeyBuf.Add(k); + _reusableIdxSet.Clear(); - while (indices.Count < count) + while (_reusableIdxSet.Count < count) { - int idx = rand.Next(min, max); - if (indices.Add(idx)) - result.Add(keyList[idx]); + int idx = _sharedRand.Next(min, max); + if (_reusableIdxSet.Add(idx)) + _reusableStrResult.Add(_reusableKeyBuf[idx]); } - return result; + return _reusableStrResult; } #endregion @@ -1312,20 +1413,28 @@ public class CreateFishManager : MonoSingleton int count, List coinNum, Vector3 v3Point, int killerSitPos = -1) { var guids = new List(count); - var tempKeys = fishDatas_hashCodekey_Small.Keys.ToList(); + // ⭐ 优化:用 Fisher-Yates 洗牌索引代替 Keys.ToList()+RemoveAt,零 GC + // 将 keys 拷贝到复用缓冲 + _reusableKeyBuf.Clear(); + foreach (var k in fishDatas_hashCodekey_Small.Keys) _reusableKeyBuf.Add(k); - // 若存活鱼不足 count,全部选取 - if (tempKeys.Count <= count) + int keyCount = _reusableKeyBuf.Count; + if (keyCount <= count) { - guids.AddRange(tempKeys); + // 若存活鱼不足 count,全部选取 + for (int i = 0; i < keyCount; i++) guids.Add(_reusableKeyBuf[i]); } else { + // ⭐ Fisher-Yates 洗牌:只洗前 count 个位置,避免全量洗牌 for (int i = 0; i < count; i++) { - int idx = Random.Range(0, tempKeys.Count); - guids.Add(tempKeys[idx]); - tempKeys.RemoveAt(idx); + int j = Random.Range(i, keyCount); + // 交换 i 和 j + var tmp = _reusableKeyBuf[i]; + _reusableKeyBuf[i] = _reusableKeyBuf[j]; + _reusableKeyBuf[j] = tmp; + guids.Add(_reusableKeyBuf[i]); } } @@ -1359,13 +1468,24 @@ public class CreateFishManager : MonoSingleton { if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster)) { +#if UNITY_EDITOR + // ⭐ 优化:调试日志用 #if UNITY_EDITOR 包裹,Release 包零 GC + // 原代码 Where().Take().ToList() + string.Join + $"" 每次 NotFound 都产生大量 GC Debug.LogWarning($"[CreateFishManager] Id_DieFish_NotFX 找不到鱼: fishId={fishId}, 字典数量={fishDatas_hashCodekey.Count}"); - // ⭐ 记录找不到鱼日志:这是bug的核心症状。附带 baseGuid 前缀匹配,看是否存在 guid 格式不一致 string baseGuid = fishId.Contains("_") ? fishId.Split('_')[0] : fishId; - var similarKeys = fishDatas_hashCodekey.Keys.Where(k => k.StartsWith(baseGuid)).Take(10).ToList(); - string similarStr = similarKeys.Count > 0 ? string.Join(",", similarKeys) : "(无)"; - GameManager.LogFishFlow("NOT_FOUND", fishId, - $"dictCount={fishDatas_hashCodekey.Count} baseGuid={baseGuid} 相似key={similarStr}"); + int matchCount = 0; + foreach (var k in fishDatas_hashCodekey.Keys) + { + if (k.StartsWith(baseGuid)) + { + matchCount++; + if (matchCount <= 10) GameManager.LogFishFlow("NOT_FOUND", fishId, $"similarKey={k}"); + } + } + if (matchCount == 0) GameManager.LogFishFlow("NOT_FOUND", fishId, "(无相似key)"); +#else + GameManager.LogFishFlow("NOT_FOUND", fishId, fishDatas_hashCodekey.Count.ToString()); +#endif return; } Pool.Instance.Return(monster.gameObject); @@ -1435,22 +1555,24 @@ public class CreateFishManager : MonoSingleton } - // 死亡粒子特效 - Instantiate( + // 死亡粒子特效(池化,粒子播完自动回池) + EffectPool.Instance.Get( monster.effect_Die != null ? monster.effect_Die : dieFXPrefab, monster.transform.position, Quaternion.identity, transform.parent); // 非 Boss 鱼:金币飞向炮台 if (monster.fishenum != FishTypeEnum.Boss鱼 && monster.fishenum != FishTypeEnum.特殊Boss) { - var award = Instantiate( + // ⭐ 池化:动画事件(coinEvent/ScoreNumScrollEvent)播完手动回池 + var award = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, - currGameType.subPanelParent.parent); + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, - UIManager.Instance.uiRoot.GetComponent()); + GetUiCanvas()); var coinScript = award.GetComponent(); if (coinScript != null) @@ -1518,12 +1640,13 @@ public class CreateFishManager : MonoSingleton // 2. 屏幕坐标 → Canvas 坐标(跟你原来金币飞行逻辑保持一致) Vector2 canvasPos = CoordinateTransformation.ScreenPoint_ToCanvas( screenPos, - UIManager.Instance.uiRoot.GetComponent()); + GetUiCanvas()); - // 3. 实例化金币特效(沿用你原来的 award 逻辑) - var award = Instantiate( - dieFXPrefabAward, // 或者用某个专门的"掉金币" prefab - currGameType.subPanelParent.parent); + // 3. 实例化金币特效(池化,动画事件播完手动回池) + var award = EffectPool.Instance.Get( + dieFXPrefabAward, + targetWorldPos, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); var rect = award.GetComponent(); rect.anchoredPosition = canvasPos; @@ -1547,7 +1670,7 @@ public class CreateFishManager : MonoSingleton var killFish = JsonConvert.DeserializeObject>(msg) .arguments[0]; - Debug.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s"); + NetLog.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s"); SitPosData sitData = null; //击杀玩家座位数据 foreach (var item in currGameType.list_Sits) { @@ -1565,8 +1688,8 @@ public class CreateFishManager : MonoSingleton if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return; - // 死亡粒子特效 - Instantiate( + // 死亡粒子特效(池化,粒子播完自动回池) + EffectPool.Instance.Get( monster.effect_Die != null ? monster.effect_Die : dieFXPrefab, monster.transform.position, Quaternion.identity, transform.parent); int goldToAdd = killFish.gold; // 立即复制需要的数据 @@ -1594,16 +1717,17 @@ public class CreateFishManager : MonoSingleton { case FishTypeEnum.普通鱼: { - // 普通鱼:金币飞向炮台 - var award = Instantiate( + // 普通鱼:金币飞向炮台(池化,动画事件播完手动回池) + var award = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, - currGameType.subPanelParent.parent); + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, - UIManager.Instance.uiRoot.GetComponent()); + GetUiCanvas()); } catch { @@ -1658,16 +1782,19 @@ public class CreateFishManager : MonoSingleton if (bossCtrl.isToUI) { - bossCtrl.currEffect_DieAward = Instantiate( + // ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池 + bossCtrl.currEffect_DieAward = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, - currGameType.subPanelParent.parent); + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); } else { - // isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理 - bossCtrl.currEffect_DieAward = Instantiate( + // isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化) + bossCtrl.currEffect_DieAward = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, - bjEffectRoot); + monster.transform.position, Quaternion.identity, bjEffectRoot, + autoReturn: false); } bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward); @@ -1676,16 +1803,17 @@ public class CreateFishManager : MonoSingleton break; case FishTypeEnum.黄金鱼: { - // 普通鱼:金币飞向炮台 - var award = Instantiate( + // 黄金鱼:金币飞向炮台(池化,动画事件播完手动回池) + var award = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, - currGameType.subPanelParent.parent); + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, - UIManager.Instance.uiRoot.GetComponent()); + GetUiCanvas()); } catch { @@ -1741,16 +1869,17 @@ public class CreateFishManager : MonoSingleton } } - // 普通鱼:金币飞向炮台 - var award = Instantiate( - monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward); + // 特殊鱼:金币飞向炮台(池化,动画事件播完手动回池) + var award = EffectPool.Instance.Get( + monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward, + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); try { award.GetComponent().anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas( monster.transform.position, - UIManager.Instance.uiRoot.GetComponent()); - award.transform.SetParent(currGameType.subPanelParent.parent); + GetUiCanvas()); } catch { @@ -1852,16 +1981,19 @@ public class CreateFishManager : MonoSingleton if (bossCtrl.isToUI) { - bossCtrl.currEffect_DieAward = Instantiate( + // ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池 + bossCtrl.currEffect_DieAward = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, - currGameType.subPanelParent.parent); + monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent, + autoReturn: false); } else { - // isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理 - bossCtrl.currEffect_DieAward = Instantiate( + // isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化) + bossCtrl.currEffect_DieAward = EffectPool.Instance.Get( monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp, - bjEffectRoot); + monster.transform.position, Quaternion.identity, bjEffectRoot, + autoReturn: false); } bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward); diff --git a/Assets/MyGame/Scripts/Net/NetLog.cs b/Assets/MyGame/Scripts/Net/NetLog.cs new file mode 100644 index 000000000..7974eb37d --- /dev/null +++ b/Assets/MyGame/Scripts/Net/NetLog.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; +using UnityEngine; + +namespace Assets.MyGame.Scripts.Net +{ + /// + /// 网络热路径日志封装。 + /// 用 [Conditional("ENABLE_VERBOSE_LOG")] 控制: + /// - 默认不定义 ENABLE_VERBOSE_LOG → 编辑器和 Release 包都不输出,参数评估也被移除,零 GC + /// - 需要调试时: Player Settings → Scripting Define Symbols 添加 ENABLE_VERBOSE_LOG + /// 只用于 WebSocket 消息处理/出鱼等高频热路径,避免每帧/每消息产生日志 GC。 + /// + public static class NetLog + { + [Conditional("ENABLE_VERBOSE_LOG")] + public static void Log(string msg) + { + Debug.Log(msg); + } + + [Conditional("ENABLE_VERBOSE_LOG")] + public static void Log(string prefix, string msg) + { + Debug.Log(prefix + msg); + } + + [Conditional("ENABLE_VERBOSE_LOG")] + public static void LogWarning(string msg) + { + Debug.LogWarning(msg); + } + } +} diff --git a/Assets/MyGame/Scripts/Net/NetLog.cs.meta b/Assets/MyGame/Scripts/Net/NetLog.cs.meta new file mode 100644 index 000000000..90f0466f2 --- /dev/null +++ b/Assets/MyGame/Scripts/Net/NetLog.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f95bc88c8c9c2e84890df4a61aefcf75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs index 6456a198c..92ea427b6 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketManager_Message.cs @@ -3,6 +3,7 @@ using Best.WebSockets; using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -15,53 +16,64 @@ namespace Assets.MyGame.Scripts.Net { public void OnMessage(WebSocket web, string msg, SocketType type) { -#if UNITY_EDITOR - Debug.Log($"[WebSocket] {type} 收到消息: {msg}"); - -#endif - string[] msgs = msg.Split('\u001e'); - foreach (var item in msgs) + NetLog.Log($"[WebSocket] {type} 收到消息: {msg}"); + // ⭐ 优化:用 char 分割避免 Split 产生 string[] 数组 GC + // 单条消息通常只有 1-2 个分隔段,直接手动解析 + int start = 0; + int sep; + while ((sep = msg.IndexOf('\u001e', start)) >= 0) { - if (string.IsNullOrWhiteSpace(item)) - continue; + ProcessSingleMessage(msg, start, sep - start, type); + start = sep + 1; + } + // 处理最后一段(或没有分隔符的整条消息) + if (start < msg.Length) + ProcessSingleMessage(msg, start, msg.Length - start, type); + } - if (item.Trim() == "{}") - { - OnHandshakeCompleted(type); - continue; - } + private void ProcessSingleMessage(string msg, int start, int length, SocketType type) + { + // 跳过空白段 + if (length <= 0) return; - var v = JsonConvert.DeserializeObject>(item); - if (v == null) - { - continue; - } - // TODO: 这里可以做统一消息分发 - #region 消息处理 - switch (type) - { - case SocketType.login: - LoginMSG(item, v); - break; - - case SocketType.room: - case SocketType.game: - FishMSG(item, v); - break; - case SocketType.chess: - ChineseChess(item, v); - break; - case SocketType.customer: - CustomerMSG(item, v); - break; - default: - break; - } + // 快速检查 "{}"(握手包),避免 Substring + Trim + if (length == 2 && msg[start] == '{' && msg[start + 1] == '}') + { + OnHandshakeCompleted(type); + return; } + // 提取子串(此处必须分配,后续反序列化需要) + string item = msg.Substring(start, length); + // ⭐ 优化:用 JsonTextReader 只读 type/target 做路由,跳过 arguments 反序列化 + // 原代码:JsonConvert.DeserializeObject>(item) + // 会反序列化整个 arguments 数组(包含鱼数据等),但这些数据在路由阶段完全不需要, + // 具体处理方法内部会重新反序列化。这能消除预解析阶段 90%+ 的 GC。 + var v = ParseHeader(item); + if (v == null) return; + + #region 消息处理 + switch (type) + { + case SocketType.login: + LoginMSG(item, v); + break; + + case SocketType.room: + case SocketType.game: + FishMSG(item, v); + break; + case SocketType.chess: + ChineseChess(item, v); + break; + case SocketType.customer: + CustomerMSG(item, v); + break; + default: + break; + } #endregion - } private static void ChineseChess(string msg, WebSocketResponseBase v) { @@ -687,5 +699,50 @@ namespace Assets.MyGame.Scripts.Net break; } } + + /// + /// 轻量解析:用 JsonTextReader 只读 type 和 target 字段做路由, + /// 跳过 arguments 数组反序列化(节省 90%+ 预解析 GC)。 + /// arguments 在具体处理方法内部按需反序列化。 + /// + private static WebSocketResponseBase ParseHeader(string item) + { + if (string.IsNullOrEmpty(item)) return null; + + try + { + using (var sr = new StringReader(item)) + using (var reader = new JsonTextReader(sr)) + { + var result = new WebSocketResponseBase(); + while (reader.Read()) + { + if (reader.TokenType == JsonToken.PropertyName) + { + string propName = (string)reader.Value; + // 读到 arguments 就停止,不再解析后续(节省大量 GC) + if (propName == "arguments") + break; + + reader.Read(); // 读值 + if (propName == "type") + { + result.type = Convert.ToInt32(reader.Value); + } + else if (propName == "target") + { + result.target = reader.Value as string; + } + // invocationId 等字段跳过 + } + } + return result; + } + } + catch + { + return null; + } + } } } diff --git a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs index 4a93194a0..955fc4e4c 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs @@ -341,7 +341,7 @@ namespace Assets.MyGame.Scripts.Net var data = JsonConvert.DeserializeObject>(msg).arguments[0]; if (data == null) return; EventCenter.Instance.EventTrigger(ConstEventString.OnFishSpawn, data); - Debug.Log("出鱼:"+msg); + NetLog.Log("出鱼:", msg); } /// 鱼列表刷新 diff --git a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs index 9cff1811a..942cde892 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketSendList_Fish.cs @@ -97,8 +97,9 @@ public partial class WebSocketSendList timestamp, sign ); - Debug.Log($"扣钱:发送打中鱼请求: {JsonConvert.SerializeObject(sendObj)}"); - WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj)); + var jsonStr = JsonConvert.SerializeObject(sendObj); + NetLog.Log($"扣钱:发送打中鱼请求: {jsonStr}"); + WebSocketManager.Instance.Send(SocketType.game, jsonStr); } /// /// 真人玩家发射子弹(对接后端最新签名) diff --git a/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/ScoreNumScrollEvent.cs b/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/ScoreNumScrollEvent.cs index 213adaa91..da2c66e66 100644 --- a/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/ScoreNumScrollEvent.cs +++ b/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/ScoreNumScrollEvent.cs @@ -32,6 +32,7 @@ public class ScoreNumScrollEvent : MonoBehaviour public void dieFen() { - Destroy(gameObject); + // ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁 + EffectPool.Instance.Return(gameObject); } } diff --git a/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/coinEvent.cs b/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/coinEvent.cs index 7ecb348fe..b1860e0d1 100644 --- a/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/coinEvent.cs +++ b/Assets/MyGame/aa/Game/DefaultDieEffect/AnimEvent/coinEvent.cs @@ -29,7 +29,8 @@ public class coinEvent : MonoBehaviour } - Destroy(coinScript.gameObject); + // ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁 + EffectPool.Instance.Return(coinScript.gameObject); }); } } diff --git a/Assets/MyGame/aa/Game/DefaultDieEffect/TeShuBJ/BaoJiangTSSmallFish.cs b/Assets/MyGame/aa/Game/DefaultDieEffect/TeShuBJ/BaoJiangTSSmallFish.cs index 4b0b68c1b..a292ae969 100644 --- a/Assets/MyGame/aa/Game/DefaultDieEffect/TeShuBJ/BaoJiangTSSmallFish.cs +++ b/Assets/MyGame/aa/Game/DefaultDieEffect/TeShuBJ/BaoJiangTSSmallFish.cs @@ -222,7 +222,8 @@ public class BaoJiangTSSmallFish : MonoBehaviour // 销毁GameObject //Destroy(target.gameObject); JiaFen?.Invoke(); - Destroy(gameObject); + // ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁 + EffectPool.Instance.Return(gameObject); } diff --git a/Assets/MyGame/aa/Game/HHWK/MonsterTSY/SDY/HHWK_SDY_DieEvent.cs b/Assets/MyGame/aa/Game/HHWK/MonsterTSY/SDY/HHWK_SDY_DieEvent.cs index 97b4a74f0..633ee5993 100644 --- a/Assets/MyGame/aa/Game/HHWK/MonsterTSY/SDY/HHWK_SDY_DieEvent.cs +++ b/Assets/MyGame/aa/Game/HHWK/MonsterTSY/SDY/HHWK_SDY_DieEvent.cs @@ -66,7 +66,7 @@ public class HHWK_SDY_DieEvent : DieEffect_TSY { yield return new WaitForSeconds(0.3f); CreateBaoJiangEffect(); - Destroy(gameObject); + EffectPool.Instance.Return(gameObject); yield break; } @@ -136,7 +136,7 @@ public class HHWK_SDY_DieEvent : DieEffect_TSY yield return new WaitForSeconds(0.3f); CreateBaoJiangEffect(); - Destroy(gameObject); + EffectPool.Instance.Return(gameObject); } /// 创建一根闪电 LineRenderer diff --git a/Assets/MyGame/aa/Game/HHWK/MonsterTSY/ZDX/HHWK_ZDX_DieEvent.cs b/Assets/MyGame/aa/Game/HHWK/MonsterTSY/ZDX/HHWK_ZDX_DieEvent.cs index 205df5b11..bc6234375 100644 --- a/Assets/MyGame/aa/Game/HHWK/MonsterTSY/ZDX/HHWK_ZDX_DieEvent.cs +++ b/Assets/MyGame/aa/Game/HHWK/MonsterTSY/ZDX/HHWK_ZDX_DieEvent.cs @@ -63,7 +63,7 @@ public class HHWK_ZDX_DieEvent : DieEffect_TSY yield return new WaitForSeconds(bombFlyTime + 0.5f); // 在销毁前创建爆奖特效 CreateBaoJiangEffect(); - Destroy(gameObject); + EffectPool.Instance.Return(gameObject); } /// 炸弹飞行到目标点