1
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Profiler .data 文件导出器
|
||||
/// 使用 ProfilerDriver.GetHierarchyFrameDataView API 提取函数级 GC Alloc / CPU 耗时 / 调用次数
|
||||
/// 菜单: Tools > Profiler Data Exporter
|
||||
/// </summary>
|
||||
public class ProfilerDataExporter : EditorWindow
|
||||
{
|
||||
[MenuItem("Tools/Profiler Data Exporter")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<ProfilerDataExporter>("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<string, FuncStats>();
|
||||
|
||||
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<FuncStats>(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<int>();
|
||||
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<int>();
|
||||
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<string, FuncStats> stats)
|
||||
{
|
||||
ProcessItem(frameData, itemId, stats);
|
||||
|
||||
// 递归子节点:每层新建 list,避免递归间共享集合被修改导致枚举异常
|
||||
var children = new List<int>();
|
||||
frameData.GetItemChildren(itemId, children);
|
||||
foreach (int childId in children)
|
||||
{
|
||||
ProcessItemRecursive(frameData, childId, stats);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessItem(HierarchyFrameDataView frameData, int itemId, Dictionary<string, FuncStats> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36fb1e0e209a62f478f5019e12f38d9d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -23,6 +23,16 @@ public class EffectPool : Singleton<EffectPool>
|
||||
|
||||
/// <summary>从池中取一个特效(池空则 Instantiate),取出后自动播放并在播完时回池</summary>
|
||||
public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent)
|
||||
{
|
||||
return Get(prefab, position, rotation, parent, autoReturn: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从池中取一个特效。
|
||||
/// autoReturn=true: 粒子特效,EffectAutoReturn 检测粒子播完自动回池
|
||||
/// autoReturn=false: 动画/UI 驱动特效(如 BJBase/DieEffect_TSY),由动画事件手动调用 Return()
|
||||
/// </summary>
|
||||
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<EffectPool>
|
||||
|
||||
activeMap[obj] = prefabID;
|
||||
|
||||
// 挂上自动回收组件(已挂则复用)
|
||||
var autoReturn = obj.GetComponent<EffectAutoReturn>();
|
||||
if (autoReturn == null)
|
||||
autoReturn = obj.AddComponent<EffectAutoReturn>();
|
||||
autoReturn.prefabID = prefabID;
|
||||
autoReturn.Play();
|
||||
if (autoReturn)
|
||||
{
|
||||
// 粒子特效:挂上自动回收组件(已挂则复用)
|
||||
var auto = obj.GetComponent<EffectAutoReturn>();
|
||||
if (auto == null)
|
||||
auto = obj.AddComponent<EffectAutoReturn>();
|
||||
auto.prefabID = prefabID;
|
||||
auto.Play();
|
||||
}
|
||||
// autoReturn=false: 动画/UI 驱动特效,不挂自动回收组件
|
||||
// 由动画事件(coinEvent/ScoreNumScrollEvent 等)调用 EffectPool.Instance.Return(gameObject) 回池
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -642,65 +642,137 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
if (data == null || data.fish == null || data.fish.Count == 0) return;
|
||||
if (isFrozen) return; // 冰冻中不创建鱼
|
||||
if (spawnDic == null) return; // 场景卸载后忽略
|
||||
|
||||
|
||||
// ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
|
||||
// 避免一次出大量普通鱼导致单帧卡顿
|
||||
var batchedQueue = new List<OnFishSpawnFishItem>(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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断该鱼是否需要分帧生成。
|
||||
/// 普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧;其他(Boss/特殊鱼/特殊Boss/鱼阵) → 立即出。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分帧出鱼协程:每帧生成 batchSize 条,避免单帧卡顿。
|
||||
/// </summary>
|
||||
private IEnumerator SpawnFishBatchedCoroutine(List<OnFishSpawnFishItem> 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; // 等待下一帧
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1203,6 +1275,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
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<CreateFishManager>
|
||||
// 随机工具方法
|
||||
// ================================================================
|
||||
|
||||
// ⭐ 静态复用 Random 实例,避免每次调用 new System.Random() 产生 GC
|
||||
private static readonly System.Random _sharedRand = new System.Random();
|
||||
// ⭐ 预分配结果 List,避免调用方反复 new List<int>
|
||||
private static readonly List<int> _reusableIntResult = new List<int>(64);
|
||||
|
||||
/// <summary>
|
||||
/// 从 [min, max) 区间随机取 count 个不重复整数。
|
||||
/// 若 min==max 返回空列表。
|
||||
/// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
|
||||
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
|
||||
/// </summary>
|
||||
public static List<int> RandomUniqueNumbers(int count, int min, int max)
|
||||
{
|
||||
_reusableIntResult.Clear();
|
||||
if (min >= max)
|
||||
{
|
||||
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
|
||||
return new List<int>();
|
||||
return _reusableIntResult;
|
||||
}
|
||||
|
||||
count = Mathf.Min(count, max - min); // 不能超过范围
|
||||
var result = new HashSet<int>();
|
||||
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<int> _reusableIntSet = new HashSet<int>(64);
|
||||
|
||||
while (result.Count < count)
|
||||
result.Add(rand.Next(min, max));
|
||||
|
||||
return result.ToList();
|
||||
// ⭐ 预分配 string 结果 List,避免每次调用 new List<string>
|
||||
private static readonly List<string> _reusableStrResult = new List<string>(64);
|
||||
// ⭐ 预分配 key 缓冲,避免 keyCollection.ToList() 产生 GC
|
||||
private static readonly List<string> _reusableKeyBuf = new List<string>(128);
|
||||
// ⭐ 预分配索引 HashSet,避免每次 new HashSet
|
||||
private static readonly HashSet<int> _reusableIdxSet = new HashSet<int>(64);
|
||||
// ⭐ 缓存 uiRoot.Canvas,避免每次杀鱼特效都 GetComponent<Canvas>()(5处复用)
|
||||
private static Canvas _cachedUiCanvas;
|
||||
private static Canvas GetUiCanvas()
|
||||
{
|
||||
if (_cachedUiCanvas == null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
|
||||
_cachedUiCanvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
|
||||
return _cachedUiCanvas;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 KeyCollection 中随机取 count 个不重复的 string key。
|
||||
/// ⭐ 优化:复用静态 Random + 预分配缓冲,零 GC 分配。
|
||||
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
|
||||
/// </summary>
|
||||
public static List<string> RandomUniqueNumbers(int count, int min, int max,
|
||||
Dictionary<string, List<RoomFishDetailConfig>>.KeyCollection keyCollection)
|
||||
{
|
||||
if (min >= max) return new List<string>();
|
||||
_reusableStrResult.Clear();
|
||||
if (min >= max) return _reusableStrResult;
|
||||
|
||||
count = Mathf.Min(count, max - min);
|
||||
var keyList = keyCollection.ToList();
|
||||
var indices = new HashSet<int>();
|
||||
var rand = new System.Random();
|
||||
var result = new List<string>(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<CreateFishManager>
|
||||
int count, List<RedPacketRecord> coinNum, Vector3 v3Point, int killerSitPos = -1)
|
||||
{
|
||||
var guids = new List<string>(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<CreateFishManager>
|
||||
{
|
||||
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<CreateFishManager>
|
||||
|
||||
}
|
||||
|
||||
// 死亡粒子特效
|
||||
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<RectTransform>().anchoredPosition =
|
||||
CoordinateTransformation.WorldPos_ToCanvas(
|
||||
monster.transform.position,
|
||||
UIManager.Instance.uiRoot.GetComponent<Canvas>());
|
||||
GetUiCanvas());
|
||||
|
||||
var coinScript = award.GetComponent<BJBase>();
|
||||
if (coinScript != null)
|
||||
@@ -1518,12 +1640,13 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
// 2. 屏幕坐标 → Canvas 坐标(跟你原来金币飞行逻辑保持一致)
|
||||
Vector2 canvasPos = CoordinateTransformation.ScreenPoint_ToCanvas(
|
||||
screenPos,
|
||||
UIManager.Instance.uiRoot.GetComponent<Canvas>());
|
||||
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<RectTransform>();
|
||||
rect.anchoredPosition = canvasPos;
|
||||
@@ -1547,7 +1670,7 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
var killFish = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomKillNotice>>(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<CreateFishManager>
|
||||
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<CreateFishManager>
|
||||
{
|
||||
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<RectTransform>().anchoredPosition =
|
||||
CoordinateTransformation.WorldPos_ToCanvas(
|
||||
monster.transform.position,
|
||||
UIManager.Instance.uiRoot.GetComponent<Canvas>());
|
||||
GetUiCanvas());
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1658,16 +1782,19 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
|
||||
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<CreateFishManager>
|
||||
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<RectTransform>().anchoredPosition =
|
||||
CoordinateTransformation.WorldPos_ToCanvas(
|
||||
monster.transform.position,
|
||||
UIManager.Instance.uiRoot.GetComponent<Canvas>());
|
||||
GetUiCanvas());
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1741,16 +1869,17 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
}
|
||||
}
|
||||
|
||||
// 普通鱼:金币飞向炮台
|
||||
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<RectTransform>().anchoredPosition =
|
||||
CoordinateTransformation.WorldPos_ToCanvas(
|
||||
monster.transform.position,
|
||||
UIManager.Instance.uiRoot.GetComponent<Canvas>());
|
||||
award.transform.SetParent(currGameType.subPanelParent.parent);
|
||||
GetUiCanvas());
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1852,16 +1981,19 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Assets.MyGame.Scripts.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// 网络热路径日志封装。
|
||||
/// 用 [Conditional("ENABLE_VERBOSE_LOG")] 控制:
|
||||
/// - 默认不定义 ENABLE_VERBOSE_LOG → 编辑器和 Release 包都不输出,参数评估也被移除,零 GC
|
||||
/// - 需要调试时: Player Settings → Scripting Define Symbols 添加 ENABLE_VERBOSE_LOG
|
||||
/// 只用于 WebSocket 消息处理/出鱼等高频热路径,避免每帧/每消息产生日志 GC。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f95bc88c8c9c2e84890df4a61aefcf75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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<WebSocketResponseBase<object>>(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<WebSocketResponseBase<object>>(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<object> v)
|
||||
{
|
||||
@@ -687,5 +699,50 @@ namespace Assets.MyGame.Scripts.Net
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轻量解析:用 JsonTextReader 只读 type 和 target 字段做路由,
|
||||
/// 跳过 arguments 数组反序列化(节省 90%+ 预解析 GC)。
|
||||
/// arguments 在具体处理方法内部按需反序列化。
|
||||
/// </summary>
|
||||
private static WebSocketResponseBase<object> 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<object>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ namespace Assets.MyGame.Scripts.Net
|
||||
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<OnFishSpawnData>>(msg).arguments[0];
|
||||
if (data == null) return;
|
||||
EventCenter.Instance.EventTrigger<OnFishSpawnData>(ConstEventString.OnFishSpawn, data);
|
||||
Debug.Log("出鱼:"+msg);
|
||||
NetLog.Log("出鱼:", msg);
|
||||
}
|
||||
|
||||
/// <summary>鱼列表刷新</summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
/// <summary>
|
||||
/// 真人玩家发射子弹(对接后端最新签名)
|
||||
|
||||
@@ -32,6 +32,7 @@ public class ScoreNumScrollEvent : MonoBehaviour
|
||||
|
||||
public void dieFen()
|
||||
{
|
||||
Destroy(gameObject);
|
||||
// ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁
|
||||
EffectPool.Instance.Return(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ public class coinEvent : MonoBehaviour
|
||||
|
||||
|
||||
}
|
||||
Destroy(coinScript.gameObject);
|
||||
// ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁
|
||||
EffectPool.Instance.Return(coinScript.gameObject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,8 @@ public class BaoJiangTSSmallFish : MonoBehaviour
|
||||
// 销毁GameObject
|
||||
//Destroy(target.gameObject);
|
||||
JiaFen?.Invoke();
|
||||
Destroy(gameObject);
|
||||
// ⭐ 池化回收:若对象来自 EffectPool 则回池复用,否则销毁
|
||||
EffectPool.Instance.Return(gameObject);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>创建一根闪电 LineRenderer</summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>炸弹飞行到目标点</summary>
|
||||
|
||||
Reference in New Issue
Block a user