MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/ParametersModule.cs
zhang hongbo cf352f2a22 11
2026-04-06 10:03:28 +08:00

979 lines
43 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Profiling;
using TMPro;
using UnityEngine.UI;
namespace MeowmentDebugTool
{
/// <summary>
/// 参数查看模块 - 仿 GameFramework Debugger 风格,带多级子标签页
/// 一级标签: Information / Profiler / Language
/// Information 子标签: System / Environment / Screen / Input / Other / Quality
/// Profiler 子标签: Summary / Memory
/// Language 标签: 中英文切换
/// </summary>
public class ParametersModule : IDebugModule
{
#region
private GameObject parametersPage;
// UI 根容器(由外部传入或内部构建)
private RectTransform rootContainer;
// ---- 一级标签 ----
private readonly List<Button> primaryTabButtons = new List<Button>();
private readonly List<GameObject> primaryPages = new List<GameObject>();
private int activePrimaryIndex = 0;
// ---- Information 二级标签 ----
private readonly List<Button> infoSubTabButtons = new List<Button>();
private readonly List<GameObject> infoSubPages = new List<GameObject>();
private int activeInfoSubIndex = 0;
// ---- Profiler 二级标签 ----
private readonly List<Button> profilerSubTabButtons = new List<Button>();
private readonly List<GameObject> profilerSubPages = new List<GameObject>();
private int activeProfilerSubIndex = 0;
// ---- Language ----
private bool showChinese = false;
// 用于存储所有 info 文本控件,以便切换语言时刷新
private readonly List<TMP_Text> allInfoTexts = new List<TMP_Text>();
// 标签页名称映射 (英文, 中文)
private static readonly string[][] PrimaryTabNames = {
new[] { "Information", "信息" },
new[] { "Profiler", "性能分析" },
new[] { "Language", "语言" }
};
private static readonly string[][] InfoSubTabNames = {
new[] { "System", "系统" },
new[] { "Environment", "环境" },
new[] { "Screen", "屏幕" },
new[] { "Input", "输入" },
new[] { "Other", "其他" },
new[] { "Quality", "画质" }
};
private static readonly string[][] ProfilerSubTabNames = {
new[] { "Summary", "概览" },
new[] { "Memory", "内存" }
};
// 颜色
private static readonly Color ActiveTabColor = new Color(0.2f, 0.6f, 0.2f, 1f);
private static readonly Color InactiveTabColor = new Color(0.25f, 0.25f, 0.25f, 1f);
private static readonly Color SubActiveTabColor = new Color(0.25f, 0.5f, 0.7f, 1f);
private static readonly Color SubInactiveTabColor = new Color(0.2f, 0.2f, 0.2f, 1f);
// Profiler 刷新计数
private float lastProfilerRefreshTime;
private const float PROFILER_REFRESH_INTERVAL = 1f;
// 各 info 文本引用
private TMP_Text systemInfoText;
private TMP_Text environmentInfoText;
private TMP_Text screenInfoText;
private TMP_Text inputInfoText;
private TMP_Text otherInfoText;
private TMP_Text qualityInfoText;
private TMP_Text profilerSummaryText;
private TMP_Text profilerMemoryText;
#endregion
#region
public ParametersModule(GameObject page)
{
parametersPage = page;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[ParametersModule] 初始化参数查看模块...");
BuildUI();
RefreshAllInfo();
}
public GameObject GetPage()
{
return parametersPage;
}
public string GetModuleName()
{
return "参数";
}
/// <summary>
/// 每帧更新(由外部调用),用于刷新 Profiler 实时数据
/// </summary>
public void Update()
{
if (activePrimaryIndex == 1 && Time.unscaledTime - lastProfilerRefreshTime > PROFILER_REFRESH_INTERVAL)
{
lastProfilerRefreshTime = Time.unscaledTime;
RefreshProfilerSummary();
RefreshProfilerMemory();
}
}
#endregion
#region
public void RefreshAllInfo()
{
RefreshSystemInfo();
RefreshEnvironmentInfo();
RefreshScreenInfo();
RefreshInputInfo();
RefreshOtherInfo();
RefreshQualityInfo();
RefreshProfilerSummary();
RefreshProfilerMemory();
}
public void CopyAllInfoToClipboard()
{
StringBuilder sb = new StringBuilder();
foreach (var t in allInfoTexts)
{
if (t != null && !string.IsNullOrEmpty(t.text))
{
sb.AppendLine(t.text);
sb.AppendLine();
}
}
if (sb.Length > 0)
{
GUIUtility.systemCopyBuffer = sb.ToString();
Debug.Log("[ParametersModule] 所有信息已复制到剪贴板");
}
}
#endregion
#region UI
private void BuildUI()
{
if (parametersPage == null) return;
// 清空已有子对象
for (int i = parametersPage.transform.childCount - 1; i >= 0; i--)
{
UnityEngine.Object.Destroy(parametersPage.transform.GetChild(i).gameObject);
}
// 根垂直布局
VerticalLayoutGroup rootLayout = parametersPage.GetComponent<VerticalLayoutGroup>();
if (rootLayout == null) rootLayout = parametersPage.AddComponent<VerticalLayoutGroup>();
rootLayout.spacing = 0;
rootLayout.padding = new RectOffset(0, 0, 0, 0);
rootLayout.childControlWidth = true;
rootLayout.childControlHeight = true;
rootLayout.childForceExpandWidth = true;
rootLayout.childForceExpandHeight = false;
// ===== 一级标签栏 =====
GameObject primaryTabBar = CreateTabBar(parametersPage.transform, "PrimaryTabBar", 90);
for (int i = 0; i < PrimaryTabNames.Length; i++)
{
int idx = i;
Button btn = CreateTabButton(primaryTabBar.transform, GetTabLabel(PrimaryTabNames[i]));
btn.onClick.AddListener(() => SwitchPrimaryTab(idx));
primaryTabButtons.Add(btn);
}
// ===== 一级页面容器 =====
// --- Information Page ---
GameObject infoPage = CreatePageContainer(parametersPage.transform, "InformationPage");
primaryPages.Add(infoPage);
BuildInformationPage(infoPage);
// --- Profiler Page ---
GameObject profilerPage = CreatePageContainer(parametersPage.transform, "ProfilerPage");
primaryPages.Add(profilerPage);
BuildProfilerPage(profilerPage);
// --- Language Page ---
GameObject languagePage = CreatePageContainer(parametersPage.transform, "LanguagePage");
primaryPages.Add(languagePage);
BuildLanguagePage(languagePage);
// 操作栏
GameObject actionBar = CreateActionBar(parametersPage.transform);
// 默认选中 Information
SwitchPrimaryTab(0);
}
private void BuildInformationPage(GameObject parent)
{
VerticalLayoutGroup vl = parent.AddComponent<VerticalLayoutGroup>();
vl.spacing = 0;
vl.padding = new RectOffset(0, 0, 0, 0);
vl.childControlWidth = true;
vl.childControlHeight = true;
vl.childForceExpandWidth = true;
vl.childForceExpandHeight = false;
// 二级标签栏
GameObject subTabBar = CreateTabBar(parent.transform, "InfoSubTabBar", 80);
for (int i = 0; i < InfoSubTabNames.Length; i++)
{
int idx = i;
Button btn = CreateTabButton(subTabBar.transform, GetTabLabel(InfoSubTabNames[i]), 26);
btn.onClick.AddListener(() => SwitchInfoSubTab(idx));
infoSubTabButtons.Add(btn);
}
// 子页面
systemInfoText = CreateScrollInfoPage(parent.transform, "SystemPage", out GameObject sysPage);
infoSubPages.Add(sysPage);
environmentInfoText = CreateScrollInfoPage(parent.transform, "EnvironmentPage", out GameObject envPage);
infoSubPages.Add(envPage);
screenInfoText = CreateScrollInfoPage(parent.transform, "ScreenPage", out GameObject scrPage);
infoSubPages.Add(scrPage);
inputInfoText = CreateScrollInfoPage(parent.transform, "InputPage", out GameObject inputPage);
infoSubPages.Add(inputPage);
otherInfoText = CreateScrollInfoPage(parent.transform, "OtherPage", out GameObject otherPage);
infoSubPages.Add(otherPage);
qualityInfoText = CreateScrollInfoPage(parent.transform, "QualityPage", out GameObject qualPage);
infoSubPages.Add(qualPage);
allInfoTexts.AddRange(new[] { systemInfoText, environmentInfoText, screenInfoText, inputInfoText, otherInfoText, qualityInfoText });
SwitchInfoSubTab(0);
}
private void BuildProfilerPage(GameObject parent)
{
VerticalLayoutGroup vl = parent.AddComponent<VerticalLayoutGroup>();
vl.spacing = 0;
vl.padding = new RectOffset(0, 0, 0, 0);
vl.childControlWidth = true;
vl.childControlHeight = true;
vl.childForceExpandWidth = true;
vl.childForceExpandHeight = false;
// 二级标签栏
GameObject subTabBar = CreateTabBar(parent.transform, "ProfilerSubTabBar", 80);
for (int i = 0; i < ProfilerSubTabNames.Length; i++)
{
int idx = i;
Button btn = CreateTabButton(subTabBar.transform, GetTabLabel(ProfilerSubTabNames[i]), 26);
btn.onClick.AddListener(() => SwitchProfilerSubTab(idx));
profilerSubTabButtons.Add(btn);
}
profilerSummaryText = CreateScrollInfoPage(parent.transform, "SummaryPage", out GameObject summaryPage);
profilerSubPages.Add(summaryPage);
// 添加嵌套 Canvas 隔离频繁文本刷新,避免整棵 UI 树 rebatch
summaryPage.AddComponent<Canvas>();
summaryPage.AddComponent<GraphicRaycaster>();
profilerMemoryText = CreateScrollInfoPage(parent.transform, "MemoryPage", out GameObject memoryPage);
profilerSubPages.Add(memoryPage);
// 同上:隔离 Profiler Memory 页的文本刷新
memoryPage.AddComponent<Canvas>();
memoryPage.AddComponent<GraphicRaycaster>();
allInfoTexts.Add(profilerSummaryText);
allInfoTexts.Add(profilerMemoryText);
SwitchProfilerSubTab(0);
}
private void BuildLanguagePage(GameObject parent)
{
VerticalLayoutGroup vl = parent.AddComponent<VerticalLayoutGroup>();
vl.spacing = 20;
vl.padding = new RectOffset(40, 40, 40, 40);
vl.childControlWidth = true;
vl.childControlHeight = false;
vl.childForceExpandWidth = true;
// 标题
GameObject title = CreateLabel(parent.transform, "LanguageTitle", "Language / 语言设置", 34);
title.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
LayoutElement titleLE = title.AddComponent<LayoutElement>();
titleLE.preferredHeight = 60;
// 中文开关
GameObject chnToggleObj = CreateLanguageToggle(parent.transform, "ChineseToggle",
"显示中文 / Show Chinese", showChinese, (val) =>
{
showChinese = val;
RefreshTabLabels();
RefreshAllInfo();
});
// 说明文字
GameObject desc = CreateLabel(parent.transform, "LanguageDesc",
"开启后信息页面将同时显示英文原文和中文翻译。\nWhen enabled, info pages show both English and Chinese translations.", 22);
desc.GetComponent<TextMeshProUGUI>().color = new Color(0.7f, 0.7f, 0.7f, 1f);
desc.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 100);
LayoutElement descLE = desc.AddComponent<LayoutElement>();
descLE.preferredHeight = 100;
}
#endregion
#region
private void SwitchPrimaryTab(int index)
{
activePrimaryIndex = index;
for (int i = 0; i < primaryPages.Count; i++)
{
primaryPages[i].SetActive(i == index);
}
for (int i = 0; i < primaryTabButtons.Count; i++)
{
primaryTabButtons[i].GetComponent<Image>().color = i == index ? ActiveTabColor : InactiveTabColor;
}
if (index == 0) RefreshCurrentInfoSubTab();
if (index == 1)
{
lastProfilerRefreshTime = 0;
RefreshProfilerSummary();
RefreshProfilerMemory();
}
}
private void SwitchInfoSubTab(int index)
{
activeInfoSubIndex = index;
for (int i = 0; i < infoSubPages.Count; i++)
{
infoSubPages[i].SetActive(i == index);
}
for (int i = 0; i < infoSubTabButtons.Count; i++)
{
infoSubTabButtons[i].GetComponent<Image>().color = i == index ? SubActiveTabColor : SubInactiveTabColor;
}
RefreshCurrentInfoSubTab();
}
private void SwitchProfilerSubTab(int index)
{
activeProfilerSubIndex = index;
for (int i = 0; i < profilerSubPages.Count; i++)
{
profilerSubPages[i].SetActive(i == index);
}
for (int i = 0; i < profilerSubTabButtons.Count; i++)
{
profilerSubTabButtons[i].GetComponent<Image>().color = i == index ? SubActiveTabColor : SubInactiveTabColor;
}
}
private void RefreshCurrentInfoSubTab()
{
switch (activeInfoSubIndex)
{
case 0: RefreshSystemInfo(); break;
case 1: RefreshEnvironmentInfo(); break;
case 2: RefreshScreenInfo(); break;
case 3: RefreshInputInfo(); break;
case 4: RefreshOtherInfo(); break;
case 5: RefreshQualityInfo(); break;
}
}
private void RefreshTabLabels()
{
for (int i = 0; i < primaryTabButtons.Count && i < PrimaryTabNames.Length; i++)
{
TMP_Text txt = primaryTabButtons[i].GetComponentInChildren<TMP_Text>();
if (txt != null)
txt.text = GetTabLabel(PrimaryTabNames[i]);
}
for (int i = 0; i < infoSubTabButtons.Count && i < InfoSubTabNames.Length; i++)
{
TMP_Text txt = infoSubTabButtons[i].GetComponentInChildren<TMP_Text>();
if (txt != null)
txt.text = GetTabLabel(InfoSubTabNames[i]);
}
for (int i = 0; i < profilerSubTabButtons.Count && i < ProfilerSubTabNames.Length; i++)
{
TMP_Text txt = profilerSubTabButtons[i].GetComponentInChildren<TMP_Text>();
if (txt != null)
txt.text = GetTabLabel(ProfilerSubTabNames[i]);
}
}
/// <summary>
/// 返回标签显示文字。中文模式: "English/中文",英文模式: "English"
/// </summary>
private string GetTabLabel(string[] pair)
{
return showChinese ? $"{pair[0]}/{pair[1]}" : pair[0];
}
#endregion
#region - Information
private void RefreshSystemInfo()
{
if (systemInfoText == null) return;
StringBuilder sb = new StringBuilder();
sb.AppendLine(L("Device Name / 设备名称", SystemInfo.deviceName));
sb.AppendLine(L("Device Model / 设备型号", SystemInfo.deviceModel));
sb.AppendLine(L("Device Type / 设备类型", SystemInfo.deviceType.ToString()));
sb.AppendLine(L("Device Unique Identifier / 设备唯一标识", SystemInfo.deviceUniqueIdentifier));
sb.AppendLine();
sb.AppendLine(L("Operating System / 操作系统", SystemInfo.operatingSystem));
sb.AppendLine(L("Operating System Family / 操作系统系列", SystemInfo.operatingSystemFamily.ToString()));
sb.AppendLine(L("Processor Type / 处理器类型", SystemInfo.processorType));
sb.AppendLine(L("Processor Count / 处理器核心数", SystemInfo.processorCount.ToString()));
sb.AppendLine(L("Processor Frequency / 处理器频率", $"{SystemInfo.processorFrequency} MHz"));
sb.AppendLine(L("System Memory Size / 系统内存大小", $"{SystemInfo.systemMemorySize} MB"));
sb.AppendLine();
sb.AppendLine(L("Graphics Device Name / 显卡名称", SystemInfo.graphicsDeviceName));
sb.AppendLine(L("Graphics Device Vendor / 显卡厂商", SystemInfo.graphicsDeviceVendor));
sb.AppendLine(L("Graphics Device Type / 图形API类型", SystemInfo.graphicsDeviceType.ToString()));
sb.AppendLine(L("Graphics Device Version / 图形API版本", SystemInfo.graphicsDeviceVersion));
sb.AppendLine(L("Graphics Memory Size / 显存大小", $"{SystemInfo.graphicsMemorySize} MB"));
sb.AppendLine(L("Graphics Shader Level / 着色器等级", SystemInfo.graphicsShaderLevel.ToString()));
sb.AppendLine(L("Graphics Multi Threaded / 多线程渲染", SystemInfo.graphicsMultiThreaded.ToString()));
sb.AppendLine(L("Max Texture Size / 最大纹理尺寸", SystemInfo.maxTextureSize.ToString()));
systemInfoText.text = sb.ToString();
}
private void RefreshEnvironmentInfo()
{
if (environmentInfoText == null) return;
StringBuilder sb = new StringBuilder();
sb.AppendLine(L("Unity Version / Unity版本", Application.unityVersion));
sb.AppendLine(L("Platform / 运行平台", Application.platform.ToString()));
sb.AppendLine(L("System Language / 系统语言", Application.systemLanguage.ToString()));
sb.AppendLine(L("Internet Reachability / 网络可达性", Application.internetReachability.ToString()));
sb.AppendLine(L("Run In Background / 允许后台运行", Application.runInBackground.ToString()));
sb.AppendLine(L("Install Mode / 安装模式", Application.installMode.ToString()));
sb.AppendLine();
sb.AppendLine(L("Product Name / 产品名称", Application.productName));
sb.AppendLine(L("Company Name / 公司名称", Application.companyName));
sb.AppendLine(L("Identifier / 应用标识", Application.identifier));
sb.AppendLine(L("Version / 版本号", Application.version));
sb.AppendLine();
sb.AppendLine(L("Data Path / 数据路径", Application.dataPath));
sb.AppendLine(L("Persistent Data Path / 持久化数据路径", Application.persistentDataPath));
sb.AppendLine(L("Temporary Cache Path / 临时缓存路径", Application.temporaryCachePath));
sb.AppendLine(L("Streaming Assets Path / 流式资源路径", Application.streamingAssetsPath));
environmentInfoText.text = sb.ToString();
}
private void RefreshScreenInfo()
{
if (screenInfoText == null) return;
StringBuilder sb = new StringBuilder();
sb.AppendLine(L("Screen Width / 屏幕宽度", Screen.width.ToString()));
sb.AppendLine(L("Screen Height / 屏幕高度", Screen.height.ToString()));
sb.AppendLine(L("DPI / 屏幕DPI", Screen.dpi.ToString("F1")));
sb.AppendLine(L("Full Screen / 是否全屏", Screen.fullScreen.ToString()));
sb.AppendLine(L("Full Screen Mode / 全屏模式", Screen.fullScreenMode.ToString()));
sb.AppendLine(L("Orientation / 屏幕方向", Screen.orientation.ToString()));
sb.AppendLine(L("Sleep Timeout / 休眠超时", Screen.sleepTimeout.ToString()));
sb.AppendLine(L("Brightness / 屏幕亮度", Screen.brightness.ToString("F2")));
sb.AppendLine();
sb.AppendLine(L("Current Resolution / 当前分辨率", $"{Screen.currentResolution.width} x {Screen.currentResolution.height}"));
sb.AppendLine(L("Refresh Rate / 刷新率", $"{Screen.currentResolution.refreshRateRatio.value:F2} Hz"));
sb.AppendLine();
Rect safeArea = Screen.safeArea;
sb.AppendLine(L("Safe Area / 安全区域", $"x:{safeArea.x:F0} y:{safeArea.y:F0} w:{safeArea.width:F0} h:{safeArea.height:F0}"));
screenInfoText.text = sb.ToString();
}
private void RefreshInputInfo()
{
if (inputInfoText == null) return;
StringBuilder sb = new StringBuilder();
sb.AppendLine(L("Supports Accelerometer / 支持加速度计", SystemInfo.supportsAccelerometer.ToString()));
sb.AppendLine(L("Supports Gyroscope / 支持陀螺仪", SystemInfo.supportsGyroscope.ToString()));
sb.AppendLine(L("Supports Location Service / 支持定位服务", SystemInfo.supportsLocationService.ToString()));
sb.AppendLine(L("Supports Vibration / 支持振动", SystemInfo.supportsVibration.ToString()));
sb.AppendLine(L("Supports Audio / 支持音频", SystemInfo.supportsAudio.ToString()));
sb.AppendLine();
sb.AppendLine(L("Device Supports Multi Touch / 支持多点触控", Input.multiTouchEnabled.ToString()));
sb.AppendLine(L("Touch Supported / 触屏支持", Input.touchSupported.ToString()));
sb.AppendLine(L("Touch Count / 当前触摸点数", Input.touchCount.ToString()));
sb.AppendLine(L("Mouse Present / 鼠标可用", Input.mousePresent.ToString()));
sb.AppendLine();
// 当前触摸信息
if (Input.touchCount > 0)
{
sb.AppendLine(L("--- Touch Details / 触摸详情 ---", ""));
for (int i = 0; i < Input.touchCount; i++)
{
Touch t = Input.GetTouch(i);
sb.AppendLine($" Touch[{i}]: pos=({t.position.x:F0},{t.position.y:F0}) phase={t.phase}");
}
}
// 加速度计
sb.AppendLine();
sb.AppendLine(L("Acceleration / 加速度", Input.acceleration.ToString("F3")));
sb.AppendLine(L("Compass Enabled / 指南针启用", Input.compass.enabled.ToString()));
inputInfoText.text = sb.ToString();
}
private void RefreshOtherInfo()
{
if (otherInfoText == null) return;
StringBuilder sb = new StringBuilder();
sb.AppendLine(L("Battery Status / 电池状态", SystemInfo.batteryStatus.ToString()));
sb.AppendLine(L("Battery Level / 电池电量",
SystemInfo.batteryLevel < 0f ? "Unknown / 未知" : $"{SystemInfo.batteryLevel * 100f:F0}%"));
sb.AppendLine();
sb.AppendLine(L("Supports Compute Shaders / 支持计算着色器", SystemInfo.supportsComputeShaders.ToString()));
sb.AppendLine(L("Supports Instancing / 支持GPU实例化", SystemInfo.supportsInstancing.ToString()));
sb.AppendLine(L("Supports 3D Textures / 支持3D纹理", SystemInfo.supports3DTextures.ToString()));
sb.AppendLine(L("Supports 3D Render Textures / 支持3D渲染纹理", SystemInfo.supports3DRenderTextures.ToString()));
sb.AppendLine(L("Supports Raw Shadow Depth Sampling / 支持阴影深度采样", SystemInfo.supportsRawShadowDepthSampling.ToString()));
sb.AppendLine(L("NPOT Support / NPOT纹理支持", SystemInfo.npotSupport.ToString()));
sb.AppendLine(L("Copy Texture Support / 纹理拷贝支持", SystemInfo.copyTextureSupport.ToString()));
sb.AppendLine(L("Supported Render Target Count / 支持渲染目标数", SystemInfo.supportedRenderTargetCount.ToString()));
sb.AppendLine(L("Supported Random Write Target Count / 支持随机写入目标数", SystemInfo.supportedRandomWriteTargetCount.ToString()));
sb.AppendLine(L("Uses Reversed ZBuffer / 使用反向ZBuffer", SystemInfo.usesReversedZBuffer.ToString()));
sb.AppendLine(L("Graphics UV Starts At Top / 图形UV起点在顶部", SystemInfo.graphicsUVStartsAtTop.ToString()));
otherInfoText.text = sb.ToString();
}
private void RefreshQualityInfo()
{
if (qualityInfoText == null) return;
StringBuilder sb = new StringBuilder();
string qualityLevel = QualitySettings.names.Length > 0
? QualitySettings.names[Mathf.Clamp(QualitySettings.GetQualityLevel(), 0, QualitySettings.names.Length - 1)]
: "Unknown";
sb.AppendLine(L("Quality Level / 画质等级", qualityLevel));
sb.AppendLine(L("Target Frame Rate / 目标帧率", Application.targetFrameRate.ToString()));
int fps = Mathf.RoundToInt(1f / Mathf.Max(0.0001f, Time.unscaledDeltaTime));
sb.AppendLine(L("Current FPS / 当前帧率", fps.ToString()));
sb.AppendLine(L("VSync Count / 垂直同步", QualitySettings.vSyncCount.ToString()));
sb.AppendLine(L("Anti Aliasing / 抗锯齿", QualitySettings.antiAliasing.ToString()));
sb.AppendLine(L("Pixel Light Count / 像素灯数量", QualitySettings.pixelLightCount.ToString()));
sb.AppendLine(L("Shadow Distance / 阴影距离", QualitySettings.shadowDistance.ToString("F1")));
sb.AppendLine(L("Shadow Resolution / 阴影分辨率", QualitySettings.shadowResolution.ToString()));
sb.AppendLine(L("Shadow Cascades / 阴影级联数", QualitySettings.shadowCascades.ToString()));
sb.AppendLine(L("LOD Bias / LOD偏移", QualitySettings.lodBias.ToString("F2")));
sb.AppendLine(L("Maximum LOD Level / 最大LOD等级", QualitySettings.maximumLODLevel.ToString()));
sb.AppendLine(L("Anisotropic Filtering / 各向异性过滤", QualitySettings.anisotropicFiltering.ToString()));
sb.AppendLine(L("Texture Quality / 贴图质量", QualitySettings.globalTextureMipmapLimit.ToString()));
qualityInfoText.text = sb.ToString();
}
#endregion
#region - Profiler
private void RefreshProfilerSummary()
{
if (profilerSummaryText == null) return;
StringBuilder sb = new StringBuilder();
int fps = Mathf.RoundToInt(1f / Mathf.Max(0.0001f, Time.unscaledDeltaTime));
sb.AppendLine(L("FPS / 帧率", fps.ToString()));
sb.AppendLine(L("Frame Time / 帧耗时", $"{Time.unscaledDeltaTime * 1000f:F2} ms"));
sb.AppendLine(L("Time Since Startup / 应用运行时间", $"{Time.realtimeSinceStartup:F1} s"));
sb.AppendLine(L("Time Scale / 时间缩放", Time.timeScale.ToString("F2")));
sb.AppendLine(L("Frame Count / 总帧数", Time.frameCount.ToString()));
sb.AppendLine();
long totalAllocated = Profiler.GetTotalAllocatedMemoryLong();
long totalReserved = Profiler.GetTotalReservedMemoryLong();
long totalUnused = Profiler.GetTotalUnusedReservedMemoryLong();
long monoHeap = Profiler.GetMonoHeapSizeLong();
long monoUsed = Profiler.GetMonoUsedSizeLong();
sb.AppendLine(L("Total Allocated Memory / 总分配内存", FormatBytes(totalAllocated)));
sb.AppendLine(L("Total Reserved Memory / 总保留内存", FormatBytes(totalReserved)));
sb.AppendLine(L("Total Unused Reserved / 未使用保留内存", FormatBytes(totalUnused)));
sb.AppendLine(L("Mono Heap Size / Mono堆大小", FormatBytes(monoHeap)));
sb.AppendLine(L("Mono Used Size / Mono已用大小", FormatBytes(monoUsed)));
sb.AppendLine();
sb.AppendLine(L("GC Collection Count (Gen0) / GC回收次数(第0代)", GC.CollectionCount(0).ToString()));
sb.AppendLine(L("GC Collection Count (Gen1) / GC回收次数(第1代)", GC.CollectionCount(1).ToString()));
sb.AppendLine(L("GC Collection Count (Gen2) / GC回收次数(第2代)", GC.CollectionCount(2).ToString()));
profilerSummaryText.text = sb.ToString();
}
private void RefreshProfilerMemory()
{
if (profilerMemoryText == null) return;
StringBuilder sb = new StringBuilder();
long totalAllocated = Profiler.GetTotalAllocatedMemoryLong();
long totalReserved = Profiler.GetTotalReservedMemoryLong();
long totalUnused = Profiler.GetTotalUnusedReservedMemoryLong();
long monoHeap = Profiler.GetMonoHeapSizeLong();
long monoUsed = Profiler.GetMonoUsedSizeLong();
long gcTotalMemory = GC.GetTotalMemory(false);
sb.AppendLine(L("=== Unity Memory / Unity内存 ===", ""));
sb.AppendLine(L("Total Allocated / 总分配", FormatBytes(totalAllocated)));
sb.AppendLine(L("Total Reserved / 总保留", FormatBytes(totalReserved)));
sb.AppendLine(L("Unused Reserved / 未用保留", FormatBytes(totalUnused)));
sb.AppendLine();
sb.AppendLine(L("=== Mono / Managed Memory / 托管内存 ===", ""));
sb.AppendLine(L("Mono Heap / Mono堆", FormatBytes(monoHeap)));
sb.AppendLine(L("Mono Used / Mono已用", FormatBytes(monoUsed)));
sb.AppendLine(L("GC Total Memory / GC总内存", FormatBytes(gcTotalMemory)));
sb.AppendLine();
sb.AppendLine(L("=== System / 系统 ===", ""));
sb.AppendLine(L("System Memory Size / 系统内存", $"{SystemInfo.systemMemorySize} MB"));
sb.AppendLine(L("Graphics Memory Size / 显存", $"{SystemInfo.graphicsMemorySize} MB"));
profilerMemoryText.text = sb.ToString();
}
#endregion
#region UI
private GameObject CreateTabBar(Transform parent, string name, float height)
{
GameObject bar = new GameObject(name);
bar.transform.SetParent(parent, false);
RectTransform rect = bar.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, height);
LayoutElement le = bar.AddComponent<LayoutElement>();
le.preferredHeight = height;
le.flexibleHeight = 0;
HorizontalLayoutGroup hl = bar.AddComponent<HorizontalLayoutGroup>();
hl.spacing = 8;
hl.padding = new RectOffset(8, 8, 4, 4);
hl.childControlWidth = true;
hl.childControlHeight = true;
hl.childForceExpandWidth = true;
hl.childForceExpandHeight = true;
Image bg = bar.AddComponent<Image>();
bg.color = new Color(0.12f, 0.12f, 0.12f, 1f);
return bar;
}
private Button CreateTabButton(Transform parent, string text, int fontSize = 30)
{
GameObject btnObj = new GameObject($"Tab_{text}");
btnObj.transform.SetParent(parent, false);
RectTransform rect = btnObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 0);
Image img = btnObj.AddComponent<Image>();
img.color = InactiveTabColor;
Button btn = btnObj.AddComponent<Button>();
btn.targetGraphic = img;
GameObject txtObj = new GameObject("Text");
txtObj.transform.SetParent(btnObj.transform, false);
RectTransform txtRect = txtObj.AddComponent<RectTransform>();
txtRect.anchorMin = Vector2.zero;
txtRect.anchorMax = Vector2.one;
txtRect.offsetMin = new Vector2(4, 2);
txtRect.offsetMax = new Vector2(-4, -2);
TextMeshProUGUI tmp = txtObj.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.enableAutoSizing = true;
tmp.fontSizeMin = 12;
tmp.fontSizeMax = 72;
tmp.enableWordWrapping = false;
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
if (font == null) font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
if (font != null) tmp.font = font;
return btn;
}
private GameObject CreatePageContainer(Transform parent, string name)
{
GameObject page = new GameObject(name);
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.sizeDelta = Vector2.zero;
LayoutElement le = page.AddComponent<LayoutElement>();
le.flexibleHeight = 1;
page.SetActive(false);
return page;
}
private TMP_Text CreateScrollInfoPage(Transform parent, string name, out GameObject pageObj)
{
pageObj = new GameObject(name);
pageObj.transform.SetParent(parent, false);
RectTransform rect = pageObj.AddComponent<RectTransform>();
rect.sizeDelta = Vector2.zero;
LayoutElement le = pageObj.AddComponent<LayoutElement>();
le.flexibleHeight = 1;
// ScrollView
ScrollRect scroll = pageObj.AddComponent<ScrollRect>();
scroll.horizontal = false;
scroll.vertical = true;
GameObject viewport = new GameObject("Viewport");
viewport.transform.SetParent(pageObj.transform, false);
RectTransform vpRect = viewport.AddComponent<RectTransform>();
vpRect.anchorMin = Vector2.zero;
vpRect.anchorMax = Vector2.one;
vpRect.sizeDelta = Vector2.zero;
viewport.AddComponent<Image>().color = new Color(0.06f, 0.06f, 0.08f, 0.6f);
viewport.AddComponent<Mask>().showMaskGraphic = true;
GameObject content = new GameObject("Content");
content.transform.SetParent(viewport.transform, false);
RectTransform contentRect = content.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentRect.sizeDelta = Vector2.zero;
ContentSizeFitter fitter = content.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
VerticalLayoutGroup vl = content.AddComponent<VerticalLayoutGroup>();
vl.padding = new RectOffset(20, 20, 16, 16);
vl.childControlWidth = true;
vl.childControlHeight = true;
vl.childForceExpandWidth = true;
vl.childForceExpandHeight = false;
scroll.viewport = vpRect;
scroll.content = contentRect;
// 文本
GameObject textObj = new GameObject("InfoText");
textObj.transform.SetParent(content.transform, false);
TextMeshProUGUI tmp = textObj.AddComponent<TextMeshProUGUI>();
tmp.text = "";
tmp.fontSize = 28;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.TopLeft;
tmp.color = new Color(0.9f, 0.92f, 0.94f, 1f);
tmp.enableWordWrapping = true;
tmp.richText = true;
ContentSizeFitter textFitter = textObj.AddComponent<ContentSizeFitter>();
textFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
if (font == null) font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
if (font != null) tmp.font = font;
pageObj.SetActive(false);
return tmp;
}
private GameObject CreateActionBar(Transform parent)
{
GameObject bar = new GameObject("ActionBar");
bar.transform.SetParent(parent, false);
RectTransform rect = bar.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 80);
LayoutElement le = bar.AddComponent<LayoutElement>();
le.preferredHeight = 80;
le.flexibleHeight = 0;
HorizontalLayoutGroup hl = bar.AddComponent<HorizontalLayoutGroup>();
hl.spacing = 12;
hl.padding = new RectOffset(16, 16, 8, 8);
hl.childControlWidth = true;
hl.childControlHeight = true;
hl.childForceExpandWidth = true;
hl.childForceExpandHeight = true;
// 刷新按钮
Button refreshBtn = CreateTabButton(bar.transform, showChinese ? "Refresh / 刷新" : "Refresh", 24);
refreshBtn.GetComponent<Image>().color = new Color(0.32f, 0.38f, 0.46f, 1f);
refreshBtn.onClick.AddListener(() => RefreshAllInfo());
// 复制全部按钮
Button copyBtn = CreateTabButton(bar.transform, showChinese ? "Copy All / 复制全部" : "Copy All", 24);
copyBtn.GetComponent<Image>().color = new Color(0.42f, 0.48f, 0.56f, 1f);
copyBtn.onClick.AddListener(() => CopyAllInfoToClipboard());
return bar;
}
private GameObject CreateLabel(Transform parent, string name, string text, int fontSize)
{
GameObject obj = new GameObject(name);
obj.transform.SetParent(parent, false);
RectTransform rect = obj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 50);
TextMeshProUGUI tmp = obj.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.fontStyle = FontStyles.Bold;
tmp.enableAutoSizing = true;
tmp.fontSizeMin = 12;
tmp.fontSizeMax = 72;
tmp.alignment = TextAlignmentOptions.Left;
tmp.color = Color.white;
tmp.enableWordWrapping = true;
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
if (font == null) font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
if (font != null) tmp.font = font;
return obj;
}
private GameObject CreateLanguageToggle(Transform parent, string name, string label, bool defaultValue, System.Action<bool> onChanged)
{
GameObject toggleObj = new GameObject(name);
toggleObj.transform.SetParent(parent, false);
RectTransform rect = toggleObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(0, 80);
LayoutElement le = toggleObj.AddComponent<LayoutElement>();
le.preferredHeight = 80;
Image bg = toggleObj.AddComponent<Image>();
bg.color = new Color(0.18f, 0.18f, 0.18f, 1f);
Toggle toggle = toggleObj.AddComponent<Toggle>();
toggle.isOn = defaultValue;
// Checkbox背景
GameObject checkBg = new GameObject("Background");
checkBg.transform.SetParent(toggleObj.transform, false);
RectTransform checkBgRect = checkBg.AddComponent<RectTransform>();
checkBgRect.anchorMin = new Vector2(0, 0.5f);
checkBgRect.anchorMax = new Vector2(0, 0.5f);
checkBgRect.pivot = new Vector2(0, 0.5f);
checkBgRect.sizeDelta = new Vector2(50, 50);
checkBgRect.anchoredPosition = new Vector2(16, 0);
Image checkBgImg = checkBg.AddComponent<Image>();
checkBgImg.color = new Color(0.8f, 0.8f, 0.8f, 1f);
// Checkmark
GameObject checkmark = new GameObject("Checkmark");
checkmark.transform.SetParent(checkBg.transform, false);
RectTransform cmRect = checkmark.AddComponent<RectTransform>();
cmRect.anchorMin = Vector2.zero;
cmRect.anchorMax = Vector2.one;
cmRect.sizeDelta = new Vector2(-8, -8);
Image cmImg = checkmark.AddComponent<Image>();
cmImg.color = new Color(0.2f, 0.8f, 0.2f, 1f);
toggle.targetGraphic = checkBgImg;
toggle.graphic = cmImg;
// Label
GameObject labelObj = new GameObject("Label");
labelObj.transform.SetParent(toggleObj.transform, false);
RectTransform labelRect = labelObj.AddComponent<RectTransform>();
labelRect.anchorMin = new Vector2(0, 0);
labelRect.anchorMax = new Vector2(1, 1);
labelRect.offsetMin = new Vector2(80, 0);
labelRect.offsetMax = new Vector2(-16, 0);
TextMeshProUGUI labelTmp = labelObj.AddComponent<TextMeshProUGUI>();
labelTmp.text = label;
labelTmp.fontSize = 28;
labelTmp.fontStyle = FontStyles.Bold;
labelTmp.enableAutoSizing = true;
labelTmp.fontSizeMin = 12;
labelTmp.fontSizeMax = 72;
labelTmp.alignment = TextAlignmentOptions.MidlineLeft;
labelTmp.color = Color.white;
TMP_FontAsset font = TMP_Settings.defaultFontAsset;
if (font == null) font = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
if (font != null) labelTmp.font = font;
toggle.onValueChanged.AddListener((val) => onChanged?.Invoke(val));
return toggleObj;
}
#endregion
#region
/// <summary>
/// 根据语言设置格式化一行信息。
/// 如果开启中文: "English / 中文: value"
/// 如果关闭中文 (纯英文): 仅显示 / 前的英文部分
/// </summary>
private string L(string bilingualKey, string value)
{
string key;
if (showChinese)
{
key = bilingualKey;
}
else
{
// 取 "/" 之前的英文部分
int slashIdx = bilingualKey.IndexOf('/');
key = slashIdx > 0 ? bilingualKey.Substring(0, slashIdx).TrimEnd() : bilingualKey;
}
if (string.IsNullOrEmpty(value))
return key;
return $"{key}: {value}";
}
private static string FormatBytes(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024f:F2} KB";
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024f * 1024f):F2} MB";
return $"{bytes / (1024f * 1024f * 1024f):F2} GB";
}
#endregion
}
}