更新包

This commit is contained in:
张宏博 2025-12-19 12:14:42 +08:00
parent 14f9f3f3f2
commit 51c111bc98
49 changed files with 1695 additions and 320 deletions

View File

@ -1,4 +1,5 @@
<Solution>
<Project Path="Hontbei.Meowmentdebugtool.Editor.csproj" />
<Project Path="Hontbei.Meowmentdebugtool.csproj" />
<Project Path="com.bywaystudios.meowmentdebugtool.Editor.csproj" />
<Project Path="com.bywaystudios.meowmentdebugtool.csproj" />
<Project Path="Assembly-CSharp.csproj" />
</Solution>

View File

@ -0,0 +1,35 @@
# Changelog
所有重要的变更都会记录在这个文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [0.2.2] - 2025-12-19
### Changed
- 默认显示浮窗而不是主窗口,提升用户体验
- 点击自定义按钮后自动关闭主窗口,方便用户直接查看功能效果
### Fixed
- 优化初始化逻辑,避免重复初始化问题
## [0.2.0] - 2025-12-XX
### Added
- 初始版本发布
- 支持多标签页调试界面系统
- 参数查看页面(设备信息、系统信息)
- 自定义按钮功能(通过反射自动加载带 `[DebugButton]` 特性的方法)
- 工具栏页面(时间调整工具)
- 设置页面(分辨率调整)
- 可拖拽的悬浮按钮
- 输入对话框功能
- 支持运行时动态创建UI
- 支持自定义 TextMeshPro SDF 字体
### Features
- Canvas层级设置为30000确保始终在最上层
- 未调用 `Init()` 前不显示任何UI
- 支持快捷键切换页面F1-F4
- DontDestroyOnLoad 确保场景切换时不销毁

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 612f48a21477a8447b81076490854b01
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,196 @@
# MeowmentDebugTool 使用指南
## 快速开始
### 1. 初始化调试工具自动创建UI
**只需一行代码自动创建完整的调试UI**
```csharp
using UnityEngine;
using MeowmentDebugTool;
public class GameStarter : MonoBehaviour
{
void Start()
{
// 一行代码初始化自动创建UI
UniversalDebugTool.Init();
}
}
```
**特点:**
- ✅ 无需手动创建预制件
- ✅ 无需拖拽引用
- ✅ 自动生成完整UI
- ✅ 开箱即用
### 2. 使用快捷键或API控制
```csharp
// 显示调试工具
UniversalDebugTool.Show();
// 隐藏调试工具
UniversalDebugTool.Hide();
// 切换显示/隐藏
UniversalDebugTool.Toggle();
```
## 如何在主项目中使用自定义调试按钮
为了确保包可以自由装卸而不影响主工程的运行,请使用条件编译符号包裹调试代码。
### 基本用法
```csharp
using UnityEngine;
#if MEOWMENT_DEBUG_TOOL
using MeowmentDebugTool;
#endif
public class GameManager : MonoBehaviour
{
// ✅ 正确的用法 - 使用条件编译
#if MEOWMENT_DEBUG_TOOL
[DebugButton("重置游戏数据")]
public static void ResetGameData()
{
Debug.Log("游戏数据已重置");
PlayerPrefs.DeleteAll();
}
#endif
// ❌ 错误的用法 - 不使用条件编译
// 当包被卸载时,这会导致编译错误
// [DebugButton("重置游戏数据")]
// public static void ResetGameData()
// {
// Debug.Log("游戏数据已重置");
// PlayerPrefs.DeleteAll();
// }
}
```
### 高级示例
#### 1. 带颜色的按钮
```csharp
#if MEOWMENT_DEBUG_TOOL
[DebugButton("危险操作", 1f, 0f, 0f)] // 红色按钮
public static void DangerousOperation()
{
Debug.LogWarning("执行危险操作!");
}
#endif
```
#### 2. 多个调试按钮
```csharp
public class DebugCommands
{
#if MEOWMENT_DEBUG_TOOL
[DebugButton("添加金币 +100")]
public static void AddGold()
{
// 你的逻辑
Debug.Log("添加100金币");
}
[DebugButton("添加经验 +500")]
public static void AddExp()
{
// 你的逻辑
Debug.Log("添加500经验");
}
[DebugButton("解锁所有关卡", 0f, 1f, 0f)] // 绿色按钮
public static void UnlockAllLevels()
{
// 你的逻辑
Debug.Log("所有关卡已解锁");
}
[DebugButton("清空背包", 1f, 0.5f, 0f)] // 橙色按钮
public static void ClearInventory()
{
// 你的逻辑
Debug.Log("背包已清空");
}
#endif
}
```
#### 3. 调用包内API
```csharp
public class GameUI : MonoBehaviour
{
private void Update()
{
// 按下 F12 打开/关闭调试工具
if (Input.GetKeyDown(KeyCode.F12))
{
#if MEOWMENT_DEBUG_TOOL
UniversalDebugTool.Toggle();
#endif
}
}
public void OnSettingsButtonClick()
{
// 显示输入对话框
#if MEOWMENT_DEBUG_TOOL
UniversalDebugTool.ShowInputDialog(
"输入玩家名称",
(name) => {
Debug.Log($"玩家名称:{name}");
},
"默认名称"
);
#endif
}
}
```
### 工作原理
1. **包安装时**Unity 会自动定义 `MEOWMENT_DEBUG_TOOL`
2. **包卸载时**`MEOWMENT_DEBUG_TOOL` 宏会被自动移除
3. **条件编译**`#if MEOWMENT_DEBUG_TOOL` 块内的代码在包卸载后会被编译器忽略
### 最佳实践
**推荐做法**
- 所有使用 `DebugButtonAttribute` 的地方都用 `#if MEOWMENT_DEBUG_TOOL` 包裹
- 所有调用 `UniversalDebugTool` API 的地方都用条件编译包裹
- 将所有调试方法集中在一个或几个专门的类中
**不推荐做法**
- 直接使用特性而不加条件编译
- 在核心业务逻辑中混入调试代码
- 依赖调试工具的功能来实现正式功能
### 常见问题
**Q: 为什么要使用条件编译?**
A: 当你卸载这个包时,`DebugButtonAttribute` 类就不存在了,会导致编译错误。使用条件编译可以让代码在包不存在时被编译器忽略。
**Q: 忘记加条件编译怎么办?**
A: 如果卸载包后出现编译错误,只需在报错的代码前后加上 `#if MEOWMENT_DEBUG_TOOL``#endif` 即可。
**Q: 可以在正式发布版本中使用吗?**
A: 可以!条件编译的代码不会影响性能。但建议在正式版本中完全移除或禁用调试工具包。
**Q: 如何完全禁用调试工具?**
A: 在 Player Settings → Scripting Define Symbols 中移除 `MEOWMENT_DEBUG_TOOL`,或直接卸载这个包。
### 参考
- [Unity 条件编译文档](https://docs.unity3d.com/Manual/PlatformDependentCompilation.html)
- [Package Manager 版本定义](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html#define-symbols)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ab342e44111b45e409658513642a8ea9
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,12 +1,15 @@
using UnityEngine;
using UnityEditor;
using MeowmentDebugTool;
/// <summary>
/// 诊断UniversalDebugTool的引用问题
/// </summary>
public class DebugToolDiagnostic : EditorWindow
namespace MeowmentDebugTool.Editor
{
[MenuItem("Tools/Debug Tool/诊断引用问题")]
/// <summary>
/// 诊断UniversalDebugTool的引用问题
/// </summary>
public class DebugToolDiagnostic : EditorWindow
{
[MenuItem("Tools/Debug Tool/诊断引用问题")]
public static void ShowWindow()
{
var window = GetWindow<DebugToolDiagnostic>("调试工具诊断");
@ -124,4 +127,5 @@ public class DebugToolDiagnostic : EditorWindow
}
}
}
}
}

View File

@ -2,14 +2,17 @@ using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
using TMPro;
using MeowmentDebugTool;
/// <summary>
/// 通用调试工具预制件生成器
/// 用于创建UniversalDebugTool所需的完整UI预制件
/// </summary>
public class DebugToolPrefabGenerator : EditorWindow
namespace MeowmentDebugTool.Editor
{
private string prefabSavePath = "Assets/UniversalDebugTool.prefab";
/// <summary>
/// 通用调试工具预制件生成器
/// 用于创建UniversalDebugTool所需的完整UI预制件
/// </summary>
public class DebugToolPrefabGenerator : EditorWindow
{
private string prefabSavePath = "Assets/UniversalDebugTool.prefab";
[MenuItem("Tools/Debug Tool/生成调试工具预制件")]
public static void ShowWindow()
@ -972,4 +975,5 @@ public class DebugToolPrefabGenerator : EditorWindow
so.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,52 @@
using UnityEditor;
using UnityEngine;
namespace MeowmentDebugTool.Editor
{
/// <summary>
/// 自动添加 MEOWMENT_DEBUG_TOOL 宏定义
/// </summary>
[InitializeOnLoad]
public static class DefineSymbolsManager
{
private const string DEFINE_SYMBOL = "MEOWMENT_DEBUG_TOOL";
static DefineSymbolsManager()
{
AddDefineSymbol();
}
private static void AddDefineSymbol()
{
var target = EditorUserBuildSettings.selectedBuildTargetGroup;
// 获取当前已定义的符号
string definesString = PlayerSettings.GetScriptingDefineSymbolsForGroup(target);
var defines = definesString.Split(';');
// 检查是否已存在
bool exists = false;
foreach (var define in defines)
{
if (define.Trim() == DEFINE_SYMBOL)
{
exists = true;
break;
}
}
// 如果不存在则添加
if (!exists)
{
if (!string.IsNullOrEmpty(definesString))
{
definesString += ";";
}
definesString += DEFINE_SYMBOL;
PlayerSettings.SetScriptingDefineSymbolsForGroup(target, definesString);
Debug.Log($"[MeowmentDebugTool] 已添加宏定义: {DEFINE_SYMBOL}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5698ea2959bec274ea3e28b53f9ba8e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +1,8 @@
{
"name": "Hontbei.Meowmentdebugtool.Editor",
"name": "com.bywaystudios.meowmentdebugtool.Editor",
"rootNamespace": "",
"references": [
"Hontbei.Meowmentdebugtool",
"com.bywaystudios.meowmentdebugtool",
"Unity.TextMeshPro"
],
"includePlatforms": [

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ee28ff85037a9844f95ee9595c069fa4
guid: a7ea52de8e3323042b5161a7449156b4
AssemblyDefinitionImporter:
externalObjects: {}
userData:

View File

@ -51,32 +51,66 @@
## 快速开始
### 1. 在场景中生成调试工具预制体
### 1. 在代码中初始化调试工具
在 Unity 菜单中执行
调试工具现在支持**运行时自动生成UI**,无需手动创建预制体。只需在代码中调用
> Tools → Debug Tool → 生成调试工具预制件
```csharp
using UnityEngine;
using MeowmentDebugTool;
using TMPro;
在弹窗中选择保存路径(默认:`Assets/UniversalDebugTool.prefab`),点击“生成预制件”。
public class GameInitializer : MonoBehaviour
{
public TMP_FontAsset customFont; // 可选:自定义字体
void Start()
{
// 初始化调试工具自动创建UI
UniversalDebugTool.Init();
// 可选设置自定义SDF字体
if (customFont != null)
{
UniversalDebugTool.SetSDFFont(customFont);
}
}
}
```
生成后的预制体包含:
**初始化说明:**
- 一个独立的 `Canvas`UI 根节点,排序顺序很高,保证在最上层渲染)。
- `UniversalDebugTool` 主组件和其所需的 UI 层级。
- 主调试窗口(含标签页、参数页、自定义按钮页、工具栏页、设置页)。
- 可拖拽的调试悬浮按钮。
- `UniversalDebugTool.Init()` 会自动创建完整的UI系统包括
- 独立的 Canvas排序顺序 30000保证在最上层渲染
- 主调试窗口(含标签页、参数页、自定义按钮页、工具栏页、设置页)
- 可拖拽的调试悬浮按钮
- EventSystem如果场景中没有
- `UniversalDebugTool.SetSDFFont()` 可以为所有UI文本设置自定义字体可选
### 2. 把预制体放入场景
### 2. 运行时使用
1. 在 Project 窗口中找到生成的 `UniversalDebugTool` 预制体。
2. 将其拖入需要调试的场景(建议放到常驻/启动场景里)。
3. 进入 Play 模式即可看到调试窗口。
进入 Play 模式后:
默认行为:
- 默认显示主调试窗口在左上角
- 点击左上角的 **X** 按钮,会隐藏主窗口并显示悬浮按钮
- 拖动悬浮按钮改变位置(自动吸附边缘)
- 点击悬浮按钮可重新打开主窗口(拖动不会误触)
- 进入游戏即显示主调试窗口,并隐藏悬浮按钮。
- 点击顶部左侧关闭按钮,会隐藏主窗口并显示悬浮按钮。
- 拖动悬浮按钮改变位置;点击一次可重新打开主窗口。
### 3. 条件编译(推荐)
为了确保在移除包后不影响主工程,建议使用条件编译:
```csharp
void Start()
{
#if MEOWMENT_DEBUG_TOOL
UniversalDebugTool.Init();
UniversalDebugTool.SetSDFFont(customFont);
#endif
}
```
`MEOWMENT_DEBUG_TOOL` 宏会在包安装时自动定义,卸载时自动移除。
---
@ -179,14 +213,25 @@ public static class DemoInputDebug
以下方法均定义在 `UniversalDebugTool` 中:
**初始化和配置:**
- `static void Init()`初始化调试工具自动创建完整的UI系统包括Canvas、主窗口、悬浮按钮、EventSystem等
- `static void SetSDFFont(TMP_FontAsset fontAsset)`为所有UI文本设置自定义SDF字体包括预制件模板和后续创建的按钮。
**显示控制:**
- `static bool InstanceExists`:当前场景中是否存在实例。
- `static UniversalDebugTool Instance`:单例实例(通过 `FindObjectOfType` 查找)。
- `static UniversalDebugTool Instance`:单例实例。
- `static void Show()`:显示调试工具 GameObject。
- `static void Hide()`:隐藏调试工具 GameObject。
- `static void Toggle()`:在“窗口显示”和“窗口关闭 + 显示悬浮按钮”之间切换。
- `static void Toggle()`:在"窗口显示"和"窗口关闭 + 显示悬浮按钮"之间切换。
**自定义按钮:**
- `void ReloadCustomButtons()`:重新扫描并生成自定义按钮。
**输入对话框:**
- `static void ShowInputDialog(string title, Action<string> onConfirmAction, string initialValue = "", TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)`:显示输入对话框。
- `static void CloseInputDialog()`:关闭输入对话框并清理回调。
**信息查看:**
- `void CopyDeviceInfoToClipboard()` / `void CopySystemInfoToClipboard()`:复制对应信息到系统剪贴板。
你也可以在自己项目的 UI / 快捷键逻辑中调用这些方法,例如:
@ -206,11 +251,25 @@ void Update()
## 注意事项
- 依赖:
- Unity 新 UI`UnityEngine.UI`)。
- TextMesh Pro`TMPro`)。
- 默认 UI 参考分辨率为 1080×2340建议以手机竖屏调试为主。可在“设置”页中修改窗口大小以适配不同设备。
- 工具栏中的“时间调整”逻辑仅是示例,需要你在 `AdjustGameTime` 中实现自己项目的时间修改逻辑。
- 悬浮按钮依赖 `DraggableFloatingButton` 组件,请不要从生成的预制体中移除此组件,否则会失去拖拽/吸附功能。
**依赖:**
- Unity 新 UI`UnityEngine.UI`
- TextMesh Pro`TMPro`
- 包会自动创建 EventSystem如果场景中没有
如需进一步定制(增删标签页、调整布局、修改颜色风格等),推荐直接在生成的 `UniversalDebugTool` 预制体上修改 UI 结构和 `UniversalDebugTool` 组件的序列化字段。
**使用建议:**
- 默认 UI 参考分辨率为 1080×2340适合手机竖屏调试。可在"设置"页中修改窗口大小以适配不同设备。
- 主窗口锚点在左上角,不受分辨率变化影响。
- 悬浮按钮可在全屏范围内拖动,会自动吸附到边缘。
- 工具栏中的"时间调整"逻辑仅是示例,需要在 `AdjustGameTime` 中实现实际的时间修改逻辑。
**字体设置:**
- 所有 UI 文本默认使用 TMP 的默认字体。
- 调用 `SetSDFFont()` 可以为所有文本(包括后续创建的自定义按钮)设置自定义字体。
**条件编译:**
- 建议使用 `#if MEOWMENT_DEBUG_TOOL` 包裹调试工具相关代码。
- 包安装时会自动定义 `MEOWMENT_DEBUG_TOOL` 宏,卸载时自动移除。
- 这样可以确保移除包后不影响主工程编译。
**进一步定制:**
如需修改 UI 布局、颜色、标签页等,可以直接修改 `RuntimeUIGenerator.cs` 中的 UI 生成逻辑。

View File

@ -0,0 +1,235 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace MeowmentDebugTool
{
/// <summary>
/// 可拖动的悬浮按钮
/// 点击打开调试工具,可以在屏幕上自由拖动
/// </summary>
public class DraggableFloatingButton : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("设置")]
[Tooltip("是否限制在屏幕范围内")]
public bool clampToScreen = true;
[Tooltip("与屏幕边缘的最小距离")]
public float edgePadding = 20f;
[Header("自动吸附")]
[Tooltip("是否自动吸附到屏幕边缘")]
public bool snapToEdge = true;
[Tooltip("吸附动画时间")]
public float snapDuration = 0.3f;
private RectTransform rectTransform;
private Canvas canvas;
private Vector2 dragOffset;
private bool isDragging = false;
private Vector2 targetPosition;
private bool isSnapping = false;
// 用于区分点击和拖动
private Vector2 dragStartPosition;
private const float clickThreshold = 5f; // 小于这个距离算点击
private bool wasDragging = false; // 标记刚才是否拖动过
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
if (canvas == null)
{
Debug.LogError("DraggableFloatingButton 必须在Canvas下");
}
targetPosition = rectTransform.anchoredPosition;
}
private void Update()
{
// 平滑移动到目标位置(吸附动画)
if (isSnapping && !isDragging)
{
rectTransform.anchoredPosition = Vector2.Lerp(
rectTransform.anchoredPosition,
targetPosition,
Time.deltaTime / snapDuration
);
if (Vector2.Distance(rectTransform.anchoredPosition, targetPosition) < 0.5f)
{
rectTransform.anchoredPosition = targetPosition;
isSnapping = false;
}
}
}
public void OnBeginDrag(PointerEventData eventData)
{
isDragging = true;
isSnapping = false;
// 记录起始位置,用于判断是点击还是拖动
dragStartPosition = eventData.position;
// 计算拖动偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
dragOffset = rectTransform.anchoredPosition - localPoint;
}
public void OnDrag(PointerEventData eventData)
{
if (!isDragging) return;
// 转换屏幕坐标到Canvas局部坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
// 应用拖动偏移
Vector2 newPosition = localPoint + dragOffset;
// 限制在屏幕范围内
if (clampToScreen)
{
newPosition = ClampToScreen(newPosition);
}
rectTransform.anchoredPosition = newPosition;
}
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
// 计算拖动距离
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
// 如果拖动距离超过阈值,说明是拖动而不是点击
if (dragDistance > clickThreshold)
{
wasDragging = true;
// 在下一帧重置标记
StartCoroutine(ResetDragFlag());
}
// 自动吸附到最近的边缘
if (snapToEdge)
{
SnapToNearestEdge();
}
}
private System.Collections.IEnumerator ResetDragFlag()
{
yield return null; // 等待一帧
wasDragging = false;
}
/// <summary>
/// 检查是否刚拖动过用于Button点击事件判断
/// </summary>
public bool GetWasDragging()
{
return wasDragging;
}
private Vector2 ClampToScreen(Vector2 position)
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 buttonSize = rectTransform.sizeDelta;
// 计算可移动范围
float minX = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
float maxX = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
float minY = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
float maxY = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
position.x = Mathf.Clamp(position.x, minX, maxX);
position.y = Mathf.Clamp(position.y, minY, maxY);
return position;
}
private void SnapToNearestEdge()
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 currentPos = rectTransform.anchoredPosition;
// 计算到各个边缘的距离
float distToLeft = Mathf.Abs(currentPos.x + canvasSize.x / 2);
float distToRight = Mathf.Abs(currentPos.x - canvasSize.x / 2);
float distToTop = Mathf.Abs(currentPos.y - canvasSize.y / 2);
float distToBottom = Mathf.Abs(currentPos.y + canvasSize.y / 2);
// 找到最近的边
float minDist = Mathf.Min(distToLeft, distToRight, distToTop, distToBottom);
Vector2 snapPosition = currentPos;
Vector2 buttonSize = rectTransform.sizeDelta;
if (minDist == distToLeft)
{
// 吸附到左边
snapPosition.x = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
}
else if (minDist == distToRight)
{
// 吸附到右边
snapPosition.x = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
}
else if (minDist == distToTop)
{
// 吸附到顶部
snapPosition.y = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
}
else if (minDist == distToBottom)
{
// 吸附到底部
snapPosition.y = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
}
// 确保在屏幕范围内
snapPosition = ClampToScreen(snapPosition);
targetPosition = snapPosition;
isSnapping = true;
}
/// <summary>
/// 设置悬浮按钮位置
/// </summary>
public void SetPosition(Vector2 position)
{
if (clampToScreen)
{
position = ClampToScreen(position);
}
rectTransform.anchoredPosition = position;
targetPosition = position;
}
/// <summary>
/// 获取当前位置
/// </summary>
public Vector2 GetPosition()
{
return rectTransform.anchoredPosition;
}
}
}

View File

@ -0,0 +1,812 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 运行时UI生成器 - 自动创建调试工具的完整UI
/// </summary>
public static class RuntimeUIGenerator
{
private const int CANVAS_WIDTH = 1080;
private const int CANVAS_HEIGHT = 2340;
/// <summary>
/// 创建完整的调试工具UI
/// </summary>
public static UniversalDebugTool CreateDebugToolUI()
{
// 检查并创建EventSystem
EnsureEventSystem();
// 创建根Canvas
GameObject canvasObj = new GameObject("UniversalDebugTool_Canvas");
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 30000;
canvas.overrideSorting = true;
CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(CANVAS_WIDTH, CANVAS_HEIGHT);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
canvasObj.AddComponent<GraphicRaycaster>();
// 创建 UniversalDebugTool 组件
UniversalDebugTool tool = canvasObj.AddComponent<UniversalDebugTool>();
// 创建主窗口
GameObject mainWindow = CreateMainWindow(canvasObj.transform);
// 创建悬浮按钮
GameObject floatingButton = CreateFloatingButton(canvasObj.transform);
// 创建标签页
GameObject tabContainer = mainWindow.transform.Find("TabButtonContainer").gameObject;
GameObject contentContainer = mainWindow.transform.Find("ContentContainer").gameObject;
GameObject parametersPage = CreateParametersPage(contentContainer.transform);
GameObject customButtonsPage = CreateCustomButtonsPage(contentContainer.transform);
GameObject toolbarPage = CreateToolbarPage(contentContainer.transform);
GameObject settingsPage = CreateSettingsPage(contentContainer.transform);
// 创建输入对话框
GameObject inputDialog = CreateInputDialog(canvasObj.transform);
// 设置引用
SetupReferences(tool, canvas, mainWindow, floatingButton,
parametersPage, customButtonsPage, toolbarPage, settingsPage, inputDialog);
return tool;
}
private static GameObject CreateMainWindow(Transform parent)
{
GameObject mainWindow = new GameObject("MainWindow");
mainWindow.transform.SetParent(parent, false);
RectTransform rect = mainWindow.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(0, 1);
rect.pivot = new Vector2(0, 1);
rect.sizeDelta = new Vector2(CANVAS_WIDTH, CANVAS_HEIGHT);
rect.anchoredPosition = Vector2.zero;
// 背景
Image bg = mainWindow.AddComponent<Image>();
bg.color = new Color(0.1f, 0.1f, 0.1f, 0.95f);
// 关闭按钮(左上角)
GameObject closeBtn = CreateButton("CloseButton", mainWindow.transform, "X");
RectTransform closeBtnRect = closeBtn.GetComponent<RectTransform>();
closeBtnRect.anchorMin = new Vector2(0, 1);
closeBtnRect.anchorMax = new Vector2(0, 1);
closeBtnRect.pivot = new Vector2(0, 1);
closeBtnRect.sizeDelta = new Vector2(100, 100);
closeBtnRect.anchoredPosition = new Vector2(10, -10);
closeBtn.GetComponent<Image>().color = new Color(0.8f, 0.2f, 0.2f, 1f);
// 标签按钮容器(从关闭按钮右侧开始)
GameObject tabContainer = new GameObject("TabButtonContainer");
tabContainer.transform.SetParent(mainWindow.transform, false);
RectTransform tabRect = tabContainer.AddComponent<RectTransform>();
tabRect.anchorMin = new Vector2(0, 1);
tabRect.anchorMax = new Vector2(1, 1);
tabRect.pivot = new Vector2(0, 1);
tabRect.sizeDelta = new Vector2(-120, 100);
tabRect.anchoredPosition = new Vector2(120, 0);
HorizontalLayoutGroup tabLayout = tabContainer.AddComponent<HorizontalLayoutGroup>();
tabLayout.childControlWidth = true;
tabLayout.childControlHeight = true;
tabLayout.childForceExpandWidth = true;
tabLayout.childForceExpandHeight = true;
tabLayout.spacing = 10;
tabLayout.padding = new RectOffset(10, 20, 10, 10);
// 内容容器
GameObject contentContainer = new GameObject("ContentContainer");
contentContainer.transform.SetParent(mainWindow.transform, false);
RectTransform contentRect = contentContainer.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 0);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 0.5f);
contentRect.offsetMin = new Vector2(20, 20);
contentRect.offsetMax = new Vector2(-20, -120);
return mainWindow;
}
private static GameObject CreateFloatingButton(Transform parent)
{
GameObject floatBtn = new GameObject("FloatingButton");
floatBtn.transform.SetParent(parent, false);
RectTransform rect = floatBtn.AddComponent<RectTransform>();
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0.5f, 0.5f);
rect.sizeDelta = new Vector2(120, 120);
rect.anchoredPosition = new Vector2(400, 0);
Image img = floatBtn.AddComponent<Image>();
img.color = new Color(0.2f, 0.6f, 1f, 0.8f);
Button btn = floatBtn.AddComponent<Button>();
// 添加文本
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(floatBtn.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
text.text = "调试";
text.fontSize = 36;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.white;
// 添加拖拽组件
DraggableFloatingButton draggable = floatBtn.AddComponent<DraggableFloatingButton>();
draggable.clampToScreen = true;
draggable.snapToEdge = true;
return floatBtn;
}
private static GameObject CreateParametersPage(Transform parent)
{
GameObject page = new GameObject("ParametersPage");
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
ScrollRect scroll = page.AddComponent<ScrollRect>();
scroll.horizontal = false;
scroll.vertical = true;
GameObject viewport = new GameObject("Viewport");
viewport.transform.SetParent(page.transform, false);
RectTransform vpRect = viewport.AddComponent<RectTransform>();
vpRect.anchorMin = Vector2.zero;
vpRect.anchorMax = Vector2.one;
vpRect.sizeDelta = Vector2.zero;
viewport.AddComponent<Image>().color = new Color(0, 0, 0, 0.1f);
viewport.AddComponent<Mask>().showMaskGraphic = false;
GameObject content = new GameObject("Content");
content.transform.SetParent(viewport.transform, false);
RectTransform contentRect = content.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentRect.sizeDelta = new Vector2(0, 2000);
VerticalLayoutGroup layout = content.AddComponent<VerticalLayoutGroup>();
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
layout.spacing = 20;
layout.padding = new RectOffset(20, 20, 20, 20);
ContentSizeFitter fitter = content.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scroll.viewport = vpRect;
scroll.content = contentRect;
// 设备信息文本
GameObject deviceInfo = CreateTextObject("DeviceInfo", content.transform, "设备信息");
// 系统信息文本
GameObject systemInfo = CreateTextObject("SystemInfo", content.transform, "系统信息");
return page;
}
private static GameObject CreateCustomButtonsPage(Transform parent)
{
GameObject page = new GameObject("CustomButtonsPage");
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
ScrollRect scroll = page.AddComponent<ScrollRect>();
GameObject viewport = new GameObject("Viewport");
viewport.transform.SetParent(page.transform, false);
RectTransform vpRect = viewport.AddComponent<RectTransform>();
vpRect.anchorMin = Vector2.zero;
vpRect.anchorMax = Vector2.one;
vpRect.sizeDelta = Vector2.zero;
viewport.AddComponent<Mask>().showMaskGraphic = false;
GameObject buttonContainer = new GameObject("ButtonContainer");
buttonContainer.transform.SetParent(viewport.transform, false);
RectTransform containerRect = buttonContainer.AddComponent<RectTransform>();
containerRect.anchorMin = new Vector2(0, 1);
containerRect.anchorMax = new Vector2(1, 1);
containerRect.pivot = new Vector2(0.5f, 1);
GridLayoutGroup grid = buttonContainer.AddComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(300, 100);
grid.spacing = new Vector2(20, 20);
grid.padding = new RectOffset(20, 20, 20, 20);
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
grid.constraintCount = 3;
ContentSizeFitter fitter = buttonContainer.AddComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scroll.viewport = vpRect;
scroll.content = containerRect;
return page;
}
private static GameObject CreateToolbarPage(Transform parent)
{
GameObject page = new GameObject("ToolbarPage");
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
// 时间调整面板
GameObject timePanel = new GameObject("TimeAdjustPanel");
timePanel.transform.SetParent(page.transform, false);
RectTransform timePanelRect = timePanel.AddComponent<RectTransform>();
timePanelRect.anchorMin = new Vector2(0, 0.7f);
timePanelRect.anchorMax = new Vector2(1, 1);
timePanelRect.offsetMin = new Vector2(20, 0);
timePanelRect.offsetMax = new Vector2(-20, -20);
VerticalLayoutGroup vLayout = timePanel.AddComponent<VerticalLayoutGroup>();
vLayout.spacing = 20;
vLayout.padding = new RectOffset(20, 20, 20, 20);
vLayout.childControlWidth = true;
vLayout.childControlHeight = false;
vLayout.childForceExpandWidth = true;
// 标题
GameObject title = CreateTextObject("Title", timePanel.transform, "时间调整");
title.GetComponent<TextMeshProUGUI>().fontSize = 36;
title.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
title.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
// 滑块
GameObject slider = CreateSlider("TimeAdjustSlider", timePanel.transform);
slider.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
// 数值显示
GameObject valueText = CreateTextObject("TimeAdjustValueText", timePanel.transform, "60 秒");
valueText.GetComponent<TextMeshProUGUI>().fontSize = 32;
valueText.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
valueText.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 50);
// 按钮容器
GameObject btnContainer = new GameObject("ButtonContainer");
btnContainer.transform.SetParent(timePanel.transform, false);
RectTransform btnRect = btnContainer.AddComponent<RectTransform>();
btnRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup hLayout = btnContainer.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = 20;
hLayout.childControlWidth = true;
hLayout.childControlHeight = true;
hLayout.childForceExpandWidth = true;
hLayout.childForceExpandHeight = true;
GameObject increaseBtn = CreateButton("TimeIncreaseButton", btnContainer.transform, "+10秒");
GameObject decreaseBtn = CreateButton("TimeDecreaseButton", btnContainer.transform, "-10秒");
return page;
}
private static GameObject CreateSettingsPage(Transform parent)
{
GameObject page = new GameObject("SettingsPage");
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
// 分辨率设置面板
GameObject resPanel = new GameObject("ResolutionPanel");
resPanel.transform.SetParent(page.transform, false);
RectTransform resPanelRect = resPanel.AddComponent<RectTransform>();
resPanelRect.anchorMin = new Vector2(0, 0.6f);
resPanelRect.anchorMax = new Vector2(1, 1);
resPanelRect.offsetMin = new Vector2(20, 0);
resPanelRect.offsetMax = new Vector2(-20, -20);
VerticalLayoutGroup vLayout = resPanel.AddComponent<VerticalLayoutGroup>();
vLayout.spacing = 30;
vLayout.padding = new RectOffset(40, 40, 40, 40);
vLayout.childControlWidth = true;
vLayout.childControlHeight = false;
vLayout.childForceExpandWidth = true;
// 标题
GameObject title = CreateTextObject("Title", resPanel.transform, "分辨率设置");
title.GetComponent<TextMeshProUGUI>().fontSize = 36;
title.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
title.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
// 输入字段容器
GameObject inputContainer = new GameObject("InputContainer");
inputContainer.transform.SetParent(resPanel.transform, false);
RectTransform inputRect = inputContainer.AddComponent<RectTransform>();
inputRect.sizeDelta = new Vector2(0, 120);
HorizontalLayoutGroup hLayout = inputContainer.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = 20;
hLayout.childControlWidth = true;
hLayout.childControlHeight = true;
hLayout.childForceExpandWidth = true;
hLayout.childForceExpandHeight = true;
// 宽度输入
GameObject widthInput = CreateInputField("WidthInputField", inputContainer.transform, "1080");
widthInput.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 100);
// x 分隔符
GameObject separator = CreateTextObject("X", inputContainer.transform, "×");
separator.GetComponent<TextMeshProUGUI>().fontSize = 48;
separator.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
separator.GetComponent<RectTransform>().sizeDelta = new Vector2(80, 100);
// 高度输入
GameObject heightInput = CreateInputField("HeightInputField", inputContainer.transform, "2340");
heightInput.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 100);
// 按钮容器
GameObject btnContainer = new GameObject("ButtonContainer");
btnContainer.transform.SetParent(resPanel.transform, false);
RectTransform btnRect = btnContainer.AddComponent<RectTransform>();
btnRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup btnLayout = btnContainer.AddComponent<HorizontalLayoutGroup>();
btnLayout.spacing = 20;
btnLayout.childControlWidth = true;
btnLayout.childControlHeight = true;
btnLayout.childForceExpandWidth = true;
btnLayout.childForceExpandHeight = true;
GameObject applyBtn = CreateButton("ApplyResolutionButton", btnContainer.transform, "应用");
GameObject resetBtn = CreateButton("ResetResolutionButton", btnContainer.transform, "重置");
// 当前分辨率显示
GameObject currentText = CreateTextObject("CurrentResolutionText", resPanel.transform, "当前: 1080×2340");
currentText.GetComponent<TextMeshProUGUI>().fontSize = 28;
currentText.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
currentText.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
return page;
}
private static GameObject CreateInputDialog(Transform parent)
{
GameObject dialog = new GameObject("InputDialog");
dialog.transform.SetParent(parent, false);
RectTransform rect = dialog.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
// 半透明背景
Image bg = dialog.AddComponent<Image>();
bg.color = new Color(0, 0, 0, 0.8f);
// 面板
GameObject panel = new GameObject("Panel");
panel.transform.SetParent(dialog.transform, false);
RectTransform panelRect = panel.AddComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(900, 500);
panel.AddComponent<Image>().color = new Color(0.2f, 0.2f, 0.2f, 1f);
VerticalLayoutGroup vLayout = panel.AddComponent<VerticalLayoutGroup>();
vLayout.spacing = 30;
vLayout.padding = new RectOffset(40, 40, 40, 40);
vLayout.childControlWidth = true;
vLayout.childControlHeight = false;
vLayout.childForceExpandWidth = true;
// 标题
GameObject title = CreateTextObject("Title", panel.transform, "输入");
title.GetComponent<TextMeshProUGUI>().fontSize = 40;
title.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
title.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 70);
// 输入框
GameObject inputField = CreateInputField("InputField", panel.transform, "请输入...");
inputField.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 120);
// 按钮容器
GameObject btnContainer = new GameObject("ButtonContainer");
btnContainer.transform.SetParent(panel.transform, false);
RectTransform btnRect = btnContainer.AddComponent<RectTransform>();
btnRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup hLayout = btnContainer.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = 40;
hLayout.childControlWidth = true;
hLayout.childControlHeight = true;
hLayout.childForceExpandWidth = true;
hLayout.childForceExpandHeight = true;
GameObject confirmBtn = CreateButton("ConfirmButton", btnContainer.transform, "确定");
confirmBtn.GetComponent<Image>().color = new Color(0.2f, 0.6f, 0.2f, 1f);
GameObject cancelBtn = CreateButton("CancelButton", btnContainer.transform, "取消");
cancelBtn.GetComponent<Image>().color = new Color(0.6f, 0.2f, 0.2f, 1f);
dialog.SetActive(false);
return dialog;
}
#region Helper Methods
/// <summary>
/// 获取TMP默认字体如果没有则返回null
/// </summary>
private static TMP_FontAsset GetDefaultFont()
{
// 尝试获取TMP的默认字体
TMP_FontAsset defaultFont = TMP_Settings.defaultFontAsset;
// 如果没有默认字体尝试加载LiberationSans
if (defaultFont == null)
{
defaultFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
}
return defaultFont;
}
private static GameObject CreateButton(string name, Transform parent, string text)
{
GameObject btn = new GameObject(name);
btn.transform.SetParent(parent, false);
RectTransform rect = btn.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(200, 80);
Image img = btn.AddComponent<Image>();
img.color = new Color(0.3f, 0.3f, 0.3f, 1f);
Button button = btn.AddComponent<Button>();
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(btn.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
TextMeshProUGUI tmp = textObj.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = 32;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
tmp.font = defaultFont;
}
return btn;
}
private static GameObject CreateTextObject(string name, Transform parent, string text)
{
GameObject textObj = new GameObject(name);
textObj.transform.SetParent(parent, false);
RectTransform rect = textObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(800, 200);
TextMeshProUGUI tmp = textObj.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = 28;
tmp.alignment = TextAlignmentOptions.TopLeft;
tmp.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
tmp.font = defaultFont;
}
return textObj;
}
private static GameObject CreateInputField(string name, Transform parent, string placeholder)
{
GameObject inputObj = new GameObject(name);
inputObj.transform.SetParent(parent, false);
RectTransform rect = inputObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(600, 80);
Image img = inputObj.AddComponent<Image>();
img.color = new Color(0.15f, 0.15f, 0.15f, 1f);
TMP_InputField input = inputObj.AddComponent<TMP_InputField>();
GameObject textArea = new GameObject("TextArea");
textArea.transform.SetParent(inputObj.transform, false);
RectTransform taRect = textArea.AddComponent<RectTransform>();
taRect.anchorMin = Vector2.zero;
taRect.anchorMax = Vector2.one;
taRect.sizeDelta = Vector2.zero;
taRect.offsetMin = new Vector2(10, 10);
taRect.offsetMax = new Vector2(-10, -10);
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(textArea.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.sizeDelta = Vector2.zero;
TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
text.fontSize = 32;
text.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
text.font = defaultFont;
}
GameObject placeholderObj = new GameObject("Placeholder");
placeholderObj.transform.SetParent(textArea.transform, false);
RectTransform phRect = placeholderObj.AddComponent<RectTransform>();
phRect.anchorMin = Vector2.zero;
phRect.anchorMax = Vector2.one;
phRect.sizeDelta = Vector2.zero;
TextMeshProUGUI phText = placeholderObj.AddComponent<TextMeshProUGUI>();
phText.text = placeholder;
phText.fontSize = 32;
phText.color = new Color(1, 1, 1, 0.5f);
// 设置默认字体
if (defaultFont != null)
{
phText.font = defaultFont;
}
input.textViewport = taRect;
input.textComponent = text;
input.placeholder = phText;
return inputObj;
}
private static GameObject CreateSlider(string name, Transform parent)
{
GameObject sliderObj = new GameObject(name);
sliderObj.transform.SetParent(parent, false);
RectTransform rect = sliderObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(800, 60);
Slider slider = sliderObj.AddComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 300;
slider.value = 60;
// Background
GameObject bg = new GameObject("Background");
bg.transform.SetParent(sliderObj.transform, false);
RectTransform bgRect = bg.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.sizeDelta = Vector2.zero;
bg.AddComponent<Image>().color = new Color(0.2f, 0.2f, 0.2f, 1f);
// Fill Area
GameObject fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(sliderObj.transform, false);
RectTransform faRect = fillArea.AddComponent<RectTransform>();
faRect.anchorMin = Vector2.zero;
faRect.anchorMax = Vector2.one;
faRect.sizeDelta = Vector2.zero;
GameObject fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
RectTransform fillRect = fill.AddComponent<RectTransform>();
fillRect.anchorMin = Vector2.zero;
fillRect.anchorMax = Vector2.one;
fillRect.sizeDelta = Vector2.zero;
fill.AddComponent<Image>().color = new Color(0.2f, 0.6f, 1f, 1f);
slider.fillRect = fillRect;
// Handle
GameObject handleArea = new GameObject("Handle Slide Area");
handleArea.transform.SetParent(sliderObj.transform, false);
RectTransform haRect = handleArea.AddComponent<RectTransform>();
haRect.anchorMin = Vector2.zero;
haRect.anchorMax = Vector2.one;
haRect.sizeDelta = Vector2.zero;
GameObject handle = new GameObject("Handle");
handle.transform.SetParent(handleArea.transform, false);
RectTransform handleRect = handle.AddComponent<RectTransform>();
handleRect.sizeDelta = new Vector2(40, 60);
handle.AddComponent<Image>().color = Color.white;
slider.handleRect = handleRect;
slider.targetGraphic = handle.GetComponent<Image>();
return sliderObj;
}
private static GameObject CreateCustomButtonPrefab()
{
// 创建一个隐藏的临时父对象来存放预制件模板
GameObject tempParent = new GameObject("_TempPrefabHolder");
tempParent.SetActive(false);
Object.DontDestroyOnLoad(tempParent);
GameObject btn = CreateButton("CustomButton", tempParent.transform, "按钮");
btn.GetComponent<RectTransform>().sizeDelta = new Vector2(280, 90);
// 确保预制件的TextMeshProUGUI也能被SetSDF设置
// 通过将其标记到特殊层或添加特殊标签
btn.tag = "Untagged"; // 保持默认,确保能被找到
return btn;
}
private static GameObject CreateTabButtonPrefab()
{
// 使用同一个临时父对象
GameObject tempParent = GameObject.Find("_TempPrefabHolder");
if (tempParent == null)
{
tempParent = new GameObject("_TempPrefabHolder");
tempParent.SetActive(false);
Object.DontDestroyOnLoad(tempParent);
}
GameObject btn = CreateButton("TabButton", tempParent.transform, "标签");
btn.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 80);
// 确保预制件的TextMeshProUGUI也能被SetSDF设置
btn.tag = "Untagged"; // 保持默认,确保能被找到
return btn;
}
/// <summary>
/// 确保场景中有EventSystem如果没有则创建一个
/// </summary>
private static void EnsureEventSystem()
{
EventSystem eventSystem = Object.FindObjectOfType<EventSystem>();
if (eventSystem == null)
{
GameObject eventSystemObj = new GameObject("EventSystem");
eventSystem = eventSystemObj.AddComponent<EventSystem>();
eventSystemObj.AddComponent<StandaloneInputModule>();
Object.DontDestroyOnLoad(eventSystemObj);
Debug.Log("[RuntimeUIGenerator] 已自动创建EventSystem");
}
else
{
Debug.Log("[RuntimeUIGenerator] EventSystem已存在");
}
}
private static void SetupReferences(UniversalDebugTool tool, Canvas canvas, GameObject mainWindow,
GameObject floatingButton, GameObject parametersPage, GameObject customButtonsPage,
GameObject toolbarPage, GameObject settingsPage, GameObject inputDialog)
{
// 使用反射设置私有字段
var type = typeof(UniversalDebugTool);
var flags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
// 主窗口
type.GetField("mainWindow", flags)?.SetValue(tool, mainWindow.GetComponent<RectTransform>());
type.GetField("canvas", flags)?.SetValue(tool, canvas);
type.GetField("tabButtonContainer", flags)?.SetValue(tool, mainWindow.transform.Find("TabButtonContainer").GetComponent<RectTransform>());
type.GetField("contentContainer", flags)?.SetValue(tool, mainWindow.transform.Find("ContentContainer").GetComponent<RectTransform>());
type.GetField("closeButton", flags)?.SetValue(tool, mainWindow.transform.Find("CloseButton").GetComponent<Button>());
// 悬浮按钮
type.GetField("floatingButton", flags)?.SetValue(tool, floatingButton);
type.GetField("draggableComponent", flags)?.SetValue(tool, floatingButton.GetComponent<DraggableFloatingButton>());
// 参数查看页面
type.GetField("parametersPage", flags)?.SetValue(tool, parametersPage);
type.GetField("deviceInfoText", flags)?.SetValue(tool, parametersPage.transform.Find("Viewport/Content/DeviceInfo").GetComponent<TextMeshProUGUI>());
type.GetField("systemInfoText", flags)?.SetValue(tool, parametersPage.transform.Find("Viewport/Content/SystemInfo").GetComponent<TextMeshProUGUI>());
type.GetField("parametersScrollRect", flags)?.SetValue(tool, parametersPage.GetComponent<ScrollRect>());
// 自定义按钮页面
type.GetField("customButtonsPage", flags)?.SetValue(tool, customButtonsPage);
type.GetField("buttonContainer", flags)?.SetValue(tool, customButtonsPage.transform.Find("Viewport/ButtonContainer").GetComponent<RectTransform>());
type.GetField("buttonsScrollRect", flags)?.SetValue(tool, customButtonsPage.GetComponent<ScrollRect>());
// 创建按钮预制件
GameObject buttonPrefab = CreateCustomButtonPrefab();
GameObject tabButtonPrefab = CreateTabButtonPrefab();
type.GetField("buttonPrefab", flags)?.SetValue(tool, buttonPrefab);
type.GetField("tabButtonPrefab", flags)?.SetValue(tool, tabButtonPrefab);
// 工具栏页面
type.GetField("toolbarPage", flags)?.SetValue(tool, toolbarPage);
Transform toolbarPanel = toolbarPage.transform.Find("TimeAdjustPanel");
if (toolbarPanel != null)
{
type.GetField("timeAdjustSlider", flags)?.SetValue(tool, toolbarPanel.Find("TimeAdjustSlider")?.GetComponent<Slider>());
type.GetField("timeAdjustValueText", flags)?.SetValue(tool, toolbarPanel.Find("TimeAdjustValueText")?.GetComponent<TextMeshProUGUI>());
Transform btnContainer = toolbarPanel.Find("ButtonContainer");
if (btnContainer != null)
{
type.GetField("timeIncreaseButton", flags)?.SetValue(tool, btnContainer.Find("TimeIncreaseButton")?.GetComponent<Button>());
type.GetField("timeDecreaseButton", flags)?.SetValue(tool, btnContainer.Find("TimeDecreaseButton")?.GetComponent<Button>());
}
}
// 设置页面
type.GetField("settingsPage", flags)?.SetValue(tool, settingsPage);
Transform resPanel = settingsPage.transform.Find("ResolutionPanel");
if (resPanel != null)
{
Transform inputContainer = resPanel.Find("InputContainer");
if (inputContainer != null)
{
type.GetField("widthInputField", flags)?.SetValue(tool, inputContainer.Find("WidthInputField")?.GetComponent<TMP_InputField>());
type.GetField("heightInputField", flags)?.SetValue(tool, inputContainer.Find("HeightInputField")?.GetComponent<TMP_InputField>());
}
Transform btnContainer = resPanel.Find("ButtonContainer");
if (btnContainer != null)
{
type.GetField("applyResolutionButton", flags)?.SetValue(tool, btnContainer.Find("ApplyResolutionButton")?.GetComponent<Button>());
type.GetField("resetResolutionButton", flags)?.SetValue(tool, btnContainer.Find("ResetResolutionButton")?.GetComponent<Button>());
}
type.GetField("currentResolutionText", flags)?.SetValue(tool, resPanel.Find("CurrentResolutionText")?.GetComponent<TextMeshProUGUI>());
}
// 输入对话框
type.GetField("inputDialog", flags)?.SetValue(tool, inputDialog);
Transform dialogPanel = inputDialog.transform.Find("Panel");
if (dialogPanel != null)
{
type.GetField("inputDialogTitle", flags)?.SetValue(tool, dialogPanel.Find("Title")?.GetComponent<TextMeshProUGUI>());
type.GetField("inputDialogInputField", flags)?.SetValue(tool, dialogPanel.Find("InputField")?.GetComponent<TMP_InputField>());
Transform btnContainer = dialogPanel.Find("ButtonContainer");
if (btnContainer != null)
{
type.GetField("inputDialogConfirmBtn", flags)?.SetValue(tool, btnContainer.Find("ConfirmButton")?.GetComponent<Button>());
type.GetField("inputDialogCancelBtn", flags)?.SetValue(tool, btnContainer.Find("CancelButton")?.GetComponent<Button>());
}
}
Debug.Log("[RuntimeUIGenerator] 所有引用已设置完成");
}
#endregion
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ddf96f73f2918f64f908000cd6d4de2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -6,11 +6,17 @@ using UnityEngine;
using UnityEngine.UI;
using TMPro;
/// <summary>
/// 通用调试工具 - 支持多标签页的调试界面系统
/// 默认分辨率: 1080x2340
/// </summary>
public class UniversalDebugTool : MonoBehaviour
namespace MeowmentDebugTool
{
/// <summary>
/// 通用调试工具 - 支持多标签页的调试界面系统
/// 默认分辨率: 1080x2340
/// 使用方法:
/// 1. 场景中放置 UniversalDebugTool 预制件
/// 2. 在代码中调用 UniversalDebugTool.Init() 初始化
/// 3. 未调用 Init() 前不会显示任何UI
/// </summary>
public class UniversalDebugTool : MonoBehaviour
{
#region
[Header("主窗口设置")]
@ -66,6 +72,7 @@ public class UniversalDebugTool : MonoBehaviour
#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 = "";
@ -76,6 +83,12 @@ public class UniversalDebugTool : MonoBehaviour
// 自定义按钮回调
private static Action<Button, TMP_Text> customButtonCallback;
// Canvas层级设置
private const int TOP_SORT_ORDER = 30000;
// 保存的SDF字体资源
private static TMP_FontAsset savedFontAsset = null;
#endregion
#region
@ -100,6 +113,7 @@ public class UniversalDebugTool : MonoBehaviour
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
@ -108,17 +122,33 @@ public class UniversalDebugTool : MonoBehaviour
}
currentCustomResolution = defaultResolution;
InitializeDebugTool();
// 设置Canvas为最上层
SetCanvasToTop();
// 未初始化前隐藏所有UI
if (!isInitialized)
{
HideAllUI();
}
// 注意如果isInitialized为true不在Awake中调用InitializeDebugTool
// 因为此时反射设置的字段可能还没生效延迟到Start中调用
}
private void Start()
{
UpdateDeviceInfo();
UpdateSystemInfo();
LoadCustomButtons();
// 初始状态:显示主窗口,隐藏悬浮按钮
ShowMainWindow();
// 只有在已初始化但还没调用过InitializeDebugTool时才执行
// 这避免了Awake和Init()的重复调用问题
if (isInitialized && pages.Count == 0)
{
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
InitializeDebugTool();
UpdateDeviceInfo();
UpdateSystemInfo();
LoadCustomButtons();
ShowMainWindow();
}
}
private void OnDestroy()
@ -131,6 +161,115 @@ public class UniversalDebugTool : MonoBehaviour
#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.UpdateDeviceInfo();
Instance.UpdateSystemInfo();
Instance.LoadCustomButtons();
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. 重新加载自定义按钮使新字体应用到已存在的CustomButton
Debug.Log("[MeowmentDebugTool] 重新加载自定义按钮以应用新字体...");
Instance.ReloadCustomButtons();
}
/// <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...");
@ -206,7 +345,10 @@ public class UniversalDebugTool : MonoBehaviour
{
if (tabButtonPrefab == null || tabButtonContainer == null)
{
Debug.LogError("❌ 标签按钮预制件或容器未设置!请重新生成预制件。");
string errorDetails = $"tabButtonPrefab={(tabButtonPrefab == null ? "null" : "")}, " +
$"tabButtonContainer={(tabButtonContainer == null ? "null" : "")}";
Debug.LogWarning($"⚠️ [MeowmentDebugTool] 标签按钮预制件或容器未完全初始化:{errorDetails}");
Debug.LogWarning("💡 这可能是Unity初始化顺序问题如果标签正常显示则可以忽略此警告");
return;
}
@ -418,6 +560,12 @@ public class UniversalDebugTool : MonoBehaviour
if (buttonText != null)
{
buttonText.text = string.IsNullOrEmpty(attribute.DisplayName) ? method.Name : attribute.DisplayName;
// 应用保存的字体
if (savedFontAsset != null)
{
buttonText.font = savedFontAsset;
}
}
// 设置按钮颜色
@ -437,6 +585,9 @@ public class UniversalDebugTool : MonoBehaviour
// 调用回调
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
// 点击按钮后关闭主窗口
CloseDebugWindow();
}
catch (Exception e)
{
@ -736,28 +887,43 @@ public class UniversalDebugTool : MonoBehaviour
private void ShowMainWindow()
{
if (mainWindow != null)
mainWindow.gameObject.SetActive(true);
mainWindow.gameObject.SetActive(false);
if (floatingButton != null)
floatingButton.SetActive(false);
floatingButton.SetActive(true);
}
#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
}
#region
/// <summary>
/// 调试按钮特性 - 标记方法为调试按钮
/// </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

View File

@ -0,0 +1,15 @@
{
"name": "com.bywaystudios.meowmentdebugtool",
"rootNamespace": "",
"references": [
"Unity.TextMeshPro"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"noEngineReferences": false
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 3672213eff4fbe048ae6efde3d783518
guid: de9c38beb5c3ed94e83a7a91fbde340d
AssemblyDefinitionImporter:
externalObjects: {}
userData:

View File

@ -1,7 +1,7 @@
{
"name": "com.bywaystudios.meowmentdebugtool",
"displayName": "MeowmentDebugTool",
"version": "0.1.6",
"version": "0.2.2",
"description": "\u8c03\u8bd5\u5de5\u5177\uff0c\u96c6\u6210\u4e86\u5ba2\u6237\u7aef\u6d4b\u8bd5\u65f6\u7684\u5e38\u7528\u529f\u80fd\uff0c\u652f\u6301\u6269\u5c55\u81ea\u5b9a\u4e49\u6309\u94ae\uff0c\u65b9\u4fbf\u5f00\u53d1\u8005\u8c03\u8bd5\u548c\u6d4b\u8bd5\u3002",
"samples": [
{

View File

@ -1,232 +0,0 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 可拖动的悬浮按钮
/// 点击打开调试工具,可以在屏幕上自由拖动
/// </summary>
public class DraggableFloatingButton : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("设置")]
[Tooltip("是否限制在屏幕范围内")]
public bool clampToScreen = true;
[Tooltip("与屏幕边缘的最小距离")]
public float edgePadding = 20f;
[Header("自动吸附")]
[Tooltip("是否自动吸附到屏幕边缘")]
public bool snapToEdge = true;
[Tooltip("吸附动画时间")]
public float snapDuration = 0.3f;
private RectTransform rectTransform;
private Canvas canvas;
private Vector2 dragOffset;
private bool isDragging = false;
private Vector2 targetPosition;
private bool isSnapping = false;
// 用于区分点击和拖动
private Vector2 dragStartPosition;
private const float clickThreshold = 5f; // 小于这个距离算点击
private bool wasDragging = false; // 标记刚才是否拖动过
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
if (canvas == null)
{
Debug.LogError("DraggableFloatingButton 必须在Canvas下");
}
targetPosition = rectTransform.anchoredPosition;
}
private void Update()
{
// 平滑移动到目标位置(吸附动画)
if (isSnapping && !isDragging)
{
rectTransform.anchoredPosition = Vector2.Lerp(
rectTransform.anchoredPosition,
targetPosition,
Time.deltaTime / snapDuration
);
if (Vector2.Distance(rectTransform.anchoredPosition, targetPosition) < 0.5f)
{
rectTransform.anchoredPosition = targetPosition;
isSnapping = false;
}
}
}
public void OnBeginDrag(PointerEventData eventData)
{
isDragging = true;
isSnapping = false;
// 记录起始位置,用于判断是点击还是拖动
dragStartPosition = eventData.position;
// 计算拖动偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
dragOffset = rectTransform.anchoredPosition - localPoint;
}
public void OnDrag(PointerEventData eventData)
{
if (!isDragging) return;
// 转换屏幕坐标到Canvas局部坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint
);
// 应用拖动偏移
Vector2 newPosition = localPoint + dragOffset;
// 限制在屏幕范围内
if (clampToScreen)
{
newPosition = ClampToScreen(newPosition);
}
rectTransform.anchoredPosition = newPosition;
}
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
// 计算拖动距离
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
// 如果拖动距离超过阈值,说明是拖动而不是点击
if (dragDistance > clickThreshold)
{
wasDragging = true;
// 在下一帧重置标记
StartCoroutine(ResetDragFlag());
}
// 自动吸附到最近的边缘
if (snapToEdge)
{
SnapToNearestEdge();
}
}
private System.Collections.IEnumerator ResetDragFlag()
{
yield return null; // 等待一帧
wasDragging = false;
}
/// <summary>
/// 检查是否刚拖动过用于Button点击事件判断
/// </summary>
public bool GetWasDragging()
{
return wasDragging;
}
private Vector2 ClampToScreen(Vector2 position)
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 buttonSize = rectTransform.sizeDelta;
// 计算可移动范围
float minX = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
float maxX = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
float minY = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
float maxY = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
position.x = Mathf.Clamp(position.x, minX, maxX);
position.y = Mathf.Clamp(position.y, minY, maxY);
return position;
}
private void SnapToNearestEdge()
{
RectTransform canvasRect = canvas.transform as RectTransform;
Vector2 canvasSize = canvasRect.sizeDelta;
Vector2 currentPos = rectTransform.anchoredPosition;
// 计算到各个边缘的距离
float distToLeft = Mathf.Abs(currentPos.x + canvasSize.x / 2);
float distToRight = Mathf.Abs(currentPos.x - canvasSize.x / 2);
float distToTop = Mathf.Abs(currentPos.y - canvasSize.y / 2);
float distToBottom = Mathf.Abs(currentPos.y + canvasSize.y / 2);
// 找到最近的边
float minDist = Mathf.Min(distToLeft, distToRight, distToTop, distToBottom);
Vector2 snapPosition = currentPos;
Vector2 buttonSize = rectTransform.sizeDelta;
if (minDist == distToLeft)
{
// 吸附到左边
snapPosition.x = -canvasSize.x / 2 + buttonSize.x / 2 + edgePadding;
}
else if (minDist == distToRight)
{
// 吸附到右边
snapPosition.x = canvasSize.x / 2 - buttonSize.x / 2 - edgePadding;
}
else if (minDist == distToTop)
{
// 吸附到顶部
snapPosition.y = canvasSize.y / 2 - buttonSize.y / 2 - edgePadding;
}
else if (minDist == distToBottom)
{
// 吸附到底部
snapPosition.y = -canvasSize.y / 2 + buttonSize.y / 2 + edgePadding;
}
// 确保在屏幕范围内
snapPosition = ClampToScreen(snapPosition);
targetPosition = snapPosition;
isSnapping = true;
}
/// <summary>
/// 设置悬浮按钮位置
/// </summary>
public void SetPosition(Vector2 position)
{
if (clampToScreen)
{
position = ClampToScreen(position);
}
rectTransform.anchoredPosition = position;
targetPosition = position;
}
/// <summary>
/// 获取当前位置
/// </summary>
public Vector2 GetPosition()
{
return rectTransform.anchoredPosition;
}
}

View File

@ -1,8 +0,0 @@
{
"name": "Hontbei.Meowmentdebugtool",
"references": [
"Unity.TextMeshPro"
],
"includePlatforms": [],
"excludePlatforms": []
}

View File

@ -1,6 +1,5 @@
{
"dependencies": {
"com.bywaystudios.meowmentdebugtool": "0.1.4",
"com.unity.asset-store-validation": "0.6.0",
"com.unity.collab-proxy": "2.10.2",
"com.unity.editorcoroutines": "1.0.1",

View File

@ -1,7 +1,7 @@
{
"dependencies": {
"com.bywaystudios.meowmentdebugtool": {
"version": "file:com.bywaystudios.meowmentdebugtool@0.1.4",
"version": "file:com.bywaystudios.meowmentdebugtool",
"depth": 0,
"source": "embedded",
"dependencies": {

View File

@ -840,7 +840,8 @@ PlayerSettings:
webGLMemoryGeometricGrowthStep: 0.2
webGLMemoryGeometricGrowthCap: 96
webGLPowerPreference: 2
scriptingDefineSymbols: {}
scriptingDefineSymbols:
Standalone: MEOWMENT_DEBUG_TOOL
additionalCompilerArguments: {}
platformArchitecture: {}
scriptingBackend: {}

Binary file not shown.