3.0.17提交

This commit is contained in:
zhang hongbo 2026-02-03 10:32:06 +08:00
parent bdb24b05d1
commit 61a51df887
26 changed files with 1899 additions and 1456 deletions

View File

@ -1,215 +0,0 @@
# Console模块错误修复说明
## 🐛 已修复的问题
### 1. Rebuild Loop 错误
**问题描述**:
```
Trying to remove/add Background (UnityEngine.UI.Image) from/to rebuild list
while we are already inside a rebuild loop. This is not supported.
```
**原因**:
- 在 `OnLogMessageReceived` 回调中立即调用 `RefreshLogDisplay()` 刷新UI
- 在Unity的Canvas rebuild循环中修改UI元素会触发此错误
- `Canvas.ForceUpdateCanvases()` 在初始化时也会导致rebuild loop
**修复方案**:
#### ConsoleModule.cs
1. ✅ **延迟刷新机制**: 添加 `needRefresh` 标志不在回调中立即刷新UI
```csharp
private bool needRefresh = false;
// 在OnLogMessageReceived中
needRefresh = true; // 标记需要刷新
// 在Update中
if (needRefresh)
{
needRefresh = false;
RefreshLogDisplay(); // 延迟到下一帧刷新
}
```
2. ✅ **滚动位置延迟设置**: 使用帧计数延迟设置滚动位置
```csharp
private int lastRefreshFrame = -1;
// 只在刷新后的第2帧设置滚动位置
if (lockScroll && Time.frameCount > lastRefreshFrame + 1)
{
logScrollRect.verticalNormalizedPosition = 0f;
}
```
3. ✅ **过滤器事件优化**: 过滤器改变时也使用延迟刷新
```csharp
private void OnInfoFilterChanged(bool value)
{
infoFilter = value;
needRefresh = true; // 而不是立即RefreshLogDisplay()
}
```
#### SettingsModule.cs
4. ✅ **移除ForceUpdateCanvases**: Canvas会自动在下一帧更新
```csharp
private void ApplyResolution(Vector2 resolution)
{
mainWindow.sizeDelta = resolution;
currentResolutionText.text = $"当前窗口尺寸: {resolution.x} x {resolution.y}";
// 移除了 Canvas.ForceUpdateCanvases();
// Canvas会自动更新不需要强制刷新
}
```
### 2. 中文字体警告
**问题描述**:
```
The character with Unicode value \u5F84 was not found in the [LiberationSans SDF]
font asset or any potential fallbacks.
```
**原因**:
- RuntimeUIGenerator使用的默认字体 `LiberationSans SDF` 不支持中文字符
- TextMeshPro需要专门的中文字体资源
**解决方案** (选择其一):
#### 方案1: 使用中文字体 (推荐)
```csharp
// 在游戏初始化时设置中文字体
UniversalDebugTool.Init();
// 加载支持中文的TMP字体
TMP_FontAsset chineseFont = Resources.Load<TMP_FontAsset>("Fonts/ChineseFont SDF");
UniversalDebugTool.SetSDFFont(chineseFont);
```
#### 方案2: 禁用字体警告
在RuntimeUIGenerator的GetDefaultFont方法中添加过滤
```csharp
private static TMP_FontAsset GetDefaultFont()
{
// 禁用TMP字体警告
TMPro.TMP_Settings.warningsDisabled = true;
// 查找默认字体...
}
```
#### 方案3: 创建中文字体资源
1. 在Unity中Window > TextMeshPro > Font Asset Creator
2. 选择支持中文的字体如Arial Unicode MS、Microsoft YaHei等
3. 设置Character Set为Unicode或Custom Characters
4. 添加常用中文字符
5. 生成字体资源并在代码中加载
## 📋 修改总结
### 文件变更
#### ConsoleModule.cs
- ✅ 添加 `needRefresh` 标志
- ✅ 添加 `lastRefreshFrame` 帧计数
- ✅ 修改 `OnLogMessageReceived` 使用延迟刷新
- ✅ 修改 `Update` 方法处理延迟刷新和滚动
- ✅ 修改所有过滤器事件使用延迟刷新
#### SettingsModule.cs
- ✅ 移除 `Canvas.ForceUpdateCanvases()` 调用
### 性能影响
**更好**: 延迟刷新减少了不必要的UI重建
**更稳定**: 避免了rebuild loop错误
**无副作用**: 延迟一帧刷新对用户体验无影响
## 🧪 测试建议
### 1. 测试Rebuild Loop修复
```csharp
void Start()
{
UniversalDebugTool.Init();
// 快速生成大量日志
for (int i = 0; i < 100; i++)
{
Debug.Log($"测试日志 {i}");
Debug.LogWarning($"警告 {i}");
Debug.LogError($"错误 {i}");
}
// 不应该再有rebuild loop错误
}
```
### 2. 测试滚动功能
```csharp
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log($"新日志 - 帧数: {Time.frameCount}");
// 应该自动滚动到底部(如果锁定滚动已启用)
}
}
```
### 3. 测试过滤器
1. 生成多种类型的日志
2. 点击Console标签页
3. 切换Info/Warning/Error/Fatal过滤器
4. 不应该有rebuild loop错误
### 4. 测试分辨率设置
1. 进入Settings标签页
2. 修改宽度和高度
3. 点击"应用"按钮
4. 不应该有rebuild loop错误
## ⚠️ 注意事项
### 关于中文字体
如果你的游戏需要显示中文,必须:
1. 创建或导入支持中文的TMP字体资源
2. 在初始化后调用 `UniversalDebugTool.SetSDFFont(yourChineseFont)`
3. 或者在RuntimeUIGenerator中修改GetDefaultFont方法返回中文字体
### 最佳实践
```csharp
public class GameInit : MonoBehaviour
{
[SerializeField] private TMP_FontAsset chineseFont;
void Start()
{
// 1. 初始化调试工具
UniversalDebugTool.Init();
// 2. 设置中文字体(如果需要)
if (chineseFont != null)
{
UniversalDebugTool.SetSDFFont(chineseFont);
}
// 3. 其他初始化...
}
}
```
## ✅ 预期结果
修复后,你应该看到:
- ✅ 没有 "Trying to remove/add ... from/to rebuild list" 错误
- ✅ Console正常显示日志
- ✅ 过滤器正常工作
- ✅ 滚动功能正常
- ⚠️ 可能仍有字体警告(如果没有设置中文字体)
字体警告不影响功能,只是文本显示为方块。如果需要中文显示,请按照上述方案添加中文字体。

View File

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

View File

@ -5,6 +5,29 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [0.3.11] - 2025-12-30
### Added
- 添加按钮分组功能
## [0.3.9] - 2025-12-25
### Added
- **数值模块运行时控制**
- 新增 `UpdateDebugValue()` 公开接口,支持运行时动态修改数值特性的当前值
- 支持完整方法名("ClassName.MethodName")或简单方法名("MethodName")查找
- 自动限制值在特性定义的最小值和最大值范围内
- 同步更新UI滑块和输入框显示
## [0.3.8] - 2025-12-25
### Improved
- **控制台模块性能优化**
- 默认不显示Info和Warning类型日志Error和Fatal保持默认显示
- 对于被过滤的日志类型不创建LogItem UI元素大幅减少初始内存占用和UI开销
- 仅在用户手动激活对应过滤器时才创建和显示相关日志UI
- 显著提升启动性能特别是在大量Info/Warning日志场景下
## [0.3.7] - 2025-12-24

View File

@ -1,236 +0,0 @@
# Console 控制台模块 - 添加说明
## ✅ 已完成的工作
### 1. 新增文件
#### [LogNode.cs](Runtime/LogNode.cs)
日志节点数据结构,包含:
- LogType: 日志类型Info/Warning/Error/Exception
- LogMessage: 日志消息
- StackTrace: 堆栈跟踪
- LogTime: 日志时间
- LogFrameCount: 日志帧数
#### [ConsoleModule.cs](Runtime/ConsoleModule.cs)
控制台模块实现基于UGUI包含以下功能
- ✅ 实时捕获Unity日志
- ✅ 日志分类和过滤Info/Warning/Error/Fatal
- ✅ 锁定滚动到底部
- ✅ 点击查看详细堆栈信息
- ✅ 清除日志功能
- ✅ 日志颜色区分
- ✅ 对象池性能优化
- ✅ 最大日志行数限制500行
- ✅ SDF字体支持
### 2. 修改的文件
#### [UniversalDebugTool.cs](Runtime/UniversalDebugTool.cs)
添加了以下内容:
1. **序列化字段** - Console页面的所有UI组件引用
2. **ConsoleModule实例** - 控制台模块对象
3. **初始化代码** - 在InitializeDebugTool中创建和注册Console模块
4. **Update方法** - 调用consoleModule.Update()更新UI
5. **OnDestroy方法** - 清理控制台模块资源
6. **SetSDFFont支持** - 为Console模块应用字体
## 📋 需要在Unity中配置的UI
### Unity Inspector 配置清单
在UniversalDebugTool组件的Inspector中需要配置以下字段
```
[控制台页面]
├── Console Page (GameObject)
├── Console Log Scroll Rect (ScrollRect)
├── Console Log Content (RectTransform)
├── Console Detail Scroll Rect (ScrollRect)
├── Console Detail Text (TMP_Text)
├── Console Clear Button (Button)
├── Console Lock Scroll Toggle (Toggle)
├── Console Info Filter Toggle (Toggle)
├── Console Warning Filter Toggle (Toggle)
├── Console Error Filter Toggle (Toggle)
├── Console Fatal Filter Toggle (Toggle)
└── Console Log Item Prefab (GameObject)
```
### UI层次结构示例
```
ConsolePage
├── ControlPanel (HorizontalLayoutGroup)
│ ├── ClearButton
│ ├── LockScrollToggle
│ ├── Spacer (LayoutElement - FlexibleWidth)
│ ├── InfoFilterToggle
│ ├── WarningFilterToggle
│ ├── ErrorFilterToggle
│ └── FatalFilterToggle
├── LogArea (60% 高度)
│ └── LogScrollView (ScrollRect)
│ ├── Viewport
│ └── Content (VerticalLayoutGroup)
│ └── [日志项动态生成在这里]
└── DetailArea (40% 高度)
└── DetailScrollView (ScrollRect)
├── Viewport
└── Content
└── DetailText
```
### 日志项预制件 (ConsoleLogItemPrefab)
创建一个预制件,结构如下:
```
LogItem (GameObject)
├── Toggle (Toggle组件)
├── Background (Image)
└── Label (TextMeshPro - Text)
- RichText: 启用
- Word Wrapping: 启用
- Overflow: Overflow
```
## 🎨 UI组件配置建议
### ControlPanel
- Component: HorizontalLayoutGroup
- Padding: (10, 10, 10, 10)
- Spacing: 10
- Child Force Expand: Width = false, Height = false
### LogScrollView
- Component: ScrollRect
- Content: 指向LogContent
- Vertical: ✅
- Horizontal: ❌
- Movement Type: Elastic
- Inertia: ✅
- Scrollbar Visibility: Auto Hide
### LogContent
- Component: VerticalLayoutGroup
- Child Alignment: Upper Center
- Child Control Size: Height = ✅
- Child Force Expand: Width = ✅
- Spacing: 2
### DetailScrollView
- Component: ScrollRect
- Vertical: ✅
- Horizontal: ❌
- Movement Type: Clamped
### DetailText
- Rich Text: ✅
- Wrapping: ✅
- Overflow: Overflow
- Font Size: 14-16
### Toggle组件
- Is On: 根据默认值
- Toggle Transition: Fade
- 每个Toggle添加Label (TMP_Text)显示文本
## 🔧 代码集成
### 模块已自动集成
ConsoleModule已经在UniversalDebugTool中自动初始化和管理无需额外代码。
### 使用示例
```csharp
// 1. 初始化调试工具会自动初始化Console
UniversalDebugTool.Init();
// 2. 设置字体(可选)
UniversalDebugTool.SetSDFFont(myFont);
// 3. 获取日志统计(可选)
if (UniversalDebugTool.InstanceExists)
{
var console = UniversalDebugTool.Instance.consoleModule;
if (console != null)
{
console.GetLogCount(out int info, out int warning,
out int error, out int fatal);
Debug.Log($"日志: Info={info}, Warn={warning}, " +
$"Error={error}, Fatal={fatal}");
}
}
```
## 📊 与GameFramework对比
### 主要差异
| 项目 | GameFramework | MeowmentDebugTool |
|------|---------------|-------------------|
| UI系统 | OnGUI | UGUI |
| 渲染方式 | 即时模式(每帧重绘) | 保留模式(按需刷新) |
| 性能 | 中等 | 更优(对象池优化) |
| 布局方式 | GUILayout | LayoutGroup + RectTransform |
| 交互组件 | GUILayout.Button/Toggle | Button/Toggle组件 |
| 滚动视图 | GUILayout.BeginScrollView | ScrollRect组件 |
| 定制性 | 通过GUISkin | 完全UGUI定制 |
### 实现等价性
**完全实现的功能**
- 日志捕获Application.logMessageReceived
- 日志分类Info/Warning/Error/Fatal
- 过滤器4种日志类型独立过滤
- 锁定滚动
- 选中查看详情
- 清除日志
- 日志颜色区分
- 最大行数限制
**优化功能**
- 对象池(避免频繁创建销毁)
- UGUI性能更好
- 更灵活的布局系统
**未实现的功能**
- 复制到剪贴板可以通过DetailText选择复制
- SettingComponent持久化使用PlayerPrefs替代即可
## 🎯 后续步骤
1. **在Unity中创建UI**
- 在Scene或Prefab中创建Console页面UI
- 按照层次结构创建各个组件
- 创建日志项预制件
2. **配置引用**
- 在UniversalDebugTool组件Inspector中
- 将所有Console相关UI组件拖拽到对应字段
3. **测试**
- 运行游戏
- 调用 `UniversalDebugTool.Init()`
- 查看Console标签页
- 测试各个过滤器和功能
4. **调整样式**(可选)
- 修改颜色、字体大小
- 调整布局间距
- 优化日志项高度
## ⚠️ 注意事项
1. **日志项预制件必须有Toggle组件**
2. **ScrollRect的Content必须正确设置**
3. **VerticalLayoutGroup的设置影响滚动性能**
4. **大量日志时注意性能已有500行限制**
5. **确保Console Page在初始化时是隐藏的**
## 📚 参考文档
- [CONSOLE_MODULE_GUIDE.md](CONSOLE_MODULE_GUIDE.md) - 详细的UI结构和配置指南
- [REFACTORING_NOTES.md](REFACTORING_NOTES.md) - 模块化重构说明

View File

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

View File

@ -1,171 +0,0 @@
# Console 控制台模块 UI 结构说明
## 概述
Console模块是一个基于UGUI的日志控制台用于实时显示Unity的日志信息Info/Warning/Error/Exception
## 功能特性
- ✅ 实时捕获Unity日志Application.logMessageReceived
- ✅ 日志分类显示Info/Warning/Error/Fatal
- ✅ 日志过滤器(可以单独开关各类型日志)
- ✅ 锁定滚动到底部
- ✅ 点击日志查看详细堆栈信息
- ✅ 清除所有日志
- ✅ 日志颜色区分
- ✅ 最大日志行数限制默认500行
- ✅ 对象池优化性能
## UI结构
### Console Page 层次结构
```
ConsolePage (GameObject)
├── ControlPanel (水平布局)
│ ├── ClearButton (Button) - 清空日志按钮
│ ├── LockScrollToggle (Toggle) - 锁定滚动
│ ├── Spacer (空白区域)
│ ├── InfoFilterToggle (Toggle) - Info过滤器
│ ├── WarningFilterToggle (Toggle) - Warning过滤器
│ ├── ErrorFilterToggle (Toggle) - Error过滤器
│ └── FatalFilterToggle (Toggle) - Fatal过滤器
├── LogScrollView (ScrollRect) - 日志列表区域
│ └── LogContent (RectTransform) - 日志内容容器
│ └── [动态生成的日志项]
└── DetailScrollView (ScrollRect) - 详细信息区域
└── DetailText (TMP_Text) - 显示选中日志的详细信息和堆栈
```
### 日志项预制件结构 (ConsoleLogItemPrefab)
```
LogItem (GameObject)
├── Toggle (Toggle组件) - 用于选中日志
├── Background (Image) - 背景
└── Label (TMP_Text) - 显示日志文本
```
## 序列化字段配置
在 UniversalDebugTool 的 Inspector 中需要配置以下字段:
### Console页面字段
```csharp
[Header("控制台页面")]
consolePage // 控制台页面根对象
consoleLogScrollRect // 日志列表ScrollRect
consoleLogContent // 日志内容容器RectTransform
consoleDetailScrollRect // 详情ScrollRect
consoleDetailText // 详情文本TMP_Text
consoleClearButton // 清空按钮
consoleLockScrollToggle // 锁定滚动Toggle
consoleInfoFilterToggle // Info过滤Toggle
consoleWarningFilterToggle // Warning过滤Toggle
consoleErrorFilterToggle // Error过滤Toggle
consoleFatalFilterToggle // Fatal过滤Toggle
consoleLogItemPrefab // 日志项预制件
```
## UI组件要求
### 1. ControlPanel
- 使用 HorizontalLayoutGroup
- 建议添加 ContentSizeFitter
- Padding: 10, 10, 10, 10
- Spacing: 10
### 2. LogScrollView
- ScrollRect 设置:
- Vertical: true
- Horizontal: false
- Movement Type: Elastic
- Scrollbar Visibility: Automatic If Needed
- LogContent 需要 VerticalLayoutGroup
- Child Alignment: Upper Center
- Child Force Expand: Width
- Child Control Size: Height
- Spacing: 2
### 3. DetailScrollView
- ScrollRect 设置:
- Vertical: true
- Horizontal: false
- Movement Type: Clamped
- 建议高度: 150-200
- DetailText 设置:
- Enable Word Wrapping
- Overflow: Overflow
- Enable Rich Text
### 4. Toggles
- 每个Toggle需要一个Label (TMP_Text)
- 建议宽度: 100-120
- Toggle Group: 不需要(可以多选)
## 日志颜色配置
在 ConsoleModule 中默认的颜色:
```csharp
Info: Color.white (255, 255, 255)
Warning: Color.yellow (255, 255, 0)
Error: Color.red (255, 0, 0)
Fatal: Color(0.7f, 0.2f, 0.2f) (178, 51, 51)
```
## 性能优化
1. **对象池机制**: 日志项使用对象池,避免频繁创建销毁
2. **最大行数限制**: 默认500行防止内存溢出
3. **按需刷新**: 只在日志变化或过滤器改变时刷新显示
4. **Toggle优化**: 使用Toggle而不是Button减少重绘
## 使用示例
### 在代码中获取日志统计
```csharp
if (UniversalDebugTool.InstanceExists)
{
var console = UniversalDebugTool.Instance.consoleModule;
if (console != null)
{
console.GetLogCount(out int info, out int warning, out int error, out int fatal);
Debug.Log($"日志统计 - Info:{info}, Warning:{warning}, Error:{error}, Fatal:{fatal}");
}
}
```
### 设置字体
```csharp
UniversalDebugTool.SetSDFFont(myFont);
// 会自动应用到控制台模块
```
## 注意事项
1. **日志项预制件**: 必须包含 Toggle 和 TMP_Text 组件
2. **ScrollRect**: 必须正确设置 Content 和 Viewport
3. **性能**: 大量日志时建议增加日志项的高度以减少可见数量
4. **线程安全**: 日志回调在主线程执行,无需担心线程问题
5. **内存管理**: 超过最大行数的日志会自动删除
## 与GameFramework的差异
| 特性 | GameFramework (GUI) | MeowmentDebugTool (UGUI) |
|------|---------------------|--------------------------|
| UI系统 | OnGUI (即时模式) | UGUI (保留模式) |
| 性能 | 每帧重绘 | 按需刷新 |
| 布局 | GUILayout自动布局 | RectTransform + LayoutGroup |
| 交互 | GUILayout.Button/Toggle | Button/Toggle组件 |
| 对象池 | 不需要 | 使用对象池优化 |
| 滚动 | GUILayout.BeginScrollView | ScrollRect组件 |
## 未来扩展
可以考虑添加的功能:
- [ ] 搜索/过滤日志内容
- [ ] 导出日志到文件
- [ ] 日志标签/分类
- [ ] 日志统计图表
- [ ] 复制日志到剪贴板
- [ ] 日志时间范围过滤

View File

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

View File

@ -1,233 +0,0 @@
# Console模块快速设置指南
## 🚀 5分钟快速设置
### 步骤1: 创建Console页面GameObject
在UniversalDebugTool的预制件中创建以下结构
```
[添加到ContentContainer下]
└── ConsolePage
├── ControlPanel
│ ├── ClearButton (Button)
│ ├── LockScrollToggle (Toggle + Label)
│ ├── Spacer (Empty GameObject + LayoutElement)
│ ├── InfoToggle (Toggle + Label)
│ ├── WarningToggle (Toggle + Label)
│ ├── ErrorToggle (Toggle + Label)
│ └── FatalToggle (Toggle + Label)
├── LogArea
│ └── LogScrollView (ScrollRect)
│ └── Viewport
│ └── Content (VerticalLayoutGroup)
└── DetailArea
└── DetailScrollView (ScrollRect)
└── Viewport
└── Content
└── DetailText (TMP_Text)
```
### 步骤2: 配置组件
#### ConsolePage
- RectTransform: Stretch (全屏)
- 默认禁用Active = false
#### ControlPanel
- Height: 50
- Add Component: HorizontalLayoutGroup
- Padding: 10, 10, 10, 10
- Spacing: 10
- Child Force Expand: 都不勾选
#### Spacer (占位符)
- Add Component: LayoutElement
- Flexible Width: 1
#### LogArea
- Height: 60% (~400)
- Add Component: VerticalLayoutGroup
#### LogScrollView
- Add Component: ScrollRect
- Content: LogContent
- Vertical: ✅
- Horizontal: ❌
- Scrollbar Visibility: Auto Hide And Expand Viewport
#### LogContent
- Add Component: VerticalLayoutGroup
- Child Alignment: Upper Center
- Spacing: 2
- Child Control Size: Height ✅
- Child Force Expand: Width ✅
- Add Component: ContentSizeFitter
- Vertical Fit: Preferred Size
#### DetailArea
- Height: 40% (~200)
#### DetailScrollView
- 同LogScrollView配置
#### DetailText
- Font Size: 14
- Color: White
- Alignment: Top Left
- Overflow: Overflow
- Wrapping: Enabled
- Rich Text: ✅
### 步骤3: 创建日志项预制件
创建一个新的预制件 `ConsoleLogItem`:
```
LogItem
├── Toggle (Toggle组件)
│ - Is On: false
│ - Transition: None或Fade
├── Background (Image)
│ - Color: (30, 30, 30, 255)
└── Label (TextMeshPro)
- Font Size: 12
- Color: White
- Alignment: Middle Left
- Overflow: Truncate
- Rich Text: ✅
```
组件设置:
- RectTransform:
- Anchor: Stretch Horizontal
- Height: 25
- Left: 0, Right: 0
- Toggle:
- Target Graphic: Background
- Graphic: Background
### 步骤4: 配置UniversalDebugTool
在Inspector中找到 `[控制台页面]` 部分,拖拽对应的对象:
```
✅ Console Page → ConsolePage
✅ Console Log Scroll Rect → LogScrollView
✅ Console Log Content → LogContent
✅ Console Detail Scroll Rect → DetailScrollView
✅ Console Detail Text → DetailText
✅ Console Clear Button → ClearButton
✅ Console Lock Scroll Toggle → LockScrollToggle
✅ Console Info Filter Toggle → InfoToggle
✅ Console Warning Filter Toggle → WarningToggle
✅ Console Error Filter Toggle → ErrorToggle
✅ Console Fatal Filter Toggle → FatalToggle
✅ Console Log Item Prefab → ConsoleLogItem预制件
```
### 步骤5: 测试
```csharp
// 在游戏开始时调用
UniversalDebugTool.Init();
// 测试日志
Debug.Log("这是一条Info日志");
Debug.LogWarning("这是一条Warning日志");
Debug.LogError("这是一条Error日志");
```
运行游戏,点击调试工具,切换到"控制台"标签页,应该能看到日志了!
## 🎨 样式建议
### 颜色方案(深色主题)
```
Background: (20, 20, 20)
ControlPanel: (40, 40, 40)
LogItem: (30, 30, 30)
LogItem Selected: (60, 60, 60)
Text: (220, 220, 220)
日志颜色:
Info: White (255, 255, 255)
Warning: Yellow (255, 235, 0)
Error: Red (255, 80, 80)
Fatal: Dark Red (180, 50, 50)
```
### 字体大小
```
ControlPanel按钮: 14
Toggle标签: 12
日志项: 11-12
详情文本: 13-14
```
### 间距
```
ControlPanel Spacing: 10
LogContent Spacing: 2
Padding: 10
```
## ⚡ 性能优化提示
1. **限制可见日志项**: 调整LogArea高度减少同时渲染的日志数量
2. **日志项高度**: 25-30像素最佳太高会影响滚动性能
3. **最大日志数**: 默认500行可在ConsoleModule构造函数中修改maxLine
4. **禁用不需要的过滤器**: 减少日志显示可以提升性能
## 🐛 常见问题
### Q: 日志不显示?
A: 检查:
1. Console Page是否已添加到pages字典
2. 是否调用了UniversalDebugTool.Init()
3. 日志项预制件是否正确配置
### Q: 滚动不工作?
A: 检查:
1. ScrollRect的Content是否正确设置
2. Content是否有VerticalLayoutGroup
3. Content Size Fitter是否启用
### Q: 点击日志无反应?
A: 检查:
1. 日志项是否有Toggle组件
2. Toggle的Target Graphic是否设置
3. EventSystem是否存在于场景中
### Q: 性能问题?
A: 尝试:
1. 减少maxLine值默认500
2. 减少LogArea高度
3. 使用Text而非TextMeshPro性能更好但效果差
4. 关闭不需要的日志类型
## 📱 移动端优化
如果在移动设备上使用:
1. 增大日志项高度35-40像素
2. 增大字体14-16
3. 减少最大日志数200-300行
4. 使用简化的日志格式(去掉帧数显示)
## ✨ 完成!
现在你已经有一个功能完整的Console控制台了
测试所有功能:
- ✅ 清除日志
- ✅ 锁定/解锁滚动
- ✅ 过滤不同类型日志
- ✅ 点击查看详情
- ✅ 滚动浏览日志
享受调试吧! 🎉

View File

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

View File

@ -1,186 +0,0 @@
# Console模块 - 运行时UI创建完成
## ✅ 已完成
Console模块的UI已经添加到RuntimeUIGenerator中会在运行时自动创建。
## 🎯 使用方法
### 1. 直接运行测试
```csharp
using UnityEngine;
using MeowmentDebugTool;
public class TestDebugTool : MonoBehaviour
{
void Start()
{
// 初始化调试工具会自动创建所有UI包括Console
UniversalDebugTool.Init();
// 测试各种日志
Debug.Log("这是一条普通日志");
Debug.LogWarning("这是一条警告日志");
Debug.LogError("这是一条错误日志");
// 测试异常
try
{
throw new System.Exception("测试异常");
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
void Update()
{
// 按空格键生成测试日志
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log($"测试日志 - 帧数: {Time.frameCount}");
}
}
}
```
### 2. 运行游戏
1. 创建一个空GameObject
2. 添加TestDebugTool脚本
3. 运行游戏
4. Console页面会自动出现在标签页中
### 3. 测试功能
- ✅ **清空按钮**: 清除所有日志
- ✅ **锁定滚动**: 自动滚动到最新日志
- ✅ **过滤器**: 显示/隐藏不同类型的日志
- Info (白色)
- Warning (黄色)
- Error (红色)
- Fatal (深红色)
- ✅ **点击日志**: 在下方显示详细信息和堆栈跟踪
- ✅ **实时计数**: Toggle显示每种日志的数量
## 📋 UI结构说明
RuntimeUIGenerator会创建以下结构
```
ConsolePage
├── ControlPanel (控制面板 - 高度80)
│ ├── ConsoleClearButton (清空按钮)
│ ├── ConsoleLockScrollToggle (锁定滚动)
│ ├── Spacer (弹性空白)
│ ├── ConsoleInfoFilterToggle (Info过滤)
│ ├── ConsoleWarningFilterToggle (Warning过滤)
│ ├── ConsoleErrorFilterToggle (Error过滤)
│ └── ConsoleFatalFilterToggle (Fatal过滤)
├── LogArea (60% 高度)
│ └── ConsoleLogScrollView
│ └── Viewport
│ └── ConsoleLogContent (垂直布局组)
│ └── [日志项动态生成]
└── DetailArea (40% 高度)
└── ConsoleDetailScrollView
└── Viewport
└── Content
└── ConsoleDetailText (详细信息)
```
## 🎨 样式配置
### 颜色方案
- **背景**: (12, 12, 12) - 深灰色
- **日志区域**: (5, 5, 5) - 更深的灰色
- **详情区域**: (8, 8, 8) - 中等深灰色
- **日志项背景**: (12, 12, 12)
- **日志项选中**: (25, 35, 45) - 蓝灰色
### 日志颜色ConsoleModule中定义
- **Info**: (255, 255, 255) - 白色
- **Warning**: (255, 255, 0) - 黄色
- **Error**: (255, 0, 0) - 红色
- **Fatal**: (178, 51, 51) - 深红色
### 字体大小
- Toggle标签: 24
- 日志项: 22
- 详情文本: 24
## 🔧 自定义配置
如果需要修改样式可以在RuntimeUIGenerator.cs中找到CreateConsolePage方法进行调整
### 修改日志项高度
```csharp
// 在CreateConsoleLogItemPrefab方法中
rect.sizeDelta = new Vector2(0, 30); // 改为你想要的高度
```
### 修改区域比例
```csharp
// 在CreateConsolePage方法中
logAreaLayout.flexibleHeight = 3; // 日志区域权重
detailAreaLayout.flexibleHeight = 2; // 详情区域权重
```
### 修改最大日志数
```csharp
// 在ConsoleModule.cs构造函数中
private int maxLine = 500; // 改为你想要的数量
```
## 🚀 性能提示
1. **默认500行限制**: 超过的日志会自动删除
2. **对象池优化**: 日志项会被重用,不会频繁创建销毁
3. **按需刷新**: 只在日志变化或过滤器改变时刷新UI
4. **适中的日志项高度**: 30像素平衡了可读性和性能
## 📱 移动端建议
如果在移动设备上运行:
```csharp
// 在CreateConsoleLogItemPrefab中调整
labelTmp.fontSize = 28; // 增大字体
rect.sizeDelta = new Vector2(0, 40); // 增加高度
```
## 🐛 调试技巧
### 查看Console模块是否初始化成功
```csharp
if (UniversalDebugTool.InstanceExists)
{
var console = UniversalDebugTool.Instance.GetType()
.GetField("consoleModule", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.GetValue(UniversalDebugTool.Instance);
if (console != null)
{
Debug.Log("✅ Console模块已初始化");
}
}
```
### 检查UI是否创建
在Hierarchy中查找
- UniversalDebugTool_Canvas
- MainWindow
- ContentContainer
- ConsolePage ← 应该能找到这个
## ✨ 完成!
现在你可以直接运行游戏测试Console模块了
所有的UI都会在调用 `UniversalDebugTool.Init()` 时自动创建。
享受调试吧! 🎉

View File

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

View File

@ -47,14 +47,14 @@ void Start()
```csharp
public static class DebugFunctions
{
[DebugButton("清空数据")]
[DebugButton("分组1","清空数据")]
private static void ClearData()
{
PlayerPrefs.DeleteAll();
}
// 设置按钮颜色 (r, g, b)
[DebugButton("添加金币", 0.2f, 0.8f, 0.2f)]
[DebugButton("分组1","添加金币", 0.2f, 0.8f, 0.2f)]
private static void AddCoins()
{
PlayerData.Coins += 1000;

View File

@ -1,111 +0,0 @@
# MeowmentDebugTool 模块化重构说明
## 重构概述
已将 UniversalDebugTool 的4个主要功能模块分离成独立的脚本文件使代码结构更清晰、更易维护。
## 新增文件
### 1. IDebugModule.cs
- **作用**: 调试模块的基础接口
- **方法**:
- `Initialize()`: 初始化模块
- `GetPage()`: 获取模块的UI页面GameObject
- `GetModuleName()`: 获取模块名称
### 2. ParametersModule.cs
- **作用**: 参数查看模块,显示设备和系统信息
- **主要功能**:
- 显示设备信息(设备名称、型号、处理器等)
- 显示系统信息Unity版本、平台、分辨率、图形设备等
- 复制信息到剪贴板
- 刷新信息
### 3. CustomButtonsModule.cs
- **作用**: 自定义按钮模块通过反射加载标记为DebugButton的方法
- **主要功能**:
- 自动扫描并加载所有带 `[DebugButton]` 特性的静态方法
- 动态创建按钮UI
- 设置SDF字体
- 支持自定义按钮回调
- 点击按钮后自动关闭调试窗口
### 4. ToolbarModule.cs
- **作用**: 工具栏模块,提供时间调整等工具
- **主要功能**:
- 时间调整滑块(支持秒和分钟显示)
- 增加/减少时间按钮
- 可扩展的游戏时间调整接口
### 5. SettingsModule.cs
- **作用**: 设置模块,提供分辨率设置等功能
- **主要功能**:
- 自定义窗口分辨率
- 重置为默认分辨率
- 实时显示当前窗口尺寸
## UniversalDebugTool.cs 的改动
### 主要变化
1. **移除了具体实现代码**
- 参数查看功能 → ParametersModule
- 自定义按钮功能 → CustomButtonsModule
- 工具栏功能 → ToolbarModule
- 分辨率设置功能 → SettingsModule
2. **新增模块管理**
```csharp
private ParametersModule parametersModule;
private CustomButtonsModule customButtonsModule;
private ToolbarModule toolbarModule;
private SettingsModule settingsModule;
private List<IDebugModule> allModules = new List<IDebugModule>();
```
3. **初始化流程**
- `InitializeDebugTool()`: 创建所有模块实例并注册页面
- `InitializeAllModules()`: 调用各模块的Initialize方法
4. **保留的公共API**
- `CopyDeviceInfoToClipboard()` - 委托给 ParametersModule
- `CopySystemInfoToClipboard()` - 委托给 ParametersModule
- `RefreshAllInfo()` - 委托给 ParametersModule
- `SetCustomButtonCallback()` - 委托给 CustomButtonsModule
- `ReloadCustomButtons()` - 委托给 CustomButtonsModule
## 优势
1. **职责分离**: 每个模块只负责一个功能领域
2. **易于维护**: 修改某个功能时只需要编辑对应的模块文件
3. **可扩展性**: 添加新模块只需实现 IDebugModule 接口
4. **代码清晰**: UniversalDebugTool.cs 现在主要负责协调各模块,代码更简洁
## 使用方式
使用方式完全不变所有现有的API和功能保持兼容
```csharp
// 初始化调试工具
UniversalDebugTool.Init();
// 设置字体
UniversalDebugTool.SetSDFFont(myFont);
// 刷新信息
UniversalDebugTool.Instance.RefreshAllInfo();
// 自定义按钮仍然使用相同的特性
[DebugButton("测试按钮")]
public static void TestMethod()
{
Debug.Log("测试");
}
```
## 未来扩展
如果需要添加新的调试功能模块,只需:
1. 创建新的类并实现 `IDebugModule` 接口
2. 在 `UniversalDebugTool.InitializeDebugTool()` 中实例化并添加到 `allModules` 列表
3. 调用 `InitializeAllModules()` 会自动初始化新模块

View File

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

View File

@ -28,11 +28,18 @@ namespace MeowmentDebugTool
private Toggle errorFilterToggle;
private Toggle fatalFilterToggle;
// 文字筛选
private TMP_InputField textFilterInputField;
private string textFilter = "";
// 日志项预制件
private GameObject logItemPrefab;
// 日志数据
private Queue<LogNode> logNodes = new Queue<LogNode>();
// 日志数据 - 分离的缓存池
private Queue<LogNode> infoLogNodes = new Queue<LogNode>();
private Queue<LogNode> warningLogNodes = new Queue<LogNode>();
private Queue<LogNode> errorLogNodes = new Queue<LogNode>();
private Queue<LogNode> fatalLogNodes = new Queue<LogNode>();
private LogNode selectedNode = null;
// 日志计数
@ -43,9 +50,12 @@ namespace MeowmentDebugTool
// 设置
private bool lockScroll = true;
private int maxLine = 200;
private bool infoFilter = true;
private bool warningFilter = true;
private int maxInfoLines = 50;
private int maxWarningLines = 50;
private int maxErrorLines = 50;
private int maxFatalLines = 50;
private bool infoFilter = false; // 默认不显示Info
private bool warningFilter = false; // 默认不显示Warning
private bool errorFilter = true;
private bool fatalFilter = true;
@ -71,7 +81,7 @@ namespace MeowmentDebugTool
public ConsoleModule(GameObject page, ScrollRect logScroll, RectTransform logContentTransform,
ScrollRect detailScroll, TMP_Text detail,
Button clearBtn, Toggle lockToggle, Toggle infoToggle, Toggle warningToggle,
Toggle errorToggle, Toggle fatalToggle, GameObject logItemPrefabObj)
Toggle errorToggle, Toggle fatalToggle, TMP_InputField textFilterInput, GameObject logItemPrefabObj)
{
consolePage = page;
logScrollRect = logScroll;
@ -84,6 +94,7 @@ namespace MeowmentDebugTool
warningFilterToggle = warningToggle;
errorFilterToggle = errorToggle;
fatalFilterToggle = fatalToggle;
textFilterInputField = textFilterInput;
logItemPrefab = logItemPrefabObj;
}
#endregion
@ -147,6 +158,13 @@ namespace MeowmentDebugTool
fatalFilterToggle.onValueChanged.AddListener(OnFatalFilterChanged);
}
// 设置文字筛选输入框事件
if (textFilterInputField != null)
{
textFilterInputField.text = textFilter;
textFilterInputField.onValueChanged.AddListener(OnTextFilterChanged);
}
// 清空详情
if (detailText != null)
detailText.text = "点击日志查看详细信息...";
@ -195,6 +213,29 @@ namespace MeowmentDebugTool
fatal = fatalCount;
}
/// <summary>
/// 设置各类型日志的最大缓存数量
/// </summary>
public void SetMaxLogLines(int maxInfo, int maxWarning, int maxError, int maxFatal)
{
maxInfoLines = Mathf.Max(1, maxInfo);
maxWarningLines = Mathf.Max(1, maxWarning);
maxErrorLines = Mathf.Max(1, maxError);
maxFatalLines = Mathf.Max(1, maxFatal);
Debug.Log($"[ConsoleModule] 设置缓存池大小 - Info:{maxInfoLines}, Warning:{maxWarningLines}, Error:{maxErrorLines}, Fatal:{maxFatalLines}");
}
/// <summary>
/// 获取各类型日志的最大缓存数量
/// </summary>
public void GetMaxLogLines(out int maxInfo, out int maxWarning, out int maxError, out int maxFatal)
{
maxInfo = maxInfoLines;
maxWarning = maxWarningLines;
maxError = maxErrorLines;
maxFatal = maxFatalLines;
}
/// <summary>
/// 更新(每帧调用)
/// </summary>
@ -246,12 +287,40 @@ namespace MeowmentDebugTool
// 创建日志节点
LogNode logNode = LogNode.Create(logType, logMessage, stackTrace);
logNodes.Enqueue(logNode);
// 限制最大行数
while (logNodes.Count > maxLine)
// 根据类型添加到对应的缓存池
Queue<LogNode> targetQueue = null;
int maxLines = 0;
switch (logType)
{
logNodes.Dequeue();
case LogType.Log:
targetQueue = infoLogNodes;
maxLines = maxInfoLines;
break;
case LogType.Warning:
targetQueue = warningLogNodes;
maxLines = maxWarningLines;
break;
case LogType.Error:
targetQueue = errorLogNodes;
maxLines = maxErrorLines;
break;
case LogType.Exception:
targetQueue = fatalLogNodes;
maxLines = maxFatalLines;
break;
}
if (targetQueue != null)
{
targetQueue.Enqueue(logNode);
// 限制该类型的最大行数
while (targetQueue.Count > maxLines)
{
targetQueue.Dequeue();
}
}
// 标记需要刷新而不是立即刷新避免rebuild loop
@ -293,13 +362,22 @@ namespace MeowmentDebugTool
}
activeLogItems.Clear();
// 遍历日志节点并创建UI
// 遍历日志节点并创建UI性能优化只为需要显示的日志创建LogItem
int index = 0;
int displayCount = 0;
foreach (LogNode logNode in logNodes)
// 合并所有缓存池的日志,按时间排序
List<LogNode> allLogs = new List<LogNode>();
allLogs.AddRange(infoLogNodes);
allLogs.AddRange(warningLogNodes);
allLogs.AddRange(errorLogNodes);
allLogs.AddRange(fatalLogNodes);
allLogs.Sort((a, b) => a.LogTime.CompareTo(b.LogTime));
foreach (LogNode logNode in allLogs)
{
// 根据过滤器判断是否显示
if (!ShouldShowLog(logNode.LogType))
// 根据过滤器判断是否显示如果不显示则跳过创建LogItem
if (!ShouldShowLog(logNode))
continue;
// 从对象池获取或创建日志项
@ -328,21 +406,41 @@ namespace MeowmentDebugTool
}
}
private bool ShouldShowLog(LogType logType)
private bool ShouldShowLog(LogNode logNode)
{
switch (logType)
// 首先检查类型筛选
bool typeMatch = false;
switch (logNode.LogType)
{
case LogType.Log:
return infoFilter;
typeMatch = infoFilter;
break;
case LogType.Warning:
return warningFilter;
typeMatch = warningFilter;
break;
case LogType.Error:
return errorFilter;
typeMatch = errorFilter;
break;
case LogType.Exception:
return fatalFilter;
typeMatch = fatalFilter;
break;
default:
return true;
typeMatch = true;
break;
}
if (!typeMatch)
return false;
// 然后检查文字筛选
if (!string.IsNullOrEmpty(textFilter))
{
// 如果日志消息不包含筛选文字,则不显示
if (!logNode.LogMessage.Contains(textFilter))
return false;
}
return true;
}
private GameObject GetLogItemFromPool()
@ -475,29 +573,10 @@ namespace MeowmentDebugTool
private void RefreshCount()
{
infoCount = 0;
warningCount = 0;
errorCount = 0;
fatalCount = 0;
foreach (LogNode logNode in logNodes)
{
switch (logNode.LogType)
{
case LogType.Log:
infoCount++;
break;
case LogType.Warning:
warningCount++;
break;
case LogType.Error:
errorCount++;
break;
case LogType.Exception:
fatalCount++;
break;
}
}
infoCount = infoLogNodes.Count;
warningCount = warningLogNodes.Count;
errorCount = errorLogNodes.Count;
fatalCount = fatalLogNodes.Count;
}
private void UpdateFilterToggleText()
@ -535,7 +614,10 @@ namespace MeowmentDebugTool
private void ClearAllLogs()
{
logNodes.Clear();
infoLogNodes.Clear();
warningLogNodes.Clear();
errorLogNodes.Clear();
fatalLogNodes.Clear();
selectedNode = null;
if (detailText != null)
@ -574,6 +656,12 @@ namespace MeowmentDebugTool
fatalFilter = value;
needRefresh = true;
}
private void OnTextFilterChanged(string value)
{
textFilter = value;
needRefresh = true;
}
#endregion
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
@ -7,7 +9,7 @@ using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 自定义按钮模块 - 通过反射加载标记为DebugButton的方法
/// 自定义按钮模块 - 通过反射加载标记为DebugButton的方法,支持分组显示
/// </summary>
public class CustomButtonsModule : IDebugModule
{
@ -25,6 +27,20 @@ namespace MeowmentDebugTool
// 关闭窗口回调
private Action onCloseWindowCallback;
// 分组相关
private Dictionary<string, List<ButtonInfo>> buttonGroups = new Dictionary<string, List<ButtonInfo>>();
#endregion
#region
/// <summary>
/// 按钮信息
/// </summary>
private class ButtonInfo
{
public MethodInfo Method;
public DebugButtonAttribute Attribute;
}
#endregion
#region
@ -43,6 +59,13 @@ namespace MeowmentDebugTool
public void Initialize()
{
Debug.Log("[CustomButtonsModule] 初始化自定义按钮模块...");
if (buttonContainer == null || buttonPrefab == null)
{
Debug.LogError("[CustomButtonsModule] 必要组件缺失,初始化失败");
return;
}
LoadCustomButtons();
}
@ -83,7 +106,7 @@ namespace MeowmentDebugTool
}
#endregion
#region
#region -
/// <summary>
/// 加载所有自定义按钮(使用反射)
/// </summary>
@ -95,11 +118,8 @@ namespace MeowmentDebugTool
return;
}
// 清空现有按钮
foreach (Transform child in buttonContainer)
{
UnityEngine.Object.Destroy(child.gameObject);
}
// 清空现有数据
buttonGroups.Clear();
// 查找所有标记为DebugButton的方法
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
@ -116,7 +136,20 @@ namespace MeowmentDebugTool
var attribute = method.GetCustomAttribute<DebugButtonAttribute>();
if (attribute != null)
{
CreateCustomButton(method, attribute);
// 获取组名(如果为空则使用"默认"
string groupName = string.IsNullOrEmpty(attribute.GroupName) ? "默认" : attribute.GroupName;
// 添加到对应的组
if (!buttonGroups.ContainsKey(groupName))
{
buttonGroups[groupName] = new List<ButtonInfo>();
}
buttonGroups[groupName].Add(new ButtonInfo
{
Method = method,
Attribute = attribute
});
}
}
}
@ -127,6 +160,252 @@ namespace MeowmentDebugTool
Debug.LogWarning($"[CustomButtonsModule] 无法访问程序集 {assembly.FullName}: {e.Message}");
}
}
Debug.Log($"[CustomButtonsModule] 共找到 {buttonGroups.Count} 个按钮组");
foreach (var group in buttonGroups)
{
Debug.Log($" - {group.Key}: {group.Value.Count} 个按钮");
}
// 显示所有组的按钮
ShowAllGroupsWithTitles();
}
/// <summary>
/// 显示所有组的按钮,每个组带标题
/// </summary>
private void ShowAllGroupsWithTitles()
{
// 清空现有内容
foreach (Transform child in buttonContainer)
{
UnityEngine.Object.Destroy(child.gameObject);
}
// 确保 buttonContainer 使用垂直布局而不是网格布局
SetupButtonContainerLayout();
// 按组名排序("默认"组排在最前面)
var sortedGroups = buttonGroups.OrderBy(kvp => kvp.Key == "默认" ? "" : kvp.Key);
foreach (var group in sortedGroups)
{
string groupName = group.Key;
List<ButtonInfo> buttons = group.Value;
// 为每个组创建一个独立的容器
GameObject groupContainer = CreateGroupContainer(groupName);
// 在组容器中创建标题
CreateGroupTitle(groupName, groupContainer.transform);
// 在组容器中创建该组的所有按钮
foreach (var buttonInfo in buttons)
{
CreateCustomButtonInGroup(buttonInfo.Method, buttonInfo.Attribute, groupContainer.transform);
}
}
// 重置滚动位置到顶部
if (buttonsScrollRect != null)
{
Canvas.ForceUpdateCanvases();
buttonsScrollRect.verticalNormalizedPosition = 1f;
}
}
/// <summary>
/// 设置 buttonContainer 的布局
/// </summary>
private void SetupButtonContainerLayout()
{
if (buttonContainer == null)
{
Debug.LogError("[CustomButtonsModule] buttonContainer 为 null无法设置布局");
return;
}
GameObject containerObject = buttonContainer.gameObject;
// 移除可能存在的 GridLayoutGroup
GridLayoutGroup gridLayout = containerObject.GetComponent<GridLayoutGroup>();
if (gridLayout != null)
{
UnityEngine.Object.DestroyImmediate(gridLayout);
}
// 添加或获取 VerticalLayoutGroup
VerticalLayoutGroup verticalLayout = containerObject.GetComponent<VerticalLayoutGroup>();
if (verticalLayout == null)
{
verticalLayout = containerObject.AddComponent<VerticalLayoutGroup>();
if (verticalLayout == null)
{
Debug.LogError("[CustomButtonsModule] 错误:无法添加 VerticalLayoutGroup");
return;
}
}
// 配置垂直布局
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.spacing = 20;
verticalLayout.padding = new RectOffset(10, 10, 10, 10);
// 确保有 ContentSizeFitter
ContentSizeFitter sizeFitter = containerObject.GetComponent<ContentSizeFitter>();
if (sizeFitter == null)
{
sizeFitter = containerObject.AddComponent<ContentSizeFitter>();
}
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
}
/// <summary>
/// 创建组容器
/// </summary>
private GameObject CreateGroupContainer(string groupName)
{
GameObject groupContainer = new GameObject($"Group_{groupName}");
groupContainer.transform.SetParent(buttonContainer, false);
// 添加RectTransform
RectTransform rectTransform = groupContainer.AddComponent<RectTransform>();
// 添加VerticalLayoutGroup让标题和按钮垂直排列
VerticalLayoutGroup verticalLayout = groupContainer.AddComponent<VerticalLayoutGroup>();
verticalLayout.childForceExpandWidth = true;
verticalLayout.childForceExpandHeight = false;
verticalLayout.childControlWidth = true;
verticalLayout.childControlHeight = true;
verticalLayout.spacing = 5;
verticalLayout.padding = new RectOffset(0, 0, 10, 10);
// 添加ContentSizeFitter让容器自动调整大小
ContentSizeFitter sizeFitter = groupContainer.AddComponent<ContentSizeFitter>();
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// 添加LayoutElement确保占满宽度
LayoutElement layoutElement = groupContainer.AddComponent<LayoutElement>();
layoutElement.flexibleWidth = 1;
return groupContainer;
}
/// <summary>
/// 创建组标题
/// </summary>
private void CreateGroupTitle(string groupName, Transform parent)
{
// 创建标题GameObject
GameObject titleObj = new GameObject($"Title_{groupName}");
titleObj.transform.SetParent(parent, false);
// 添加RectTransform
RectTransform rectTransform = titleObj.AddComponent<RectTransform>();
// 添加TextMeshProUGUI组件
TMP_Text titleText = titleObj.AddComponent<TextMeshProUGUI>();
titleText.text = groupName;
titleText.fontSize = 32;
titleText.fontStyle = FontStyles.Bold;
titleText.alignment = TextAlignmentOptions.Left;
titleText.color = new Color(1f, 1f, 1f, 0.9f);
titleText.margin = new Vector4(10, 5, 0, 5);
// 应用保存的字体
if (savedFontAsset != null)
{
titleText.font = savedFontAsset;
}
// 添加LayoutElement
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 50;
layoutElement.flexibleWidth = 1;
}
/// <summary>
/// 在组容器中创建按钮(需要创建一个按钮区域容器)
/// </summary>
private void CreateCustomButtonInGroup(MethodInfo method, DebugButtonAttribute attribute, Transform groupParent)
{
// 查找或创建按钮区域容器
Transform buttonArea = groupParent.Find("ButtonArea");
if (buttonArea == null)
{
GameObject buttonAreaObj = new GameObject("ButtonArea");
buttonAreaObj.transform.SetParent(groupParent, false);
// 添加GridLayoutGroup让按钮以网格形式排列
GridLayoutGroup gridLayout = buttonAreaObj.AddComponent<GridLayoutGroup>();
gridLayout.cellSize = new Vector2(200, 80); // 按钮大小根据你的buttonPrefab调整
gridLayout.spacing = new Vector2(10, 10);
gridLayout.constraint = GridLayoutGroup.Constraint.Flexible;
gridLayout.childAlignment = TextAnchor.UpperLeft;
// 添加ContentSizeFitter
ContentSizeFitter sizeFitter = buttonAreaObj.AddComponent<ContentSizeFitter>();
sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
sizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// 添加LayoutElement
LayoutElement layoutElement = buttonAreaObj.AddComponent<LayoutElement>();
layoutElement.flexibleWidth = 1;
buttonArea = buttonAreaObj.transform;
}
// 在按钮区域中创建按钮
GameObject buttonObj = UnityEngine.Object.Instantiate(buttonPrefab, buttonArea);
Button button = buttonObj.GetComponent<Button>();
TMP_Text buttonText = buttonObj.GetComponentInChildren<TMP_Text>();
Image buttonImage = buttonObj.GetComponent<Image>();
// 设置按钮文本
if (buttonText != null)
{
buttonText.text = string.IsNullOrEmpty(attribute.DisplayName) ? method.Name : attribute.DisplayName;
// 应用保存的字体
if (savedFontAsset != null)
{
buttonText.font = savedFontAsset;
}
}
// 设置按钮颜色
if (buttonImage != null && attribute.ButtonColor != default(Color))
{
buttonImage.color = attribute.ButtonColor;
}
// 设置按钮点击事件
button.onClick.AddListener(() =>
{
try
{
method.Invoke(null, null);
Debug.Log($"[CustomButtonsModule] 执行调试方法: {method.Name}");
// 调用回调
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
// 点击按钮后关闭主窗口
onCloseWindowCallback?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"[CustomButtonsModule] 执行调试方法 {method.Name} 时出错: {e.Message}");
}
});
}
private void CreateCustomButton(MethodInfo method, DebugButtonAttribute attribute)

View File

@ -24,8 +24,22 @@ namespace MeowmentDebugTool
// 保存每个数值的当前值
private Dictionary<string, int> valueStates = new Dictionary<string, int>();
// 保存每个数值的滑动条和输入框引用(用于动态更新范围)
private Dictionary<string, ValueControlData> valueControls = new Dictionary<string, ValueControlData>();
// 关闭窗口回调
private Action onCloseWindowCallback;
/// <summary>
/// 数值控件数据结构
/// </summary>
private class ValueControlData
{
public Slider slider;
public TMP_InputField inputField;
public MethodInfo method;
public DebugValueAttribute attribute;
}
#endregion
#region
@ -74,6 +88,138 @@ namespace MeowmentDebugTool
{
LoadCustomValues();
}
/// <summary>
/// 更新指定方法的数值范围
/// </summary>
/// <param name="methodName">方法名称(类名.方法名 或 方法名)</param>
/// <param name="minValue">新的最小值</param>
/// <param name="maxValue">新的最大值</param>
/// <returns>是否成功更新</returns>
public bool UpdateValueRange(string methodName, int minValue, int maxValue)
{
if (minValue >= maxValue)
{
Debug.LogWarning($"[CustomValuesModule] 无效的范围: minValue({minValue}) >= maxValue({maxValue})");
return false;
}
// 尝试精确匹配
if (valueControls.ContainsKey(methodName))
{
return UpdateValueRangeInternal(methodName, minValue, maxValue);
}
// 尝试模糊匹配(只用方法名)
foreach (var kvp in valueControls)
{
string key = kvp.Key;
string simpleMethodName = key.Substring(key.LastIndexOf('.') + 1);
if (simpleMethodName == methodName)
{
return UpdateValueRangeInternal(key, minValue, maxValue);
}
}
Debug.LogWarning($"[CustomValuesModule] 未找到方法: {methodName}");
return false;
}
/// <summary>
/// 更新指定方法的当前值
/// </summary>
/// <param name="methodName">方法名称(类名.方法名 或 方法名)</param>
/// <param name="value">新的值</param>
/// <returns>是否成功更新</returns>
public bool UpdateValue(string methodName, int value)
{
// 尝试精确匹配
if (valueControls.ContainsKey(methodName))
{
return UpdateValueInternal(methodName, value);
}
// 尝试模糊匹配(只用方法名)
foreach (var kvp in valueControls)
{
string key = kvp.Key;
string simpleMethodName = key.Substring(key.LastIndexOf('.') + 1);
if (simpleMethodName == methodName)
{
return UpdateValueInternal(key, value);
}
}
Debug.LogWarning($"[CustomValuesModule] 未找到方法: {methodName}");
return false;
}
private bool UpdateValueRangeInternal(string methodKey, int minValue, int maxValue)
{
var data = valueControls[methodKey];
// 更新attribute的范围
data.attribute.MinValue = minValue;
data.attribute.MaxValue = maxValue;
// 更新slider的范围
if (data.slider != null)
{
data.slider.minValue = minValue;
data.slider.maxValue = maxValue;
// 确保当前值在新范围内
int currentValue = valueStates[methodKey];
int clampedValue = Mathf.Clamp(currentValue, minValue, maxValue);
if (currentValue != clampedValue)
{
valueStates[methodKey] = clampedValue;
data.slider.value = clampedValue;
if (data.inputField != null)
{
data.inputField.text = clampedValue.ToString();
}
}
}
Debug.Log($"[CustomValuesModule] 已更新 {methodKey} 的范围: [{minValue}, {maxValue}]");
return true;
}
private bool UpdateValueInternal(string methodKey, int value)
{
var data = valueControls[methodKey];
// 确保值在范围内
int clampedValue = Mathf.Clamp(value, data.attribute.MinValue, data.attribute.MaxValue);
if (clampedValue != value)
{
Debug.LogWarning($"[CustomValuesModule] 值 {value} 超出范围 [{data.attribute.MinValue}, {data.attribute.MaxValue}],已限制为 {clampedValue}");
}
// 更新保存的状态
valueStates[methodKey] = clampedValue;
// 更新slider
if (data.slider != null)
{
data.slider.value = clampedValue;
}
// 更新输入框
if (data.inputField != null)
{
data.inputField.text = clampedValue.ToString();
}
Debug.Log($"[CustomValuesModule] 已更新 {methodKey} 的值: {clampedValue}");
return true;
}
#endregion
#region
@ -93,6 +239,9 @@ namespace MeowmentDebugTool
{
UnityEngine.Object.Destroy(child.gameObject);
}
// 清空控件引用
valueControls.Clear();
// 查找所有标记为DebugValue的方法
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
@ -167,6 +316,15 @@ namespace MeowmentDebugTool
}
int currentValue = valueStates[methodKey];
// 保存控件引用
valueControls[methodKey] = new ValueControlData
{
slider = slider,
inputField = inputField,
method = method,
attribute = attribute
};
// 设置滑动条
if (slider != null)
@ -192,7 +350,19 @@ namespace MeowmentDebugTool
if (inputField != null)
{
inputField.text = currentValue.ToString();
inputField.contentType = TMP_InputField.ContentType.IntegerNumber;
// 根据最小值判断是否允许负数
// 如果最小值小于0使用Standard类型并限制只能输入数字和负号
inputField.contentType = attribute.MinValue < 0
? TMP_InputField.ContentType.Standard
: TMP_InputField.ContentType.IntegerNumber;
// 如果允许负数,需要设置字符验证来只允许数字和负号
if (attribute.MinValue < 0)
{
inputField.characterValidation = TMP_InputField.CharacterValidation.None;
inputField.inputType = TMP_InputField.InputType.Standard;
inputField.keyboardType = TouchScreenKeyboardType.NumbersAndPunctuation;
}
// 应用保存的字体
if (savedFontAsset != null)
@ -203,6 +373,12 @@ namespace MeowmentDebugTool
// 输入框值改变时更新滑动条和保存状态
inputField.onValueChanged.AddListener((string text) =>
{
// 允许输入过程中暂时为空或只有负号
if (string.IsNullOrEmpty(text) || text == "-")
{
return;
}
if (int.TryParse(text, out int intValue))
{
// 限制范围

View File

@ -8,9 +8,12 @@ namespace MeowmentDebugTool
/// 可拖动的悬浮按钮
/// 点击打开调试工具,可以在屏幕上自由拖动
/// </summary>
public class DraggableFloatingButton : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
public class DraggableFloatingButton : MonoBehaviour, IPointerDownHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerClickHandler
{
[Header("设置")]
[Tooltip("是否启用拖动功能")]
public bool enableDragging = false;
[Tooltip("是否限制在屏幕范围内")]
public bool clampToScreen = true;
@ -22,7 +25,7 @@ namespace MeowmentDebugTool
public bool snapToEdge = true;
[Tooltip("吸附动画时间")]
public float snapDuration = 0.3f;
public float snapDuration = 0.1f;
private RectTransform rectTransform;
private Canvas canvas;
@ -32,9 +35,9 @@ namespace MeowmentDebugTool
private bool isSnapping = false;
// 用于区分点击和拖动
private Vector2 dragStartPosition;
private const float clickThreshold = 5f; // 小于这个距离算点击
private bool wasDragging = false; // 标记刚才是否拖动过
private bool hasDragged = false; // 标记本次操作是否进行了拖动
private Vector2 dragStartPosition; // 拖动起始位置
private const float dragThreshold = 5f; // 拖动阈值,移动超过这个距离才算拖动
private void Awake()
{
@ -68,13 +71,27 @@ namespace MeowmentDebugTool
}
}
/// <summary>
/// 每次按下时重置拖动标记在OnBeginDrag之前触发
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
hasDragged = false; // 重置拖动标记
Debug.Log("[DraggableFloatingButton] OnPointerDown - 重置拖动标记");
}
public void OnBeginDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// 如果禁用拖动将事件传递给父级让其他UI元素能够接收拖动事件
PassEventToParent(eventData, ExecuteEvents.beginDragHandler);
return;
}
isDragging = true;
isSnapping = false;
// 记录起始位置,用于判断是点击还是拖动
dragStartPosition = eventData.position;
dragStartPosition = eventData.position; // 记录起始位置
// 计算拖动偏移
RectTransformUtility.ScreenPointToLocalPointInRectangle(
@ -89,8 +106,22 @@ namespace MeowmentDebugTool
public void OnDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// 如果禁用拖动,将事件传递给父级
PassEventToParent(eventData, ExecuteEvents.dragHandler);
return;
}
if (!isDragging) return;
// 检查是否超过拖动阈值
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
if (dragDistance > dragThreshold)
{
hasDragged = true; // 标记已经拖动
}
// 转换屏幕坐标到Canvas局部坐标
RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvas.transform as RectTransform,
@ -113,18 +144,14 @@ namespace MeowmentDebugTool
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
// 计算拖动距离
float dragDistance = Vector2.Distance(dragStartPosition, eventData.position);
// 如果拖动距离超过阈值,说明是拖动而不是点击
if (dragDistance > clickThreshold)
if (!enableDragging)
{
wasDragging = true;
// 在下一帧重置标记
StartCoroutine(ResetDragFlag());
// 如果禁用拖动,将事件传递给父级
PassEventToParent(eventData, ExecuteEvents.endDragHandler);
return;
}
isDragging = false;
// 自动吸附到最近的边缘
if (snapToEdge)
@ -133,18 +160,31 @@ namespace MeowmentDebugTool
}
}
private System.Collections.IEnumerator ResetDragFlag()
{
yield return null; // 等待一帧
wasDragging = false;
}
/// <summary>
/// 检查是否刚拖动过用于Button点击事件判断
/// 处理点击事件 - 只有在没有拖动时才打开窗口
/// </summary>
public bool GetWasDragging()
public void OnPointerClick(PointerEventData eventData)
{
return wasDragging;
Debug.Log($"[DraggableFloatingButton] OnPointerClick 被触发hasDragged={hasDragged}");
// 如果进行了拖动,不执行点击操作
if (hasDragged)
{
Debug.Log("[!] 检测到拖动,不打开窗口");
return;
}
// 纯点击操作,打开调试窗口
Debug.Log("[OK] 点击浮窗,打开调试窗口");
UniversalDebugTool debugTool = FindObjectOfType<UniversalDebugTool>();
if (debugTool != null)
{
debugTool.OpenDebugWindow();
}
else
{
Debug.LogWarning("[X] 未找到 UniversalDebugTool 实例!");
}
}
private Vector2 ClampToScreen(Vector2 position)
@ -231,5 +271,40 @@ namespace MeowmentDebugTool
{
return rectTransform.anchoredPosition;
}
/// <summary>
/// 将事件传递给父级或下层的UI元素
/// 当拖动功能被禁用时确保其他UI元素仍能接收拖动事件
/// </summary>
private void PassEventToParent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction) where T : IEventSystemHandler
{
Transform parent = transform.parent;
while (parent != null)
{
// 尝试执行父级对象上的事件
if (ExecuteEvents.Execute(parent.gameObject, eventData, eventFunction))
{
// 如果父级处理了事件,停止传递
return;
}
parent = parent.parent;
}
// 如果父级都没有处理,尝试传递给射线检测到的下一个对象
var raycastResults = new System.Collections.Generic.List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
// 跳过当前对象,找到下一个可以处理事件的对象
foreach (var result in raycastResults)
{
if (result.gameObject == gameObject)
continue;
if (ExecuteEvents.Execute(result.gameObject, eventData, eventFunction))
{
return;
}
}
}
}
}

View File

@ -127,16 +127,19 @@ namespace MeowmentDebugTool
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);
// 使用左上角锚点,这样在任何屏幕宽高比下都能正确定位到左上角
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(0, 1);
rect.pivot = new Vector2(0, 1);
rect.sizeDelta = new Vector2(90, 90);
// 距离左上角边缘10像素
rect.anchoredPosition = new Vector2(10, -10);
Image img = floatBtn.AddComponent<Image>();
img.color = new Color(0.2f, 0.6f, 1f, 0.8f);
img.raycastTarget = true; // 确保可以接收点击事件
Button btn = floatBtn.AddComponent<Button>();
// 不再添加Button组件点击事件由DraggableFloatingButton的OnPointerClick处理
// 添加文本
GameObject textObj = new GameObject("Text");
@ -148,11 +151,11 @@ namespace MeowmentDebugTool
TextMeshProUGUI text = textObj.AddComponent<TextMeshProUGUI>();
text.text = "调试";
text.fontSize = 36;
text.fontSize = 28;
text.alignment = TextAlignmentOptions.Center;
text.color = Color.white;
// 添加拖拽组件
// 添加拖拽组件(包含点击处理)
DraggableFloatingButton draggable = floatBtn.AddComponent<DraggableFloatingButton>();
draggable.clampToScreen = true;
draggable.snapToEdge = true;
@ -222,6 +225,8 @@ namespace MeowmentDebugTool
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);
@ -229,6 +234,7 @@ namespace MeowmentDebugTool
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 buttonContainer = new GameObject("ButtonContainer");
@ -237,6 +243,8 @@ namespace MeowmentDebugTool
containerRect.anchorMin = new Vector2(0, 1);
containerRect.anchorMax = new Vector2(1, 1);
containerRect.pivot = new Vector2(0.5f, 1);
containerRect.offsetMin = new Vector2(0, containerRect.offsetMin.y);
containerRect.offsetMax = new Vector2(0, containerRect.offsetMax.y);
GridLayoutGroup grid = buttonContainer.AddComponent<GridLayoutGroup>();
grid.cellSize = new Vector2(300, 100);
@ -264,6 +272,8 @@ namespace MeowmentDebugTool
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);
@ -271,6 +281,7 @@ namespace MeowmentDebugTool
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 checkBoxContainer = new GameObject("CheckBoxContainer");
@ -279,6 +290,8 @@ namespace MeowmentDebugTool
containerRect.anchorMin = new Vector2(0, 1);
containerRect.anchorMax = new Vector2(1, 1);
containerRect.pivot = new Vector2(0.5f, 1);
containerRect.offsetMin = new Vector2(0, containerRect.offsetMin.y); // left = 0
containerRect.offsetMax = new Vector2(0, containerRect.offsetMax.y); // right = 0
// 使用垂直布局,一行一个复选框
VerticalLayoutGroup vertLayout = checkBoxContainer.AddComponent<VerticalLayoutGroup>();
@ -307,6 +320,8 @@ namespace MeowmentDebugTool
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);
@ -314,6 +329,7 @@ namespace MeowmentDebugTool
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 valueContainer = new GameObject("ValueContainer");
@ -322,6 +338,8 @@ namespace MeowmentDebugTool
containerRect.anchorMin = new Vector2(0, 1);
containerRect.anchorMax = new Vector2(1, 1);
containerRect.pivot = new Vector2(0.5f, 1);
containerRect.offsetMin = new Vector2(0, containerRect.offsetMin.y); // left = 0
containerRect.offsetMax = new Vector2(0, containerRect.offsetMax.y); // right = 0
// 使用垂直布局,一行一个数值项
VerticalLayoutGroup vertLayout = valueContainer.AddComponent<VerticalLayoutGroup>();
@ -488,6 +506,124 @@ namespace MeowmentDebugTool
currentText.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
currentText.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
// Console Log缓存池设置面板
GameObject bufferPanel = new GameObject("BufferPanel");
bufferPanel.transform.SetParent(page.transform, false);
RectTransform bufferPanelRect = bufferPanel.AddComponent<RectTransform>();
bufferPanelRect.anchorMin = new Vector2(0, 0);
bufferPanelRect.anchorMax = new Vector2(1, 0.5f);
bufferPanelRect.offsetMin = new Vector2(20, 20);
bufferPanelRect.offsetMax = new Vector2(-20, 0);
VerticalLayoutGroup bufferVLayout = bufferPanel.AddComponent<VerticalLayoutGroup>();
bufferVLayout.spacing = 30;
bufferVLayout.padding = new RectOffset(40, 40, 40, 40);
bufferVLayout.childControlWidth = true;
bufferVLayout.childControlHeight = false;
bufferVLayout.childForceExpandWidth = true;
// 标题
GameObject bufferTitle = CreateTextObject("BufferTitle", bufferPanel.transform, "Console Log缓存池设置");
bufferTitle.GetComponent<TextMeshProUGUI>().fontSize = 36;
bufferTitle.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Center;
bufferTitle.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 60);
// Info缓存池输入
GameObject infoBufferContainer = new GameObject("InfoBufferContainer");
infoBufferContainer.transform.SetParent(bufferPanel.transform, false);
RectTransform infoBufferRect = infoBufferContainer.AddComponent<RectTransform>();
infoBufferRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup infoBufferHLayout = infoBufferContainer.AddComponent<HorizontalLayoutGroup>();
infoBufferHLayout.spacing = 20;
infoBufferHLayout.childControlWidth = true;
infoBufferHLayout.childControlHeight = true;
infoBufferHLayout.childForceExpandWidth = false;
infoBufferHLayout.childForceExpandHeight = true;
GameObject infoLabel = CreateTextObject("InfoLabel", infoBufferContainer.transform, "Info:");
infoLabel.GetComponent<TextMeshProUGUI>().fontSize = 28;
infoLabel.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 100);
GameObject infoBufferInput = CreateInputField("InfoBufferInputField", infoBufferContainer.transform, "50");
infoBufferInput.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 100);
// Warning缓存池输入
GameObject warningBufferContainer = new GameObject("WarningBufferContainer");
warningBufferContainer.transform.SetParent(bufferPanel.transform, false);
RectTransform warningBufferRect = warningBufferContainer.AddComponent<RectTransform>();
warningBufferRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup warningBufferHLayout = warningBufferContainer.AddComponent<HorizontalLayoutGroup>();
warningBufferHLayout.spacing = 20;
warningBufferHLayout.childControlWidth = true;
warningBufferHLayout.childControlHeight = true;
warningBufferHLayout.childForceExpandWidth = false;
warningBufferHLayout.childForceExpandHeight = true;
GameObject warningLabel = CreateTextObject("WarningLabel", warningBufferContainer.transform, "Warning:");
warningLabel.GetComponent<TextMeshProUGUI>().fontSize = 28;
warningLabel.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 100);
GameObject warningBufferInput = CreateInputField("WarningBufferInputField", warningBufferContainer.transform, "50");
warningBufferInput.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 100);
// Error缓存池输入
GameObject errorBufferContainer = new GameObject("ErrorBufferContainer");
errorBufferContainer.transform.SetParent(bufferPanel.transform, false);
RectTransform errorBufferRect = errorBufferContainer.AddComponent<RectTransform>();
errorBufferRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup errorBufferHLayout = errorBufferContainer.AddComponent<HorizontalLayoutGroup>();
errorBufferHLayout.spacing = 20;
errorBufferHLayout.childControlWidth = true;
errorBufferHLayout.childControlHeight = true;
errorBufferHLayout.childForceExpandWidth = false;
errorBufferHLayout.childForceExpandHeight = true;
GameObject errorLabel = CreateTextObject("ErrorLabel", errorBufferContainer.transform, "Error:");
errorLabel.GetComponent<TextMeshProUGUI>().fontSize = 28;
errorLabel.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 100);
GameObject errorBufferInput = CreateInputField("ErrorBufferInputField", errorBufferContainer.transform, "50");
errorBufferInput.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 100);
// Fatal缓存池输入
GameObject fatalBufferContainer = new GameObject("FatalBufferContainer");
fatalBufferContainer.transform.SetParent(bufferPanel.transform, false);
RectTransform fatalBufferRect = fatalBufferContainer.AddComponent<RectTransform>();
fatalBufferRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup fatalBufferHLayout = fatalBufferContainer.AddComponent<HorizontalLayoutGroup>();
fatalBufferHLayout.spacing = 20;
fatalBufferHLayout.childControlWidth = true;
fatalBufferHLayout.childControlHeight = true;
fatalBufferHLayout.childForceExpandWidth = false;
fatalBufferHLayout.childForceExpandHeight = true;
GameObject fatalLabel = CreateTextObject("FatalLabel", fatalBufferContainer.transform, "Fatal:");
fatalLabel.GetComponent<TextMeshProUGUI>().fontSize = 28;
fatalLabel.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 100);
GameObject fatalBufferInput = CreateInputField("FatalBufferInputField", fatalBufferContainer.transform, "50");
fatalBufferInput.GetComponent<RectTransform>().sizeDelta = new Vector2(300, 100);
// 缓存池按钮容器
GameObject bufferBtnContainer = new GameObject("BufferButtonContainer");
bufferBtnContainer.transform.SetParent(bufferPanel.transform, false);
RectTransform bufferBtnRect = bufferBtnContainer.AddComponent<RectTransform>();
bufferBtnRect.sizeDelta = new Vector2(0, 100);
HorizontalLayoutGroup bufferBtnLayout = bufferBtnContainer.AddComponent<HorizontalLayoutGroup>();
bufferBtnLayout.spacing = 20;
bufferBtnLayout.childControlWidth = true;
bufferBtnLayout.childControlHeight = true;
bufferBtnLayout.childForceExpandWidth = true;
bufferBtnLayout.childForceExpandHeight = true;
GameObject applyBufferBtn = CreateButton("ApplyBufferButton", bufferBtnContainer.transform, "应用");
GameObject resetBufferBtn = CreateButton("ResetBufferButton", bufferBtnContainer.transform, "重置");
return page;
}
@ -563,13 +699,48 @@ namespace MeowmentDebugTool
LayoutElement fatalLayout = fatalToggle.AddComponent<LayoutElement>();
fatalLayout.preferredHeight = 35;
// 文字筛选面板
GameObject textFilterPanel = new GameObject("TextFilterPanel");
textFilterPanel.transform.SetParent(page.transform, false);
RectTransform textFilterRect = textFilterPanel.AddComponent<RectTransform>();
textFilterRect.sizeDelta = new Vector2(0, 120);
LayoutElement textFilterLayout = textFilterPanel.AddComponent<LayoutElement>();
textFilterLayout.preferredHeight = 120;
textFilterLayout.flexibleHeight = 0;
HorizontalLayoutGroup textFilterHLayout = textFilterPanel.AddComponent<HorizontalLayoutGroup>();
textFilterHLayout.spacing = 20;
textFilterHLayout.padding = new RectOffset(10, 10, 10, 10);
textFilterHLayout.childControlWidth = true;
textFilterHLayout.childControlHeight = true;
textFilterHLayout.childForceExpandWidth = true;
textFilterHLayout.childForceExpandHeight = true;
// 文字筛选标签
GameObject textFilterLabel = CreateTextObject("TextFilterLabel", textFilterPanel.transform, "文字筛选:");
textFilterLabel.GetComponent<TextMeshProUGUI>().fontSize = 28;
textFilterLabel.GetComponent<TextMeshProUGUI>().alignment = TextAlignmentOptions.Left;
textFilterLabel.GetComponent<RectTransform>().sizeDelta = new Vector2(200, 0);
LayoutElement labelLayout = textFilterLabel.AddComponent<LayoutElement>();
labelLayout.preferredWidth = 200;
labelLayout.flexibleWidth = 0;
// 文字筛选输入框
GameObject textFilterInput = CreateInputField("ConsoleTextFilterInputField", textFilterPanel.transform, "");
textFilterInput.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 100);
TMP_InputField inputField = textFilterInput.GetComponent<TMP_InputField>();
if (inputField != null && inputField.placeholder is TextMeshProUGUI placeholder)
{
placeholder.text = "输入筛选文字...";
}
// 日志区域 (60%)
GameObject logArea = new GameObject("LogArea");
logArea.transform.SetParent(page.transform, false);
RectTransform logAreaRect = logArea.AddComponent<RectTransform>();
logAreaRect.sizeDelta = new Vector2(0, 1300);
logAreaRect.sizeDelta = new Vector2(0, 1180);
LayoutElement logAreaLayout = logArea.AddComponent<LayoutElement>();
logAreaLayout.preferredHeight = 1300;
logAreaLayout.preferredHeight = 1180;
logAreaLayout.flexibleHeight = 0;
// 日志ScrollView
@ -658,9 +829,21 @@ namespace MeowmentDebugTool
detailContentRect.anchorMin = new Vector2(0, 1);
detailContentRect.anchorMax = new Vector2(1, 1);
detailContentRect.pivot = new Vector2(0.5f, 1);
detailContentRect.sizeDelta = new Vector2(-20, 0);
detailContentRect.sizeDelta = new Vector2(0, 100);
detailContentRect.anchoredPosition = Vector2.zero;
// 为 Content 添加 ContentSizeFitter 以自动调整高度
ContentSizeFitter contentFitter = detailContent.AddComponent<ContentSizeFitter>();
contentFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// 添加 VerticalLayoutGroup 用于布局
VerticalLayoutGroup contentLayout = detailContent.AddComponent<VerticalLayoutGroup>();
contentLayout.childControlHeight = false;
contentLayout.childControlWidth = true;
contentLayout.childForceExpandHeight = false;
contentLayout.childForceExpandWidth = true;
contentLayout.padding = new RectOffset(10, 10, 10, 10);
// 详情文本
GameObject detailText = CreateTextObject("ConsoleDetailText", detailContent.transform, "点击日志查看详细信息...");
TextMeshProUGUI detailTMP = detailText.GetComponent<TextMeshProUGUI>();
@ -670,14 +853,21 @@ namespace MeowmentDebugTool
detailTMP.overflowMode = TextOverflowModes.Overflow;
detailTMP.richText = true;
detailTMP.color = new Color(0.8f, 0.8f, 0.8f, 1f);
RectTransform detailTextRect = detailText.GetComponent<RectTransform>();
detailTextRect.anchorMin = new Vector2(0, 1);
detailTextRect.anchorMax = new Vector2(1, 1);
detailTextRect.pivot = new Vector2(0.5f, 1);
detailTextRect.sizeDelta = new Vector2(0, 0);
ContentSizeFitter detailFitter = detailText.AddComponent<ContentSizeFitter>();
detailFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// 为文本添加 ContentSizeFitter 使其根据内容自动调整大小
ContentSizeFitter detailTextFitter = detailText.AddComponent<ContentSizeFitter>();
detailTextFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// 添加 LayoutElement 使文本在 VerticalLayoutGroup 中正确工作
LayoutElement detailTextLayout = detailText.AddComponent<LayoutElement>();
detailTextLayout.flexibleHeight = 0;
detailTextLayout.preferredHeight = -1;
detailScroll.content = detailContentRect;
@ -1189,6 +1379,13 @@ namespace MeowmentDebugTool
labelText.alignment = TextAlignmentOptions.Left;
labelText.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
labelText.font = defaultFont;
}
// 添加Toggle组件
Toggle toggle = toggleObj.AddComponent<Toggle>();
toggle.targetGraphic = checkboxBgImage;
@ -1234,6 +1431,13 @@ namespace MeowmentDebugTool
labelText.alignment = TextAlignmentOptions.Left;
labelText.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
labelText.font = defaultFont;
}
// 创建滑动条标题下方占据左侧60%
GameObject sliderObj = new GameObject("Slider");
sliderObj.transform.SetParent(valueObj.transform, false);
@ -1445,6 +1649,27 @@ namespace MeowmentDebugTool
type.GetField("currentResolutionText", flags)?.SetValue(tool, resPanel.Find("CurrentResolutionText")?.GetComponent<TextMeshProUGUI>());
}
// 设置缓存池大小输入框
Transform bufferPanel = settingsPage.transform.Find("BufferPanel");
if (bufferPanel != null)
{
type.GetField("infoBufferInputField", flags)?.SetValue(tool,
bufferPanel.Find("InfoBufferContainer/InfoBufferInputField")?.GetComponent<TMP_InputField>());
type.GetField("warningBufferInputField", flags)?.SetValue(tool,
bufferPanel.Find("WarningBufferContainer/WarningBufferInputField")?.GetComponent<TMP_InputField>());
type.GetField("errorBufferInputField", flags)?.SetValue(tool,
bufferPanel.Find("ErrorBufferContainer/ErrorBufferInputField")?.GetComponent<TMP_InputField>());
type.GetField("fatalBufferInputField", flags)?.SetValue(tool,
bufferPanel.Find("FatalBufferContainer/FatalBufferInputField")?.GetComponent<TMP_InputField>());
Transform bufferBtnContainer = bufferPanel.Find("BufferButtonContainer");
if (bufferBtnContainer != null)
{
type.GetField("applyBufferButton", flags)?.SetValue(tool, bufferBtnContainer.Find("ApplyBufferButton")?.GetComponent<Button>());
type.GetField("resetBufferButton", flags)?.SetValue(tool, bufferBtnContainer.Find("ResetBufferButton")?.GetComponent<Button>());
}
}
// 控制台页面
type.GetField("consolePage", flags)?.SetValue(tool, consolePage);
type.GetField("consoleLogScrollRect", flags)?.SetValue(tool, consolePage.transform.Find("LogArea/ConsoleLogScrollView")?.GetComponent<ScrollRect>());
@ -1462,6 +1687,14 @@ namespace MeowmentDebugTool
type.GetField("consoleFatalFilterToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleFatalFilterToggle")?.GetComponent<Toggle>());
}
// 设置控制台文字筛选输入框
Transform textFilterPanel = consolePage.transform.Find("TextFilterPanel");
if (textFilterPanel != null)
{
type.GetField("consoleTextFilterInputField", flags)?.SetValue(tool,
textFilterPanel.Find("ConsoleTextFilterInputField")?.GetComponent<TMP_InputField>());
}
// 创建日志项预制件
GameObject consoleLogItemPrefab = CreateConsoleLogItemPrefab();
type.GetField("consoleLogItemPrefab", flags)?.SetValue(tool, consoleLogItemPrefab);

View File

@ -17,6 +17,15 @@ namespace MeowmentDebugTool
private Button resetResolutionButton;
private TMP_Text currentResolutionText;
// Console Log缓存池设置
private TMP_InputField infoBufferInputField;
private TMP_InputField warningBufferInputField;
private TMP_InputField errorBufferInputField;
private TMP_InputField fatalBufferInputField;
private Button applyBufferButton;
private Button resetBufferButton;
private ConsoleModule consoleModule;
// 主窗口引用
private RectTransform mainWindow;
private Canvas canvas;
@ -24,11 +33,17 @@ namespace MeowmentDebugTool
// 默认分辨率
private Vector2 defaultResolution = new Vector2(1080, 2340);
private Vector2 currentCustomResolution;
// 默认缓存池大小
private const int DEFAULT_BUFFER_SIZE = 50;
#endregion
#region
public SettingsModule(GameObject page, TMP_InputField widthInput, TMP_InputField heightInput,
Button applyButton, Button resetButton, TMP_Text resolutionText,
TMP_InputField infoBufferInput, TMP_InputField warningBufferInput,
TMP_InputField errorBufferInput, TMP_InputField fatalBufferInput,
Button applyBufferBtn, Button resetBufferBtn,
RectTransform mainWin, Canvas canvasRef)
{
settingsPage = page;
@ -37,6 +52,14 @@ namespace MeowmentDebugTool
applyResolutionButton = applyButton;
resetResolutionButton = resetButton;
currentResolutionText = resolutionText;
infoBufferInputField = infoBufferInput;
warningBufferInputField = warningBufferInput;
errorBufferInputField = errorBufferInput;
fatalBufferInputField = fatalBufferInput;
applyBufferButton = applyBufferBtn;
resetBufferButton = resetBufferBtn;
mainWindow = mainWin;
canvas = canvasRef;
@ -55,8 +78,17 @@ namespace MeowmentDebugTool
if (resetResolutionButton != null)
resetResolutionButton.onClick.AddListener(ResetToDefaultResolution);
if (applyBufferButton != null)
applyBufferButton.onClick.AddListener(ApplyBufferSize);
if (resetBufferButton != null)
resetBufferButton.onClick.AddListener(ResetBufferSize);
// 应用默认分辨率
ApplyResolution(defaultResolution);
// 设置默认缓存池大小
ResetBufferSize();
}
public GameObject GetPage()
@ -68,6 +100,28 @@ namespace MeowmentDebugTool
{
return "设置";
}
/// <summary>
/// 设置 Console Module 引用
/// </summary>
public void SetConsoleModule(ConsoleModule module)
{
consoleModule = module;
// 更新输入框显示当前值
if (consoleModule != null)
{
consoleModule.GetMaxLogLines(out int maxInfo, out int maxWarning, out int maxError, out int maxFatal);
if (infoBufferInputField != null)
infoBufferInputField.text = maxInfo.ToString();
if (warningBufferInputField != null)
warningBufferInputField.text = maxWarning.ToString();
if (errorBufferInputField != null)
errorBufferInputField.text = maxError.ToString();
if (fatalBufferInputField != null)
fatalBufferInputField.text = maxFatal.ToString();
}
}
#endregion
#region
@ -123,6 +177,50 @@ namespace MeowmentDebugTool
// 不需要强制刷新Canvas会导致rebuild loop
// Canvas会自动在下一帧更新
}
private void ApplyBufferSize()
{
if (consoleModule == null)
{
Debug.LogWarning("[SettingsModule] ConsoleModule未设置无法应用缓存池大小");
return;
}
int maxInfo = DEFAULT_BUFFER_SIZE;
int maxWarning = DEFAULT_BUFFER_SIZE;
int maxError = DEFAULT_BUFFER_SIZE;
int maxFatal = DEFAULT_BUFFER_SIZE;
if (infoBufferInputField != null && int.TryParse(infoBufferInputField.text, out int info) && info > 0)
maxInfo = info;
if (warningBufferInputField != null && int.TryParse(warningBufferInputField.text, out int warning) && warning > 0)
maxWarning = warning;
if (errorBufferInputField != null && int.TryParse(errorBufferInputField.text, out int error) && error > 0)
maxError = error;
if (fatalBufferInputField != null && int.TryParse(fatalBufferInputField.text, out int fatal) && fatal > 0)
maxFatal = fatal;
consoleModule.SetMaxLogLines(maxInfo, maxWarning, maxError, maxFatal);
Debug.Log($"[SettingsModule] 已应用缓存池大小 - Info:{maxInfo}, Warning:{maxWarning}, Error:{maxError}, Fatal:{maxFatal}");
}
private void ResetBufferSize()
{
if (infoBufferInputField != null)
infoBufferInputField.text = DEFAULT_BUFFER_SIZE.ToString();
if (warningBufferInputField != null)
warningBufferInputField.text = DEFAULT_BUFFER_SIZE.ToString();
if (errorBufferInputField != null)
errorBufferInputField.text = DEFAULT_BUFFER_SIZE.ToString();
if (fatalBufferInputField != null)
fatalBufferInputField.text = DEFAULT_BUFFER_SIZE.ToString();
if (consoleModule != null)
{
consoleModule.SetMaxLogLines(DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_SIZE, DEFAULT_BUFFER_SIZE);
Debug.Log($"[SettingsModule] 已重置缓存池大小为默认值: {DEFAULT_BUFFER_SIZE}");
}
}
#endregion
}
}

View File

@ -131,8 +131,8 @@ namespace MeowmentDebugTool
// 方案3: 临时演示 - 修改Time.timeScale来模拟时间变化
// 实际项目中应该修改你自己的时间变量
Debug.Log($"💡 提示: 请在AdjustGameTime方法中实现你的时间调整逻辑");
Debug.Log($"💡 建议: 修改游戏内的时间变量,例如 GameTime.Current.AddSeconds({seconds})");
Debug.Log($"[!] 提示: 请在AdjustGameTime方法中实现你的时间调整逻辑");
Debug.Log($"[!] 建议: 修改游戏内的时间变量,例如 GameTime.Current.AddSeconds({seconds})");
// 示例:如果你有一个静态的游戏时间偏移量
// GameTimeOffset += seconds;

View File

@ -66,6 +66,12 @@ namespace MeowmentDebugTool
[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;
@ -79,6 +85,7 @@ namespace MeowmentDebugTool
[SerializeField] private Toggle consoleWarningFilterToggle;
[SerializeField] private Toggle consoleErrorFilterToggle;
[SerializeField] private Toggle consoleFatalFilterToggle;
[SerializeField] private TMP_InputField consoleTextFilterInputField;
[SerializeField] private GameObject consoleLogItemPrefab;
// [Header("输入对话框")]
@ -142,6 +149,13 @@ namespace MeowmentDebugTool
#region Unity生命周期
private void Awake()
{
// 如果未初始化直接销毁GameObject不执行任何操作
if (!isInitialized)
{
Destroy(gameObject);
return;
}
if (instance == null)
{
instance = this;
@ -156,23 +170,24 @@ namespace MeowmentDebugTool
// 设置Canvas为最上层
SetCanvasToTop();
// 未初始化前隐藏所有UI
if (!isInitialized)
{
HideAllUI();
}
// 隐藏所有UI
HideAllUI();
// 注意如果isInitialized为true不在Awake中调用InitializeDebugTool
// 因为此时反射设置的字段可能还没生效延迟到Start中调用
}
private void Start()
{
// 如果未初始化,不执行任何操作
if (!isInitialized)
return;
// 只有在已初始化但还没调用过InitializeDebugTool时才执行
// 这避免了Awake和Init()的重复调用问题
if (isInitialized && allModules.Count == 0)
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
if (allModules.Count == 0)
{
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
InitializeDebugTool();
InitializeAllModules();
ShowMainWindow();
@ -191,11 +206,11 @@ namespace MeowmentDebugTool
OnFourFingerTap();
}
// 检测四指点击(仅在非键盘测试模式下)
if (!useKeyboardTestMode)
{
DetectFourFingerTap();
}
// 四指点击检测已禁用如需使用请在主程序中通过公共API控制显示/隐藏
// if (!useKeyboardTestMode)
// {
// DetectFourFingerTap();
// }
// 更新控制台模块
if (consoleModule != null)
@ -206,6 +221,10 @@ namespace MeowmentDebugTool
private void OnDestroy()
{
// 如果未初始化,不执行任何操作
if (!isInitialized)
return;
if (instance == this)
{
instance = null;
@ -257,7 +276,7 @@ namespace MeowmentDebugTool
/// <param name="fontAsset">TMP字体资源</param>
public static void SetSDFFont(TMP_FontAsset fontAsset)
{
if (!InstanceExists)
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法设置字体");
return;
@ -355,7 +374,7 @@ namespace MeowmentDebugTool
private void InitializeDebugTool()
{
Debug.Log("🚀 初始化UniversalDebugTool...");
Debug.Log("[MeowmentDebugTool] 初始化UniversalDebugTool...");
// 创建模块实例
parametersModule = new ParametersModule(
@ -373,6 +392,9 @@ namespace MeowmentDebugTool
settingsModule = new SettingsModule(
settingsPage, widthInputField, heightInputField,
applyResolutionButton, resetResolutionButton, currentResolutionText,
infoBufferInputField, warningBufferInputField,
errorBufferInputField, fatalBufferInputField,
applyBufferButton, resetBufferButton,
mainWindow, canvas);
consoleModule = new ConsoleModule(
@ -380,14 +402,15 @@ namespace MeowmentDebugTool
consoleDetailScrollRect, consoleDetailText,
consoleClearButton, consoleLockScrollToggle,
consoleInfoFilterToggle, consoleWarningFilterToggle,
consoleErrorFilterToggle, consoleFatalFilterToggle, consoleLogItemPrefab);
consoleErrorFilterToggle, consoleFatalFilterToggle,
consoleTextFilterInputField, consoleLogItemPrefab);
// 添加所有模块到列表
// 添加所有模块到列表(顺序:控制台、按钮、开关、数值、参数、设置)
allModules.Add(consoleModule);
allModules.Add(parametersModule);
allModules.Add(customButtonsModule);
allModules.Add(customCheckBoxesModule);
allModules.Add(customValuesModule);
allModules.Add(parametersModule);
allModules.Add(settingsModule);
// 注册所有页面
@ -396,7 +419,7 @@ namespace MeowmentDebugTool
RegisterPage(module.GetModuleName(), module.GetPage());
}
Debug.Log($"📄 已注册{pages.Count}个页面");
Debug.Log($"[MeowmentDebugTool] 已注册{pages.Count}个页面");
// 创建标签按钮
CreateTabButtons();
@ -405,24 +428,16 @@ namespace MeowmentDebugTool
if (closeButton != null)
closeButton.onClick.AddListener(CloseDebugWindow);
// 设置悬浮按钮点击事件
if (floatingButton != null)
{
Button floatBtn = floatingButton.GetComponent<Button>();
if (floatBtn != null)
{
floatBtn.onClick.AddListener(OnFloatingButtonClick);
}
}
// 注意浮窗点击事件现在由DraggableFloatingButton的OnPointerClick处理
// 默认显示第一个页面
// 默认显示按钮页面
if (pages.Count > 0)
{
ShowPage("参数");
ShowPage("按钮");
}
Debug.Log("[OK] UniversalDebugTool初始化完成");
Debug.Log("💡 提示: 点击窗口顶部的标签切换页面或按F1-F4使用快捷键");
Debug.Log("[MeowmentDebugTool] 提示: 点击窗口顶部的标签切换页面或按F1-F4使用快捷键");
}
/// <summary>
@ -435,6 +450,13 @@ namespace MeowmentDebugTool
{
module.Initialize();
}
// 设置ConsoleModule引用到SettingsModule
if (settingsModule != null && consoleModule != null)
{
settingsModule.SetConsoleModule(consoleModule);
}
Debug.Log("[OK] 所有模块初始化完成!");
}
@ -453,12 +475,12 @@ namespace MeowmentDebugTool
{
string errorDetails = $"tabButtonPrefab={(tabButtonPrefab == null ? "null" : "")}, " +
$"tabButtonContainer={(tabButtonContainer == null ? "null" : "")}";
Debug.LogWarning($"⚠ [MeowmentDebugTool] 标签按钮预制件或容器未完全初始化:{errorDetails}");
Debug.LogWarning("💡 这可能是Unity初始化顺序问题如果标签正常显示则可以忽略此警告");
Debug.LogWarning($"⚠ [MeowmentDebugTool] 标签按钮预制件或容器未完全初始化:{errorDetails}");
Debug.LogWarning("[!] 这可能是Unity初始化顺序问题如果标签正常显示则可以忽略此警告");
return;
}
Debug.Log($"📋 开始创建{pages.Count}个标签按钮...");
Debug.Log($"[MeowmentDebugTool] 开始创建{pages.Count}个标签按钮...");
foreach (var page in pages)
{
@ -494,6 +516,12 @@ namespace MeowmentDebugTool
/// </summary>
public void ShowPage(string pageName)
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示页面");
return;
}
if (!pages.ContainsKey(pageName))
{
Debug.LogWarning($"页面 '{pageName}' 不存在!");
@ -533,6 +561,12 @@ namespace MeowmentDebugTool
/// </summary>
public void CopyDeviceInfoToClipboard()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
parametersModule?.CopyDeviceInfoToClipboard();
}
@ -541,6 +575,12 @@ namespace MeowmentDebugTool
/// </summary>
public void CopySystemInfoToClipboard()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
parametersModule?.CopySystemInfoToClipboard();
}
@ -549,6 +589,12 @@ namespace MeowmentDebugTool
/// </summary>
public void RefreshAllInfo()
{
if (!isInitialized)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化");
return;
}
parametersModule?.RefreshAllInfo();
}
#endregion
@ -559,7 +605,13 @@ namespace MeowmentDebugTool
/// </summary>
public static void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
if (InstanceExists && Instance.customButtonsModule != null)
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法设置按钮回调");
return;
}
if (Instance.customButtonsModule != null)
{
Instance.customButtonsModule.SetCustomButtonCallback(callback);
}
@ -570,9 +622,64 @@ namespace MeowmentDebugTool
/// </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
@ -642,15 +749,43 @@ namespace MeowmentDebugTool
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 (InstanceExists)
if (!isInitialized || !InstanceExists)
{
Instance.gameObject.SetActive(true);
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法显示");
return;
}
Instance.gameObject.SetActive(true);
}
/// <summary>
@ -658,10 +793,13 @@ namespace MeowmentDebugTool
/// </summary>
public static void Hide()
{
if (InstanceExists)
if (!isInitialized || !InstanceExists)
{
Instance.gameObject.SetActive(false);
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法隐藏");
return;
}
Instance.gameObject.SetActive(false);
}
/// <summary>
@ -669,16 +807,19 @@ namespace MeowmentDebugTool
/// </summary>
public static void Toggle()
{
if (InstanceExists)
if (!isInitialized || !InstanceExists)
{
if (Instance.mainWindow != null && Instance.mainWindow.gameObject.activeSelf)
{
Instance.CloseDebugWindow();
}
else
{
Instance.OpenDebugWindow();
}
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法切换状态");
return;
}
if (Instance.mainWindow != null && Instance.mainWindow.gameObject.activeSelf)
{
Instance.CloseDebugWindow();
}
else
{
Instance.OpenDebugWindow();
}
}
@ -696,23 +837,6 @@ namespace MeowmentDebugTool
Debug.Log("[关闭] 调试窗口已关闭");
}
/// <summary>
/// 悬浮按钮点击回调(检查是否为拖动)
/// </summary>
private void OnFloatingButtonClick()
{
// 检查是否刚拖动过
if (draggableComponent != null && draggableComponent.GetWasDragging())
{
// 如果刚拖动过,不打开窗口
Debug.Log("🚫 拖动操作,不打开窗口");
return;
}
// 否则打开窗口
OpenDebugWindow();
}
/// <summary>
/// 打开调试窗口,隐藏悬浮按钮
/// </summary>
@ -745,7 +869,7 @@ namespace MeowmentDebugTool
/// <param name="seconds">隐藏的秒数默认5秒</param>
public static void HideTemporarily(float seconds = 5f)
{
if (!InstanceExists)
if (!isInitialized || !InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法隐藏");
return;
@ -801,7 +925,7 @@ namespace MeowmentDebugTool
// 5. 验证恢复结果
if (canvas.enabled)
{
Debug.Log("[MeowmentDebugTool] UI已成功恢复浮窗状态");
Debug.Log("[MeowmentDebugTool] [OK] UI已成功恢复浮窗状态");
}
else
{
@ -813,65 +937,66 @@ namespace 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
}
// #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] 检测到四指点击");
Debug.Log("[MeowmentDebugTool] 检测到F键触发测试模式");
// 检查主窗口或浮窗是否有任意一个显示
bool anyUIVisible = (mainWindow != null && mainWindow.gameObject.activeSelf) ||
@ -898,10 +1023,10 @@ namespace MeowmentDebugTool
Debug.Log("[MeowmentDebugTool] 已显示浮窗");
}
}
#endregion
}
// #endregion
}
#region
#region
/// <summary>
/// 调试按钮特性 - 标记方法为调试按钮
///
@ -909,7 +1034,7 @@ namespace MeowmentDebugTool
/// 1. 在主项目中使用此特性时,请用条件编译包裹:
///
/// #if MEOWMENT_DEBUG_TOOL
/// [DebugButton("我的调试按钮")]
/// [DebugButton("默认", "我的调试按钮")]
/// public static void MyDebugMethod()
/// {
/// Debug.Log("调试方法被执行");
@ -918,19 +1043,22 @@ namespace MeowmentDebugTool
///
/// 2. 这样当包被卸载时,代码不会报错,也不会影响主工程的正常运行
/// 3. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// 4. 组名参数用于对按钮进行分组显示,未指定组名的按钮会归入"默认"组
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugButtonAttribute : Attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugButtonAttribute : Attribute
{
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)
{
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);
}
GroupName = string.IsNullOrEmpty(groupName) ? "默认" : groupName;
DisplayName = displayName;
ButtonColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试复选框特性 - 标记方法为调试复选框开关
@ -951,18 +1079,18 @@ namespace MeowmentDebugTool
/// 3. 这样当包被卸载时,代码不会报错,也不会影响主工程的正常运行
/// 4. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugCheckBoxAttribute : Attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugCheckBoxAttribute : Attribute
{
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)
{
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)
{
DisplayName = displayName;
CheckBoxColor = new Color(r, g, b, 1f);
}
DisplayName = displayName;
CheckBoxColor = new Color(r, g, b, 1f);
}
}
/// <summary>
/// 调试数值特性 - 标记方法为调试数值调整器
@ -984,24 +1112,24 @@ namespace MeowmentDebugTool
/// 4. 可以设置自定义颜色
/// 5. MEOWMENT_DEBUG_TOOL 宏会在包安装时自动定义,卸载时自动移除
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugValueAttribute : Attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DebugValueAttribute : Attribute
{
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)
{
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)
{
DisplayName = displayName;
MinValue = minValue;
MaxValue = maxValue;
// 如果defaultValue为-1未设置则使用minValue作为默认值
DefaultValue = defaultValue == -1 ? minValue : defaultValue;
ValueColor = new Color(r, g, b, 1f);
}
DisplayName = displayName;
MinValue = minValue;
MaxValue = maxValue;
// 如果defaultValue为-1未设置则使用minValue作为默认值
DefaultValue = defaultValue == -1 ? minValue : defaultValue;
ValueColor = new Color(r, g, b, 1f);
}
#endregion
}
#endregion
}

View File

@ -1,7 +1,7 @@
{
"name": "com.bywaystudios.meowmentdebugtool",
"displayName": "MeowmentDebugTool",
"version": "0.3.7",
"version": "0.3.17",
"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": [
{

156
功能实现总结.md Normal file
View File

@ -0,0 +1,156 @@
# 功能实现完成 - 总结报告
## 实现的功能
### ✅ 1. Debugger显示状态查询
新增方法:`UniversalDebugTool.IsDebuggerVisible()`
**用途**
- 返回bool值表示Debugger是否正在显示
- 用于判断主窗口或悬浮按钮是否可见
**代码位置**
- [UniversalDebugTool.cs](Packages/com.bywaystudios.meowmentdebugtool/Runtime/UniversalDebugTool.cs#L719-L732)
**使用示例**
```csharp
bool isVisible = UniversalDebugTool.IsDebuggerVisible();
Debug.Log($"Debugger显示状态: {isVisible}");
```
---
### ✅ 2. 按钮分组功能
**改动内容**
1. **特性修改** - `DebugButtonAttribute`
- 新增 `GroupName` 属性
- 构造函数第一个参数改为组名
- 默认组名为"默认"
2. **模块重构** - `CustomButtonsModule`
- 添加分组数据结构
- 自动创建分组切换UI容器
- 实现按组加载和显示按钮
- 添加组切换视觉反馈
**使用方式**
```csharp
#if MEOWMENT_DEBUG_TOOL
[MeowmentDebugTool.UniversalDebugTool.DebugButton("玩家", "加金币")]
public static void AddGold()
{
Debug.Log("加金币");
}
#endif
```
**功能特点**
- ✨ 自动分组:根据特性中的组名自动分类
- 🎨 UI优化顶部显示组切换按钮
- 🔄 智能排序:"默认"组排在最前
- 🎯 视觉反馈:当前组显示为绿色
---
## 文件修改清单
### 修改的文件
1. ✏️ [UniversalDebugTool.cs](Packages/com.bywaystudios.meowmentdebugtool/Runtime/UniversalDebugTool.cs)
- 添加 `IsDebuggerVisible()` 方法
- 修改 `DebugButtonAttribute` 构造函数
2. ✏️ [CustomButtonsModule.cs](Packages/com.bywaystudios.meowmentdebugtool/Runtime/CustomButtonsModule.cs)
- 完全重构以支持分组功能
- 添加分组数据结构和UI逻辑
3. ✏️ [Test.cs](Assets/Scenes/Scripts/Test.cs)
- 添加使用示例代码
- 展示如何使用新的分组功能
### 新增的文件
1. 📄 [新功能说明.md](新功能说明.md)
- 详细的功能使用文档
- 包含示例代码和注意事项
2. 📄 [功能实现总结.md](功能实现总结.md)
- 本文件,快速查看实现内容
---
## 重要提示
### ⚠️ 破坏性更新
`DebugButton` 特性的参数顺序已改变:
**旧代码** ❌:
```csharp
[DebugButton("按钮名")]
```
**新代码** ✅:
```csharp
[MeowmentDebugTool.UniversalDebugTool.DebugButton("默认", "按钮名")]
```
### 🔧 迁移指南
如果有旧代码,需要:
1. 在所有 `DebugButton` 特性前添加组名参数
2. 使用完整的命名空间路径 `MeowmentDebugTool.UniversalDebugTool.DebugButton`
3. 建议使用 `#if MEOWMENT_DEBUG_TOOL` 条件编译
---
## 测试建议
1. **基础功能测试**
- 调用 `IsDebuggerVisible()` 测试返回值
- 尝试 Show/Hide 后检查状态
2. **分组功能测试**
- 创建多个不同组的按钮
- 测试组切换是否正常
- 验证按钮是否正确归类
3. **兼容性测试**
- 测试没有组名的按钮(应归入"默认"组)
- 测试只有一个组的情况
- 测试大量按钮的性能
---
## 已知问题
---
## 后续优化建议
1. **可配置性**
- 允许自定义分组按钮颜色
- 允许自定义分组容器高度
2. **功能增强**
- 支持搜索/过滤按钮
- 支持收藏常用按钮
- 支持按钮排序
3. **UI改进**
- 更好的分组按钮布局(可滚动)
- 添加分组图标支持
---
## 联系方式
如有问题或建议,请联系开发者。
---
**完成日期**2025年12月30日
**版本**v1.1.0

View File

@ -0,0 +1,148 @@
# 拖动功能问题修复说明
## 问题描述
禁用调试工具的拖动功能后(`enableDragging = false`发现其他UI上的棋子也无法拖动了。
## 问题原因
### Unity事件系统机制
在Unity的事件系统中当一个GameObject实现了拖动接口`IBeginDragHandler`, `IDragHandler`, `IEndDragHandler`)时:
1. **事件会被该对象"拦截"**:即使在方法内部直接 `return`Unity也认为该对象已经处理了事件
2. **事件不会继续传递**被拦截的事件不会传递给下层或其他UI元素
3. **影响范围广泛**这会导致场景中所有在该对象下方的可拖动UI元素都无法接收拖动事件
### 之前的实现问题
```csharp
public void OnBeginDrag(PointerEventData eventData)
{
if (!enableDragging) return; // ❌ 直接返回,但事件已被拦截
// ... 正常的拖动逻辑
}
```
这种实现方式会导致:
- 调试工具的悬浮按钮拦截了所有拖动事件
- 其他UI元素如棋子无法接收到拖动事件
- 即使 `enableDragging = false`,问题依然存在
## 解决方案
### 核心思路
`enableDragging = false` 时,不应该简单地返回,而是应该将事件**主动传递**给其他可以处理该事件的UI元素。
### 实现细节
#### 1. 修改拖动事件处理方法
`OnBeginDrag`, `OnDrag`, `OnEndDrag` 中添加事件传递逻辑:
```csharp
public void OnBeginDrag(PointerEventData eventData)
{
if (!enableDragging)
{
// ✅ 将事件传递给父级或下层UI元素
PassEventToParent(eventData, ExecuteEvents.beginDragHandler);
return;
}
// ... 正常的拖动逻辑
}
```
#### 2. 实现事件传递方法
添加 `PassEventToParent` 方法,按以下顺序尝试传递事件:
```csharp
private void PassEventToParent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction)
where T : IEventSystemHandler
{
// 步骤1: 向上传递给父级对象
Transform parent = transform.parent;
while (parent != null)
{
if (ExecuteEvents.Execute(parent.gameObject, eventData, eventFunction))
{
return; // 父级处理了,停止传递
}
parent = parent.parent;
}
// 步骤2: 如果父级都没处理,传递给射线检测到的下一个对象
var raycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, raycastResults);
foreach (var result in raycastResults)
{
if (result.gameObject == gameObject)
continue; // 跳过自己
if (ExecuteEvents.Execute(result.gameObject, eventData, eventFunction))
{
return; // 下层对象处理了,停止传递
}
}
}
```
### 工作原理
1. **事件拦截**:悬浮按钮首先接收到拖动事件
2. **条件判断**:检查 `enableDragging` 是否为 `false`
3. **向上传递**:尝试将事件传递给父级对象树
4. **射线检测**如果父级未处理通过射线检测找到下层UI元素
5. **事件执行**:让找到的对象执行相应的拖动事件处理
## 修改文件
- `Packages/com.bywaystudios.meowmentdebugtool/Runtime/DraggableFloatingButton.cs`
- 修改 `OnBeginDrag` 方法
- 修改 `OnDrag` 方法
- 修改 `OnEndDrag` 方法
- 新增 `PassEventToParent` 方法
## 测试建议
### 测试场景1禁用拖动模式
1. 设置 `enableDragging = false`
2. 点击悬浮按钮 → 应该正常打开调试窗口
3. 尝试拖动其他UI上的棋子 → **应该能够正常拖动**
### 测试场景2启用拖动模式
1. 设置 `enableDragging = true`
2. 拖动悬浮按钮 → 应该能够正常拖动
3. 点击悬浮按钮(不拖动)→ 应该打开调试窗口
### 测试场景3多层UI叠加
1. 创建多个可拖动的UI元素部分重叠
2. 确保悬浮按钮在最上层
3. 禁用悬浮按钮拖动后测试下层UI元素能否正常拖动
## 技术要点
### Unity事件传递机制
- `ExecuteEvents.Execute()`:手动触发事件处理
- `EventSystem.RaycastAll()`获取所有被射线击中的UI元素
- 事件处理器接口:`IEventSystemHandler`
### 泛型约束
使用泛型方法支持不同类型的事件处理:
```csharp
private void PassEventToParent<T>(...) where T : IEventSystemHandler
```
### 事件类型
- `ExecuteEvents.beginDragHandler` - 开始拖动
- `ExecuteEvents.dragHandler` - 拖动中
- `ExecuteEvents.endDragHandler` - 结束拖动
## 可能的副作用
### 性能考虑
- `RaycastAll` 会执行完整的射线检测,可能有轻微性能开销
- 仅在 `enableDragging = false` 时触发,实际影响很小
### 兼容性
- 需要确保场景中有 `EventSystem`
- 依赖 Unity 的 EventSystem 模块
## 总结
通过主动传递事件而不是简单地忽略事件解决了禁用拖动后影响其他UI元素的问题。这是一个典型的UI事件系统问题需要理解Unity的事件传递机制才能正确处理。

233
新功能说明.md Normal file
View File

@ -0,0 +1,233 @@
# MeowmentDebugTool 新功能说明
## 更新日期
2025年12月30日
## 新增功能
### 1. 获取Debugger显示状态
新增了一个公共静态方法 `IsDebuggerVisible()`用于获取Debugger的当前显示状态。
#### 使用方法
```csharp
// 获取Debugger是否正在显示
bool isVisible = UniversalDebugTool.IsDebuggerVisible();
if (isVisible)
{
Debug.Log("Debugger正在显示");
}
else
{
Debug.Log("Debugger已隐藏或未初始化");
}
```
#### 方法说明
- **方法签名**: `public static bool IsDebuggerVisible()`
- **返回值**:
- `true` - Debugger正在显示主窗口或悬浮按钮可见
- `false` - Debugger完全隐藏或未初始化
- **使用场景**:
- 判断是否需要显示/隐藏Debugger
- 根据Debugger状态执行不同的逻辑
- 在截图或录屏前检查Debugger状态
---
### 2. 按钮分组功能
为调试按钮添加了分组功能,可以将按钮按功能分类显示,提高大量按钮时的使用体验。
#### 使用方法
在使用 `DebugButton` 特性时,在第一个参数指定组名:
```csharp
using MeowmentDebugTool;
#if MEOWMENT_DEBUG_TOOL
// 默认组
[DebugButton("默认", "我的按钮")]
public static void MyButton()
{
Debug.Log("按钮被点击");
}
// 玩家相关按钮
[DebugButton("玩家", "加金币")]
public static void AddGold()
{
Debug.Log("给玩家加了1000金币");
}
[DebugButton("玩家", "升级")]
public static void LevelUp()
{
Debug.Log("玩家升了1级");
}
// 关卡相关按钮
[DebugButton("关卡", "跳过当前关卡")]
public static void SkipLevel()
{
Debug.Log("跳过当前关卡");
}
[DebugButton("关卡", "解锁所有关卡", 0.8f, 0.6f, 0.2f)]
public static void UnlockAllLevels()
{
Debug.Log("解锁所有关卡");
}
#endif
```
**注意**:确保在文件顶部添加 `using MeowmentDebugTool;` 以便可以直接使用 `DebugButton` 特性。
#### 特性参数说明
`DebugButton` 特性的新参数顺序:
```csharp
DebugButtonAttribute(
string groupName = "默认", // 组名(新增)
string displayName = "", // 显示名称
float r = 0.8f, // 按钮颜色 R
float g = 0.8f, // 按钮颜色 G
float b = 0.8f // 按钮颜色 B
)
```
#### 功能特点
1. **自动分组**: 系统会自动收集所有带有 `DebugButton` 特性的方法,并按组名分类
2. **组标题显示**: 每个组会显示一个标题行,标题格式为:`━━━━━━ 组名 ━━━━━━`
3. **默认组**: 如果不指定组名或组名为空,按钮会被归入"默认"组
4. **组排序**: "默认"组会排在最前面,其他组按字母顺序排列
5. **一次显示所有**: 所有组的按钮会在一个滚动视图中垂直显示,无需切换
#### 界面布局
```
┌─────────────────────────────────┐
│ ━━━━━━ 默认 ━━━━━━ │ ← 组标题
│ [测试按钮1] │
│ [测试按钮2] │ ← 该组的按钮
│ │
│ ━━━━━━ 玩家 ━━━━━━ │ ← 组标题
│ [加金币] │
│ [升级] │
│ [满血复活] │ ← 该组的按钮
│ │
│ ━━━━━━ 关卡 ━━━━━━ │ ← 组标题
│ [跳过当前关卡] │
│ [重置关卡进度] │
│ [解锁所有关卡] │ ← 该组的按钮
│ ... │
└─────────────────────────────────┘
```
#### 注意事项
1. **兼容性**: 这是一个破坏性更新,旧代码需要调整参数顺序
**旧代码**:
```csharp
[DebugButton("按钮名称")]
```
**新代码** (需要添加组名):
```csharp
[DebugButton("默认", "按钮名称")]
```
2. **组名建议**:
- 使用简短明确的组名(如:玩家、关卡、道具、系统等)
- 相关功能的按钮放在同一组
- 常用功能建议放在"默认"组
3. **性能**: 分组功能不会影响性能,因为:
- 只在初始化和手动重载时扫描方法
- 所有按钮一次性创建,无需动态切换
---
## 示例代码
完整的使用示例请参考 `Assets/Scenes/Scripts/Test.cs` 文件。
## 向后兼容性说明
⚠️ **重要**: `DebugButton` 特性的参数顺序已更改,旧代码需要手动调整。
如果你之前这样使用:
```csharp
[DebugButton("我的按钮", 0.5f, 0.5f, 0.5f)]
```
现在需要改为:
```csharp
[DebugButton("默认", "我的按钮", 0.5f, 0.5f, 0.5f)]
```
或者如果不需要自定义颜色:
```csharp
[DebugButton("默认", "我的按钮")]
```
**注意**:请确保在文件顶部添加 `using MeowmentDebugTool;`
---
## 技术细节
### CustomButtonsModule 的改动
1. 添加了分组数据结构 `Dictionary<string, List<ButtonInfo>>`
2. 添加了分组UI容器的自动创建逻辑
3. 修改了按钮加载流程,先分组再显示
4. 添加了组切换功能和视觉反馈
### UniversalDebugTool 的改动
1. 添加了 `IsDebuggerVisible()` 方法
2. 修改了 `DebugButtonAttribute` 的构造函数,添加了 `groupName` 参数
3. 更新了文档注释
---
## 常见问题
### Q: 如何知道当前有哪些组?
A: 在初始化时,控制台会输出所有找到的组及其按钮数量:
```
[CustomButtonsModule] 共找到 3 个按钮组
- 默认: 2 个按钮
- 玩家: 3 个按钮
- 关卡: 3 个按钮
```
### Q: 能否动态添加组?
A: 可以,调用 `ReloadCustomButtons()` 方法即可重新扫描所有按钮并更新分组。
### Q: 分组按钮的样式可以自定义吗?
A: 可以,分组按钮使用与普通按钮相同的预制件,可以通过修改预制件来自定义样式。
### Q: IsDebuggerVisible() 什么情况下返回 false
A: 以下情况会返回 false
- Debugger 未初始化(未调用 `Init()`
- 主窗口和悬浮按钮都被隐藏
- Debugger GameObject 被销毁
---
## 更新日志
### v1.1.0 (2025-12-30)
- ✨ 新增 `IsDebuggerVisible()` 方法用于获取显示状态
- ✨ 新增按钮分组功能
- 🔧 修改 `DebugButtonAttribute` 构造函数参数顺序(破坏性更新)
- 📝 更新文档和示例代码