MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/UniversalDebugTool.cs
zhang hongbo 0fca77ea9f 新增参数窗口按钮
新增参数窗口按钮
2026-04-14 14:58:35 +08:00

3087 lines
110 KiB
C#
Raw Permalink 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.Globalization;
using System.Linq;
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 customCheckBoxesPage;
[SerializeField] private RectTransform checkBoxContainer;
[SerializeField] private GameObject checkBoxPrefab;
[SerializeField] private ScrollRect checkBoxesScrollRect;
[Header("数值页面")]
[SerializeField] private GameObject customValuesPage;
[SerializeField] private RectTransform valueContainer;
[SerializeField] private GameObject valuePrefab;
[SerializeField] private ScrollRect valuesScrollRect;
[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;
[SerializeField] private TMP_InputField infoBufferInputField;
[SerializeField] private TMP_InputField warningBufferInputField;
[SerializeField] private TMP_InputField errorBufferInputField;
[SerializeField] private TMP_InputField fatalBufferInputField;
[SerializeField] private Button applyBufferButton;
[SerializeField] private Button resetBufferButton;
[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 TMP_InputField consoleTextFilterInputField;
[SerializeField] private GameObject consoleLogItemPrefab;
[Header("输入对话框")]
[SerializeField] private GameObject inputDialog;
[SerializeField] private TMP_Text inputDialogTitle;
[SerializeField] private TMP_Text inputDialogDescription;
[SerializeField] private RectTransform inputDialogFieldContainer;
[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 CustomCheckBoxesModule customCheckBoxesModule;
private CustomValuesModule customValuesModule;
private SettingsModule settingsModule;
private ConsoleModule consoleModule;
private List<IDebugModule> allModules = new List<IDebugModule>();
// 暂时隐藏状态标志
private bool isHiding = false;
// 四指点击检测
private bool wasFourFingerTouch = false;
private const float fourFingerTapTime = 0.3f; // 四指同时按下的时间阈值
private float fourFingerTouchStartTime = 0f;
// 键盘测试模式用于测试可以用F键代替四指点击
private static bool useKeyboardTestMode = false; // 默认开启键盘测试模式 f
private readonly List<GameObject> inputDialogGeneratedObjects = new List<GameObject>();
#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()
{
// 如果未初始化直接销毁GameObject不执行任何操作
if (!isInitialized)
{
Destroy(gameObject);
return;
}
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
return;
}
// 设置Canvas为最上层
SetCanvasToTop();
// 隐藏所有UI
HideAllUI();
// 注意如果isInitialized为true不在Awake中调用InitializeDebugTool
// 因为此时反射设置的字段可能还没生效延迟到Start中调用
}
private void Start()
{
// 如果未初始化,不执行任何操作
if (!isInitialized)
return;
// 只有在已初始化但还没调用过InitializeDebugTool时才执行
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
if (allModules.Count == 0)
{
InitializeDebugTool();
InitializeAllModules();
ShowMainWindow();
}
}
private void Update()
{
// 只有初始化后才执行任何逻辑
if (!isInitialized)
return;
// 键盘测试模式按F键触发
if (useKeyboardTestMode && Input.GetKeyDown(KeyCode.F))
{
OnFourFingerTap();
}
// 四指点击检测已禁用如需使用请在主程序中通过公共API控制显示/隐藏
// if (!useKeyboardTestMode)
// {
// DetectFourFingerTap();
// }
// 仅在控制台页实际显示时更新控制台UI
if (consoleModule != null && currentPageKey == "控制台" && mainWindow != null && mainWindow.gameObject.activeInHierarchy)
{
consoleModule.Update();
}
// 参数页 Profiler 实时刷新
if (parametersModule != null && currentPageKey == "参数" && mainWindow != null && mainWindow.gameObject.activeInHierarchy)
{
parametersModule.Update();
}
}
private void OnDestroy()
{
// 如果未初始化,不执行任何操作
if (!isInitialized)
return;
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 (!isInitialized || !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);
}
// 4.5. 更新自定义复选框字体,不再整页重建
if (Instance.customCheckBoxesModule != null)
{
Debug.Log("[MeowmentDebugTool] 应用自定义复选框字体...");
Instance.customCheckBoxesModule.SetSDFFont(fontAsset);
}
// 4.6. 更新数值模块字体,不再整页重建
if (Instance.customValuesModule != null)
{
Debug.Log("[MeowmentDebugTool] 应用数值模块字体...");
Instance.customValuesModule.SetSDFFont(fontAsset);
}
// 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("[MeowmentDebugTool] 初始化UniversalDebugTool...");
// 参数页按需初始化,避免影响启动速度
parametersModule = null;
if (parametersPage != null)
{
RegisterPage("参数", parametersPage);
}
customButtonsModule = new CustomButtonsModule(
customButtonsPage, buttonContainer, buttonPrefab, buttonsScrollRect, CloseDebugWindow);
customCheckBoxesModule = new CustomCheckBoxesModule(
customCheckBoxesPage, checkBoxContainer, checkBoxPrefab, checkBoxesScrollRect);
customValuesModule = new CustomValuesModule(
customValuesPage, valueContainer, valuePrefab, valuesScrollRect, CloseDebugWindow);
settingsModule = new SettingsModule(
settingsPage, widthInputField, heightInputField,
applyResolutionButton, resetResolutionButton, currentResolutionText,
infoBufferInputField, warningBufferInputField,
errorBufferInputField, fatalBufferInputField,
applyBufferButton, resetBufferButton,
mainWindow);
consoleModule = new ConsoleModule(
consolePage, consoleLogScrollRect, consoleLogContent,
consoleDetailScrollRect, consoleDetailText,
consoleClearButton, consoleLockScrollToggle,
consoleInfoFilterToggle, consoleWarningFilterToggle,
consoleErrorFilterToggle, consoleFatalFilterToggle,
consoleTextFilterInputField, consoleLogItemPrefab);
// 添加会显示在顶部标签栏的模块(顺序:控制台、按钮、开关、数值、设置)
allModules.Add(consoleModule);
allModules.Add(customButtonsModule);
allModules.Add(customCheckBoxesModule);
allModules.Add(customValuesModule);
allModules.Add(settingsModule);
// 注册所有页面
foreach (var module in allModules)
{
RegisterPage(module.GetModuleName(), module.GetPage());
}
Debug.Log($"[MeowmentDebugTool] 已注册{pages.Count}个页面");
// 创建标签按钮
CreateTabButtons();
// 设置按钮事件
if (closeButton != null)
closeButton.onClick.AddListener(CloseDebugWindow);
// 注意浮窗点击事件现在由DraggableFloatingButton的OnPointerClick处理
// 默认页仅记录,不在初始化阶段真正构建页面内容
if (pages.Count > 0)
{
currentPageKey = pages.ContainsKey("按钮") ? "按钮" : pages.Keys.First();
}
Debug.Log("[OK] UniversalDebugTool初始化完成");
Debug.Log("[MeowmentDebugTool] 提示: 点击窗口顶部的标签切换页面");
}
/// <summary>
/// 初始化所有模块
/// </summary>
private void InitializeAllModules()
{
Debug.Log("🔧 初始化所有模块...");
foreach (var module in allModules)
{
module.Initialize();
}
// 设置ConsoleModule引用到SettingsModule
if (settingsModule != null && consoleModule != null)
{
settingsModule.SetConsoleModule(consoleModule);
}
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($"[MeowmentDebugTool] 开始创建{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 (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示页面");
return;
}
if (!pages.ContainsKey(pageName))
{
Debug.LogWarning($"页面 '{pageName}' 不存在!");
return;
}
// 隐藏所有页面
foreach (var page in pages.Values)
{
page.SetActive(false);
}
// 显示目标页面
pages[pageName].SetActive(true);
currentPageKey = pageName;
if (pageName == "参数")
{
EnsureParametersModule()?.RefreshAllInfo();
}
else if (pageName == "按钮" && customButtonsModule != null)
{
customButtonsModule.EnsurePageViewInitialized();
}
else if (pageName == "开关" && customCheckBoxesModule != null)
{
customCheckBoxesModule.EnsurePageViewInitialized();
}
else if (pageName == "数值" && customValuesModule != null)
{
customValuesModule.EnsurePageViewInitialized();
}
else if (pageName == "控制台" && consoleModule != null)
{
consoleModule.EnsurePageViewInitialized();
}
// 更新标签按钮状态
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
private ParametersModule EnsureParametersModule()
{
if (parametersModule != null)
{
return parametersModule;
}
if (parametersPage == null)
{
return null;
}
parametersModule = new ParametersModule(parametersPage);
parametersModule.Initialize();
return parametersModule;
}
/// <summary>
/// 复制所有参数信息到剪贴板
/// </summary>
public void CopyDeviceInfoToClipboard()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
EnsureParametersModule()?.CopyAllInfoToClipboard();
}
/// <summary>
/// 复制所有参数信息到剪贴板
/// </summary>
public void CopySystemInfoToClipboard()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
EnsureParametersModule()?.CopyAllInfoToClipboard();
}
/// <summary>
/// 刷新所有信息
/// </summary>
public void RefreshAllInfo()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
EnsureParametersModule()?.RefreshAllInfo();
}
#endregion
#region CustomButtonsModule
/// <summary>
/// 设置自定义按钮回调
/// </summary>
public static void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法设置按钮回调");
return;
}
if (Instance.customButtonsModule != null)
{
Instance.customButtonsModule.SetCustomButtonCallback(callback);
}
}
/// <summary>
/// 运行时动态注册一个按钮
/// </summary>
public static bool RegisterRuntimeButton(string buttonId, Action callback, string tabName = "默认",
string groupName = "默认", string displayName = "", Color? buttonColor = null, Color? tabColor = null,
bool reloadImmediately = true)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法注册运行时按钮");
return false;
}
if (Instance.customButtonsModule == null)
{
Debug.LogWarning("[MeowmentDebugTool] 按钮模块未初始化");
return false;
}
return Instance.customButtonsModule.RegisterRuntimeButton(buttonId, callback, tabName, groupName, displayName, buttonColor, tabColor, reloadImmediately);
}
/// <summary>
/// 运行时动态注册一个带输入面板的按钮
/// </summary>
public static bool RegisterRuntimeInputButton(RuntimeDebugInputButtonDefinition definition, bool reloadImmediately = true)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法注册运行时输入按钮");
return false;
}
if (Instance.customButtonsModule == null)
{
Debug.LogWarning("[MeowmentDebugTool] 按钮模块未初始化");
return false;
}
return Instance.customButtonsModule.RegisterRuntimeInputButton(definition, reloadImmediately);
}
/// <summary>
/// 开始批量更新运行时按钮,期间的注册/移除/清空不会立即重建页面。
/// </summary>
public static void BeginRuntimeButtonBatchUpdate()
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法开始批量更新运行时按钮");
return;
}
Instance.customButtonsModule?.BeginRuntimeButtonBatchUpdate();
}
/// <summary>
/// 结束批量更新运行时按钮,并在需要时统一刷新一次。
/// </summary>
public static void EndRuntimeButtonBatchUpdate(bool reloadIfDirty = true)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法结束批量更新运行时按钮");
return;
}
Instance.customButtonsModule?.EndRuntimeButtonBatchUpdate(reloadIfDirty);
}
/// <summary>
/// 手动刷新运行时动态按钮显示。
/// </summary>
public static void ReloadRuntimeButtons()
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法刷新运行时按钮");
return;
}
Instance.customButtonsModule?.ReloadCustomButtons();
}
/// <summary>
/// 移除一个运行时动态按钮
/// </summary>
public static bool UnregisterRuntimeButton(string buttonId, bool reloadImmediately = true)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法移除运行时按钮");
return false;
}
if (Instance.customButtonsModule == null)
{
Debug.LogWarning("[MeowmentDebugTool] 按钮模块未初始化");
return false;
}
return Instance.customButtonsModule.UnregisterRuntimeButton(buttonId, reloadImmediately);
}
/// <summary>
/// 清空所有运行时动态按钮
/// </summary>
public static void ClearRuntimeButtons(bool reloadImmediately = true)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法清空运行时按钮");
return;
}
Instance.customButtonsModule?.ClearRuntimeButtons(reloadImmediately);
}
/// <summary>
/// 重新加载自定义按钮
/// </summary>
public void ReloadCustomButtons()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
customButtonsModule?.ReloadCustomButtons();
}
#endregion
#region CustomValuesModule
/// <summary>
/// 更新指定方法的数值范围
/// </summary>
/// <param name="methodName">方法名称(完整路径如"ClassName.MethodName"或简单的"MethodName"</param>
/// <param name="minValue">新的最小值</param>
/// <param name="maxValue">新的最大值</param>
/// <returns>是否成功更新</returns>
public static bool UpdateDebugValueRange(string methodName, int minValue, int maxValue)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法更新数值范围");
return false;
}
if (Instance.customValuesModule == null)
{
Debug.LogWarning("[MeowmentDebugTool] 数值模块未初始化");
return false;
}
return Instance.customValuesModule.UpdateValueRange(methodName, minValue, maxValue);
}
/// <summary>
/// 更新指定方法的当前值
/// </summary>
/// <param name="methodName">方法名称(完整路径如"ClassName.MethodName"或简单的"MethodName"</param>
/// <param name="value">新的值</param>
/// <returns>是否成功更新</returns>
public static bool UpdateDebugValue(string methodName, int value)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法更新数值");
return false;
}
if (Instance.customValuesModule == null)
{
Debug.LogWarning("[MeowmentDebugTool] 数值模块未初始化");
return false;
}
return Instance.customValuesModule.UpdateValue(methodName, value);
}
#endregion
#region
private class InputDialogValueResult
{
public bool Success;
public object Value;
public string Error;
}
private class InputDialogDynamicOption
{
public string Label;
public object Value;
}
internal static void ShowMethodInputDialog(MethodInfo method, DebugInputButtonAttribute inputButtonAttribute, Action onCompleted = null)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示输入对话框");
return;
}
Instance.ShowMethodInputDialogInternal(method, inputButtonAttribute, onCompleted);
}
internal static void ShowRuntimeInputDialog(RuntimeDebugInputButtonDefinition definition, Action onCompleted = null)
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示运行时输入对话框");
return;
}
Instance.ShowRuntimeInputDialogInternal(definition, onCompleted);
}
public static void CloseInputDialog()
{
if (!isInitialized || !InstanceExists)
{
return;
}
Instance.CloseInputDialogInternal();
}
private void ShowMethodInputDialogInternal(MethodInfo method, DebugInputButtonAttribute inputButtonAttribute, Action onCompleted)
{
if (method == null)
{
Debug.LogWarning("[MeowmentDebugTool] 输入型按钮的方法为空");
return;
}
if (inputDialog == null || inputDialogTitle == null || inputDialogFieldContainer == null || inputDialogConfirmBtn == null || inputDialogCancelBtn == null)
{
Debug.LogWarning("[MeowmentDebugTool] 输入对话框组件未正确初始化");
return;
}
ClearInputDialogFields();
string buttonName = !string.IsNullOrEmpty(inputButtonAttribute?.DisplayName) ? inputButtonAttribute.DisplayName : method.Name;
string dialogTitle = !string.IsNullOrEmpty(inputButtonAttribute?.DialogTitle) ? inputButtonAttribute.DialogTitle : buttonName;
inputDialogTitle.text = dialogTitle;
if (inputDialogDescription != null)
{
inputDialogDescription.text = $"请填写 {buttonName} 所需的参数";
}
List<Func<InputDialogValueResult>> readers = new List<Func<InputDialogValueResult>>();
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length == 0)
{
readers.Add(() => new InputDialogValueResult { Success = true, Value = null });
}
else
{
Dictionary<string, List<Action<string>>> linkedFieldCallbacks = new Dictionary<string, List<Action<string>>>();
List<KeyValuePair<string, string>> initialLinkedValues = new List<KeyValuePair<string, string>>();
foreach (ParameterInfo parameter in parameters)
{
DebugInputParameterAttribute paramAttr = parameter.GetCustomAttribute<DebugInputParameterAttribute>();
if (paramAttr != null && paramAttr.ReadOnly)
{
string readOnlyLabel = !string.IsNullOrEmpty(paramAttr.Label) ? paramAttr.Label : parameter.Name;
Func<string, string> displayProvider = ResolveDisplayValueProvider(parameter.Member.DeclaringType, paramAttr.DisplayValueProvider);
Func<InputDialogValueResult> readOnlyReader;
Action<string> readOnlyUpdater;
CreateReadOnlyDisplayField(readOnlyLabel, string.Empty, out readOnlyReader, out readOnlyUpdater);
if (!string.IsNullOrEmpty(paramAttr.LinkedTo))
{
string linkedTo = paramAttr.LinkedTo;
if (!linkedFieldCallbacks.ContainsKey(linkedTo))
linkedFieldCallbacks[linkedTo] = new List<Action<string>>();
linkedFieldCallbacks[linkedTo].Add(selectedValue =>
{
string displayValue = displayProvider != null ? displayProvider(selectedValue) : selectedValue;
readOnlyUpdater(displayValue);
});
}
readers.Add(readOnlyReader);
}
else
{
string paramName = parameter.Name;
Action<string> onSelectionChanged = selectedValue =>
{
if (linkedFieldCallbacks.TryGetValue(paramName, out List<Action<string>> callbacks))
foreach (Action<string> cb in callbacks)
cb(selectedValue);
};
Func<InputDialogValueResult> reader = CreateInputDialogField(parameter, onSelectionChanged);
if (reader == null)
{
Debug.LogWarning($"[MeowmentDebugTool] 暂不支持参数类型: {parameter.ParameterType.Name}");
return;
}
readers.Add(reader);
object defaultVal = GetParameterDefaultValue(parameter, paramAttr, out bool hasDefault);
if (hasDefault && defaultVal != null)
{
initialLinkedValues.Add(new KeyValuePair<string, string>(paramName, defaultVal.ToString()));
}
}
}
foreach (KeyValuePair<string, string> kvp in initialLinkedValues)
{
if (linkedFieldCallbacks.TryGetValue(kvp.Key, out List<Action<string>> callbacks))
foreach (Action<string> cb in callbacks)
cb(kvp.Value);
}
}
inputDialogConfirmBtn.onClick.RemoveAllListeners();
inputDialogConfirmBtn.onClick.AddListener(() =>
{
try
{
object[] values = parameters.Length == 0 ? null : new object[parameters.Length];
for (int i = 0; i < readers.Count; i++)
{
InputDialogValueResult result = readers[i].Invoke();
if (!result.Success)
{
Debug.LogWarning($"[MeowmentDebugTool] 参数输入无效: {result.Error}");
return;
}
if (parameters.Length > 0)
{
values[i] = result.Value;
}
}
method.Invoke(null, values);
Debug.Log($"[MeowmentDebugTool] 已执行输入型按钮方法: {method.Name}");
CloseInputDialogInternal();
onCompleted?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"[MeowmentDebugTool] 执行输入型按钮 {method.Name} 失败: {e.Message}");
}
});
inputDialogCancelBtn.onClick.RemoveAllListeners();
inputDialogCancelBtn.onClick.AddListener(CloseInputDialogInternal);
inputDialog.SetActive(true);
}
private void ShowRuntimeInputDialogInternal(RuntimeDebugInputButtonDefinition definition, Action onCompleted)
{
if (definition == null)
{
Debug.LogWarning("[MeowmentDebugTool] 运行时输入按钮定义为空");
return;
}
if (definition.Callback == null)
{
Debug.LogWarning($"[MeowmentDebugTool] 运行时输入按钮 {definition.Id} 缺少回调");
return;
}
if (inputDialog == null || inputDialogTitle == null || inputDialogFieldContainer == null || inputDialogConfirmBtn == null || inputDialogCancelBtn == null)
{
Debug.LogWarning("[MeowmentDebugTool] 输入对话框组件未正确初始化");
return;
}
ClearInputDialogFields();
string buttonName = !string.IsNullOrEmpty(definition.DisplayName) ? definition.DisplayName : definition.Id;
string dialogTitle = !string.IsNullOrEmpty(definition.DialogTitle) ? definition.DialogTitle : buttonName;
inputDialogTitle.text = dialogTitle;
if (inputDialogDescription != null)
{
inputDialogDescription.text = $"请填写 {buttonName} 所需的参数";
}
List<RuntimeDebugInputParameterDefinition> parameters = definition.Parameters ?? new List<RuntimeDebugInputParameterDefinition>();
List<Func<InputDialogValueResult>> readers = new List<Func<InputDialogValueResult>>();
Dictionary<string, List<Action<string>>> linkedFieldCallbacks = new Dictionary<string, List<Action<string>>>();
List<KeyValuePair<string, string>> initialLinkedValues = new List<KeyValuePair<string, string>>();
foreach (RuntimeDebugInputParameterDefinition parameter in parameters)
{
if (parameter != null && parameter.ReadOnly)
{
string readOnlyLabel = string.IsNullOrEmpty(parameter.Label)
? (string.IsNullOrEmpty(parameter.Key) ? "display" : parameter.Key)
: parameter.Label;
Func<string, string> displayProvider = parameter.DisplayValueProvider;
Func<InputDialogValueResult> readOnlyReader;
Action<string> readOnlyUpdater;
CreateReadOnlyDisplayField(readOnlyLabel, string.Empty, out readOnlyReader, out readOnlyUpdater);
if (!string.IsNullOrEmpty(parameter.LinkedTo))
{
string linkedTo = parameter.LinkedTo;
if (!linkedFieldCallbacks.ContainsKey(linkedTo))
linkedFieldCallbacks[linkedTo] = new List<Action<string>>();
linkedFieldCallbacks[linkedTo].Add(selectedValue =>
{
string displayValue = displayProvider != null ? displayProvider(selectedValue) : selectedValue;
readOnlyUpdater(displayValue);
});
}
readers.Add(readOnlyReader);
}
else
{
string paramKey = parameter != null && !string.IsNullOrEmpty(parameter.Key) ? parameter.Key : $"param{readers.Count}";
Action<string> onSelectionChanged = selectedValue =>
{
if (linkedFieldCallbacks.TryGetValue(paramKey, out List<Action<string>> callbacks))
foreach (Action<string> cb in callbacks)
cb(selectedValue);
};
Func<InputDialogValueResult> reader = CreateRuntimeInputDialogField(parameter, onSelectionChanged);
if (reader == null)
{
Debug.LogWarning($"[MeowmentDebugTool] 暂不支持运行时输入参数类型: {parameter?.ParameterType?.Name}");
return;
}
readers.Add(reader);
if (parameter != null)
{
object defaultVal = GetRuntimeParameterDefaultValue(parameter, out bool hasDefault);
if (hasDefault && defaultVal != null)
{
initialLinkedValues.Add(new KeyValuePair<string, string>(paramKey, defaultVal.ToString()));
}
}
}
}
foreach (KeyValuePair<string, string> kvp in initialLinkedValues)
{
if (linkedFieldCallbacks.TryGetValue(kvp.Key, out List<Action<string>> callbacks))
foreach (Action<string> cb in callbacks)
cb(kvp.Value);
}
inputDialogConfirmBtn.onClick.RemoveAllListeners();
inputDialogConfirmBtn.onClick.AddListener(() =>
{
try
{
Dictionary<string, object> values = new Dictionary<string, object>();
for (int i = 0; i < readers.Count; i++)
{
InputDialogValueResult result = readers[i].Invoke();
if (!result.Success)
{
Debug.LogWarning($"[MeowmentDebugTool] 参数输入无效: {result.Error}");
return;
}
RuntimeDebugInputParameterDefinition parameter = parameters[i];
string key = string.IsNullOrEmpty(parameter.Key) ? $"param{i}" : parameter.Key;
values[key] = result.Value;
}
definition.Callback.Invoke(new RuntimeDebugInputResult(values));
Debug.Log($"[MeowmentDebugTool] 已执行运行时输入按钮: {definition.Id}");
CloseInputDialogInternal();
onCompleted?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"[MeowmentDebugTool] 执行运行时输入按钮 {definition.Id} 失败: {e.Message}");
}
});
inputDialogCancelBtn.onClick.RemoveAllListeners();
inputDialogCancelBtn.onClick.AddListener(CloseInputDialogInternal);
inputDialog.SetActive(true);
}
private void CloseInputDialogInternal()
{
if (inputDialog == null)
{
return;
}
inputDialog.SetActive(false);
inputDialogConfirmBtn?.onClick.RemoveAllListeners();
inputDialogCancelBtn?.onClick.RemoveAllListeners();
ClearInputDialogFields();
}
private void ClearInputDialogFields()
{
foreach (GameObject fieldObject in inputDialogGeneratedObjects)
{
if (fieldObject != null)
{
Destroy(fieldObject);
}
}
inputDialogGeneratedObjects.Clear();
}
private Func<InputDialogValueResult> CreateInputDialogField(ParameterInfo parameter, Action<string> onSelectionChanged = null)
{
Type parameterType = parameter.ParameterType;
DebugInputParameterAttribute parameterAttribute = parameter.GetCustomAttribute<DebugInputParameterAttribute>();
string label = parameterAttribute != null && !string.IsNullOrEmpty(parameterAttribute.Label)
? parameterAttribute.Label
: parameter.Name;
object defaultValue = GetParameterDefaultValue(parameter, parameterAttribute, out bool hasDefaultValue);
bool isRequired = parameterAttribute != null && parameterAttribute.Required;
GameObject fieldRoot = CreateInputFieldRoot(label, isRequired);
inputDialogGeneratedObjects.Add(fieldRoot);
if (parameterAttribute != null && !string.IsNullOrEmpty(parameterAttribute.OptionsProvider))
{
List<InputDialogDynamicOption> dynamicOptions = ResolveDynamicOptions(parameter, parameterAttribute);
if (dynamicOptions.Count == 0)
{
GameObject hint = CreateDialogHint(fieldRoot.transform, $"{label} 当前没有可选项");
inputDialogGeneratedObjects.Add(hint);
return () => CreateInputError(label, $"{label} 当前没有可选项");
}
return CreateDynamicOptionField(fieldRoot.transform, parameterType, label, parameterAttribute, dynamicOptions, defaultValue, hasDefaultValue, onSelectionChanged);
}
if (parameterType == typeof(string))
{
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterAttribute?.Placeholder ?? "请输入内容", defaultValue as string ?? string.Empty, TMP_InputField.ContentType.Standard);
return () =>
{
string text = inputField.text;
if (string.IsNullOrEmpty(text))
{
if (hasDefaultValue)
{
text = defaultValue as string ?? string.Empty;
}
else if (isRequired)
{
return CreateInputError(label, $"{label} 不能为空");
}
}
return new InputDialogValueResult { Success = true, Value = text ?? string.Empty };
};
}
if (parameterType == typeof(int))
{
string initialText = hasDefaultValue ? Convert.ToInt32(defaultValue).ToString() : string.Empty;
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterAttribute?.Placeholder ?? "请输入整数", initialText, TMP_InputField.ContentType.Standard);
return () => ParseNumericInput(parameter, label, inputField.text, parameterAttribute, hasDefaultValue ? Convert.ToInt32(defaultValue) : (int?)null);
}
if (parameterType == typeof(float))
{
string initialText = hasDefaultValue ? Convert.ToSingle(defaultValue).ToString(CultureInfo.InvariantCulture) : string.Empty;
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterAttribute?.Placeholder ?? "请输入数字", initialText, TMP_InputField.ContentType.Standard);
return () => ParseFloatInput(parameter, label, inputField.text, parameterAttribute, hasDefaultValue ? Convert.ToSingle(defaultValue) : (float?)null);
}
if (parameterType == typeof(bool))
{
Toggle toggle = CreateDialogToggle(fieldRoot.transform, hasDefaultValue && Convert.ToBoolean(defaultValue));
return () => new InputDialogValueResult { Success = true, Value = toggle.isOn };
}
if (parameterType.IsEnum)
{
if (Attribute.IsDefined(parameterType, typeof(FlagsAttribute)))
{
return CreateFlagsEnumField(fieldRoot.transform, parameterType, label, parameterAttribute, defaultValue, hasDefaultValue);
}
return CreateEnumField(fieldRoot.transform, parameterType, label, parameterAttribute, defaultValue, hasDefaultValue, onSelectionChanged);
}
GameObject unsupported = CreateDialogHint(fieldRoot.transform, $"暂不支持类型: {parameterType.Name}");
inputDialogGeneratedObjects.Add(unsupported);
return null;
}
private Func<InputDialogValueResult> CreateRuntimeInputDialogField(RuntimeDebugInputParameterDefinition parameterDefinition, Action<string> onSelectionChanged = null)
{
if (parameterDefinition == null || parameterDefinition.ParameterType == null)
{
return null;
}
Type parameterType = parameterDefinition.ParameterType;
string label = string.IsNullOrEmpty(parameterDefinition.Label)
? (string.IsNullOrEmpty(parameterDefinition.Key) ? parameterType.Name : parameterDefinition.Key)
: parameterDefinition.Label;
object defaultValue = GetRuntimeParameterDefaultValue(parameterDefinition, out bool hasDefaultValue);
bool isRequired = parameterDefinition.Required;
GameObject fieldRoot = CreateInputFieldRoot(label, isRequired);
inputDialogGeneratedObjects.Add(fieldRoot);
if (parameterDefinition.OptionsProvider != null)
{
List<InputDialogDynamicOption> dynamicOptions = ResolveDynamicOptions(parameterDefinition);
if (dynamicOptions.Count == 0)
{
GameObject hint = CreateDialogHint(fieldRoot.transform, $"{label} 当前没有可选项");
inputDialogGeneratedObjects.Add(hint);
return () => CreateInputError(label, $"{label} 当前没有可选项");
}
return CreateDynamicOptionField(fieldRoot.transform, parameterType, label, parameterDefinition.Required, dynamicOptions, defaultValue, hasDefaultValue, onSelectionChanged);
}
if (parameterType == typeof(string))
{
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterDefinition.Placeholder ?? "请输入内容", defaultValue as string ?? string.Empty, TMP_InputField.ContentType.Standard);
return () =>
{
string text = inputField.text;
if (string.IsNullOrEmpty(text))
{
if (hasDefaultValue)
{
text = defaultValue as string ?? string.Empty;
}
else if (isRequired)
{
return CreateInputError(label, $"{label} 不能为空");
}
}
return new InputDialogValueResult { Success = true, Value = text ?? string.Empty };
};
}
if (parameterType == typeof(int))
{
string initialText = hasDefaultValue ? Convert.ToInt32(defaultValue).ToString() : string.Empty;
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterDefinition.Placeholder ?? "请输入整数", initialText, TMP_InputField.ContentType.Standard);
return () => ParseNumericInput(label, inputField.text, parameterDefinition, hasDefaultValue ? Convert.ToInt32(defaultValue) : (int?)null);
}
if (parameterType == typeof(float))
{
string initialText = hasDefaultValue ? Convert.ToSingle(defaultValue).ToString(CultureInfo.InvariantCulture) : string.Empty;
TMP_InputField inputField = CreateDialogTextInput(fieldRoot.transform, parameterDefinition.Placeholder ?? "请输入数字", initialText, TMP_InputField.ContentType.Standard);
return () => ParseFloatInput(label, inputField.text, parameterDefinition, hasDefaultValue ? Convert.ToSingle(defaultValue) : (float?)null);
}
if (parameterType == typeof(bool))
{
Toggle toggle = CreateDialogToggle(fieldRoot.transform, hasDefaultValue && Convert.ToBoolean(defaultValue));
return () => new InputDialogValueResult { Success = true, Value = toggle.isOn };
}
if (parameterType.IsEnum)
{
if (Attribute.IsDefined(parameterType, typeof(FlagsAttribute)))
{
return CreateFlagsEnumField(fieldRoot.transform, parameterType, label, parameterDefinition.Required, defaultValue, hasDefaultValue);
}
return CreateEnumField(fieldRoot.transform, parameterType, label, parameterDefinition.Required, defaultValue, hasDefaultValue, onSelectionChanged);
}
GameObject unsupported = CreateDialogHint(fieldRoot.transform, $"暂不支持类型: {parameterType.Name}");
inputDialogGeneratedObjects.Add(unsupported);
return null;
}
private object GetParameterDefaultValue(ParameterInfo parameter, DebugInputParameterAttribute parameterAttribute, out bool hasDefaultValue)
{
hasDefaultValue = false;
if (parameterAttribute != null && !string.IsNullOrEmpty(parameterAttribute.DefaultValue))
{
if (TryConvertStringToParameterType(parameter.ParameterType, parameterAttribute.DefaultValue, out object attributeValue))
{
hasDefaultValue = true;
return attributeValue;
}
}
if (parameter.HasDefaultValue && parameter.DefaultValue != DBNull.Value)
{
hasDefaultValue = true;
return parameter.DefaultValue;
}
return parameter.ParameterType.IsValueType ? Activator.CreateInstance(parameter.ParameterType) : null;
}
private object GetRuntimeParameterDefaultValue(RuntimeDebugInputParameterDefinition parameterDefinition, out bool hasDefaultValue)
{
hasDefaultValue = false;
if (parameterDefinition != null && !string.IsNullOrEmpty(parameterDefinition.DefaultValue))
{
if (TryConvertStringToParameterType(parameterDefinition.ParameterType, parameterDefinition.DefaultValue, out object attributeValue))
{
hasDefaultValue = true;
return attributeValue;
}
}
return parameterDefinition != null && parameterDefinition.ParameterType != null && parameterDefinition.ParameterType.IsValueType
? Activator.CreateInstance(parameterDefinition.ParameterType)
: null;
}
private bool TryConvertStringToParameterType(Type targetType, string value, out object result)
{
result = null;
if (targetType == typeof(string))
{
result = value;
return true;
}
if (targetType == typeof(int))
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intValue))
{
result = intValue;
return true;
}
return false;
}
if (targetType == typeof(float))
{
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatValue))
{
result = floatValue;
return true;
}
return false;
}
if (targetType == typeof(bool))
{
if (bool.TryParse(value, out bool boolValue))
{
result = boolValue;
return true;
}
return false;
}
if (targetType.IsEnum)
{
try
{
result = Enum.Parse(targetType, value, true);
return true;
}
catch
{
return false;
}
}
return false;
}
private List<InputDialogDynamicOption> ResolveDynamicOptions(ParameterInfo parameter, DebugInputParameterAttribute parameterAttribute)
{
List<InputDialogDynamicOption> options = new List<InputDialogDynamicOption>();
if (parameterAttribute == null || string.IsNullOrEmpty(parameterAttribute.OptionsProvider))
{
return options;
}
Type declaringType = parameter.Member.DeclaringType;
if (declaringType == null)
{
return options;
}
MethodInfo providerMethod = declaringType.GetMethod(
parameterAttribute.OptionsProvider,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null,
Type.EmptyTypes,
null);
if (providerMethod == null)
{
Debug.LogWarning($"[MeowmentDebugTool] 未找到动态选项提供方法: {declaringType.Name}.{parameterAttribute.OptionsProvider}()");
return options;
}
if (!typeof(IEnumerable).IsAssignableFrom(providerMethod.ReturnType))
{
Debug.LogWarning($"[MeowmentDebugTool] 动态选项提供方法 {providerMethod.Name} 必须返回 IEnumerable");
return options;
}
try
{
IEnumerable result = providerMethod.Invoke(null, null) as IEnumerable;
if (result == null)
{
return options;
}
foreach (object item in result)
{
if (TryConvertDynamicOptionItem(parameter.ParameterType, item, out InputDialogDynamicOption option))
{
options.Add(option);
}
}
}
catch (Exception e)
{
Debug.LogError($"[MeowmentDebugTool] 获取动态选项失败: {e.Message}");
}
return options;
}
private List<InputDialogDynamicOption> ResolveDynamicOptions(RuntimeDebugInputParameterDefinition parameterDefinition)
{
List<InputDialogDynamicOption> options = new List<InputDialogDynamicOption>();
if (parameterDefinition == null || parameterDefinition.OptionsProvider == null || parameterDefinition.ParameterType == null)
{
return options;
}
try
{
IEnumerable result = parameterDefinition.OptionsProvider.Invoke();
if (result == null)
{
return options;
}
foreach (object item in result)
{
if (TryConvertDynamicOptionItem(parameterDefinition.ParameterType, item, out InputDialogDynamicOption option))
{
options.Add(option);
}
}
}
catch (Exception e)
{
Debug.LogError($"[MeowmentDebugTool] 获取运行时动态选项失败: {e.Message}");
}
return options;
}
private bool TryConvertDynamicOptionItem(Type parameterType, object item, out InputDialogDynamicOption option)
{
option = null;
if (item == null)
{
return false;
}
if (item is DebugInputOption debugOption)
{
if (!TryConvertStringToParameterType(parameterType, debugOption.Value ?? string.Empty, out object convertedValue))
{
return false;
}
option = new InputDialogDynamicOption
{
Label = string.IsNullOrEmpty(debugOption.Label) ? debugOption.Value : debugOption.Label,
Value = convertedValue
};
return true;
}
if (parameterType.IsInstanceOfType(item))
{
option = new InputDialogDynamicOption
{
Label = item.ToString(),
Value = item
};
return true;
}
if (TryConvertStringToParameterType(parameterType, item.ToString(), out object valueFromString))
{
option = new InputDialogDynamicOption
{
Label = item.ToString(),
Value = valueFromString
};
return true;
}
return false;
}
private Func<InputDialogValueResult> CreateDynamicOptionField(Transform parent, Type parameterType, string label,
DebugInputParameterAttribute parameterAttribute, List<InputDialogDynamicOption> options, object defaultValue, bool hasDefaultValue, Action<string> onSelectionChanged = null)
{
GameObject optionContainer = new GameObject("DynamicOptions");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
List<Button> buttons = new List<Button>();
int selectedIndex = -1;
if (hasDefaultValue)
{
selectedIndex = options.FindIndex(option => AreInputOptionValuesEqual(option.Value, defaultValue, parameterType));
}
for (int i = 0; i < options.Count; i++)
{
InputDialogDynamicOption currentOption = options[i];
Button optionButton = CreateDialogOptionButton(optionContainer.transform, currentOption.Label);
int currentIndex = i;
optionButton.onClick.AddListener(() =>
{
selectedIndex = currentIndex;
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
onSelectionChanged?.Invoke(currentOption.Value?.ToString() ?? string.Empty);
});
buttons.Add(optionButton);
}
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
return () =>
{
if (selectedIndex < 0)
{
if (parameterAttribute != null && parameterAttribute.Required)
{
return CreateInputError(label, $"请为 {label} 选择一个值");
}
if (hasDefaultValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue };
}
return new InputDialogValueResult { Success = true, Value = options[0].Value };
}
return new InputDialogValueResult { Success = true, Value = options[selectedIndex].Value };
};
}
private Func<InputDialogValueResult> CreateDynamicOptionField(Transform parent, Type parameterType, string label,
bool required, List<InputDialogDynamicOption> options, object defaultValue, bool hasDefaultValue, Action<string> onSelectionChanged = null)
{
GameObject optionContainer = new GameObject("DynamicOptions");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
List<Button> buttons = new List<Button>();
int selectedIndex = -1;
if (hasDefaultValue)
{
selectedIndex = options.FindIndex(option => AreInputOptionValuesEqual(option.Value, defaultValue, parameterType));
}
for (int i = 0; i < options.Count; i++)
{
InputDialogDynamicOption currentOption = options[i];
Button optionButton = CreateDialogOptionButton(optionContainer.transform, currentOption.Label);
int currentIndex = i;
optionButton.onClick.AddListener(() =>
{
selectedIndex = currentIndex;
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
onSelectionChanged?.Invoke(currentOption.Value?.ToString() ?? string.Empty);
});
buttons.Add(optionButton);
}
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
return () =>
{
if (selectedIndex < 0)
{
if (required)
{
return CreateInputError(label, $"请为 {label} 选择一个值");
}
if (hasDefaultValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue };
}
return new InputDialogValueResult { Success = true, Value = options[0].Value };
}
return new InputDialogValueResult { Success = true, Value = options[selectedIndex].Value };
};
}
private bool AreInputOptionValuesEqual(object left, object right, Type parameterType)
{
if (left == null || right == null)
{
return left == right;
}
if (parameterType == typeof(float))
{
return Mathf.Approximately(Convert.ToSingle(left), Convert.ToSingle(right));
}
return left.Equals(right);
}
private Func<InputDialogValueResult> CreateEnumField(Transform parent, Type enumType, string label,
DebugInputParameterAttribute parameterAttribute, object defaultValue, bool hasDefaultValue, Action<string> onSelectionChanged = null)
{
GameObject optionContainer = new GameObject("Options");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
Array enumValues = Enum.GetValues(enumType);
List<Button> buttons = new List<Button>();
int selectedIndex = hasDefaultValue ? Array.IndexOf(enumValues, defaultValue) : -1;
for (int i = 0; i < enumValues.Length; i++)
{
object optionValue = enumValues.GetValue(i);
string optionName = optionValue.ToString();
Button optionButton = CreateDialogOptionButton(optionContainer.transform, optionName);
int currentIndex = i;
optionButton.onClick.AddListener(() =>
{
selectedIndex = currentIndex;
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
onSelectionChanged?.Invoke(optionValue.ToString());
});
buttons.Add(optionButton);
}
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
return () =>
{
if (selectedIndex < 0)
{
if (parameterAttribute != null && parameterAttribute.Required)
{
return CreateInputError(label, $"请为 {label} 选择一个值");
}
return new InputDialogValueResult
{
Success = true,
Value = hasDefaultValue ? defaultValue : enumValues.GetValue(0)
};
}
return new InputDialogValueResult { Success = true, Value = enumValues.GetValue(selectedIndex) };
};
}
private Func<InputDialogValueResult> CreateEnumField(Transform parent, Type enumType, string label,
bool required, object defaultValue, bool hasDefaultValue, Action<string> onSelectionChanged = null)
{
GameObject optionContainer = new GameObject("Options");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
Array enumValues = Enum.GetValues(enumType);
List<Button> buttons = new List<Button>();
int selectedIndex = hasDefaultValue ? Array.IndexOf(enumValues, defaultValue) : -1;
for (int i = 0; i < enumValues.Length; i++)
{
object optionValue = enumValues.GetValue(i);
string optionName = optionValue.ToString();
Button optionButton = CreateDialogOptionButton(optionContainer.transform, optionName);
int currentIndex = i;
optionButton.onClick.AddListener(() =>
{
selectedIndex = currentIndex;
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
onSelectionChanged?.Invoke(optionValue.ToString());
});
buttons.Add(optionButton);
}
UpdateEnumOptionButtonStyles(buttons, selectedIndex);
return () =>
{
if (selectedIndex < 0)
{
if (required)
{
return CreateInputError(label, $"请为 {label} 选择一个值");
}
return new InputDialogValueResult
{
Success = true,
Value = hasDefaultValue ? defaultValue : enumValues.GetValue(0)
};
}
return new InputDialogValueResult { Success = true, Value = enumValues.GetValue(selectedIndex) };
};
}
private Func<InputDialogValueResult> CreateFlagsEnumField(Transform parent, Type enumType, string label,
DebugInputParameterAttribute parameterAttribute, object defaultValue, bool hasDefaultValue)
{
GameObject optionContainer = new GameObject("FlagsOptions");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
List<KeyValuePair<object, Toggle>> toggles = new List<KeyValuePair<object, Toggle>>();
Array enumValues = Enum.GetValues(enumType);
long defaultMask = hasDefaultValue ? Convert.ToInt64(defaultValue) : 0L;
foreach (object enumValue in enumValues)
{
long numericValue = Convert.ToInt64(enumValue);
if (numericValue == 0 && enumValues.Length > 1)
{
continue;
}
Toggle toggle = CreateDialogToggle(optionContainer.transform, (defaultMask & numericValue) == numericValue && numericValue != 0L, enumValue.ToString());
toggles.Add(new KeyValuePair<object, Toggle>(enumValue, toggle));
}
return () =>
{
long resultMask = 0L;
foreach (var toggleInfo in toggles)
{
if (toggleInfo.Value.isOn)
{
resultMask |= Convert.ToInt64(toggleInfo.Key);
}
}
if (parameterAttribute != null && parameterAttribute.Required && resultMask == 0L)
{
return CreateInputError(label, $"请至少为 {label} 选择一项");
}
return new InputDialogValueResult { Success = true, Value = Enum.ToObject(enumType, resultMask) };
};
}
private Func<InputDialogValueResult> CreateFlagsEnumField(Transform parent, Type enumType, string label,
bool required, object defaultValue, bool hasDefaultValue)
{
GameObject optionContainer = new GameObject("FlagsOptions");
optionContainer.transform.SetParent(parent, false);
inputDialogGeneratedObjects.Add(optionContainer);
VerticalLayoutGroup layout = optionContainer.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
List<KeyValuePair<object, Toggle>> toggles = new List<KeyValuePair<object, Toggle>>();
Array enumValues = Enum.GetValues(enumType);
long defaultMask = hasDefaultValue ? Convert.ToInt64(defaultValue) : 0L;
foreach (object enumValue in enumValues)
{
long numericValue = Convert.ToInt64(enumValue);
if (numericValue == 0 && enumValues.Length > 1)
{
continue;
}
Toggle toggle = CreateDialogToggle(optionContainer.transform, (defaultMask & numericValue) == numericValue && numericValue != 0L, enumValue.ToString());
toggles.Add(new KeyValuePair<object, Toggle>(enumValue, toggle));
}
return () =>
{
long resultMask = 0L;
foreach (var toggleInfo in toggles)
{
if (toggleInfo.Value.isOn)
{
resultMask |= Convert.ToInt64(toggleInfo.Key);
}
}
if (required && resultMask == 0L)
{
return CreateInputError(label, $"请至少为 {label} 选择一项");
}
return new InputDialogValueResult { Success = true, Value = Enum.ToObject(enumType, resultMask) };
};
}
private InputDialogValueResult ParseNumericInput(ParameterInfo parameter, string label, string text,
DebugInputParameterAttribute attribute, int? defaultValue)
{
if (string.IsNullOrWhiteSpace(text))
{
if (defaultValue.HasValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue.Value };
}
if (attribute != null && attribute.Required)
{
return CreateInputError(label, $"{label} 不能为空");
}
return new InputDialogValueResult { Success = true, Value = 0 };
}
if (!int.TryParse(text, out int value))
{
return CreateInputError(label, $"{label} 必须是整数");
}
if (attribute != null)
{
if (!float.IsNaN(attribute.Min) && value < attribute.Min)
{
return CreateInputError(label, $"{label} 不能小于 {attribute.Min}");
}
if (!float.IsNaN(attribute.Max) && value > attribute.Max)
{
return CreateInputError(label, $"{label} 不能大于 {attribute.Max}");
}
}
return new InputDialogValueResult { Success = true, Value = value };
}
private InputDialogValueResult ParseNumericInput(string label, string text,
RuntimeDebugInputParameterDefinition definition, int? defaultValue)
{
if (string.IsNullOrWhiteSpace(text))
{
if (defaultValue.HasValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue.Value };
}
if (definition != null && definition.Required)
{
return CreateInputError(label, $"{label} 不能为空");
}
return new InputDialogValueResult { Success = true, Value = 0 };
}
if (!int.TryParse(text, out int value))
{
return CreateInputError(label, $"{label} 必须是整数");
}
if (definition != null)
{
if (!float.IsNaN(definition.Min) && value < definition.Min)
{
return CreateInputError(label, $"{label} 不能小于 {definition.Min}");
}
if (!float.IsNaN(definition.Max) && value > definition.Max)
{
return CreateInputError(label, $"{label} 不能大于 {definition.Max}");
}
}
return new InputDialogValueResult { Success = true, Value = value };
}
private InputDialogValueResult ParseFloatInput(ParameterInfo parameter, string label, string text,
DebugInputParameterAttribute attribute, float? defaultValue)
{
if (string.IsNullOrWhiteSpace(text))
{
if (defaultValue.HasValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue.Value };
}
if (attribute != null && attribute.Required)
{
return CreateInputError(label, $"{label} 不能为空");
}
return new InputDialogValueResult { Success = true, Value = 0f };
}
if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) && !float.TryParse(text, out value))
{
return CreateInputError(label, $"{label} 必须是数字");
}
if (attribute != null)
{
if (!float.IsNaN(attribute.Min) && value < attribute.Min)
{
return CreateInputError(label, $"{label} 不能小于 {attribute.Min}");
}
if (!float.IsNaN(attribute.Max) && value > attribute.Max)
{
return CreateInputError(label, $"{label} 不能大于 {attribute.Max}");
}
}
return new InputDialogValueResult { Success = true, Value = value };
}
private InputDialogValueResult ParseFloatInput(string label, string text,
RuntimeDebugInputParameterDefinition definition, float? defaultValue)
{
if (string.IsNullOrWhiteSpace(text))
{
if (defaultValue.HasValue)
{
return new InputDialogValueResult { Success = true, Value = defaultValue.Value };
}
if (definition != null && definition.Required)
{
return CreateInputError(label, $"{label} 不能为空");
}
return new InputDialogValueResult { Success = true, Value = 0f };
}
if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out float value) && !float.TryParse(text, out value))
{
return CreateInputError(label, $"{label} 必须是数字");
}
if (definition != null)
{
if (!float.IsNaN(definition.Min) && value < definition.Min)
{
return CreateInputError(label, $"{label} 不能小于 {definition.Min}");
}
if (!float.IsNaN(definition.Max) && value > definition.Max)
{
return CreateInputError(label, $"{label} 不能大于 {definition.Max}");
}
}
return new InputDialogValueResult { Success = true, Value = value };
}
private InputDialogValueResult CreateInputError(string label, string error)
{
return new InputDialogValueResult
{
Success = false,
Error = error,
Value = null
};
}
private void CreateReadOnlyDisplayField(string label, string initialValue, out Func<InputDialogValueResult> reader, out Action<string> updater)
{
GameObject fieldRoot = CreateInputFieldRoot(label, false);
inputDialogGeneratedObjects.Add(fieldRoot);
GameObject displayObj = new GameObject("ReadOnlyDisplay");
displayObj.transform.SetParent(fieldRoot.transform, false);
Image displayBg = displayObj.AddComponent<Image>();
displayBg.color = new Color(0.14f, 0.14f, 0.14f, 1f);
LayoutElement layoutEl = displayObj.AddComponent<LayoutElement>();
layoutEl.preferredHeight = 64f;
layoutEl.flexibleWidth = 1f;
GameObject textContainer = new GameObject("TextContainer");
textContainer.transform.SetParent(displayObj.transform, false);
RectTransform containerRect = textContainer.AddComponent<RectTransform>();
containerRect.anchorMin = Vector2.zero;
containerRect.anchorMax = Vector2.one;
containerRect.offsetMin = new Vector2(20, 0);
containerRect.offsetMax = new Vector2(-20, 0);
TextMeshProUGUI displayText = textContainer.AddComponent<TextMeshProUGUI>();
displayText.text = string.IsNullOrEmpty(initialValue) ? "—" : initialValue;
displayText.fontSize = 28;
displayText.color = new Color(1f, 1f, 1f, 0.6f);
displayText.alignment = TextAlignmentOptions.MidlineLeft;
ApplySavedFont(displayText);
string currentValue = initialValue ?? string.Empty;
reader = () => new InputDialogValueResult { Success = true, Value = currentValue };
updater = value =>
{
currentValue = value ?? string.Empty;
displayText.text = string.IsNullOrEmpty(value) ? "—" : value;
};
}
private Func<string, string> ResolveDisplayValueProvider(Type declaringType, string methodName)
{
if (declaringType == null || string.IsNullOrEmpty(methodName))
{
return null;
}
MethodInfo method = declaringType.GetMethod(
methodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null,
new Type[] { typeof(string) },
null);
if (method == null)
{
Debug.LogWarning($"[MeowmentDebugTool] 未找到显示值提供方法: {declaringType.Name}.{methodName}(string)");
return null;
}
if (method.ReturnType != typeof(string))
{
Debug.LogWarning($"[MeowmentDebugTool] 显示值提供方法 {methodName} 必须返回 string");
return null;
}
return selectedValue => method.Invoke(null, new object[] { selectedValue }) as string ?? string.Empty;
}
private GameObject CreateInputFieldRoot(string label, bool required)
{
GameObject fieldRoot = new GameObject($"Field_{label}");
fieldRoot.transform.SetParent(inputDialogFieldContainer, false);
VerticalLayoutGroup layout = fieldRoot.AddComponent<VerticalLayoutGroup>();
layout.spacing = 10;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
layout.padding = new RectOffset(12, 12, 12, 12);
Image bg = fieldRoot.AddComponent<Image>();
bg.color = new Color(0.16f, 0.16f, 0.16f, 0.95f);
ContentSizeFitter fitter = fieldRoot.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
LayoutElement element = fieldRoot.AddComponent<LayoutElement>();
element.flexibleWidth = 1f;
TMP_Text labelText = CreateDialogText(label + (required ? " *" : string.Empty), 28, TextAlignmentOptions.Left);
labelText.transform.SetParent(fieldRoot.transform, false);
return fieldRoot;
}
private TMP_InputField CreateDialogTextInput(Transform parent, string placeholder, string initialValue, TMP_InputField.ContentType contentType)
{
GameObject inputObj = new GameObject("InputField");
inputObj.transform.SetParent(parent, false);
Image bg = inputObj.AddComponent<Image>();
bg.color = new Color(0.24f, 0.24f, 0.24f, 1f);
LayoutElement layoutElement = inputObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 80f;
layoutElement.flexibleWidth = 1f;
TMP_InputField inputField = inputObj.AddComponent<TMP_InputField>();
inputField.contentType = contentType;
GameObject textViewport = new GameObject("Text Area");
textViewport.transform.SetParent(inputObj.transform, false);
RectTransform viewportRect = textViewport.AddComponent<RectTransform>();
viewportRect.anchorMin = Vector2.zero;
viewportRect.anchorMax = Vector2.one;
viewportRect.offsetMin = new Vector2(20, 12);
viewportRect.offsetMax = new Vector2(-20, -12);
GameObject placeholderObj = new GameObject("Placeholder");
placeholderObj.transform.SetParent(textViewport.transform, false);
RectTransform placeholderRect = placeholderObj.AddComponent<RectTransform>();
placeholderRect.anchorMin = Vector2.zero;
placeholderRect.anchorMax = Vector2.one;
placeholderRect.offsetMin = Vector2.zero;
placeholderRect.offsetMax = Vector2.zero;
TMP_Text placeholderText = placeholderObj.AddComponent<TextMeshProUGUI>();
placeholderText.text = placeholder;
placeholderText.fontSize = 26;
placeholderText.color = new Color(1f, 1f, 1f, 0.35f);
placeholderText.alignment = TextAlignmentOptions.Left;
ApplySavedFont(placeholderText);
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(textViewport.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
TMP_Text text = textObj.AddComponent<TextMeshProUGUI>();
text.text = initialValue;
text.fontSize = 28;
text.color = Color.white;
text.alignment = TextAlignmentOptions.Left;
ApplySavedFont(text);
inputField.textViewport = viewportRect;
inputField.textComponent = text as TextMeshProUGUI;
inputField.placeholder = placeholderText;
inputField.text = initialValue;
return inputField;
}
private Button CreateDialogOptionButton(Transform parent, string text)
{
GameObject buttonObj = new GameObject($"Option_{text}");
buttonObj.transform.SetParent(parent, false);
Image image = buttonObj.AddComponent<Image>();
image.color = new Color(0.25f, 0.25f, 0.25f, 1f);
LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 68f;
layoutElement.flexibleWidth = 1f;
Button button = buttonObj.AddComponent<Button>();
TMP_Text textLabel = CreateDialogText(text, 26, TextAlignmentOptions.Center);
textLabel.transform.SetParent(buttonObj.transform, false);
RectTransform textRect = textLabel.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
return button;
}
private void UpdateEnumOptionButtonStyles(List<Button> buttons, int selectedIndex)
{
for (int i = 0; i < buttons.Count; i++)
{
Image image = buttons[i].GetComponent<Image>();
if (image != null)
{
image.color = i == selectedIndex
? new Color(0.2f, 0.65f, 1f, 1f)
: new Color(0.25f, 0.25f, 0.25f, 1f);
}
}
}
private Toggle CreateDialogToggle(Transform parent, bool isOn, string labelText = "启用")
{
GameObject toggleObj = new GameObject($"Toggle_{labelText}");
toggleObj.transform.SetParent(parent, false);
Image rootImage = toggleObj.AddComponent<Image>();
rootImage.color = new Color(1f, 1f, 1f, 0.001f);
HorizontalLayoutGroup layout = toggleObj.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 12;
layout.childAlignment = TextAnchor.MiddleLeft;
layout.childControlWidth = false;
layout.childControlHeight = true;
layout.childForceExpandWidth = false;
layout.childForceExpandHeight = false;
LayoutElement layoutElement = toggleObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 64f;
layoutElement.flexibleWidth = 1f;
GameObject background = new GameObject("Background");
background.transform.SetParent(toggleObj.transform, false);
Image backgroundImage = background.AddComponent<Image>();
backgroundImage.color = new Color(0.8f, 0.8f, 0.8f, 1f);
RectTransform bgRect = background.GetComponent<RectTransform>();
bgRect.sizeDelta = new Vector2(42, 42);
LayoutElement bgLayout = background.AddComponent<LayoutElement>();
bgLayout.preferredWidth = 42f;
bgLayout.preferredHeight = 42f;
bgLayout.minWidth = 42f;
bgLayout.minHeight = 42f;
GameObject checkmark = new GameObject("Checkmark");
checkmark.transform.SetParent(background.transform, false);
Image checkmarkImage = checkmark.AddComponent<Image>();
checkmarkImage.color = new Color(0.2f, 0.8f, 0.2f, 1f);
RectTransform checkRect = checkmark.GetComponent<RectTransform>();
checkRect.anchorMin = Vector2.zero;
checkRect.anchorMax = Vector2.one;
checkRect.offsetMin = new Vector2(6, 6);
checkRect.offsetMax = new Vector2(-6, -6);
TMP_Text text = CreateDialogText(labelText, 26, TextAlignmentOptions.Left);
text.transform.SetParent(toggleObj.transform, false);
LayoutElement textElement = text.gameObject.AddComponent<LayoutElement>();
textElement.flexibleWidth = 1f;
Toggle toggle = toggleObj.AddComponent<Toggle>();
toggle.transition = Selectable.Transition.ColorTint;
toggle.targetGraphic = rootImage;
toggle.targetGraphic = backgroundImage;
toggle.graphic = checkmarkImage;
toggle.isOn = isOn;
return toggle;
}
private GameObject CreateDialogHint(Transform parent, string text)
{
TMP_Text hint = CreateDialogText(text, 24, TextAlignmentOptions.Left);
hint.color = new Color(1f, 0.6f, 0.4f, 1f);
hint.transform.SetParent(parent, false);
return hint.gameObject;
}
private TMP_Text CreateDialogText(string content, float fontSize, TextAlignmentOptions alignment)
{
GameObject textObj = new GameObject("Text");
TMP_Text text = textObj.AddComponent<TextMeshProUGUI>();
text.text = content;
text.fontSize = fontSize;
text.alignment = alignment;
text.color = Color.white;
ApplySavedFont(text);
return text;
}
private void ApplySavedFont(TMP_Text text)
{
if (text == null)
{
return;
}
if (savedFontAsset != null)
{
text.font = savedFontAsset;
}
}
#endregion
#region API
/// <summary>
/// 设置键盘测试模式用F键代替四指点击
/// </summary>
/// <param name="enable">true=使用F键测试, false=使用四指点击</param>
public static void SetKeyboardTestMode(bool enable)
{
useKeyboardTestMode = enable;
Debug.Log($"[MeowmentDebugTool] 键盘测试模式: {(enable ? " (F键触发)" : " (使)")}");
}
/// <summary>
/// 获取当前是否为键盘测试模式
/// </summary>
public static bool IsKeyboardTestMode()
{
return useKeyboardTestMode;
}
/// <summary>
/// 获取Debugger是否正在显示
/// </summary>
/// <returns>true=Debugger正在显示主窗口或悬浮按钮false=完全隐藏或未初始化</returns>
public static bool IsDebuggerVisible()
{
if (!isInitialized || !InstanceExists)
{
return false;
}
// 首先检查整个GameObject是否激活
if (!Instance.gameObject.activeSelf)
{
return false;
}
// 如果GameObject激活再检查主窗口或悬浮按钮是否可见
// 注意:使用 activeInHierarchy 而不是 activeSelf因为需要考虑父对象的状态
bool mainWindowVisible = Instance.mainWindow != null && Instance.mainWindow.gameObject.activeInHierarchy;
bool floatingButtonVisible = Instance.floatingButton != null && Instance.floatingButton.activeInHierarchy;
return mainWindowVisible || floatingButtonVisible;
}
/// <summary>
/// 显示调试工具
/// </summary>
public static void Show()
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示");
return;
}
Instance.gameObject.SetActive(true);
}
/// <summary>
/// 隐藏调试工具
/// </summary>
public static void Hide()
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法隐藏");
return;
}
Instance.gameObject.SetActive(false);
}
/// <summary>
/// 切换调试工具显示状态
/// </summary>
public static void Toggle()
{
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法切换状态");
return;
}
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>
public void OpenDebugWindow()
{
if (floatingButton != null)
floatingButton.SetActive(false);
if (settingsModule != null)
settingsModule.RefreshAutoResolution();
if (mainWindow != null)
mainWindow.gameObject.SetActive(true);
if (!string.IsNullOrEmpty(currentPageKey) && pages.ContainsKey(currentPageKey))
{
ShowPage(currentPageKey);
}
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 (!isInitialized || !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] [OK] UI已成功恢复浮窗状态");
}
else
{
Debug.LogError("[MeowmentDebugTool] ✗ Canvas恢复失败");
}
isHiding = false;
Debug.Log("[MeowmentDebugTool] 暂时隐藏流程结束");
}
#endregion
// #region 四指点击检测(已禁用)
// /// <summary>
// /// 检测四指点击屏幕
// /// </summary>
// private void DetectFourFingerTap()
// {
// // 只在移动设备或Unity编辑器模拟触摸上检测
// #if UNITY_ANDROID || UNITY_IOS || UNITY_EDITOR
//
// // 检测四指同时按下
// if (Input.touchCount == 4)
// {
// // 检查是否所有触摸都刚开始
// bool allBegan = true;
// foreach (Touch touch in Input.touches)
// {
// if (touch.phase != TouchPhase.Began)
// {
// allBegan = false;
// break;
// }
// }
//
// if (allBegan && !wasFourFingerTouch)
// {
// wasFourFingerTouch = true;
// fourFingerTouchStartTime = Time.time;
// }
// }
//
// // 检测四指抬起(点击完成)
// if (wasFourFingerTouch && Input.touchCount == 0)
// {
// float touchDuration = Time.time - fourFingerTouchStartTime;
//
// // 如果触摸时间小于阈值,认为是点击(而非长按)
// if (touchDuration < fourFingerTapTime)
// {
// OnFourFingerTap();
// }
//
// wasFourFingerTouch = false;
// }
//
// // 如果触摸数量变化,重置状态
// if (wasFourFingerTouch && Input.touchCount != 4)
// {
// wasFourFingerTouch = false;
// }
//
// #endif
// }
/// <summary>
/// 四指点击触发的事件 - 切换所有UI的显示/隐藏(包括主窗口和浮窗)
/// 注意此方法保留用于键盘测试模式按F键四指点击检测已禁用
/// </summary>
private void OnFourFingerTap()
{
Debug.Log("[MeowmentDebugTool] 检测到F键触发测试模式");
// 检查主窗口或浮窗是否有任意一个显示
bool anyUIVisible = (mainWindow != null && mainWindow.gameObject.activeSelf) ||
(floatingButton != null && floatingButton.activeSelf);
if (anyUIVisible)
{
// 隐藏所有UI
if (mainWindow != null)
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(false);
Debug.Log("[MeowmentDebugTool] 所有UI已隐藏");
}
else
{
// 显示浮窗
if (mainWindow != null)
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(true);
Debug.Log("[MeowmentDebugTool] 已显示浮窗");
}
}
// #endregion
}
#region
/// <summary>
/// 调试按钮特性 - 标记方法为调试按钮
///
/// 使用说明:
/// 1. 在主项目中使用此特性时,请用条件编译包裹:
///
/// #if MEOWMENT_DEBUG_TOOL
/// [DebugButton("默认", "我的调试按钮")]
/// public static void MyDebugMethod()
/// {
/// Debug.Log("调试方法被执行");
/// }
/// #endif
///
/// 2. 这样当包被卸载时,代码不会报错,也不会影响主工程的正常运行
/// 3. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// 4. 组名参数用于对按钮进行分组显示,未指定组名的按钮会归入"默认"组
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugButtonAttribute : Attribute
{
public string TabName { get; set; }
public string GroupName { get; set; }
public string DisplayName { get; set; }
public Color ButtonColor { get; set; }
public DebugButtonAttribute(string groupName = "默认", string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.Empty;
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
DisplayName = displayName;
ButtonColor = new Color(r, g, b, 1f);
}
public DebugButtonAttribute(string tabName, string groupName, string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
DisplayName = displayName;
ButtonColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试按钮类级别分组特性(可选)
///
/// 使用说明:
/// 1. 可以标记在 class 上,为该类下所有 DebugButton 提供默认 Tab 和默认组
/// 2. 方法上的 DebugButton 若明确指定了 Tab/组,则优先使用方法上的配置
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DebugButtonClassAttribute : Attribute
{
public string TabName { get; private set; }
public string GroupName { get; private set; }
public Color TabColor { get; private set; }
public DebugButtonClassAttribute(string tabName = "默认", string groupName = "默认", float r = 0.2f, float g = 0.6f, float b = 1f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
TabColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 输入型调试按钮特性 - 点击按钮后弹出输入面板,再执行目标方法
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugInputButtonAttribute : Attribute
{
public string TabName { get; set; }
public string GroupName { get; set; }
public string DisplayName { get; set; }
public string DialogTitle { get; set; }
public Color ButtonColor { get; set; }
public DebugInputButtonAttribute(string groupName = "默认", string displayName = "", string dialogTitle = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.Empty;
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
DisplayName = displayName;
DialogTitle = dialogTitle;
ButtonColor = new Color(r, g, b, 1f);
}
public DebugInputButtonAttribute(string tabName, string groupName, string displayName = "", string dialogTitle = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
DisplayName = displayName;
DialogTitle = dialogTitle;
ButtonColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 输入型调试按钮的参数显示配置
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class DebugInputParameterAttribute : Attribute
{
public string Label { get; private set; }
public string Placeholder { get; set; }
public string DefaultValue { get; set; }
public string OptionsProvider { get; set; }
public bool Required { get; set; }
public float Min { get; set; } = float.NaN;
public float Max { get; set; } = float.NaN;
public bool ReadOnly { get; set; }
public string LinkedTo { get; set; }
public string DisplayValueProvider { get; set; }
public DebugInputParameterAttribute(string label = "")
{
Label = label;
Placeholder = string.Empty;
DefaultValue = string.Empty;
OptionsProvider = string.Empty;
Required = false;
LinkedTo = string.Empty;
DisplayValueProvider = string.Empty;
}
}
public class DebugInputOption
{
public string Label { get; private set; }
public string Value { get; private set; }
public DebugInputOption(string label, string value)
{
Label = label;
Value = value;
}
}
public class RuntimeDebugInputButtonDefinition
{
public string Id { get; set; }
public string TabName { get; set; }
public string GroupName { get; set; }
public string DisplayName { get; set; }
public string DialogTitle { get; set; }
public Color ButtonColor { get; set; }
public Color TabColor { get; set; }
public List<RuntimeDebugInputParameterDefinition> Parameters { get; set; }
public Action<RuntimeDebugInputResult> Callback { get; set; }
public RuntimeDebugInputButtonDefinition(string id, Action<RuntimeDebugInputResult> callback)
{
Id = id;
Callback = callback;
TabName = "默认";
GroupName = "默认";
DisplayName = string.Empty;
DialogTitle = string.Empty;
ButtonColor = new Color(0.8f, 0.8f, 0.8f, 1f);
TabColor = new Color(0.2f, 0.6f, 1f, 1f);
Parameters = new List<RuntimeDebugInputParameterDefinition>();
}
}
public class RuntimeDebugInputParameterDefinition
{
public string Key { get; set; }
public Type ParameterType { get; set; }
public string Label { get; set; }
public string Placeholder { get; set; }
public string DefaultValue { get; set; }
public bool Required { get; set; }
public float Min { get; set; } = float.NaN;
public float Max { get; set; } = float.NaN;
public Func<IEnumerable> OptionsProvider { get; set; }
public bool ReadOnly { get; set; }
public string LinkedTo { get; set; }
public Func<string, string> DisplayValueProvider { get; set; }
public RuntimeDebugInputParameterDefinition(string key, Type parameterType, string label = "")
{
Key = key;
ParameterType = parameterType;
Label = label;
Placeholder = string.Empty;
DefaultValue = string.Empty;
Required = false;
LinkedTo = string.Empty;
}
}
public class RuntimeDebugInputResult
{
private readonly Dictionary<string, object> values;
internal RuntimeDebugInputResult(Dictionary<string, object> values)
{
this.values = values ?? new Dictionary<string, object>();
}
public object this[string key] => values.TryGetValue(key, out object value) ? value : null;
public T Get<T>(string key)
{
if (!values.TryGetValue(key, out object value))
{
throw new KeyNotFoundException($"未找到参数: {key}");
}
if (value is T typedValue)
{
return typedValue;
}
if (value == null)
{
return default;
}
Type targetType = typeof(T);
if (targetType.IsEnum)
{
return (T)Enum.Parse(targetType, value.ToString(), true);
}
return (T)Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
}
public bool TryGet<T>(string key, out T value)
{
try
{
value = Get<T>(key);
return true;
}
catch
{
value = default;
return false;
}
}
}
/// <summary>
/// 调试复选框特性 - 标记方法为调试复选框开关
///
/// 使用说明:
/// 1. 在主项目中使用此特性时,请用条件编译包裹:
///
/// #if MEOWMENT_DEBUG_TOOL
/// [DebugCheckBox("开关名称")]
/// public static void MyToggleMethod(bool isOn)
/// {
/// Debug.Log($"开关状态: {isOn}");
/// // 在这里设置某些参数的值
/// }
/// #endif
///
/// 2. 方法必须接受一个 bool 参数,表示复选框的状态
/// 3. 这样当包被卸载时,代码不会报错,也不会影响主工程的正常运行
/// 4. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugCheckBoxAttribute : Attribute
{
public string TabName { get; set; }
public string DisplayName { get; set; }
public Color CheckBoxColor { get; set; }
public DebugCheckBoxAttribute(string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.Empty;
DisplayName = displayName;
CheckBoxColor = new Color(r, g, b, 1f);
}
public DebugCheckBoxAttribute(string tabName, string displayName, float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
DisplayName = displayName;
CheckBoxColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试复选框类级别Tab特性可选
///
/// 使用说明:
/// 1. 可以标记在 class 上,为该类下所有 DebugCheckBox 提供默认 Tab 和 Tab 颜色
/// 2. 方法上的 DebugCheckBox 若明确指定了 Tab则优先使用方法上的配置
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DebugCheckBoxClassAttribute : Attribute
{
public string TabName { get; private set; }
public Color TabColor { get; private set; }
public DebugCheckBoxClassAttribute(string tabName = "默认", float r = 0.2f, float g = 0.6f, float b = 1f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
TabColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试数值特性 - 标记方法为调试数值调整器
///
/// 使用说明:
/// 1. 在主项目中使用此特性时,请用条件编译包裹:
///
/// #if MEOWMENT_DEBUG_TOOL
/// [DebugValue("数值名称", 最小值, 最大值)]
/// public static void MyValueMethod(int value)
/// {
/// Debug.Log($"设置数值: {value}");
/// // 在这里使用value来设置游戏参数
/// }
/// #endif
///
/// 2. 方法必须接受一个 int 参数,表示调整后的数值
/// 3. 可以指定最小值和最大值来限制范围
/// 4. 可以设置自定义颜色
/// 5. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugValueAttribute : Attribute
{
public string TabName { get; set; }
public string DisplayName { get; set; }
public Color ValueColor { get; set; }
public int MinValue { get; set; }
public int MaxValue { get; set; }
public int DefaultValue { get; set; }
public DebugValueAttribute(string displayName = "", int minValue = 0, int maxValue = 100, int defaultValue = -1, float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.Empty;
DisplayName = displayName;
MinValue = minValue;
MaxValue = maxValue;
// 如果defaultValue为-1未设置则使用minValue作为默认值
DefaultValue = defaultValue == -1 ? minValue : defaultValue;
ValueColor = new Color(r, g, b, 1f);
}
public DebugValueAttribute(string tabName, string displayName, int minValue, int maxValue, int defaultValue = -1, float r = 0.8f, float g = 0.8f, float b = 0.8f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
DisplayName = displayName;
MinValue = minValue;
MaxValue = maxValue;
DefaultValue = defaultValue == -1 ? minValue : defaultValue;
ValueColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试数值类级别Tab特性可选
///
/// 使用说明:
/// 1. 可以标记在 class 上,为该类下所有 DebugValue 提供默认 Tab 和 Tab 颜色
/// 2. 方法上的 DebugValue 若明确指定了 Tab则优先使用方法上的配置
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class DebugValueClassAttribute : Attribute
{
public string TabName { get; private set; }
public Color TabColor { get; private set; }
public DebugValueClassAttribute(string tabName = "默认", float r = 0.2f, float g = 0.6f, float b = 1f)
{
TabName = string.IsNullOrEmpty(tabName) ? "默认" : tabName;
TabColor = new Color(r, g, b, 1f);
}
}
#endregion
}