using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEngine.SceneManagement; using System.Linq; using UnityEngine.Profiling; using System.Text; namespace SoraTools { public class SceneAnalyzer : EditorWindow { private enum TargetDevice { Android_8G, Android_12G } private TargetDevice currentTarget = TargetDevice.Android_8G; private Vector2 scrollPos; // 分析结果缓存 private bool hasAnalyzed = false; private AnalysisData currentData; // GUI Styles private GUIStyle titleStyle; private GUIStyle headerStyle; private GUIStyle boxStyle; private GUIStyle resultGoodStyle; private GUIStyle resultWarnStyle; private GUIStyle resultBadStyle; private GUIStyle labelStyle; private GUIStyle linkStyle; [MenuItem("SoraTools/Scene Analyzer %&r")] public static void ShowWindow() { var window = GetWindow("Scene Analyzer"); window.minSize = new Vector2(400, 600); window.Show(); } private void OnEnable() { // 初始化样式 InitStyles(); } private void InitStyles() { titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 18, alignment = TextAnchor.MiddleCenter, margin = new RectOffset(0, 0, 10, 10) }; headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, margin = new RectOffset(0, 0, 10, 5) }; boxStyle = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(10, 10, 10, 10), margin = new RectOffset(5, 5, 5, 5) }; labelStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, richText = true }; resultGoodStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.2f, 0.8f, 0.2f) }, fontStyle = FontStyle.Bold }; resultWarnStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.7f, 0.0f) }, fontStyle = FontStyle.Bold }; resultBadStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.3f, 0.3f) }, fontStyle = FontStyle.Bold }; linkStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.3f, 0.6f, 1.0f) }, hover = { textColor = new Color(0.4f, 0.7f, 1.0f) }, fontStyle = FontStyle.Italic }; } private void OnGUI() { if (titleStyle == null) InitStyles(); EditorGUILayout.LabelField("场景性能分析工具", titleStyle); EditorGUILayout.Space(); // 设置区域 EditorGUILayout.BeginVertical(boxStyle); EditorGUILayout.LabelField("目标设备设置", headerStyle); currentTarget = (TargetDevice)EditorGUILayout.EnumPopup("目标设备内存", currentTarget); EditorGUILayout.Space(); if (GUILayout.Button("开始分析当前场景", GUILayout.Height(40))) { AnalyzeScene(); } EditorGUILayout.EndVertical(); if (!hasAnalyzed || currentData == null) { EditorGUILayout.HelpBox("请点击上方按钮开始分析当前场景。", MessageType.Info); return; } // 结果展示区域 EditorGUILayout.Space(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); DrawSection("场景概览", () => { DrawStatItem("GameObjects 总数", currentData.objectCount, GetThreshold(MetricType.ObjectCount).warning, GetThreshold(MetricType.ObjectCount).limit); DrawStatItem("总顶点数 (Vertices)", currentData.totalVertices, GetThreshold(MetricType.Vertices).warning, GetThreshold(MetricType.Vertices).limit, FormatNumber); DrawStatItem("总面数 (Triangles)", currentData.totalTriangles, GetThreshold(MetricType.Triangles).warning, GetThreshold(MetricType.Triangles).limit, FormatNumber); DrawStatItem("非 Static 物体", currentData.nonStaticCount, 500, 1000, null, "点击选中非Static物体", currentData.nonStaticObjects); DrawStatItem("Missing Scripts", currentData.missingScriptCount, 0, 0, null, "点击选中Missing Script物体", currentData.missingScriptObjects); }); DrawSection("灯光与渲染 (Lighting & Rendering)", () => { DrawStatItem("实时光源 (Realtime Lights)", currentData.realtimeLightCount, GetThreshold(MetricType.RealtimeLights).warning, GetThreshold(MetricType.RealtimeLights).limit, null, "点击选中实时光源", currentData.realtimeLights); DrawStatItem("阴影投射光源 (Shadow Casters)", currentData.shadowCastingLights, GetThreshold(MetricType.ShadowLights).warning, GetThreshold(MetricType.ShadowLights).limit, null, "点击选中阴影光源", currentData.shadowLights); DrawStatItem("总光源数量 (All Lights)", currentData.totalLightCount, GetThreshold(MetricType.TotalLights).warning, GetThreshold(MetricType.TotalLights).limit); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("是否开启 Fog", GUILayout.Width(200)); EditorGUILayout.LabelField(currentData.hasFog ? "开启 (可能有性能消耗)" : "关闭", currentData.hasFog ? resultWarnStyle : resultGoodStyle); EditorGUILayout.EndHorizontal(); DrawStatItem("半透明物体 (Overdraw风险)", currentData.transparentObjectCount, GetThreshold(MetricType.TransparentObjects).warning, GetThreshold(MetricType.TransparentObjects).limit, null, "点击选中半透明物体", currentData.transparentObjects); }); DrawSection("动画与特效 (Animation & FX)", () => { DrawStatItem("蒙皮网格 (Skinned Mesh)", currentData.skinnedMeshCount, GetThreshold(MetricType.SkinnedMesh).warning, GetThreshold(MetricType.SkinnedMesh).limit, null, "点击选中蒙皮网格", currentData.skinnedMeshes); DrawStatItem("粒子系统 (Particle Systems)", currentData.particleSystemCount, GetThreshold(MetricType.ParticleSystems).warning, GetThreshold(MetricType.ParticleSystems).limit, null, "点击选中粒子系统", currentData.particleSystems); }); DrawSection("物理 (Physics)", () => { DrawStatItem("复杂 MeshCollider", currentData.complexMeshColliderCount, 0, 10, null, "点击选中复杂碰撞体", currentData.complexMeshColliders); }); DrawSection("内存占用 (估算)", () => { DrawStatItem("纹理内存 (Texture)", currentData.textureMemory, GetThreshold(MetricType.TextureMem).warning, GetThreshold(MetricType.TextureMem).limit, FormatBytes, "查看 Top 10 纹理", null, () => ShowTopResourcesWindow("Top 10 Textures", currentData.topTextures)); DrawStatItem("网格内存 (Mesh)", currentData.meshMemory, GetThreshold(MetricType.MeshMem).warning, GetThreshold(MetricType.MeshMem).limit, FormatBytes, "查看 Top 10 网格", null, () => ShowTopResourcesWindow("Top 10 Meshes", currentData.topMeshes)); DrawStatItem("音频内存 (Audio)", currentData.audioMemory, GetThreshold(MetricType.AudioMem).warning, GetThreshold(MetricType.AudioMem).limit, FormatBytes); }); DrawSection("资源详情", () => { DrawStatItem("独立纹理数量", currentData.uniqueTextureCount, 1000, 2000); DrawStatItem("独立材质数量", currentData.uniqueMaterialCount, 200, 500); DrawStatItem("独立网格数量", currentData.uniqueMeshCount, 500, 1000); }); EditorGUILayout.EndScrollView(); } private void AnalyzeScene() { EditorUtility.DisplayProgressBar("场景分析中", "正在收集场景对象...", 0.1f); try { currentData = new AnalysisData(); var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects(); var allRenderers = new List(); var allColliders = new List(); // 递归收集所有对象 int totalRoots = rootObjects.Length; for (int i = 0; i < totalRoots; i++) { var root = rootObjects[i]; EditorUtility.DisplayProgressBar("场景分析中", $"正在分析对象: {root.name}", 0.1f + (float)i / totalRoots * 0.4f); allRenderers.AddRange(root.GetComponentsInChildren(true)); var colliders = root.GetComponentsInChildren(true); allColliders.AddRange(colliders); var transforms = root.GetComponentsInChildren(true); currentData.objectCount += transforms.Length; // 检查 Static Batching 和 Missing Scripts foreach (var t in transforms) { if (!t.gameObject.isStatic) { currentData.nonStaticCount++; currentData.nonStaticObjects.Add(t.gameObject); } // 检查 Missing Scripts var components = t.GetComponents(); foreach (var c in components) { if (c == null) { currentData.missingScriptCount++; currentData.missingScriptObjects.Add(t.gameObject); break; } } } // 检查复杂 MeshCollider foreach(var col in colliders) { if(col is MeshCollider mc && !mc.convex) { currentData.complexMeshColliderCount++; currentData.complexMeshColliders.Add(col.gameObject); } } } EditorUtility.DisplayProgressBar("场景分析中", "正在分析网格与材质...", 0.6f); // 分析网格 HashSet uniqueMeshes = new HashSet(); List meshInfos = new List(); foreach (var renderer in allRenderers) { Mesh mesh = null; if (renderer is MeshRenderer) { var filter = renderer.GetComponent(); if (filter != null) mesh = filter.sharedMesh; } else if (renderer is SkinnedMeshRenderer smr) { mesh = smr.sharedMesh; currentData.skinnedMeshCount++; currentData.skinnedMeshes.Add(smr.gameObject); } if (mesh != null) { currentData.totalVertices += mesh.vertexCount; currentData.totalTriangles += mesh.triangles.Length / 3; if (uniqueMeshes.Add(mesh)) { long mem = Profiler.GetRuntimeMemorySizeLong(mesh); currentData.meshMemory += mem; meshInfos.Add(new ResourceInfo { name = mesh.name, size = mem, obj = mesh }); } } // 检查半透明物体 foreach(var mat in renderer.sharedMaterials) { if (mat != null && (mat.renderQueue >= 2500 || mat.FindPass("Transparent") >= 0 || mat.GetTag("RenderType", false) == "Transparent")) { currentData.transparentObjectCount++; currentData.transparentObjects.Add(renderer.gameObject); break; } } } currentData.uniqueMeshCount = uniqueMeshes.Count; currentData.topMeshes = meshInfos.OrderByDescending(x => x.size).Take(10).ToList(); // 分析材质和纹理 HashSet uniqueMaterials = new HashSet(); HashSet uniqueTextures = new HashSet(); List textureInfos = new List(); foreach (var renderer in allRenderers) { foreach (var mat in renderer.sharedMaterials) { if (mat != null && uniqueMaterials.Add(mat)) { var shader = mat.shader; int propertyCount = ShaderUtil.GetPropertyCount(shader); for (int i = 0; i < propertyCount; i++) { if (ShaderUtil.GetPropertyType(shader, i) == ShaderUtil.ShaderPropertyType.TexEnv) { string propertyName = ShaderUtil.GetPropertyName(shader, i); Texture tex = mat.GetTexture(propertyName); if (tex != null) { uniqueTextures.Add(tex); } } } } } } currentData.uniqueMaterialCount = uniqueMaterials.Count; currentData.uniqueTextureCount = uniqueTextures.Count; foreach (var tex in uniqueTextures) { long mem = Profiler.GetRuntimeMemorySizeLong(tex); currentData.textureMemory += mem; textureInfos.Add(new ResourceInfo { name = tex.name, size = mem, obj = tex }); } currentData.topTextures = textureInfos.OrderByDescending(x => x.size).Take(10).ToList(); EditorUtility.DisplayProgressBar("场景分析中", "正在分析音频与灯光...", 0.8f); // 分析音频 var audioSources = Resources.FindObjectsOfTypeAll(); HashSet uniqueClips = new HashSet(); foreach(var root in rootObjects) { var sources = root.GetComponentsInChildren(true); foreach(var source in sources) { if(source.clip != null) uniqueClips.Add(source.clip); } } foreach(var clip in uniqueClips) { currentData.audioMemory += Profiler.GetRuntimeMemorySizeLong(clip); } // 分析灯光 var lights = Object.FindObjectsOfType(); currentData.totalLightCount = lights.Length; foreach (var light in lights) { if (light.lightmapBakeType == LightmapBakeType.Realtime || light.lightmapBakeType == LightmapBakeType.Mixed) { currentData.realtimeLightCount++; currentData.realtimeLights.Add(light.gameObject); } if (light.shadows != LightShadows.None) { currentData.shadowCastingLights++; currentData.shadowLights.Add(light.gameObject); } } // 分析粒子系统 var particles = Object.FindObjectsOfType(); currentData.particleSystemCount = particles.Length; foreach(var ps in particles) { currentData.particleSystems.Add(ps.gameObject); } // 分析 RenderSettings currentData.hasFog = RenderSettings.fog; hasAnalyzed = true; } finally { EditorUtility.ClearProgressBar(); } } private void ShowTopResourcesWindow(string title, List resources) { TopResourcesWindow.Show(title, resources); } private void DrawSection(string title, System.Action content) { EditorGUILayout.BeginVertical(boxStyle); EditorGUILayout.LabelField(title, headerStyle); EditorGUILayout.Space(5); content(); EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } private void DrawStatItem(string label, long value, long warningThreshold, long limitThreshold, System.Func formatter = null, string selectButtonText = null, List objectsToSelect = null, System.Action onDetailClick = null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(label, GUILayout.Width(200)); string displayValue = formatter != null ? formatter(value) : value.ToString(); string suggestion = ""; GUIStyle style = resultGoodStyle; if (value > limitThreshold) { style = resultBadStyle; suggestion = $"(严重超标! 建议 < {formatter?.Invoke(limitThreshold) ?? limitThreshold.ToString()})"; } else if (value > warningThreshold) { style = resultWarnStyle; suggestion = $"(偏高, 建议 < {formatter?.Invoke(warningThreshold) ?? warningThreshold.ToString()})"; } EditorGUILayout.LabelField(displayValue, style, GUILayout.Width(100)); // 按钮逻辑 if (onDetailClick != null) { if (GUILayout.Button(selectButtonText ?? "Detail", GUILayout.Width(120))) { onDetailClick.Invoke(); } } else if (!string.IsNullOrEmpty(selectButtonText) && objectsToSelect != null && objectsToSelect.Count > 0) { if (GUILayout.Button("Select", GUILayout.Width(60))) { Selection.objects = objectsToSelect.ToArray(); EditorGUIUtility.PingObject(objectsToSelect[0]); } } if (!string.IsNullOrEmpty(suggestion)) { EditorGUILayout.LabelField(suggestion, labelStyle); } EditorGUILayout.EndHorizontal(); } private string FormatNumber(long num) { if (num > 1000000) return (num / 1000000f).ToString("F1") + "M"; if (num > 1000) return (num / 1000f).ToString("F1") + "K"; return num.ToString(); } private string FormatBytes(long bytes) { if (bytes > 1024 * 1024 * 1024) return (bytes / (1024f * 1024f * 1024f)).ToString("F2") + " GB"; if (bytes > 1024 * 1024) return (bytes / (1024f * 1024f)).ToString("F1") + " MB"; if (bytes > 1024) return (bytes / 1024f).ToString("F1") + " KB"; return bytes + " B"; } private (long warning, long limit) GetThreshold(MetricType type) { bool is12G = currentTarget == TargetDevice.Android_12G; switch (type) { case MetricType.ObjectCount: return is12G ? (10000, 15000) : (5000, 10000); case MetricType.Vertices: return is12G ? (3000000, 5000000) : (1500000, 3000000); case MetricType.Triangles: return is12G ? (3000000, 5000000) : (1500000, 3000000); case MetricType.TextureMem: return is12G ? (1536L * 1024 * 1024, 2560L * 1024 * 1024) : (1024L * 1024 * 1024, 1536L * 1024 * 1024); // 1.5G/2.5G vs 1G/1.5G case MetricType.MeshMem: return is12G ? (512L * 1024 * 1024, 800L * 1024 * 1024) : (256L * 1024 * 1024, 512L * 1024 * 1024); case MetricType.AudioMem: return is12G ? (100L * 1024 * 1024, 200L * 1024 * 1024) : (50L * 1024 * 1024, 100L * 1024 * 1024); case MetricType.RealtimeLights: return is12G ? (3, 5) : (1, 3); case MetricType.ShadowLights: return is12G ? (1, 2) : (1, 1); case MetricType.TotalLights: return is12G ? (20, 50) : (10, 20); // 包含烘焙灯光 case MetricType.SkinnedMesh: return is12G ? (50, 100) : (30, 50); case MetricType.ParticleSystems: return is12G ? (50, 100) : (30, 50); case MetricType.TransparentObjects: return is12G ? (500, 1000) : (300, 500); // 预估数量,非严格标准 default: return (0, 0); } } public class ResourceInfo { public string name; public long size; public Object obj; } private class AnalysisData { public int objectCount; public long totalVertices; public long totalTriangles; public long textureMemory; public long meshMemory; public long audioMemory; public int uniqueTextureCount; public int uniqueMaterialCount; public int uniqueMeshCount; public int realtimeLightCount; public int shadowCastingLights; public int totalLightCount; public bool hasFog; public int skinnedMeshCount; public int particleSystemCount; public int transparentObjectCount; public int nonStaticCount; // 新增:Missing Script 和 MeshCollider 统计 public int missingScriptCount; public int complexMeshColliderCount; public List realtimeLights = new List(); public List shadowLights = new List(); public List skinnedMeshes = new List(); public List particleSystems = new List(); public List transparentObjects = new List(); public List nonStaticObjects = new List(); public List missingScriptObjects = new List(); public List complexMeshColliders = new List(); // Top Resources public List topTextures = new List(); public List topMeshes = new List(); } // 独立的窗口类用于显示详细列表 public class TopResourcesWindow : EditorWindow { private List resources; private Vector2 scroll; public static void Show(string title, List resources) { var win = GetWindow(true, title, true); win.resources = resources; win.minSize = new Vector2(400, 300); win.Show(); } private void OnGUI() { if (resources == null) return; scroll = EditorGUILayout.BeginScrollView(scroll); EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField($"资源名称", "大小", EditorStyles.boldLabel); EditorGUILayout.Space(); foreach (var res in resources) { EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); EditorGUILayout.LabelField(res.name, GUILayout.Width(250)); EditorGUILayout.LabelField(EditorUtility.FormatBytes(res.size), GUILayout.Width(80)); if (GUILayout.Button("Select", GUILayout.Width(60))) { Selection.activeObject = res.obj; EditorGUIUtility.PingObject(res.obj); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } } private enum MetricType { ObjectCount, Vertices, Triangles, TextureMem, MeshMem, AudioMem, RealtimeLights, ShadowLights, TotalLights, SkinnedMesh, ParticleSystems, TransparentObjects } } }