MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/UniversalDebugTool.cs
2025-12-22 15:29:55 +08:00

782 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 通用调试工具 - 支持多标签页的调试界面系统
/// 默认分辨率: 1080x2340
/// 使用方法:
/// 1. 场景中放置 UniversalDebugTool 预制件
/// 2. 在代码中调用 UniversalDebugTool.Init() 初始化
/// 3. 未调用 Init() 前不会显示任何UI
/// </summary>
public class UniversalDebugTool : MonoBehaviour
{
#region
[Header("主窗口设置")]
[SerializeField] private RectTransform mainWindow;
[SerializeField] private Canvas canvas;
[Header("标签页系统")]
[SerializeField] private RectTransform tabButtonContainer;
[SerializeField] private RectTransform contentContainer;
[SerializeField] private GameObject tabButtonPrefab;
[SerializeField] private Button closeButton;
[SerializeField] private Color activeTabColor = Color.green;
[SerializeField] private Color inactiveTabColor = Color.gray;
[Header("悬浮按钮")]
[SerializeField] private GameObject floatingButton;
[SerializeField] private DraggableFloatingButton draggableComponent;
[Header("参数查看页面")]
[SerializeField] private GameObject parametersPage;
[SerializeField] private TMP_Text deviceInfoText;
[SerializeField] private TMP_Text systemInfoText;
[SerializeField] private ScrollRect parametersScrollRect;
[Header("自定义按钮页面")]
[SerializeField] private GameObject customButtonsPage;
[SerializeField] private RectTransform buttonContainer;
[SerializeField] private GameObject buttonPrefab;
[SerializeField] private ScrollRect buttonsScrollRect;
[Header("工具栏页面")]
[SerializeField] private GameObject toolbarPage;
[SerializeField] private Slider timeAdjustSlider;
[SerializeField] private TMP_Text timeAdjustValueText;
[SerializeField] private Button timeIncreaseButton;
[SerializeField] private Button timeDecreaseButton;
[Header("设置页面")]
[SerializeField] private GameObject settingsPage;
[SerializeField] private TMP_InputField widthInputField;
[SerializeField] private TMP_InputField heightInputField;
[SerializeField] private Button applyResolutionButton;
[SerializeField] private Button resetResolutionButton;
[SerializeField] private TMP_Text currentResolutionText;
[Header("控制台页面")]
[SerializeField] private GameObject consolePage;
[SerializeField] private ScrollRect consoleLogScrollRect;
[SerializeField] private RectTransform consoleLogContent;
[SerializeField] private ScrollRect consoleDetailScrollRect;
[SerializeField] private TMP_Text consoleDetailText;
[SerializeField] private Button consoleClearButton;
[SerializeField] private Toggle consoleLockScrollToggle;
[SerializeField] private Toggle consoleInfoFilterToggle;
[SerializeField] private Toggle consoleWarningFilterToggle;
[SerializeField] private Toggle consoleErrorFilterToggle;
[SerializeField] private Toggle consoleFatalFilterToggle;
[SerializeField] private GameObject consoleLogItemPrefab;
// [Header("输入对话框")]
// [SerializeField] private GameObject inputDialog;
// [SerializeField] private TMP_Text inputDialogTitle;
// [SerializeField] private TMP_InputField inputDialogInputField;
// [SerializeField] private Button inputDialogConfirmBtn;
// [SerializeField] private Button inputDialogCancelBtn;
#endregion
#region
private static UniversalDebugTool instance;
private static bool isInitialized = false;
private Dictionary<string, GameObject> pages = new Dictionary<string, GameObject>();
private Dictionary<string, Button> tabButtons = new Dictionary<string, Button>();
private string currentPageKey = "";
// Canvas层级设置
private const int TOP_SORT_ORDER = 30000;
// 保存的SDF字体资源
private static TMP_FontAsset savedFontAsset = null;
// 模块实例
private ParametersModule parametersModule;
private CustomButtonsModule customButtonsModule;
private ToolbarModule toolbarModule;
private SettingsModule settingsModule;
private ConsoleModule consoleModule;
private List<IDebugModule> allModules = new List<IDebugModule>();
// 暂时隐藏状态标志
private bool isHiding = false;
#endregion
#region
public static UniversalDebugTool Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<UniversalDebugTool>();
}
return instance;
}
}
public static bool InstanceExists => instance != null;
#endregion
#region Unity生命周期
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
return;
}
// 设置Canvas为最上层
SetCanvasToTop();
// 未初始化前隐藏所有UI
if (!isInitialized)
{
HideAllUI();
}
// 注意如果isInitialized为true不在Awake中调用InitializeDebugTool
// 因为此时反射设置的字段可能还没生效延迟到Start中调用
}
private void Start()
{
// 只有在已初始化但还没调用过InitializeDebugTool时才执行
// 这避免了Awake和Init()的重复调用问题
if (isInitialized && allModules.Count == 0)
{
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
InitializeDebugTool();
InitializeAllModules();
ShowMainWindow();
}
}
private void Update()
{
// 更新控制台模块
if (consoleModule != null)
{
consoleModule.Update();
}
}
private void OnDestroy()
{
if (instance == this)
{
instance = null;
// 销毁控制台模块
if (consoleModule != null)
{
consoleModule.Shutdown();
}
}
}
#endregion
#region
/// <summary>
/// 初始化调试工具自动创建UI
/// </summary>
public static void Init()
{
if (isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 已经初始化过了");
return;
}
isInitialized = true;
Debug.Log("[MeowmentDebugTool] 开始初始化调试工具...");
// 如果场景中没有实例运行时自动创建UI
if (!InstanceExists)
{
Debug.Log("[MeowmentDebugTool] 运行时自动创建UI...");
instance = RuntimeUIGenerator.CreateDebugToolUI();
}
if (InstanceExists)
{
Instance.InitializeDebugTool();
Instance.InitializeAllModules();
Instance.ShowMainWindow();
Debug.Log("[MeowmentDebugTool] 初始化完成!");
}
}
/// <summary>
/// 设置所有TextMeshProUGUI的SDF字体
/// </summary>
/// <param name="fontAsset">TMP字体资源</param>
public static void SetSDFFont(TMP_FontAsset fontAsset)
{
if (!InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法设置字体");
return;
}
if (fontAsset == null)
{
Debug.LogWarning("[MeowmentDebugTool] 字体资源为空");
return;
}
int count = 0;
// 1. 获取调试工具Canvas下的所有TextMeshProUGUI组件包括隐藏的
TextMeshProUGUI[] allTexts = Instance.GetComponentsInChildren<TextMeshProUGUI>(true);
foreach (var text in allTexts)
{
text.font = fontAsset;
count++;
}
// 2. 查找预制件模板_TempPrefabHolder下的TextMeshProUGUI
GameObject tempPrefabHolder = GameObject.Find("_TempPrefabHolder");
if (tempPrefabHolder != null)
{
TextMeshProUGUI[] prefabTexts = tempPrefabHolder.GetComponentsInChildren<TextMeshProUGUI>(true);
foreach (var text in prefabTexts)
{
text.font = fontAsset;
count++;
}
Debug.Log($"[MeowmentDebugTool] 已设置预制件模板中的 {prefabTexts.Length} 个文本组件");
}
Debug.Log($"[MeowmentDebugTool] 共将 {count} 个文本组件的字体设置为: {fontAsset.name}");
// 3. 保存字体资源,供后续创建的按钮使用
savedFontAsset = fontAsset;
Debug.Log("[MeowmentDebugTool] 字体资源已保存,后续创建的按钮将自动应用此字体");
// 4. 通过模块重新加载自定义按钮
if (Instance.customButtonsModule != null)
{
Debug.Log("[MeowmentDebugTool] 重新加载自定义按钮以应用新字体...");
Instance.customButtonsModule.SetSDFFont(fontAsset);
Instance.customButtonsModule.ReloadCustomButtons();
}
// 5. 为控制台模块应用字体
if (Instance.consoleModule != null)
{
Instance.consoleModule.SetSDFFont(fontAsset);
}
}
/// <summary>
/// 设置Canvas为最上层
/// </summary>
private void SetCanvasToTop()
{
if (canvas != null)
{
canvas.sortingOrder = TOP_SORT_ORDER;
canvas.overrideSorting = true;
Debug.Log($"[MeowmentDebugTool] Canvas层级设置为: {TOP_SORT_ORDER}");
}
}
/// <summary>
/// 隐藏所有UI
/// </summary>
private void HideAllUI()
{
if (mainWindow != null)
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(false);
}
private void InitializeDebugTool()
{
Debug.Log("🚀 初始化UniversalDebugTool...");
// 创建模块实例
parametersModule = new ParametersModule(
parametersPage, deviceInfoText, systemInfoText, parametersScrollRect);
customButtonsModule = new CustomButtonsModule(
customButtonsPage, buttonContainer, buttonPrefab, buttonsScrollRect, CloseDebugWindow);
toolbarModule = new ToolbarModule(
toolbarPage, timeAdjustSlider, timeAdjustValueText,
timeIncreaseButton, timeDecreaseButton);
settingsModule = new SettingsModule(
settingsPage, widthInputField, heightInputField,
applyResolutionButton, resetResolutionButton, currentResolutionText,
mainWindow, canvas);
consoleModule = new ConsoleModule(
consolePage, consoleLogScrollRect, consoleLogContent,
consoleDetailScrollRect, consoleDetailText,
consoleClearButton, consoleLockScrollToggle,
consoleInfoFilterToggle, consoleWarningFilterToggle,
consoleErrorFilterToggle, consoleFatalFilterToggle, consoleLogItemPrefab);
// 添加所有模块到列表
allModules.Add(consoleModule);
allModules.Add(parametersModule);
allModules.Add(customButtonsModule);
allModules.Add(toolbarModule);
allModules.Add(settingsModule);
// 注册所有页面
foreach (var module in allModules)
{
RegisterPage(module.GetModuleName(), module.GetPage());
}
Debug.Log($"📄 已注册{pages.Count}个页面");
// 创建标签按钮
CreateTabButtons();
// 设置按钮事件
if (closeButton != null)
closeButton.onClick.AddListener(CloseDebugWindow);
// 设置悬浮按钮点击事件
if (floatingButton != null)
{
Button floatBtn = floatingButton.GetComponent<Button>();
if (floatBtn != null)
{
floatBtn.onClick.AddListener(OnFloatingButtonClick);
}
}
// 默认显示第一个页面
if (pages.Count > 0)
{
ShowPage("参数");
}
Debug.Log("[OK] UniversalDebugTool初始化完成");
Debug.Log("💡 提示: 点击窗口顶部的标签切换页面或按F1-F4使用快捷键");
}
/// <summary>
/// 初始化所有模块
/// </summary>
private void InitializeAllModules()
{
Debug.Log("🔧 初始化所有模块...");
foreach (var module in allModules)
{
module.Initialize();
}
Debug.Log("[OK] 所有模块初始化完成!");
}
private void RegisterPage(string pageName, GameObject pageObject)
{
if (pageObject != null && !pages.ContainsKey(pageName))
{
pages[pageName] = pageObject;
pageObject.SetActive(false);
}
}
private void CreateTabButtons()
{
if (tabButtonPrefab == null || tabButtonContainer == null)
{
string errorDetails = $"tabButtonPrefab={(tabButtonPrefab == null ? "null" : "")}, " +
$"tabButtonContainer={(tabButtonContainer == null ? "null" : "")}";
Debug.LogWarning($"⚠️ [MeowmentDebugTool] 标签按钮预制件或容器未完全初始化:{errorDetails}");
Debug.LogWarning("💡 这可能是Unity初始化顺序问题如果标签正常显示则可以忽略此警告");
return;
}
Debug.Log($"📋 开始创建{pages.Count}个标签按钮...");
foreach (var page in pages)
{
GameObject buttonObj = Instantiate(tabButtonPrefab, tabButtonContainer);
buttonObj.name = $"Tab_{page.Key}";
Button button = buttonObj.GetComponent<Button>();
TMP_Text buttonText = buttonObj.GetComponentInChildren<TMP_Text>();
if (buttonText != null)
{
buttonText.text = page.Key;
Debug.Log($"[OK] 创建标签: [{page.Key}]");
}
else
{
Debug.LogWarning($"⚠️ 标签按钮 [{page.Key}] 缺少文本组件");
}
string pageName = page.Key; // 闭包捕获
button.onClick.AddListener(() => ShowPage(pageName));
tabButtons[page.Key] = button;
}
Debug.Log("[OK] 标签按钮创建完成!");
}
#endregion
#region
/// <summary>
/// 显示指定的页面
/// </summary>
public void ShowPage(string pageName)
{
if (!pages.ContainsKey(pageName))
{
Debug.LogWarning($"页面 '{pageName}' 不存在!");
return;
}
// 隐藏所有页面
foreach (var page in pages.Values)
{
page.SetActive(false);
}
// 显示目标页面
pages[pageName].SetActive(true);
currentPageKey = pageName;
// 更新标签按钮状态
UpdateTabButtonStates(pageName);
}
private void UpdateTabButtonStates(string activePageName)
{
foreach (var kvp in tabButtons)
{
Image buttonImage = kvp.Value.GetComponent<Image>();
if (buttonImage != null)
{
buttonImage.color = kvp.Key == activePageName ? activeTabColor : inactiveTabColor;
}
}
}
#endregion
#region ParametersModule
/// <summary>
/// 复制设备信息到剪贴板
/// </summary>
public void CopyDeviceInfoToClipboard()
{
parametersModule?.CopyDeviceInfoToClipboard();
}
/// <summary>
/// 复制系统信息到剪贴板
/// </summary>
public void CopySystemInfoToClipboard()
{
parametersModule?.CopySystemInfoToClipboard();
}
/// <summary>
/// 刷新所有信息
/// </summary>
public void RefreshAllInfo()
{
parametersModule?.RefreshAllInfo();
}
#endregion
#region CustomButtonsModule
/// <summary>
/// 设置自定义按钮回调
/// </summary>
public static void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
if (InstanceExists && Instance.customButtonsModule != null)
{
Instance.customButtonsModule.SetCustomButtonCallback(callback);
}
}
/// <summary>
/// 重新加载自定义按钮
/// </summary>
public void ReloadCustomButtons()
{
customButtonsModule?.ReloadCustomButtons();
}
#endregion
// #region 输入对话框
// /// <summary>
// /// 显示输入对话框
// /// </summary>
// public static void ShowInputDialog(string title, Action<string> onConfirmAction,
// string initialValue = "", TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)
// {
// if (!InstanceExists) return;
// var inst = Instance;
// if (inst.inputDialog == null) return;
// inst.inputDialogInputField.contentType = contentType;
// inst.inputDialog.SetActive(true);
// inst.inputDialogTitle.text = title;
// inst.inputDialogInputField.text = initialValue;
// inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
// inst.inputDialogConfirmBtn.onClick.AddListener(() =>
// {
// onConfirmAction?.Invoke(inst.inputDialogInputField.text);
// CloseInputDialog();
// });
// inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
// inst.inputDialogCancelBtn.onClick.AddListener(CloseInputDialog);
// }
// /// <summary>
// /// 关闭输入对话框
// /// </summary>
// public static void CloseInputDialog()
// {
// if (!InstanceExists) return;
// var inst = Instance;
// if (inst.inputDialog == null) return;
// inst.inputDialog.SetActive(false);
// inst.inputDialogInputField.text = "";
// inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
// inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
// }
// #endregion
#region API
/// <summary>
/// 显示调试工具
/// </summary>
public static void Show()
{
if (InstanceExists)
{
Instance.gameObject.SetActive(true);
}
}
/// <summary>
/// 隐藏调试工具
/// </summary>
public static void Hide()
{
if (InstanceExists)
{
Instance.gameObject.SetActive(false);
}
}
/// <summary>
/// 切换调试工具显示状态
/// </summary>
public static void Toggle()
{
if (InstanceExists)
{
if (Instance.mainWindow != null && Instance.mainWindow.gameObject.activeSelf)
{
Instance.CloseDebugWindow();
}
else
{
Instance.OpenDebugWindow();
}
}
}
/// <summary>
/// 关闭调试窗口,显示悬浮按钮
/// </summary>
public void CloseDebugWindow()
{
if (mainWindow != null)
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(true);
Debug.Log("[关闭] 调试窗口已关闭");
}
/// <summary>
/// 悬浮按钮点击回调(检查是否为拖动)
/// </summary>
private void OnFloatingButtonClick()
{
// 检查是否刚拖动过
if (draggableComponent != null && draggableComponent.GetWasDragging())
{
// 如果刚拖动过,不打开窗口
Debug.Log("🚫 拖动操作,不打开窗口");
return;
}
// 否则打开窗口
OpenDebugWindow();
}
/// <summary>
/// 打开调试窗口,隐藏悬浮按钮
/// </summary>
public void OpenDebugWindow()
{
if (floatingButton != null)
floatingButton.SetActive(false);
if (mainWindow != null)
mainWindow.gameObject.SetActive(true);
Debug.Log("[打开] 调试窗口已打开");
}
/// <summary>
/// 显示主窗口(初始化时调用)
/// </summary>
private void ShowMainWindow()
{
if (mainWindow != null)
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(true);
}
/// <summary>
/// 暂时隐藏调试工具UI用于截图等场景
/// </summary>
/// <param name="seconds">隐藏的秒数默认5秒</param>
public static void HideTemporarily(float seconds = 5f)
{
if (!InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法隐藏");
return;
}
if (Instance.isHiding)
{
Debug.LogWarning("[MeowmentDebugTool] 已经在隐藏状态中,请等待恢复");
return;
}
Instance.StartCoroutine(Instance.HideTemporarilyCoroutine(seconds));
}
private IEnumerator HideTemporarilyCoroutine(float seconds)
{
isHiding = true;
Debug.Log($"[MeowmentDebugTool] 开始暂时隐藏流程,时长: {seconds}秒");
// 保存Canvas初始状态
if (canvas == null)
{
Debug.LogError("[MeowmentDebugTool] Canvas为空");
isHiding = false;
yield break;
}
bool canvasWasEnabled = canvas.enabled;
Debug.Log($"[MeowmentDebugTool] Canvas初始状态: {(canvasWasEnabled ? "" : "")}");
// 1. 先关闭主窗口(如果是打开状态)
bool wasMainWindowOpen = mainWindow != null && mainWindow.gameObject.activeSelf;
Debug.Log($"[MeowmentDebugTool] 主窗口初始状态: {(wasMainWindowOpen ? "" : "")}");
if (wasMainWindowOpen)
{
CloseDebugWindow();
yield return null;
Debug.Log("[MeowmentDebugTool] 主窗口已关闭");
}
// 2. 隐藏Canvas包括浮窗
canvas.enabled = false;
Debug.Log($"[MeowmentDebugTool] Canvas已禁用UI完全隐藏");
// 3. 等待指定时间
yield return new WaitForSeconds(seconds);
// 4. 恢复Canvas
canvas.enabled = true;
Debug.Log($"[MeowmentDebugTool] Canvas已启用UI恢复显示");
// 5. 验证恢复结果
if (canvas.enabled)
{
Debug.Log("[MeowmentDebugTool] ✓ UI已成功恢复浮窗状态");
}
else
{
Debug.LogError("[MeowmentDebugTool] ✗ Canvas恢复失败");
}
isHiding = false;
Debug.Log("[MeowmentDebugTool] 暂时隐藏流程结束");
}
#endregion
}
#region
/// <summary>
/// 调试按钮特性 - 标记方法为调试按钮
///
/// 使用说明:
/// 1. 在主项目中使用此特性时,请用条件编译包裹:
///
/// #if MEOWMENT_DEBUG_TOOL
/// [DebugButton("我的调试按钮")]
/// public static void MyDebugMethod()
/// {
/// Debug.Log("调试方法被执行");
/// }
/// #endif
///
/// 2. 这样当包被卸载时,代码不会报错,也不会影响主工程的正常运行
/// 3. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugButtonAttribute : Attribute
{
public string DisplayName { get; set; }
public Color ButtonColor { get; set; }
public DebugButtonAttribute(string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
DisplayName = displayName;
ButtonColor = new Color(r, g, b, 1f);
}
}
#endregion
}