0.3更新,新增控制台

This commit is contained in:
zhang hongbo 2025-12-22 15:29:55 +08:00
parent 1152c5032c
commit 62f990f702
33 changed files with 3611 additions and 568 deletions

View File

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

View File

@ -0,0 +1,215 @@
# 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

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

View File

@ -5,6 +5,50 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [0.3.0] - 2025-12-22
### Added
- **控制台模块 (Console)**
- 实时捕获并显示Unity日志Info、Warning、Error、Fatal
- 支持日志类型过滤(可单独开关各类型日志显示)
- 支持日志点击查看详细信息(堆栈跟踪)
- 日志数量统计和实时更新
- 支持清空日志功能
- 支持锁定滚动功能(自动滚动到最新日志)
- 日志项对象池优化,提升性能
- 最大日志数限制默认100条可配置
- 日志详情区域显示完整堆栈信息
- 使用UGUI实现支持ScrollRect滚动
- **暂时隐藏功能**
- 工具栏新增"暂时隐藏(5秒)"按钮
- 用于截图时临时隐藏调试工具UI
- 自动在指定时间后恢复显示
- 支持防重入保护,避免重复触发
- 恢复后保持浮窗状态,不自动打开主窗口
### Changed
- 重构代码架构,将功能模块化
- 新增 `IDebugModule` 接口
- 拆分为独立模块:`ParametersModule`、`CustomButtonsModule`、`ToolbarModule`、`SettingsModule`、`ConsoleModule`
- 优化控制台UI布局
- ControlPanel高度优化为40像素
- LogArea和DetailArea固定高度
- 移除不常用的Spacer元素
- 优化日志显示
- 日志项高度从30增加到50像素
- 字体大小从22增加到28
- 添加上下边距提升可读性
- 移除所有emoji字符避免字体缺失警告
- 锁定滚动功能默认开启
### Fixed
- 修复Canvas rebuild loop错误延迟刷新机制
- 修复日志项RectTransform锚点设置问题
- 修复VerticalLayoutGroup子元素高度控制问题
- 修复页面未激活时不必要的更新操作
- 修复暂时隐藏功能第二次执行失败的问题
## [0.2.2] - 2025-12-19
### Changed

View File

@ -0,0 +1,236 @@
# 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

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

View File

@ -0,0 +1,171 @@
# 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

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

View File

@ -0,0 +1,233 @@
# 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

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

View File

@ -0,0 +1,186 @@
# 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

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

View File

@ -1,59 +1,129 @@
# MeowMent Debug Tool通用调试工具)
# MeowMent Debug Tool喵刻调试工具)
一个面向运行时的 Unity 调试工具,内置多标签页调试面板、可拖拽悬浮按钮、设备/系统信息查看,以及通过特性自动生成的“自定义调试按钮”
一个强大的 Unity 运行时调试工具,提供多标签页调试面板、控制台日志、可拖拽悬浮按钮、系统信息查看以及自定义调试按钮等功能
核心脚本:
- `Runtime/UniversalDebugTool.cs`
- `Runtime/DraggableFloatingButton.cs`
- `Editor/DebugToolPrefabGenerator.cs`
- `Editor/CreateTabButtonPrefab.cs`
**版本:** 0.3.0
**适用平台:** 全平台Windows、Android、iOS等
**Unity版本** 2021.3 及以上
---
## 功能概览
## 📋 目录
- 多标签页调试面板(默认分辨率 1080x2340
- 参数:设备信息、系统信息,一键复制到剪贴板。
- 自定义按钮:通过特性自动收集项目中的调试方法并生成按钮。
- 工具栏:简单的“时间调整”示例入口,可按项目需要扩展。
- 设置:运行时修改调试窗口尺寸、重置为默认分辨率。
- 悬浮调试按钮:
- 关闭主窗口后,会显示一个可拖拽、自动吸附边缘的浮动按钮。
- 点击可再次打开调试主窗口,拖动不会误触打开(内部做了拖拽判定)。
- 运行时公共 API
- `UniversalDebugTool.Show() / Hide() / Toggle()` 控制显示。
- `UniversalDebugTool.ShowInputDialog(...)` 弹出输入对话框。
- `UniversalDebugTool.Instance.ReloadCustomButtons()` 重新扫描并生成自定义按钮。
- [功能特性](#功能特性)
- [给策划使用 - 基础功能](#给策划使用---基础功能)
- [给客户端使用 - 初始化配置](#给客户端使用---初始化配置)
- [给后端使用 - 添加自定义按钮](#给后端使用---添加自定义按钮)
- [常见问题](#常见问题)
---
## 安装
## 🎯 功能特性
### 方式一:本地拷贝(推荐在开发环境使用)
### 核心功能模块
- **参数查看**:实时查看设备信息、系统信息,支持一键复制
- **自定义按钮**:通过特性自动生成调试按钮,无需手动添加
- **工具栏**:时间调整工具、截图隐藏功能
- **控制台**实时捕获Unity日志支持过滤、搜索、查看堆栈
- **设置**:运行时修改分辨率
1. 将 `com.bywaystudios.meowmentdebugtool@0.1.4` 整个文件夹放到你项目的 `Packages/` 目录下。
2. 打开 Unity等待重新导入即可在菜单中看到调试工具相关项。
### 方式二:通过自建 Git / 私有仓库
如果你已经把此包发布到自己的 Git 仓库或私有 Package Registry可参考 Unity 官方文档,通过修改 `Packages/manifest.json``dependencies` / `scopedRegistries` 来添加:
```jsonc
"dependencies": {
"com.bywaystudios.meowmentdebugtool": "0.1.4"
}
```
> 具体部署方式视团队实际环境而定,这里不展开。
### UI特性
- 可拖拽悬浮按钮,自动吸附边缘
- 多标签页设计,清晰分类
- Canvas层级30000始终在最上层
- 支持自定义TextMeshPro字体
---
## 快速开始
## 👥 给策划使用 - 基础功能
### 1. 在代码中初始化调试工具
### 1. 如何打开调试工具
调试工具现在支持**运行时自动生成UI**,无需手动创建预制体。只需在代码中调用:
游戏运行后,屏幕左侧会显示一个**蓝色圆形浮窗按钮**
- **点击浮窗** → 打开调试主窗口
- **拖动浮窗** → 改变位置(会自动吸附到屏幕边缘)
- **点击左上角X** → 关闭主窗口,回到浮窗状态
### 2. 功能页面说明
调试窗口包含5个标签页
#### 📊 参数查看
- **设备信息**:设备型号、系统版本、处理器、内存等
- **系统信息**Unity版本、图形API、显卡信息等
- **复制按钮**点击可复制信息到剪贴板方便粘贴到bug报告
#### 🎮 自定义按钮
- 显示后端程序员添加的调试功能按钮
- 点击按钮即可执行对应功能(如清理存档、添加道具等)
- 不同按钮有不同颜色,方便区分
**常见按钮示例:**
- "清空玩家数据" - 重置游戏进度
- "添加金币" - 增加游戏货币
- "解锁所有关卡" - 开启所有内容
- "重置今日任务" - 刷新每日任务
#### 🛠️ 工具栏
- **时间调整**:模拟时间流逝(用于测试时间相关功能)
- **暂时隐藏(5秒)**截图前点击此按钮调试工具会隐藏5秒方便截取干净的游戏画面
**使用场景:**
1. 需要截图游戏画面时
2. 点击"暂时隐藏(5秒)"
3. 调试工具消失,快速截图
4. 5秒后自动恢复继续调试
#### 📝 控制台
实时显示游戏日志,帮助定位问题:
- **Info白色**:普通信息日志
- **Warning黄色**:警告信息
- **Error红色**:错误信息
- **Fatal深红**:严重错误/异常
**功能按钮:**
- **清空**:清除所有日志
- **锁定滚动**:勾选后自动滚动到最新日志
- **过滤器**点击Info/Warning/Error切换显示对应类型日志
**查看详情:**
- 点击任意日志条目 → 下方显示完整堆栈信息
- 方便向程序员反馈详细错误
#### ⚙️ 设置
- **分辨率调整**:修改游戏窗口大小(仅编辑器/Windows有效
- **当前分辨率显示**:显示当前屏幕分辨率
### 3. 日常测试工作流
**测试新功能:**
1. 打开调试工具
2. 切换到"自定义按钮"页面
3. 点击相关测试按钮(如"开启活动"
4. 观察游戏效果
5. 如有问题,切换到"控制台"查看错误
**截图报bug**
1. 复现问题
2. 点击"工具栏" → "暂时隐藏(5秒)"
3. 截取游戏画面
4. 切换回"控制台"截取错误日志
5. 一起提交给程序
**收集设备信息:**
1. 切换到"参数查看"
2. 点击"复制设备信息"或"复制系统信息"
3. 粘贴到bug报告中
---
## 💻 给客户端使用 - 初始化配置
### 1. 基础初始化
在游戏启动时(通常在启动场景的初始化脚本中)调用:
```csharp
using UnityEngine;
@ -62,14 +132,14 @@ using TMPro;
public class GameInitializer : MonoBehaviour
{
public TMP_FontAsset customFont; // 可选:自定义字体
[SerializeField] private TMP_FontAsset customFont; // 拖入自定义字体
void Start()
{
// 初始化调试工具自动创建UI
// 初始化调试工具
UniversalDebugTool.Init();
// 可选:设置自定义SDF字体
// 设置自定义字体(可选)
if (customFont != null)
{
UniversalDebugTool.SetSDFFont(customFont);
@ -78,27 +148,34 @@ public class GameInitializer : MonoBehaviour
}
```
**初始化说明:**
### 2. API说明
- `UniversalDebugTool.Init()` 会自动创建完整的UI系统包括
- 独立的 Canvas排序顺序 30000保证在最上层渲染
- 主调试窗口(含标签页、参数页、自定义按钮页、工具栏页、设置页
- 可拖拽的调试悬浮按钮
- EventSystem如果场景中没有
- `UniversalDebugTool.SetSDFFont()` 可以为所有UI文本设置自定义字体可选
#### `UniversalDebugTool.Init()`
初始化调试工具,自动创建:
- Canvas排序顺序30000
- 主调试窗口
- 悬浮按钮
- EventSystem如果没有
### 2. 运行时使用
**注意:**
- 只需调用一次
- 工具会自动DontDestroyOnLoad场景切换不销毁
- 未调用Init()前不显示任何UI
进入 Play 模式后:
#### `UniversalDebugTool.SetSDFFont(TMP_FontAsset fontAsset)`
设置所有UI文本的字体
- 包括已创建的UI和后续创建的按钮
- 支持中文、特殊字符
- 建议使用支持中文的SDF字体
- 默认显示主调试窗口在左上角
- 点击左上角的 **X** 按钮,会隐藏主窗口并显示悬浮按钮
- 拖动悬浮按钮改变位置(自动吸附边缘)
- 点击悬浮按钮可重新打开主窗口(拖动不会误触)
**字体推荐:**
- 思源黑体 SDF
- 阿里巴巴普惠体 SDF
- 或项目现有的中文字体
### 3. 条件编译(推荐)
为了确保在移除包后不影响主工程,建议使用条件编译
为了确保打包时不包含调试代码
```csharp
void Start()
@ -110,166 +187,361 @@ void Start()
}
```
`MEOWMENT_DEBUG_TOOL` 宏会在包安装时自动定义,卸载时自动移除。
**说明:**
- `MEOWMENT_DEBUG_TOOL` 宏在包安装时自动定义
- 移除包时自动移除,不影响项目编译
- 正式包不会包含调试工具代码
### 4. 完整示例
```csharp
using UnityEngine;
using MeowmentDebugTool;
using TMPro;
public class Test : MonoBehaviour
{
public TMP_FontAsset fontAsset;
void Start()
{
#if MEOWMENT_DEBUG_TOOL
// 初始化调试工具
UniversalDebugTool.Init();
// 设置字体
if (fontAsset != null)
{
UniversalDebugTool.SetSDFFont(fontAsset);
}
#endif
}
}
```
---
## 自定义调试按钮
## 🔧 给后端使用 - 添加自定义按钮
调试工具会在运行时通过反射扫描所有程序集,查找带有 `DebugButtonAttribute` 特性的方法,并在“自定义按钮”标签页中为其自动生成一个按钮。
### 1. 基础用法
### 编写一个自定义调试方法
> 要求:
> - 方法必须是 `static`
> - 当前实现假定为**无参数**方法。如果需要参数,可结合 `ShowInputDialog` 自行封装。
示例:
使用 `[DebugButton]` 特性标记静态方法,调试工具会自动生成按钮:
```csharp
using UnityEngine;
public static class DemoDebugActions
public static class PlayerDebugFunctions
{
// 在“自定义按钮”页中会生成一个名为“打印金币数量”的按钮
[DebugButton("打印金币数量", 0.3f, 0.6f, 1f)]
private static void PrintCoinCount()
[DebugButton("清空玩家数据")]
private static void ClearPlayerData()
{
Debug.Log($"Coins: {PlayerData.Coin}");
PlayerPrefs.DeleteAll();
Debug.Log("玩家数据已清空");
}
// 不传 displayName 时,按钮文字默认使用方法名
[DebugButton]
private static void KillAllEnemies()
[DebugButton("添加1000金币")]
private static void AddCoins()
{
EnemyManager.KillAll();
PlayerData.Coins += 1000;
Debug.Log($"当前金币:{PlayerData.Coins}");
}
}
```
`DebugButtonAttribute` 构造函数签名:
**运行效果:**
- 在"自定义按钮"页面会出现两个按钮
- 点击按钮执行对应方法
### 2. 特性参数详解
```csharp
public DebugButtonAttribute(string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f);
[DebugButton(string displayName = "", float r = 0.8f, float g = 0.8f, float b = 0.8f)]
```
- `displayName`:按钮显示文字(可为空,为空则使用方法名)。
- `r g b`:按钮背景色的 RGB 分量0~1
#### 参数说明:
- **displayName**:按钮显示文字(不填则使用方法名)
- **r, g, b**按钮背景颜色RGB值范围0-1
写好方法后:
1. 进入 Play 模式。
2. 打开调试工具 → 切换到“自定义按钮”标签页。
3. 即可看到刚才添加的调试按钮,点击执行对应逻辑。
如果你在运行时新增/修改了带有 `DebugButtonAttribute` 的方法(如通过热更等方式),可以调用:
#### 示例:不同颜色按钮
```csharp
UniversalDebugTool.Instance.ReloadCustomButtons();
public static class GameDebugFunctions
{
// 绿色按钮 - 安全操作
[DebugButton("保存游戏", 0.2f, 0.6f, 0.2f)]
private static void SaveGame()
{
GameManager.Save();
}
// 红色按钮 - 危险操作
[DebugButton("删除存档", 0.9f, 0.2f, 0.2f)]
private static void DeleteSave()
{
GameManager.DeleteSave();
}
// 蓝色按钮 - 功能测试
[DebugButton("跳到第10关", 0.2f, 0.5f, 0.9f)]
private static void JumpToLevel10()
{
LevelManager.LoadLevel(10);
}
// 默认颜色(灰色)
[DebugButton("打印游戏状态")]
private static void PrintGameState()
{
Debug.Log($"Level: {GameManager.CurrentLevel}");
}
}
```
来重新扫描并刷新按钮列表。
### 3. 使用输入对话框
---
## 输入对话框(可选)
调试工具内置了一个简单的输入对话框,可与自定义按钮组合使用:
需要用户输入参数时,使用 `ShowInputDialog`
```csharp
using TMPro;
public static class DemoInputDebug
public static class AdvancedDebugFunctions
{
[DebugButton("设置玩家等级")]
private static void SetPlayerLevel()
{
UniversalDebugTool.ShowInputDialog(
"输入玩家等级",
"输入等级",
onConfirmAction: text =>
{
if (int.TryParse(text, out var level))
if (int.TryParse(text, out int level))
{
PlayerData.Level = level;
Debug.Log($"设置玩家等级为: {level}");
Debug.Log($"等级已设置为:{level}");
}
else
{
Debug.LogWarning($"无效等级: {text}");
Debug.LogWarning("请输入有效数字");
}
},
initialValue: "1",
contentType: TMP_InputField.ContentType.IntegerNumber
);
}
[DebugButton("设置玩家名称")]
private static void SetPlayerName()
{
UniversalDebugTool.ShowInputDialog(
"输入名称",
onConfirmAction: name =>
{
PlayerData.Name = name;
Debug.Log($"名称已设置为:{name}");
},
initialValue: PlayerData.Name,
contentType: TMP_InputField.ContentType.Standard
);
}
}
```
调用后会弹出一个输入框,用户点击确认时会把输入字符串传回回调函数中。
### 4. 条件编译(推荐)
---
## 公共 API 一览
以下方法均定义在 `UniversalDebugTool` 中:
**初始化和配置:**
- `static void Init()`初始化调试工具自动创建完整的UI系统包括Canvas、主窗口、悬浮按钮、EventSystem等
- `static void SetSDFFont(TMP_FontAsset fontAsset)`为所有UI文本设置自定义SDF字体包括预制件模板和后续创建的按钮。
**显示控制:**
- `static bool InstanceExists`:当前场景中是否存在实例。
- `static UniversalDebugTool Instance`:单例实例。
- `static void Show()`:显示调试工具 GameObject。
- `static void Hide()`:隐藏调试工具 GameObject。
- `static void Toggle()`:在"窗口显示"和"窗口关闭 + 显示悬浮按钮"之间切换。
**自定义按钮:**
- `void ReloadCustomButtons()`:重新扫描并生成自定义按钮。
**输入对话框:**
- `static void ShowInputDialog(string title, Action<string> onConfirmAction, string initialValue = "", TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)`:显示输入对话框。
- `static void CloseInputDialog()`:关闭输入对话框并清理回调。
**信息查看:**
- `void CopyDeviceInfoToClipboard()` / `void CopySystemInfoToClipboard()`:复制对应信息到系统剪贴板。
你也可以在自己项目的 UI / 快捷键逻辑中调用这些方法,例如:
为了确保正式版不包含调试代码:
```csharp
// 示例:按 F1 切换调试面板
void Update()
public static class ItemDebugFunctions
{
if (Input.GetKeyDown(KeyCode.F1))
#if MEOWMENT_DEBUG_TOOL
[DebugButton("添加道具")]
private static void AddItem()
{
UniversalDebugTool.Toggle();
ItemManager.AddItem(1001, 10);
}
#endif
}
```
### 5. 高级技巧
#### 技巧1分类管理
按功能模块创建不同的类:
```csharp
// 玩家相关
public static class PlayerDebug { ... }
// 关卡相关
public static class LevelDebug { ... }
// 道具相关
public static class ItemDebug { ... }
```
#### 技巧2快速测试流程
```csharp
[DebugButton("快速进入战斗")]
private static void QuickEnterBattle()
{
// 1. 设置测试数据
PlayerData.Level = 10;
PlayerData.Coins = 9999;
// 2. 解锁功能
FeatureManager.UnlockAll();
// 3. 跳转场景
SceneManager.LoadScene("Battle");
}
```
#### 技巧3开发辅助
```csharp
[DebugButton("开启所有调试选项")]
private static void EnableAllDebug()
{
GameConfig.ShowFPS = true;
GameConfig.GodMode = true;
GameConfig.UnlimitedEnergy = true;
Debug.Log("所有调试选项已开启");
}
```
### 6. 实战示例
```csharp
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
public static class SampleDebugFunctions
{
#region 玩家数据
[DebugButton("重置游戏", 0.9f, 0.3f, 0.3f)]
private static void ResetGame()
{
PlayerPrefs.DeleteAll();
SceneManager.LoadScene(0);
Debug.Log("游戏已重置");
}
[DebugButton("满级满资源", 0.2f, 0.8f, 0.2f)]
private static void MaxEverything()
{
PlayerData.Level = 99;
PlayerData.Coins = 999999;
PlayerData.Gems = 99999;
Debug.Log("已设置为满级满资源");
}
#endregion
#region 关卡测试
[DebugButton("跳关", 0.3f, 0.6f, 0.9f)]
private static void JumpToLevel()
{
UniversalDebugTool.ShowInputDialog(
"输入关卡号",
text =>
{
if (int.TryParse(text, out int level))
{
LevelManager.LoadLevel(level);
}
},
"1",
TMP_InputField.ContentType.IntegerNumber
);
}
[DebugButton("解锁所有关卡", 0.2f, 0.8f, 0.8f)]
private static void UnlockAllLevels()
{
for (int i = 0; i < 100; i++)
{
LevelManager.UnlockLevel(i);
}
Debug.Log("已解锁所有关卡");
}
#endregion
#region 系统测试
[DebugButton("清理未使用资源", 0.9f, 0.5f, 0.3f)]
private static void UnloadUnusedAssets()
{
Resources.UnloadUnusedAssets();
Debug.Log("已清理未使用的资源");
}
[DebugButton("触发GC")]
private static void ForceGC()
{
System.GC.Collect();
Debug.Log("已触发垃圾回收");
}
#endregion
}
```
---
## 注意事项
## ❓ 常见问题
**依赖:**
- Unity 新 UI`UnityEngine.UI`
- TextMesh Pro`TMPro`
- 包会自动创建 EventSystem如果场景中没有
### Q1: 字体显示方块/乱码怎么办?
**A:** 调用 `UniversalDebugTool.SetSDFFont()` 设置支持中文的SDF字体。
**使用建议:**
- 默认 UI 参考分辨率为 1080×2340适合手机竖屏调试。可在"设置"页中修改窗口大小以适配不同设备。
- 主窗口锚点在左上角,不受分辨率变化影响。
- 悬浮按钮可在全屏范围内拖动,会自动吸附到边缘。
- 工具栏中的"时间调整"逻辑仅是示例,需要在 `AdjustGameTime` 中实现实际的时间修改逻辑。
### Q2: 自定义按钮不显示?
**A:** 检查:
- 方法是否为 `static`
- 是否添加了 `[DebugButton]` 特性
- 是否在 `#if MEOWMENT_DEBUG_TOOL`
**字体设置:**
- 所有 UI 文本默认使用 TMP 的默认字体。
- 调用 `SetSDFFont()` 可以为所有文本(包括后续创建的自定义按钮)设置自定义字体。
### Q3: 第二次点击"暂时隐藏"无效?
**A:** 等待上一次隐藏结束,或查看控制台是否有"已经在隐藏状态中"的警告。
**条件编译:**
- 建议使用 `#if MEOWMENT_DEBUG_TOOL` 包裹调试工具相关代码。
- 包安装时会自动定义 `MEOWMENT_DEBUG_TOOL` 宏,卸载时自动移除。
- 这样可以确保移除包后不影响主工程编译。
### Q4: 如何在正式版移除调试工具?
**A:** 删除Packages目录下的工具包文件夹即可`#if MEOWMENT_DEBUG_TOOL` 包裹的代码会自动失效。
**进一步定制:**
如需修改 UI 布局、颜色、标签页等,可以直接修改 `RuntimeUIGenerator.cs` 中的 UI 生成逻辑。
### Q5: 控制台日志太多怎么办?
**A:**
- 点击"清空"按钮清除日志
- 使用过滤器只显示Error/Warning
- 工具默认只保留最新100条日志
### Q6: 可以在多个场景使用吗?
**A:** 可以工具使用DontDestroyOnLoad场景切换不会销毁。
---
## 📝 更新日志
### [0.3.0] - 2025-12-22
- ✨ 新增控制台模块实时显示Unity日志
- ✨ 新增暂时隐藏功能,方便截图
- 🔨 重构代码架构,模块化设计
- 🐛 修复多个UI布局问题
### [0.2.2] - 2025-12-19
- 🔨 默认显示浮窗而非主窗口
- 🐛 优化初始化逻辑
详见 [CHANGELOG.md](CHANGELOG.md)
---
## 📦 依赖
- Unity 2021.3+
- TextMesh Pro (com.unity.textmeshpro)
- Unity UI (com.unity.ugui)
---
## 📧 联系方式
如有问题或建议,请联系开发团队。
---
**祝调试顺利!** 🐱

View File

@ -0,0 +1,111 @@
# 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

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

View File

@ -0,0 +1,579 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 控制台模块 - 显示Unity日志基于UGUI实现
/// </summary>
public class ConsoleModule : IDebugModule
{
#region
private GameObject consolePage;
// UI组件
private ScrollRect logScrollRect;
private RectTransform logContent;
private ScrollRect detailScrollRect;
private TMP_Text detailText;
// 按钮
private Button clearButton;
private Toggle lockScrollToggle;
private Toggle infoFilterToggle;
private Toggle warningFilterToggle;
private Toggle errorFilterToggle;
private Toggle fatalFilterToggle;
// 日志项预制件
private GameObject logItemPrefab;
// 日志数据
private Queue<LogNode> logNodes = new Queue<LogNode>();
private LogNode selectedNode = null;
// 日志计数
private int infoCount = 0;
private int warningCount = 0;
private int errorCount = 0;
private int fatalCount = 0;
// 设置
private bool lockScroll = true;
private int maxLine = 200;
private bool infoFilter = true;
private bool warningFilter = true;
private bool errorFilter = true;
private bool fatalFilter = true;
// 颜色设置
private Color32 infoColor = Color.white;
private Color32 warningColor = Color.yellow;
private Color32 errorColor = Color.red;
private Color32 fatalColor = new Color(0.7f, 0.2f, 0.2f);
// UI对象池
private List<GameObject> logItemPool = new List<GameObject>();
private List<GameObject> activeLogItems = new List<GameObject>();
// 保存的字体
private TMP_FontAsset savedFontAsset = null;
// 延迟刷新标记
private bool needRefresh = false;
private int lastRefreshFrame = -1;
#endregion
#region
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)
{
consolePage = page;
logScrollRect = logScroll;
logContent = logContentTransform;
detailScrollRect = detailScroll;
detailText = detail;
clearButton = clearBtn;
lockScrollToggle = lockToggle;
infoFilterToggle = infoToggle;
warningFilterToggle = warningToggle;
errorFilterToggle = errorToggle;
fatalFilterToggle = fatalToggle;
logItemPrefab = logItemPrefabObj;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[ConsoleModule] 初始化控制台模块...");
// 检查必要的组件
if (consolePage == null)
Debug.LogError("[ConsoleModule] consolePage is null!");
if (logScrollRect == null)
Debug.LogError("[ConsoleModule] logScrollRect is null!");
if (logContent == null)
Debug.LogError("[ConsoleModule] logContent is null!");
if (detailScrollRect == null)
Debug.LogError("[ConsoleModule] detailScrollRect is null!");
if (detailText == null)
Debug.LogError("[ConsoleModule] detailText is null!");
if (clearButton == null)
Debug.LogError("[ConsoleModule] clearButton is null!");
if (logItemPrefab == null)
Debug.LogError("[ConsoleModule] logItemPrefab is null!");
// 注册Unity日志回调
Application.logMessageReceived += OnLogMessageReceived;
// 设置按钮事件
if (clearButton != null)
clearButton.onClick.AddListener(ClearAllLogs);
// 设置Toggle事件
if (lockScrollToggle != null)
{
lockScrollToggle.isOn = lockScroll;
lockScrollToggle.onValueChanged.AddListener(OnLockScrollChanged);
}
if (infoFilterToggle != null)
{
infoFilterToggle.isOn = infoFilter;
infoFilterToggle.onValueChanged.AddListener(OnInfoFilterChanged);
}
if (warningFilterToggle != null)
{
warningFilterToggle.isOn = warningFilter;
warningFilterToggle.onValueChanged.AddListener(OnWarningFilterChanged);
}
if (errorFilterToggle != null)
{
errorFilterToggle.isOn = errorFilter;
errorFilterToggle.onValueChanged.AddListener(OnErrorFilterChanged);
}
if (fatalFilterToggle != null)
{
fatalFilterToggle.isOn = fatalFilter;
fatalFilterToggle.onValueChanged.AddListener(OnFatalFilterChanged);
}
// 清空详情
if (detailText != null)
detailText.text = "点击日志查看详细信息...";
Debug.Log("[ConsoleModule] 控制台模块初始化完成");
}
public GameObject GetPage()
{
return consolePage;
}
public string GetModuleName()
{
return "控制台";
}
#endregion
#region
/// <summary>
/// 设置SDF字体
/// </summary>
public void SetSDFFont(TMP_FontAsset fontAsset)
{
savedFontAsset = fontAsset;
// 应用到详情文本
if (detailText != null && fontAsset != null)
{
detailText.font = fontAsset;
}
// 刷新已有日志项
RefreshLogDisplay();
}
/// <summary>
/// 获取日志统计信息
/// </summary>
public void GetLogCount(out int info, out int warning, out int error, out int fatal)
{
RefreshCount();
info = infoCount;
warning = warningCount;
error = errorCount;
fatal = fatalCount;
}
/// <summary>
/// 更新(每帧调用)
/// </summary>
public void Update()
{
// 如果页面未激活,跳过更新
if (consolePage == null || !consolePage.activeSelf)
{
return;
}
// 先处理延迟刷新
if (needRefresh)
{
needRefresh = false;
lastRefreshFrame = Time.frameCount;
RefreshLogDisplay();
}
// 如果锁定滚动,自动滚动到底部
// 只在刷新后的下一帧设置避免rebuild loop
if (lockScroll && logScrollRect != null && Time.frameCount > lastRefreshFrame + 1)
{
logScrollRect.verticalNormalizedPosition = 0f;
}
// 更新Toggle文本显示
UpdateFilterToggleText();
}
/// <summary>
/// 销毁
/// </summary>
public void Shutdown()
{
Application.logMessageReceived -= OnLogMessageReceived;
ClearAllLogs();
}
#endregion
#region
private void OnLogMessageReceived(string logMessage, string stackTrace, LogType logType)
{
// 将Assert转换为Error
if (logType == LogType.Assert)
{
logType = LogType.Error;
}
// 创建日志节点
LogNode logNode = LogNode.Create(logType, logMessage, stackTrace);
logNodes.Enqueue(logNode);
// 限制最大行数
while (logNodes.Count > maxLine)
{
logNodes.Dequeue();
}
// 标记需要刷新而不是立即刷新避免rebuild loop
needRefresh = true;
}
private void RefreshLogDisplay()
{
// 如果页面未激活,延迟刷新到下次页面激活时
if (consolePage == null || !consolePage.activeSelf)
{
needRefresh = true;
return;
}
if (logContent == null)
{
Debug.LogError("[ConsoleModule] logContent is null!");
return;
}
if (logItemPrefab == null)
{
Debug.LogError("[ConsoleModule] logItemPrefab is null!");
return;
}
// 回收所有激活的日志项
foreach (var item in activeLogItems)
{
if (item != null)
{
item.SetActive(false);
if (!logItemPool.Contains(item))
{
logItemPool.Add(item);
}
}
}
activeLogItems.Clear();
// 遍历日志节点并创建UI
int index = 0;
int displayCount = 0;
foreach (LogNode logNode in logNodes)
{
// 根据过滤器判断是否显示
if (!ShouldShowLog(logNode.LogType))
continue;
// 从对象池获取或创建日志项
GameObject logItem = GetLogItemFromPool();
if (logItem == null)
{
Debug.LogError("[ConsoleModule] Failed to get log item from pool!");
continue;
}
logItem.transform.SetParent(logContent, false);
logItem.SetActive(true);
activeLogItems.Add(logItem);
// 调试:确认对象状态
if (!logItem.activeSelf)
{
Debug.LogWarning($"[ConsoleModule] LogItem {logItem.name} is not active after SetActive(true)!");
}
// 设置日志项内容
SetupLogItem(logItem, logNode, index);
index++;
displayCount++;
}
}
private bool ShouldShowLog(LogType logType)
{
switch (logType)
{
case LogType.Log:
return infoFilter;
case LogType.Warning:
return warningFilter;
case LogType.Error:
return errorFilter;
case LogType.Exception:
return fatalFilter;
default:
return true;
}
}
private GameObject GetLogItemFromPool()
{
if (logItemPool.Count > 0)
{
GameObject item = logItemPool[0];
logItemPool.RemoveAt(0);
return item;
}
else
{
if (logItemPrefab == null)
{
Debug.LogError("[ConsoleModule] logItemPrefab is null!");
return null;
}
// 创建新的日志项
GameObject newItem = UnityEngine.Object.Instantiate(logItemPrefab);
newItem.name = "LogItem_" + activeLogItems.Count;
return newItem;
}
}
private void SetupLogItem(GameObject logItem, LogNode logNode, int index)
{
if (logItem == null)
{
Debug.LogError("[ConsoleModule] logItem is null in SetupLogItem!");
return;
}
// 获取日志项的Toggle和Text组件
Toggle toggle = logItem.GetComponent<Toggle>();
TMP_Text text = logItem.GetComponentInChildren<TMP_Text>();
if (toggle == null)
{
Debug.LogWarning($"[ConsoleModule] Toggle component not found on {logItem.name}!");
}
if (text == null)
{
Debug.LogWarning($"[ConsoleModule] TMP_Text component not found on {logItem.name}!");
return;
}
// 设置文本内容和颜色
Color32 color = GetLogStringColor(logNode.LogType);
string logText = GetLogString(logNode);
text.text = logText;
text.color = color;
// 应用字体
if (savedFontAsset != null)
{
text.font = savedFontAsset;
}
if (toggle != null)
{
// 设置Toggle状态
toggle.isOn = (selectedNode == logNode);
// 设置Toggle事件
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener((isOn) =>
{
if (isOn)
{
OnLogItemSelected(logNode);
}
});
}
}
private void OnLogItemSelected(LogNode logNode)
{
selectedNode = logNode;
// 显示详细信息
if (detailText != null && logNode != null)
{
Color32 color = GetLogStringColor(logNode.LogType);
string colorHex = ColorUtility.ToHtmlStringRGBA(color);
string detailInfo = $"<color=#{colorHex}><b>{logNode.LogMessage}</b></color>\n\n";
detailInfo += $"<color=#888888>时间: {logNode.LogTime.ToLocalTime():HH:mm:ss.fff}\n";
detailInfo += $"帧数: {logNode.LogFrameCount}\n";
detailInfo += $"类型: {logNode.LogType}</color>\n\n";
if (!string.IsNullOrEmpty(logNode.StackTrace))
{
detailInfo += $"<color=#AAAAAA>堆栈跟踪:\n{logNode.StackTrace}</color>";
}
detailText.text = detailInfo;
}
// 重置详情滚动位置
if (detailScrollRect != null)
{
detailScrollRect.verticalNormalizedPosition = 1f;
}
}
private string GetLogString(LogNode logNode)
{
return $"[{logNode.LogTime.ToLocalTime():HH:mm:ss.fff}][{logNode.LogFrameCount}] {logNode.LogMessage}";
}
private Color32 GetLogStringColor(LogType logType)
{
switch (logType)
{
case LogType.Log:
return infoColor;
case LogType.Warning:
return warningColor;
case LogType.Error:
return errorColor;
case LogType.Exception:
return fatalColor;
default:
return Color.white;
}
}
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;
}
}
}
private void UpdateFilterToggleText()
{
RefreshCount();
if (infoFilterToggle != null)
{
var label = infoFilterToggle.GetComponentInChildren<TMP_Text>();
if (label != null)
label.text = $"Info ({infoCount})";
}
if (warningFilterToggle != null)
{
var label = warningFilterToggle.GetComponentInChildren<TMP_Text>();
if (label != null)
label.text = $"Warning ({warningCount})";
}
if (errorFilterToggle != null)
{
var label = errorFilterToggle.GetComponentInChildren<TMP_Text>();
if (label != null)
label.text = $"Error ({errorCount})";
}
if (fatalFilterToggle != null)
{
var label = fatalFilterToggle.GetComponentInChildren<TMP_Text>();
if (label != null)
label.text = $"Fatal ({fatalCount})";
}
}
private void ClearAllLogs()
{
logNodes.Clear();
selectedNode = null;
if (detailText != null)
detailText.text = "点击日志查看详细信息...";
RefreshLogDisplay();
Debug.Log("[ConsoleModule] 已清空所有日志");
}
private void OnLockScrollChanged(bool value)
{
lockScroll = value;
}
private void OnInfoFilterChanged(bool value)
{
infoFilter = value;
needRefresh = true;
}
private void OnWarningFilterChanged(bool value)
{
warningFilter = value;
needRefresh = true;
}
private void OnErrorFilterChanged(bool value)
{
errorFilter = value;
needRefresh = true;
}
private void OnFatalFilterChanged(bool value)
{
fatalFilter = value;
needRefresh = true;
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,180 @@
using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 自定义按钮模块 - 通过反射加载标记为DebugButton的方法
/// </summary>
public class CustomButtonsModule : IDebugModule
{
#region
private GameObject customButtonsPage;
private RectTransform buttonContainer;
private GameObject buttonPrefab;
private ScrollRect buttonsScrollRect;
// 自定义按钮回调
private Action<Button, TMP_Text> customButtonCallback;
// 保存的SDF字体资源
private TMP_FontAsset savedFontAsset = null;
// 关闭窗口回调
private Action onCloseWindowCallback;
#endregion
#region
public CustomButtonsModule(GameObject page, RectTransform container, GameObject prefab,
ScrollRect scrollRect, Action closeWindowCallback)
{
customButtonsPage = page;
buttonContainer = container;
buttonPrefab = prefab;
buttonsScrollRect = scrollRect;
onCloseWindowCallback = closeWindowCallback;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[CustomButtonsModule] 初始化自定义按钮模块...");
LoadCustomButtons();
}
public GameObject GetPage()
{
return customButtonsPage;
}
public string GetModuleName()
{
return "自定义按钮";
}
#endregion
#region
/// <summary>
/// 设置SDF字体
/// </summary>
public void SetSDFFont(TMP_FontAsset fontAsset)
{
savedFontAsset = fontAsset;
}
/// <summary>
/// 设置自定义按钮回调
/// </summary>
public void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
customButtonCallback = callback;
}
/// <summary>
/// 重新加载自定义按钮
/// </summary>
public void ReloadCustomButtons()
{
LoadCustomButtons();
}
#endregion
#region
/// <summary>
/// 加载所有自定义按钮(使用反射)
/// </summary>
private void LoadCustomButtons()
{
if (buttonContainer == null || buttonPrefab == null)
{
Debug.LogWarning("[CustomButtonsModule] 按钮容器或按钮预制件未设置");
return;
}
// 清空现有按钮
foreach (Transform child in buttonContainer)
{
UnityEngine.Object.Destroy(child.gameObject);
}
// 查找所有标记为DebugButton的方法
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<DebugButtonAttribute>();
if (attribute != null)
{
CreateCustomButton(method, attribute);
}
}
}
}
catch (Exception e)
{
// 某些程序集可能无法访问,跳过
Debug.LogWarning($"[CustomButtonsModule] 无法访问程序集 {assembly.FullName}: {e.Message}");
}
}
}
private void CreateCustomButton(MethodInfo method, DebugButtonAttribute attribute)
{
GameObject buttonObj = UnityEngine.Object.Instantiate(buttonPrefab, buttonContainer);
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}");
}
});
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,25 @@
using UnityEngine;
namespace MeowmentDebugTool
{
/// <summary>
/// 调试模块接口
/// </summary>
public interface IDebugModule
{
/// <summary>
/// 初始化模块
/// </summary>
void Initialize();
/// <summary>
/// 获取模块页面
/// </summary>
GameObject GetPage();
/// <summary>
/// 模块名称
/// </summary>
string GetModuleName();
}
}

View File

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

View File

@ -0,0 +1,50 @@
using System;
using UnityEngine;
namespace MeowmentDebugTool
{
/// <summary>
/// 日志节点
/// </summary>
public class LogNode
{
/// <summary>
/// 日志类型
/// </summary>
public LogType LogType { get; private set; }
/// <summary>
/// 日志消息
/// </summary>
public string LogMessage { get; private set; }
/// <summary>
/// 堆栈跟踪
/// </summary>
public string StackTrace { get; private set; }
/// <summary>
/// 日志时间
/// </summary>
public DateTime LogTime { get; private set; }
/// <summary>
/// 日志帧数
/// </summary>
public int LogFrameCount { get; private set; }
/// <summary>
/// 创建日志节点
/// </summary>
public static LogNode Create(LogType logType, string logMessage, string stackTrace)
{
LogNode logNode = new LogNode();
logNode.LogType = logType;
logNode.LogMessage = logMessage;
logNode.StackTrace = stackTrace;
logNode.LogTime = DateTime.UtcNow;
logNode.LogFrameCount = Time.frameCount;
return logNode;
}
}
}

View File

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

View File

@ -0,0 +1,132 @@
using UnityEngine;
using TMPro;
using UnityEngine.UI;
namespace MeowmentDebugTool
{
/// <summary>
/// 参数查看模块 - 显示设备和系统信息
/// </summary>
public class ParametersModule : IDebugModule
{
#region
private GameObject parametersPage;
private TMP_Text deviceInfoText;
private TMP_Text systemInfoText;
private ScrollRect parametersScrollRect;
#endregion
#region
public ParametersModule(GameObject page, TMP_Text deviceInfo, TMP_Text systemInfo, ScrollRect scrollRect)
{
parametersPage = page;
deviceInfoText = deviceInfo;
systemInfoText = systemInfo;
parametersScrollRect = scrollRect;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[ParametersModule] 初始化参数查看模块...");
UpdateDeviceInfo();
UpdateSystemInfo();
}
public GameObject GetPage()
{
return parametersPage;
}
public string GetModuleName()
{
return "参数";
}
#endregion
#region
/// <summary>
/// 复制设备信息到剪贴板
/// </summary>
public void CopyDeviceInfoToClipboard()
{
if (deviceInfoText != null)
{
GUIUtility.systemCopyBuffer = deviceInfoText.text;
Debug.Log("设备信息已复制到剪贴板");
}
}
/// <summary>
/// 复制系统信息到剪贴板
/// </summary>
public void CopySystemInfoToClipboard()
{
if (systemInfoText != null)
{
GUIUtility.systemCopyBuffer = systemInfoText.text;
Debug.Log("系统信息已复制到剪贴板");
}
}
/// <summary>
/// 刷新所有信息
/// </summary>
public void RefreshAllInfo()
{
UpdateDeviceInfo();
UpdateSystemInfo();
}
#endregion
#region
private void UpdateDeviceInfo()
{
if (deviceInfoText == null) return;
string info = "===== 设备信息 =====\n";
info += $"设备名称: {SystemInfo.deviceName}\n";
info += $"设备型号: {SystemInfo.deviceModel}\n";
info += $"设备类型: {SystemInfo.deviceType}\n";
info += $"操作系统: {SystemInfo.operatingSystem}\n";
info += $"处理器: {SystemInfo.processorType}\n";
info += $"处理器核心数: {SystemInfo.processorCount}\n";
info += $"系统内存: {SystemInfo.systemMemorySize} MB\n";
info += $"显存: {SystemInfo.graphicsMemorySize} MB\n";
info += $"设备标识符: {SystemInfo.deviceUniqueIdentifier}\n";
deviceInfoText.text = info;
}
private void UpdateSystemInfo()
{
if (systemInfoText == null) return;
string info = "===== 系统信息 =====\n";
info += $"Unity版本: {Application.unityVersion}\n";
info += $"平台: {Application.platform}\n";
info += $"产品名称: {Application.productName}\n";
info += $"公司名称: {Application.companyName}\n";
info += $"版本: {Application.version}\n";
info += $"数据路径: {Application.dataPath}\n";
info += $"持久化数据路径: {Application.persistentDataPath}\n";
info += $"临时缓存路径: {Application.temporaryCachePath}\n";
info += $"屏幕分辨率: {Screen.width} x {Screen.height} @ {Screen.currentResolution.refreshRateRatio.value:F2}Hz\n";
info += $"屏幕DPI: {Screen.dpi}\n";
info += $"是否全屏: {Screen.fullScreen}\n";
info += $"目标帧率: {Application.targetFrameRate}\n";
info += $"当前帧率: {(int)(1f / Time.smoothDeltaTime)}\n";
info += $"\n===== 图形信息 =====\n";
info += $"图形设备名称: {SystemInfo.graphicsDeviceName}\n";
info += $"图形设备供应商: {SystemInfo.graphicsDeviceVendor}\n";
info += $"图形设备类型: {SystemInfo.graphicsDeviceType}\n";
info += $"图形设备版本: {SystemInfo.graphicsDeviceVersion}\n";
info += $"着色器等级: {SystemInfo.graphicsShaderLevel}\n";
info += $"多线程渲染: {SystemInfo.graphicsMultiThreaded}\n";
systemInfoText.text = info;
}
#endregion
}
}

View File

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

View File

@ -53,13 +53,14 @@ namespace MeowmentDebugTool
GameObject customButtonsPage = CreateCustomButtonsPage(contentContainer.transform);
GameObject toolbarPage = CreateToolbarPage(contentContainer.transform);
GameObject settingsPage = CreateSettingsPage(contentContainer.transform);
GameObject consolePage = CreateConsolePage(contentContainer.transform);
// 创建输入对话框
GameObject inputDialog = CreateInputDialog(canvasObj.transform);
// 设置引用
SetupReferences(tool, canvas, mainWindow, floatingButton,
parametersPage, customButtonsPage, toolbarPage, settingsPage, inputDialog);
parametersPage, customButtonsPage, toolbarPage, settingsPage, consolePage, inputDialog);
return tool;
}
@ -309,6 +310,14 @@ namespace MeowmentDebugTool
GameObject increaseBtn = CreateButton("TimeIncreaseButton", btnContainer.transform, "+10秒");
GameObject decreaseBtn = CreateButton("TimeDecreaseButton", btnContainer.transform, "-10秒");
// 暂时隐藏按钮
GameObject hideBtn = CreateButton("HideTemporarilyButton", timePanel.transform, "暂时隐藏 (5秒)");
hideBtn.GetComponent<RectTransform>().sizeDelta = new Vector2(0, 100);
hideBtn.GetComponent<Image>().color = new Color(0.5f, 0.3f, 0.1f, 1f); // 橙褐色
hideBtn.GetComponent<Button>().onClick.AddListener(() => {
UniversalDebugTool.HideTemporarily(5f);
});
return page;
}
@ -395,6 +404,199 @@ namespace MeowmentDebugTool
return page;
}
private static GameObject CreateConsolePage(Transform parent)
{
GameObject page = new GameObject("ConsolePage");
page.transform.SetParent(parent, false);
RectTransform rect = page.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.sizeDelta = Vector2.zero;
VerticalLayoutGroup vLayout = page.AddComponent<VerticalLayoutGroup>();
vLayout.spacing = 10;
vLayout.padding = new RectOffset(10, 10, 10, 10);
vLayout.childControlWidth = true;
vLayout.childControlHeight = false;
vLayout.childForceExpandWidth = true;
vLayout.childForceExpandHeight = false;
// 控制面板
GameObject controlPanel = new GameObject("ControlPanel");
controlPanel.transform.SetParent(page.transform, false);
RectTransform controlRect = controlPanel.AddComponent<RectTransform>();
controlRect.sizeDelta = new Vector2(0, 40);
// 添加LayoutElement来控制高度
LayoutElement controlLayout = controlPanel.AddComponent<LayoutElement>();
controlLayout.preferredHeight = 40;
controlLayout.flexibleHeight = 0;
HorizontalLayoutGroup hLayout = controlPanel.AddComponent<HorizontalLayoutGroup>();
hLayout.spacing = 10;
hLayout.padding = new RectOffset(10, 10, 10, 10);
hLayout.childControlWidth = false;
hLayout.childControlHeight = true;
hLayout.childForceExpandHeight = false;
// 清空按钮
GameObject clearBtn = CreateButton("ConsoleClearButton", controlPanel.transform, "清空");
clearBtn.GetComponent<RectTransform>().sizeDelta = new Vector2(150, 0);
LayoutElement clearLayout = clearBtn.AddComponent<LayoutElement>();
clearLayout.preferredHeight = 35;
// 锁定滚动Toggle
GameObject lockToggle = CreateToggle("ConsoleLockScrollToggle", controlPanel.transform, "锁定滚动");
lockToggle.GetComponent<RectTransform>().sizeDelta = new Vector2(180, 0);
LayoutElement lockLayout = lockToggle.AddComponent<LayoutElement>();
lockLayout.preferredHeight = 35;
// 过滤器Toggles
GameObject infoToggle = CreateToggle("ConsoleInfoFilterToggle", controlPanel.transform, "Info (0)");
infoToggle.GetComponent<RectTransform>().sizeDelta = new Vector2(150, 0);
infoToggle.GetComponent<Toggle>().isOn = true;
LayoutElement infoLayout = infoToggle.AddComponent<LayoutElement>();
infoLayout.preferredHeight = 35;
GameObject warningToggle = CreateToggle("ConsoleWarningFilterToggle", controlPanel.transform, "Warning (0)");
warningToggle.GetComponent<RectTransform>().sizeDelta = new Vector2(180, 0);
warningToggle.GetComponent<Toggle>().isOn = true;
LayoutElement warningLayout = warningToggle.AddComponent<LayoutElement>();
warningLayout.preferredHeight = 35;
GameObject errorToggle = CreateToggle("ConsoleErrorFilterToggle", controlPanel.transform, "Error (0)");
errorToggle.GetComponent<RectTransform>().sizeDelta = new Vector2(150, 0);
errorToggle.GetComponent<Toggle>().isOn = true;
LayoutElement errorLayout = errorToggle.AddComponent<LayoutElement>();
errorLayout.preferredHeight = 35;
GameObject fatalToggle = CreateToggle("ConsoleFatalFilterToggle", controlPanel.transform, "Fatal (0)");
fatalToggle.GetComponent<RectTransform>().sizeDelta = new Vector2(150, 0);
fatalToggle.GetComponent<Toggle>().isOn = true;
LayoutElement fatalLayout = fatalToggle.AddComponent<LayoutElement>();
fatalLayout.preferredHeight = 35;
// 日志区域 (60%)
GameObject logArea = new GameObject("LogArea");
logArea.transform.SetParent(page.transform, false);
RectTransform logAreaRect = logArea.AddComponent<RectTransform>();
logAreaRect.sizeDelta = new Vector2(0, 1300);
LayoutElement logAreaLayout = logArea.AddComponent<LayoutElement>();
logAreaLayout.preferredHeight = 1300;
logAreaLayout.flexibleHeight = 0;
// 日志ScrollView
GameObject logScrollView = new GameObject("ConsoleLogScrollView");
logScrollView.transform.SetParent(logArea.transform, false);
RectTransform logScrollRect = logScrollView.AddComponent<RectTransform>();
logScrollRect.anchorMin = Vector2.zero;
logScrollRect.anchorMax = Vector2.one;
logScrollRect.sizeDelta = Vector2.zero;
ScrollRect logScroll = logScrollView.AddComponent<ScrollRect>();
logScroll.horizontal = false;
logScroll.vertical = true;
logScroll.movementType = ScrollRect.MovementType.Elastic;
// Viewport
GameObject logViewport = new GameObject("Viewport");
logViewport.transform.SetParent(logScrollView.transform, false);
RectTransform logViewportRect = logViewport.AddComponent<RectTransform>();
logViewportRect.anchorMin = Vector2.zero;
logViewportRect.anchorMax = Vector2.one;
logViewportRect.sizeDelta = Vector2.zero;
logViewport.AddComponent<Image>().color = new Color(0.05f, 0.05f, 0.05f, 1f);
logViewport.AddComponent<Mask>().showMaskGraphic = true;
logScroll.viewport = logViewportRect;
// Content
GameObject logContent = new GameObject("ConsoleLogContent");
logContent.transform.SetParent(logViewport.transform, false);
RectTransform logContentRect = logContent.AddComponent<RectTransform>();
logContentRect.anchorMin = new Vector2(0, 1);
logContentRect.anchorMax = new Vector2(1, 1);
logContentRect.pivot = new Vector2(0.5f, 1);
logContentRect.sizeDelta = new Vector2(0, 0);
VerticalLayoutGroup logVLayout = logContent.AddComponent<VerticalLayoutGroup>();
logVLayout.spacing = 2;
logVLayout.childAlignment = TextAnchor.UpperCenter;
logVLayout.childControlWidth = true;
logVLayout.childControlHeight = true;
logVLayout.childForceExpandWidth = true;
logVLayout.childForceExpandHeight = false;
ContentSizeFitter logFitter = logContent.AddComponent<ContentSizeFitter>();
logFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
logScroll.content = logContentRect;
// 详情区域 (40%)
GameObject detailArea = new GameObject("DetailArea");
detailArea.transform.SetParent(page.transform, false);
RectTransform detailAreaRect = detailArea.AddComponent<RectTransform>();
detailAreaRect.sizeDelta = new Vector2(0, 800);
LayoutElement detailAreaLayout = detailArea.AddComponent<LayoutElement>();
detailAreaLayout.preferredHeight = 800;
detailAreaLayout.flexibleHeight = 0;
// 详情ScrollView
GameObject detailScrollView = new GameObject("ConsoleDetailScrollView");
detailScrollView.transform.SetParent(detailArea.transform, false);
RectTransform detailScrollRect = detailScrollView.AddComponent<RectTransform>();
detailScrollRect.anchorMin = Vector2.zero;
detailScrollRect.anchorMax = Vector2.one;
detailScrollRect.sizeDelta = Vector2.zero;
ScrollRect detailScroll = detailScrollView.AddComponent<ScrollRect>();
detailScroll.horizontal = false;
detailScroll.vertical = true;
detailScroll.movementType = ScrollRect.MovementType.Clamped;
// Viewport
GameObject detailViewport = new GameObject("Viewport");
detailViewport.transform.SetParent(detailScrollView.transform, false);
RectTransform detailViewportRect = detailViewport.AddComponent<RectTransform>();
detailViewportRect.anchorMin = Vector2.zero;
detailViewportRect.anchorMax = Vector2.one;
detailViewportRect.sizeDelta = Vector2.zero;
detailViewport.AddComponent<Image>().color = new Color(0.08f, 0.08f, 0.08f, 1f);
detailViewport.AddComponent<Mask>().showMaskGraphic = true;
detailScroll.viewport = detailViewportRect;
// Content
GameObject detailContent = new GameObject("Content");
detailContent.transform.SetParent(detailViewport.transform, false);
RectTransform detailContentRect = detailContent.AddComponent<RectTransform>();
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.anchoredPosition = Vector2.zero;
// 详情文本
GameObject detailText = CreateTextObject("ConsoleDetailText", detailContent.transform, "点击日志查看详细信息...");
TextMeshProUGUI detailTMP = detailText.GetComponent<TextMeshProUGUI>();
detailTMP.fontSize = 24;
detailTMP.alignment = TextAlignmentOptions.TopLeft;
detailTMP.enableWordWrapping = true;
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;
detailScroll.content = detailContentRect;
return page;
}
private static GameObject CreateInputDialog(Transform parent)
{
GameObject dialog = new GameObject("InputDialog");
@ -535,6 +737,151 @@ namespace MeowmentDebugTool
return textObj;
}
private static GameObject CreateToggle(string name, Transform parent, string labelText)
{
GameObject toggleObj = new GameObject(name);
toggleObj.transform.SetParent(parent, false);
RectTransform rect = toggleObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(160, 60);
// Background
Image bgImg = toggleObj.AddComponent<Image>();
bgImg.color = new Color(0.2f, 0.2f, 0.2f, 1f);
Toggle toggle = toggleObj.AddComponent<Toggle>();
toggle.isOn = true;
toggle.transition = Selectable.Transition.ColorTint;
// Checkmark Background
GameObject checkmarkBg = new GameObject("Background");
checkmarkBg.transform.SetParent(toggleObj.transform, false);
RectTransform checkmarkBgRect = checkmarkBg.AddComponent<RectTransform>();
checkmarkBgRect.anchorMin = new Vector2(0, 0.5f);
checkmarkBgRect.anchorMax = new Vector2(0, 0.5f);
checkmarkBgRect.pivot = new Vector2(0, 0.5f);
checkmarkBgRect.sizeDelta = new Vector2(40, 40);
checkmarkBgRect.anchoredPosition = new Vector2(10, 0);
Image checkmarkBgImg = checkmarkBg.AddComponent<Image>();
checkmarkBgImg.color = new Color(0.8f, 0.8f, 0.8f, 1f);
// Checkmark
GameObject checkmark = new GameObject("Checkmark");
checkmark.transform.SetParent(checkmarkBg.transform, false);
RectTransform checkmarkRect = checkmark.AddComponent<RectTransform>();
checkmarkRect.anchorMin = Vector2.zero;
checkmarkRect.anchorMax = Vector2.one;
checkmarkRect.sizeDelta = new Vector2(-10, -10);
Image checkmarkImg = checkmark.AddComponent<Image>();
checkmarkImg.color = new Color(0.2f, 0.8f, 0.2f, 1f);
toggle.targetGraphic = checkmarkBgImg;
toggle.graphic = checkmarkImg;
// Label
GameObject label = new GameObject("Label");
label.transform.SetParent(toggleObj.transform, false);
RectTransform labelRect = label.AddComponent<RectTransform>();
labelRect.anchorMin = new Vector2(0, 0);
labelRect.anchorMax = new Vector2(1, 1);
labelRect.offsetMin = new Vector2(60, 0);
labelRect.offsetMax = new Vector2(-10, 0);
TextMeshProUGUI labelTmp = label.AddComponent<TextMeshProUGUI>();
labelTmp.text = labelText;
labelTmp.fontSize = 24;
labelTmp.alignment = TextAlignmentOptions.MidlineLeft;
labelTmp.color = Color.white;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
labelTmp.font = defaultFont;
}
return toggleObj;
}
private static GameObject CreateConsoleLogItemPrefab()
{
GameObject logItem = new GameObject("ConsoleLogItem");
RectTransform rect = logItem.AddComponent<RectTransform>();
// 设置锚点为水平拉伸,顶部对齐
rect.anchorMin = new Vector2(0, 1);
rect.anchorMax = new Vector2(1, 1);
rect.pivot = new Vector2(0.5f, 1);
// 设置固定高度50宽度自动拉伸
rect.sizeDelta = new Vector2(0, 50);
// 添加LayoutElement来确保布局组件正确控制尺寸
LayoutElement layoutElement = logItem.AddComponent<LayoutElement>();
layoutElement.minHeight = 50;
layoutElement.preferredHeight = 50;
layoutElement.flexibleWidth = 1;
// Toggle组件
Toggle toggle = logItem.AddComponent<Toggle>();
toggle.isOn = false;
toggle.transition = Selectable.Transition.ColorTint;
// Background
GameObject background = new GameObject("Background");
background.transform.SetParent(logItem.transform, false);
RectTransform bgRect = background.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.sizeDelta = Vector2.zero;
Image bgImg = background.AddComponent<Image>();
bgImg.color = new Color(0.12f, 0.12f, 0.12f, 1f);
toggle.targetGraphic = bgImg;
// Label (日志文本)
GameObject label = new GameObject("Label");
label.transform.SetParent(logItem.transform, false);
RectTransform labelRect = label.AddComponent<RectTransform>();
labelRect.anchorMin = Vector2.zero;
labelRect.anchorMax = Vector2.one;
labelRect.offsetMin = new Vector2(10, 5);
labelRect.offsetMax = new Vector2(-10, -5);
TextMeshProUGUI labelTmp = label.AddComponent<TextMeshProUGUI>();
labelTmp.fontSize = 28;
labelTmp.alignment = TextAlignmentOptions.MidlineLeft;
labelTmp.color = Color.white;
labelTmp.overflowMode = TextOverflowModes.Truncate;
labelTmp.richText = true;
labelTmp.enableWordWrapping = false;
// 设置默认字体
TMP_FontAsset defaultFont = GetDefaultFont();
if (defaultFont != null)
{
labelTmp.font = defaultFont;
}
// 设置ColorBlock以支持选中状态
ColorBlock colors = toggle.colors;
colors.normalColor = new Color(0.12f, 0.12f, 0.12f, 1f);
colors.highlightedColor = new Color(0.2f, 0.2f, 0.2f, 1f);
colors.selectedColor = new Color(0.25f, 0.35f, 0.45f, 1f);
colors.pressedColor = new Color(0.15f, 0.15f, 0.15f, 1f);
toggle.colors = colors;
// 不需要设置为不激活因为Instantiate时会继承这个状态
// 而且作为模板时不会显示在场景中
// 不销毁预制件
Object.DontDestroyOnLoad(logItem);
Debug.Log("[RuntimeUIGenerator] Created ConsoleLogItem prefab");
return logItem;
}
private static GameObject CreateInputField(string name, Transform parent, string placeholder)
{
GameObject inputObj = new GameObject(name);
@ -717,7 +1064,7 @@ namespace MeowmentDebugTool
private static void SetupReferences(UniversalDebugTool tool, Canvas canvas, GameObject mainWindow,
GameObject floatingButton, GameObject parametersPage, GameObject customButtonsPage,
GameObject toolbarPage, GameObject settingsPage, GameObject inputDialog)
GameObject toolbarPage, GameObject settingsPage, GameObject consolePage, GameObject inputDialog)
{
// 使用反射设置私有字段
var type = typeof(UniversalDebugTool);
@ -788,6 +1135,27 @@ namespace MeowmentDebugTool
type.GetField("currentResolutionText", flags)?.SetValue(tool, resPanel.Find("CurrentResolutionText")?.GetComponent<TextMeshProUGUI>());
}
// 控制台页面
type.GetField("consolePage", flags)?.SetValue(tool, consolePage);
type.GetField("consoleLogScrollRect", flags)?.SetValue(tool, consolePage.transform.Find("LogArea/ConsoleLogScrollView")?.GetComponent<ScrollRect>());
type.GetField("consoleLogContent", flags)?.SetValue(tool, consolePage.transform.Find("LogArea/ConsoleLogScrollView/Viewport/ConsoleLogContent")?.GetComponent<RectTransform>());
type.GetField("consoleDetailScrollRect", flags)?.SetValue(tool, consolePage.transform.Find("DetailArea/ConsoleDetailScrollView")?.GetComponent<ScrollRect>());
type.GetField("consoleDetailText", flags)?.SetValue(tool, consolePage.transform.Find("DetailArea/ConsoleDetailScrollView/Viewport/Content/ConsoleDetailText")?.GetComponent<TextMeshProUGUI>());
Transform controlPanel = consolePage.transform.Find("ControlPanel");
if (controlPanel != null)
{
type.GetField("consoleClearButton", flags)?.SetValue(tool, controlPanel.Find("ConsoleClearButton")?.GetComponent<Button>());
type.GetField("consoleLockScrollToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleLockScrollToggle")?.GetComponent<Toggle>());
type.GetField("consoleInfoFilterToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleInfoFilterToggle")?.GetComponent<Toggle>());
type.GetField("consoleWarningFilterToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleWarningFilterToggle")?.GetComponent<Toggle>());
type.GetField("consoleErrorFilterToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleErrorFilterToggle")?.GetComponent<Toggle>());
type.GetField("consoleFatalFilterToggle", flags)?.SetValue(tool, controlPanel.Find("ConsoleFatalFilterToggle")?.GetComponent<Toggle>());
}
// 创建日志项预制件
GameObject consoleLogItemPrefab = CreateConsoleLogItemPrefab();
type.GetField("consoleLogItemPrefab", flags)?.SetValue(tool, consoleLogItemPrefab);
// 输入对话框
type.GetField("inputDialog", flags)?.SetValue(tool, inputDialog);
Transform dialogPanel = inputDialog.transform.Find("Panel");

View File

@ -0,0 +1,128 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 设置模块 - 提供分辨率设置等功能
/// </summary>
public class SettingsModule : IDebugModule
{
#region
private GameObject settingsPage;
private TMP_InputField widthInputField;
private TMP_InputField heightInputField;
private Button applyResolutionButton;
private Button resetResolutionButton;
private TMP_Text currentResolutionText;
// 主窗口引用
private RectTransform mainWindow;
private Canvas canvas;
// 默认分辨率
private Vector2 defaultResolution = new Vector2(1080, 2340);
private Vector2 currentCustomResolution;
#endregion
#region
public SettingsModule(GameObject page, TMP_InputField widthInput, TMP_InputField heightInput,
Button applyButton, Button resetButton, TMP_Text resolutionText,
RectTransform mainWin, Canvas canvasRef)
{
settingsPage = page;
widthInputField = widthInput;
heightInputField = heightInput;
applyResolutionButton = applyButton;
resetResolutionButton = resetButton;
currentResolutionText = resolutionText;
mainWindow = mainWin;
canvas = canvasRef;
currentCustomResolution = defaultResolution;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[SettingsModule] 初始化设置模块...");
if (applyResolutionButton != null)
applyResolutionButton.onClick.AddListener(ApplyCustomResolution);
if (resetResolutionButton != null)
resetResolutionButton.onClick.AddListener(ResetToDefaultResolution);
// 应用默认分辨率
ApplyResolution(defaultResolution);
}
public GameObject GetPage()
{
return settingsPage;
}
public string GetModuleName()
{
return "设置";
}
#endregion
#region
private void ApplyCustomResolution()
{
if (widthInputField == null || heightInputField == null) return;
if (float.TryParse(widthInputField.text, out float width) &&
float.TryParse(heightInputField.text, out float height))
{
if (width > 0 && height > 0)
{
currentCustomResolution = new Vector2(width, height);
ApplyResolution(currentCustomResolution);
Debug.Log($"[SettingsModule] 已应用自定义分辨率: {width} x {height}");
}
else
{
Debug.LogWarning("[SettingsModule] 分辨率值必须大于0");
}
}
else
{
Debug.LogWarning("[SettingsModule] 无效的分辨率值");
}
}
private void ResetToDefaultResolution()
{
currentCustomResolution = defaultResolution;
ApplyResolution(defaultResolution);
if (widthInputField != null)
widthInputField.text = defaultResolution.x.ToString();
if (heightInputField != null)
heightInputField.text = defaultResolution.y.ToString();
Debug.Log($"[SettingsModule] 已重置为默认分辨率: {defaultResolution.x} x {defaultResolution.y}");
}
private void ApplyResolution(Vector2 resolution)
{
if (mainWindow != null)
{
mainWindow.sizeDelta = resolution;
}
if (currentResolutionText != null)
{
currentResolutionText.text = $"当前窗口尺寸: {resolution.x} x {resolution.y}";
}
// 不需要强制刷新Canvas会导致rebuild loop
// Canvas会自动在下一帧更新
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,142 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MeowmentDebugTool
{
/// <summary>
/// 工具栏模块 - 提供时间调整等工具
/// </summary>
public class ToolbarModule : IDebugModule
{
#region
private GameObject toolbarPage;
private Slider timeAdjustSlider;
private TMP_Text timeAdjustValueText;
private Button timeIncreaseButton;
private Button timeDecreaseButton;
private float timeAdjustValue = 60f; // 当前设置的时间调整值(秒)
#endregion
#region
public ToolbarModule(GameObject page, Slider slider, TMP_Text valueText,
Button increaseButton, Button decreaseButton)
{
toolbarPage = page;
timeAdjustSlider = slider;
timeAdjustValueText = valueText;
timeIncreaseButton = increaseButton;
timeDecreaseButton = decreaseButton;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[ToolbarModule] 初始化工具栏模块...");
// 设置时间调整工具
if (timeAdjustSlider != null)
{
timeAdjustSlider.onValueChanged.AddListener(OnTimeAdjustValueChanged);
// 初始化显示
UpdateTimeAdjustDisplay(timeAdjustSlider.value);
}
if (timeIncreaseButton != null)
timeIncreaseButton.onClick.AddListener(OnIncreaseTime);
if (timeDecreaseButton != null)
timeDecreaseButton.onClick.AddListener(OnDecreaseTime);
}
public GameObject GetPage()
{
return toolbarPage;
}
public string GetModuleName()
{
return "工具栏";
}
#endregion
#region
/// <summary>
/// 时间调整Slider值改变回调只更新显示不改变时间
/// </summary>
private void OnTimeAdjustValueChanged(float value)
{
timeAdjustValue = value;
UpdateTimeAdjustDisplay(value);
}
/// <summary>
/// 更新时间调整显示
/// </summary>
private void UpdateTimeAdjustDisplay(float seconds)
{
if (timeAdjustValueText != null)
{
if (seconds < 60)
{
timeAdjustValueText.text = $"{seconds:F0}秒";
}
else
{
float minutes = seconds / 60f;
timeAdjustValueText.text = $"{minutes:F1}分钟";
}
}
}
/// <summary>
/// 增加时间
/// </summary>
private void OnIncreaseTime()
{
// 修改系统时间这里演示修改Time.time的偏移
float adjustSeconds = timeAdjustValue;
// 注意Time.time本身无法直接修改这里提供一个可以被重写的方法
// 实际项目中,你应该修改游戏内的时间变量
AdjustGameTime(adjustSeconds);
Debug.Log($"⏰ [ToolbarModule] 增加时间: +{adjustSeconds}秒");
}
/// <summary>
/// 减少时间
/// </summary>
private void OnDecreaseTime()
{
float adjustSeconds = -timeAdjustValue;
AdjustGameTime(adjustSeconds);
Debug.Log($"⏰ [ToolbarModule] 减少时间: {adjustSeconds}秒");
}
/// <summary>
/// 调整游戏时间(示例方法,需要根据实际项目修改)
/// </summary>
private void AdjustGameTime(float seconds)
{
// 方案1: 如果你的项目有全局时间管理器
// TimeManager.Instance?.AddTime(seconds);
// 方案2: 如果你使用DateTime
// GameTime.Current = GameTime.Current.AddSeconds(seconds);
// 方案3: 临时演示 - 修改Time.timeScale来模拟时间变化
// 实际项目中应该修改你自己的时间变量
Debug.Log($"💡 提示: 请在AdjustGameTime方法中实现你的时间调整逻辑");
Debug.Log($"💡 建议: 修改游戏内的时间变量,例如 GameTime.Current.AddSeconds({seconds})");
// 示例:如果你有一个静态的游戏时间偏移量
// GameTimeOffset += seconds;
}
#endregion
}
}

View File

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

View File

@ -62,12 +62,26 @@ namespace MeowmentDebugTool
[SerializeField] private Button resetResolutionButton;
[SerializeField] private TMP_Text currentResolutionText;
[Header("输入对话框")]
[SerializeField] private GameObject inputDialog;
[SerializeField] private TMP_Text inputDialogTitle;
[SerializeField] private TMP_InputField inputDialogInputField;
[SerializeField] private Button inputDialogConfirmBtn;
[SerializeField] private Button inputDialogCancelBtn;
[Header("控制台页面")]
[SerializeField] private GameObject consolePage;
[SerializeField] private ScrollRect consoleLogScrollRect;
[SerializeField] private RectTransform consoleLogContent;
[SerializeField] private ScrollRect consoleDetailScrollRect;
[SerializeField] private TMP_Text consoleDetailText;
[SerializeField] private Button consoleClearButton;
[SerializeField] private Toggle consoleLockScrollToggle;
[SerializeField] private Toggle consoleInfoFilterToggle;
[SerializeField] private Toggle consoleWarningFilterToggle;
[SerializeField] private Toggle consoleErrorFilterToggle;
[SerializeField] private Toggle consoleFatalFilterToggle;
[SerializeField] private GameObject consoleLogItemPrefab;
// [Header("输入对话框")]
// [SerializeField] private GameObject inputDialog;
// [SerializeField] private TMP_Text inputDialogTitle;
// [SerializeField] private TMP_InputField inputDialogInputField;
// [SerializeField] private Button inputDialogConfirmBtn;
// [SerializeField] private Button inputDialogCancelBtn;
#endregion
#region
@ -77,18 +91,22 @@ namespace MeowmentDebugTool
private Dictionary<string, Button> tabButtons = new Dictionary<string, Button>();
private string currentPageKey = "";
// 默认分辨率
private Vector2 defaultResolution = new Vector2(1080, 2340);
private Vector2 currentCustomResolution;
// 自定义按钮回调
private static Action<Button, TMP_Text> customButtonCallback;
// Canvas层级设置
private const int TOP_SORT_ORDER = 30000;
// 保存的SDF字体资源
private static TMP_FontAsset savedFontAsset = null;
// 模块实例
private ParametersModule parametersModule;
private CustomButtonsModule customButtonsModule;
private ToolbarModule toolbarModule;
private SettingsModule settingsModule;
private ConsoleModule consoleModule;
private List<IDebugModule> allModules = new List<IDebugModule>();
// 暂时隐藏状态标志
private bool isHiding = false;
#endregion
#region
@ -121,8 +139,6 @@ namespace MeowmentDebugTool
return;
}
currentCustomResolution = defaultResolution;
// 设置Canvas为最上层
SetCanvasToTop();
@ -139,23 +155,36 @@ namespace MeowmentDebugTool
{
// 只有在已初始化但还没调用过InitializeDebugTool时才执行
// 这避免了Awake和Init()的重复调用问题
if (isInitialized && pages.Count == 0)
if (isInitialized && allModules.Count == 0)
{
// 这是通过Init()创建的实例需要在Start中完成初始化
// 此时反射设置的字段已经生效
InitializeDebugTool();
UpdateDeviceInfo();
UpdateSystemInfo();
LoadCustomButtons();
InitializeAllModules();
ShowMainWindow();
}
}
private void Update()
{
// 更新控制台模块
if (consoleModule != null)
{
consoleModule.Update();
}
}
private void OnDestroy()
{
if (instance == this)
{
instance = null;
// 销毁控制台模块
if (consoleModule != null)
{
consoleModule.Shutdown();
}
}
}
#endregion
@ -185,12 +214,10 @@ namespace MeowmentDebugTool
if (InstanceExists)
{
Instance.InitializeDebugTool();
Instance.UpdateDeviceInfo();
Instance.UpdateSystemInfo();
Instance.LoadCustomButtons();
Instance.InitializeAllModules();
Instance.ShowMainWindow();
Debug.Log("[MeowmentDebugTool] 初始化完成!");
Debug.Log("[MeowmentDebugTool] 初始化完成!");
}
}
@ -241,9 +268,19 @@ namespace MeowmentDebugTool
savedFontAsset = fontAsset;
Debug.Log("[MeowmentDebugTool] 字体资源已保存,后续创建的按钮将自动应用此字体");
// 4. 重新加载自定义按钮使新字体应用到已存在的CustomButton
// 4. 通过模块重新加载自定义按钮
if (Instance.customButtonsModule != null)
{
Debug.Log("[MeowmentDebugTool] 重新加载自定义按钮以应用新字体...");
Instance.ReloadCustomButtons();
Instance.customButtonsModule.SetSDFFont(fontAsset);
Instance.customButtonsModule.ReloadCustomButtons();
}
// 5. 为控制台模块应用字体
if (Instance.consoleModule != null)
{
Instance.consoleModule.SetSDFFont(fontAsset);
}
}
/// <summary>
@ -274,11 +311,41 @@ namespace MeowmentDebugTool
{
Debug.Log("🚀 初始化UniversalDebugTool...");
// 创建模块实例
parametersModule = new ParametersModule(
parametersPage, deviceInfoText, systemInfoText, parametersScrollRect);
customButtonsModule = new CustomButtonsModule(
customButtonsPage, buttonContainer, buttonPrefab, buttonsScrollRect, CloseDebugWindow);
toolbarModule = new ToolbarModule(
toolbarPage, timeAdjustSlider, timeAdjustValueText,
timeIncreaseButton, timeDecreaseButton);
settingsModule = new SettingsModule(
settingsPage, widthInputField, heightInputField,
applyResolutionButton, resetResolutionButton, currentResolutionText,
mainWindow, canvas);
consoleModule = new ConsoleModule(
consolePage, consoleLogScrollRect, consoleLogContent,
consoleDetailScrollRect, consoleDetailText,
consoleClearButton, consoleLockScrollToggle,
consoleInfoFilterToggle, consoleWarningFilterToggle,
consoleErrorFilterToggle, consoleFatalFilterToggle, consoleLogItemPrefab);
// 添加所有模块到列表
allModules.Add(consoleModule);
allModules.Add(parametersModule);
allModules.Add(customButtonsModule);
allModules.Add(toolbarModule);
allModules.Add(settingsModule);
// 注册所有页面
RegisterPage("参数", parametersPage);
RegisterPage("自定义按钮", customButtonsPage);
RegisterPage("工具栏", toolbarPage);
RegisterPage("设置", settingsPage);
foreach (var module in allModules)
{
RegisterPage(module.GetModuleName(), module.GetPage());
}
Debug.Log($"📄 已注册{pages.Count}个页面");
@ -289,26 +356,6 @@ namespace MeowmentDebugTool
if (closeButton != null)
closeButton.onClick.AddListener(CloseDebugWindow);
if (applyResolutionButton != null)
applyResolutionButton.onClick.AddListener(ApplyCustomResolution);
if (resetResolutionButton != null)
resetResolutionButton.onClick.AddListener(ResetToDefaultResolution);
// 设置时间调整工具
if (timeAdjustSlider != null)
{
timeAdjustSlider.onValueChanged.AddListener(OnTimeAdjustValueChanged);
// 初始化显示
UpdateTimeAdjustDisplay(timeAdjustSlider.value);
}
if (timeIncreaseButton != null)
timeIncreaseButton.onClick.AddListener(OnIncreaseTime);
if (timeDecreaseButton != null)
timeDecreaseButton.onClick.AddListener(OnDecreaseTime);
// 设置悬浮按钮点击事件
if (floatingButton != null)
{
@ -325,13 +372,23 @@ namespace MeowmentDebugTool
ShowPage("参数");
}
// 应用默认分辨率
ApplyResolution(defaultResolution);
Debug.Log("✅ UniversalDebugTool初始化完成");
Debug.Log("[OK] UniversalDebugTool初始化完成");
Debug.Log("💡 提示: 点击窗口顶部的标签切换页面或按F1-F4使用快捷键");
}
/// <summary>
/// 初始化所有模块
/// </summary>
private void InitializeAllModules()
{
Debug.Log("🔧 初始化所有模块...");
foreach (var module in allModules)
{
module.Initialize();
}
Debug.Log("[OK] 所有模块初始化完成!");
}
private void RegisterPage(string pageName, GameObject pageObject)
{
if (pageObject != null && !pages.ContainsKey(pageName))
@ -365,7 +422,7 @@ namespace MeowmentDebugTool
if (buttonText != null)
{
buttonText.text = page.Key;
Debug.Log($" 创建标签: [{page.Key}]");
Debug.Log($"[OK] 创建标签: [{page.Key}]");
}
else
{
@ -378,7 +435,7 @@ namespace MeowmentDebugTool
tabButtons[page.Key] = button;
}
Debug.Log(" 标签按钮创建完成!");
Debug.Log("[OK] 标签按钮创建完成!");
}
#endregion
@ -421,64 +478,13 @@ namespace MeowmentDebugTool
}
#endregion
#region
private void UpdateDeviceInfo()
{
if (deviceInfoText == null) return;
string info = "===== 设备信息 =====\n";
info += $"设备名称: {SystemInfo.deviceName}\n";
info += $"设备型号: {SystemInfo.deviceModel}\n";
info += $"设备类型: {SystemInfo.deviceType}\n";
info += $"操作系统: {SystemInfo.operatingSystem}\n";
info += $"处理器: {SystemInfo.processorType}\n";
info += $"处理器核心数: {SystemInfo.processorCount}\n";
info += $"系统内存: {SystemInfo.systemMemorySize} MB\n";
info += $"显存: {SystemInfo.graphicsMemorySize} MB\n";
info += $"设备标识符: {SystemInfo.deviceUniqueIdentifier}\n";
deviceInfoText.text = info;
}
private void UpdateSystemInfo()
{
if (systemInfoText == null) return;
string info = "===== 系统信息 =====\n";
info += $"Unity版本: {Application.unityVersion}\n";
info += $"平台: {Application.platform}\n";
info += $"产品名称: {Application.productName}\n";
info += $"公司名称: {Application.companyName}\n";
info += $"版本: {Application.version}\n";
info += $"数据路径: {Application.dataPath}\n";
info += $"持久化数据路径: {Application.persistentDataPath}\n";
info += $"临时缓存路径: {Application.temporaryCachePath}\n";
info += $"屏幕分辨率: {Screen.width} x {Screen.height} @ {Screen.currentResolution.refreshRateRatio.value:F2}Hz\n";
info += $"屏幕DPI: {Screen.dpi}\n";
info += $"是否全屏: {Screen.fullScreen}\n";
info += $"目标帧率: {Application.targetFrameRate}\n";
info += $"当前帧率: {(int)(1f / Time.smoothDeltaTime)}\n";
info += $"\n===== 图形信息 =====\n";
info += $"图形设备名称: {SystemInfo.graphicsDeviceName}\n";
info += $"图形设备供应商: {SystemInfo.graphicsDeviceVendor}\n";
info += $"图形设备类型: {SystemInfo.graphicsDeviceType}\n";
info += $"图形设备版本: {SystemInfo.graphicsDeviceVersion}\n";
info += $"着色器等级: {SystemInfo.graphicsShaderLevel}\n";
info += $"多线程渲染: {SystemInfo.graphicsMultiThreaded}\n";
systemInfoText.text = info;
}
#region ParametersModule
/// <summary>
/// 复制设备信息到剪贴板
/// </summary>
public void CopyDeviceInfoToClipboard()
{
if (deviceInfoText != null)
{
GUIUtility.systemCopyBuffer = deviceInfoText.text;
Debug.Log("设备信息已复制到剪贴板");
}
parametersModule?.CopyDeviceInfoToClipboard();
}
/// <summary>
@ -486,11 +492,7 @@ namespace MeowmentDebugTool
/// </summary>
public void CopySystemInfoToClipboard()
{
if (systemInfoText != null)
{
GUIUtility.systemCopyBuffer = systemInfoText.text;
Debug.Log("系统信息已复制到剪贴板");
}
parametersModule?.CopySystemInfoToClipboard();
}
/// <summary>
@ -498,110 +500,20 @@ namespace MeowmentDebugTool
/// </summary>
public void RefreshAllInfo()
{
UpdateDeviceInfo();
UpdateSystemInfo();
parametersModule?.RefreshAllInfo();
}
#endregion
#region
/// <summary>
/// 加载所有自定义按钮(使用反射)
/// </summary>
private void LoadCustomButtons()
{
if (buttonContainer == null || buttonPrefab == null)
{
Debug.LogWarning("按钮容器或按钮预制件未设置");
return;
}
// 清空现有按钮
foreach (Transform child in buttonContainer)
{
Destroy(child.gameObject);
}
// 查找所有标记为DebugButton的方法
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var methods = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var method in methods)
{
var attribute = method.GetCustomAttribute<DebugButtonAttribute>();
if (attribute != null)
{
CreateCustomButton(method, attribute);
}
}
}
}
catch (Exception e)
{
// 某些程序集可能无法访问,跳过
Debug.LogWarning($"无法访问程序集 {assembly.FullName}: {e.Message}");
}
}
}
private void CreateCustomButton(MethodInfo method, DebugButtonAttribute attribute)
{
GameObject buttonObj = Instantiate(buttonPrefab, buttonContainer);
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($"执行调试方法: {method.Name}");
// 调用回调
customButtonCallback?.Invoke(button, buttonText);
customButtonCallback = null;
// 点击按钮后关闭主窗口
CloseDebugWindow();
}
catch (Exception e)
{
Debug.LogError($"执行调试方法 {method.Name} 时出错: {e.Message}");
}
});
}
#region CustomButtonsModule
/// <summary>
/// 设置自定义按钮回调
/// </summary>
public static void SetCustomButtonCallback(Action<Button, TMP_Text> callback)
{
customButtonCallback = callback;
if (InstanceExists && Instance.customButtonsModule != null)
{
Instance.customButtonsModule.SetCustomButtonCallback(callback);
}
}
/// <summary>
@ -609,191 +521,58 @@ namespace MeowmentDebugTool
/// </summary>
public void ReloadCustomButtons()
{
LoadCustomButtons();
customButtonsModule?.ReloadCustomButtons();
}
#endregion
#region
private float timeAdjustValue = 60f; // 当前设置的时间调整值(秒)
/// <summary>
/// 时间调整Slider值改变回调只更新显示不改变时间
/// </summary>
private void OnTimeAdjustValueChanged(float value)
{
timeAdjustValue = value;
UpdateTimeAdjustDisplay(value);
}
/// <summary>
/// 更新时间调整显示
/// </summary>
private void UpdateTimeAdjustDisplay(float seconds)
{
if (timeAdjustValueText != null)
{
if (seconds < 60)
{
timeAdjustValueText.text = $"{seconds:F0}秒";
}
else
{
float minutes = seconds / 60f;
timeAdjustValueText.text = $"{minutes:F1}分钟";
}
}
}
/// <summary>
/// 增加时间
/// </summary>
private void OnIncreaseTime()
{
// 修改系统时间这里演示修改Time.time的偏移
float adjustSeconds = timeAdjustValue;
// 注意Time.time本身无法直接修改这里提供一个可以被重写的方法
// 实际项目中,你应该修改游戏内的时间变量
AdjustGameTime(adjustSeconds);
// #region 输入对话框
// /// <summary>
// /// 显示输入对话框
// /// </summary>
// public static void ShowInputDialog(string title, Action<string> onConfirmAction,
// string initialValue = "", TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)
// {
// if (!InstanceExists) return;
Debug.Log($"⏰ 增加时间: +{adjustSeconds}秒");
}
// var inst = Instance;
// if (inst.inputDialog == null) return;
/// <summary>
/// 减少时间
/// </summary>
private void OnDecreaseTime()
{
float adjustSeconds = -timeAdjustValue;
// inst.inputDialogInputField.contentType = contentType;
// inst.inputDialog.SetActive(true);
// inst.inputDialogTitle.text = title;
// inst.inputDialogInputField.text = initialValue;
AdjustGameTime(adjustSeconds);
// inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
// inst.inputDialogConfirmBtn.onClick.AddListener(() =>
// {
// onConfirmAction?.Invoke(inst.inputDialogInputField.text);
// CloseInputDialog();
// });
Debug.Log($"⏰ 减少时间: {adjustSeconds}秒");
}
// inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
// inst.inputDialogCancelBtn.onClick.AddListener(CloseInputDialog);
// }
/// <summary>
/// 调整游戏时间(示例方法,需要根据实际项目修改)
/// </summary>
private void AdjustGameTime(float seconds)
{
// 方案1: 如果你的项目有全局时间管理器
// TimeManager.Instance?.AddTime(seconds);
// /// <summary>
// /// 关闭输入对话框
// /// </summary>
// public static void CloseInputDialog()
// {
// if (!InstanceExists) return;
// 方案2: 如果你使用DateTime
// GameTime.Current = GameTime.Current.AddSeconds(seconds);
// var inst = Instance;
// if (inst.inputDialog == null) return;
// 方案3: 临时演示 - 修改Time.timeScale来模拟时间变化
// 实际项目中应该修改你自己的时间变量
Debug.Log($"💡 提示: 请在AdjustGameTime方法中实现你的时间调整逻辑");
Debug.Log($"💡 建议: 修改游戏内的时间变量,例如 GameTime.Current.AddSeconds({seconds})");
// 示例:如果你有一个静态的游戏时间偏移量
// GameTimeOffset += seconds;
}
#endregion
#region
private void ApplyCustomResolution()
{
if (widthInputField == null || heightInputField == null) return;
if (float.TryParse(widthInputField.text, out float width) &&
float.TryParse(heightInputField.text, out float height))
{
if (width > 0 && height > 0)
{
currentCustomResolution = new Vector2(width, height);
ApplyResolution(currentCustomResolution);
Debug.Log($"已应用自定义分辨率: {width} x {height}");
}
else
{
Debug.LogWarning("分辨率值必须大于0");
}
}
else
{
Debug.LogWarning("无效的分辨率值");
}
}
private void ResetToDefaultResolution()
{
currentCustomResolution = defaultResolution;
ApplyResolution(defaultResolution);
if (widthInputField != null)
widthInputField.text = defaultResolution.x.ToString();
if (heightInputField != null)
heightInputField.text = defaultResolution.y.ToString();
Debug.Log($"已重置为默认分辨率: {defaultResolution.x} x {defaultResolution.y}");
}
private void ApplyResolution(Vector2 resolution)
{
if (mainWindow != null)
{
mainWindow.sizeDelta = resolution;
}
if (currentResolutionText != null)
{
currentResolutionText.text = $"当前窗口尺寸: {resolution.x} x {resolution.y}";
}
// 强制刷新布局
if (canvas != null)
{
Canvas.ForceUpdateCanvases();
}
}
#endregion
#region
/// <summary>
/// 显示输入对话框
/// </summary>
public static void ShowInputDialog(string title, Action<string> onConfirmAction,
string initialValue = "", TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)
{
if (!InstanceExists) return;
var inst = Instance;
if (inst.inputDialog == null) return;
inst.inputDialogInputField.contentType = contentType;
inst.inputDialog.SetActive(true);
inst.inputDialogTitle.text = title;
inst.inputDialogInputField.text = initialValue;
inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
inst.inputDialogConfirmBtn.onClick.AddListener(() =>
{
onConfirmAction?.Invoke(inst.inputDialogInputField.text);
CloseInputDialog();
});
inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
inst.inputDialogCancelBtn.onClick.AddListener(CloseInputDialog);
}
/// <summary>
/// 关闭输入对话框
/// </summary>
public static void CloseInputDialog()
{
if (!InstanceExists) return;
var inst = Instance;
if (inst.inputDialog == null) return;
inst.inputDialog.SetActive(false);
inst.inputDialogInputField.text = "";
inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
}
#endregion
// inst.inputDialog.SetActive(false);
// inst.inputDialogInputField.text = "";
// inst.inputDialogConfirmBtn.onClick.RemoveAllListeners();
// inst.inputDialogCancelBtn.onClick.RemoveAllListeners();
// }
// #endregion
#region API
/// <summary>
@ -847,7 +626,7 @@ namespace MeowmentDebugTool
if (floatingButton != null)
floatingButton.SetActive(true);
Debug.Log("🔽 调试窗口已关闭");
Debug.Log("[关闭] 调试窗口已关闭");
}
/// <summary>
@ -878,7 +657,7 @@ namespace MeowmentDebugTool
if (mainWindow != null)
mainWindow.gameObject.SetActive(true);
Debug.Log("🔼 调试窗口已打开");
Debug.Log("[打开] 调试窗口已打开");
}
/// <summary>
@ -892,6 +671,79 @@ namespace MeowmentDebugTool
if (floatingButton != null)
floatingButton.SetActive(true);
}
/// <summary>
/// 暂时隐藏调试工具UI用于截图等场景
/// </summary>
/// <param name="seconds">隐藏的秒数默认5秒</param>
public static void HideTemporarily(float seconds = 5f)
{
if (!InstanceExists)
{
Debug.LogWarning("[MeowmentDebugTool] 调试工具未初始化,无法隐藏");
return;
}
if (Instance.isHiding)
{
Debug.LogWarning("[MeowmentDebugTool] 已经在隐藏状态中,请等待恢复");
return;
}
Instance.StartCoroutine(Instance.HideTemporarilyCoroutine(seconds));
}
private IEnumerator HideTemporarilyCoroutine(float seconds)
{
isHiding = true;
Debug.Log($"[MeowmentDebugTool] 开始暂时隐藏流程,时长: {seconds}秒");
// 保存Canvas初始状态
if (canvas == null)
{
Debug.LogError("[MeowmentDebugTool] Canvas为空");
isHiding = false;
yield break;
}
bool canvasWasEnabled = canvas.enabled;
Debug.Log($"[MeowmentDebugTool] Canvas初始状态: {(canvasWasEnabled ? "" : "")}");
// 1. 先关闭主窗口(如果是打开状态)
bool wasMainWindowOpen = mainWindow != null && mainWindow.gameObject.activeSelf;
Debug.Log($"[MeowmentDebugTool] 主窗口初始状态: {(wasMainWindowOpen ? "" : "")}");
if (wasMainWindowOpen)
{
CloseDebugWindow();
yield return null;
Debug.Log("[MeowmentDebugTool] 主窗口已关闭");
}
// 2. 隐藏Canvas包括浮窗
canvas.enabled = false;
Debug.Log($"[MeowmentDebugTool] Canvas已禁用UI完全隐藏");
// 3. 等待指定时间
yield return new WaitForSeconds(seconds);
// 4. 恢复Canvas
canvas.enabled = true;
Debug.Log($"[MeowmentDebugTool] Canvas已启用UI恢复显示");
// 5. 验证恢复结果
if (canvas.enabled)
{
Debug.Log("[MeowmentDebugTool] ✓ UI已成功恢复浮窗状态");
}
else
{
Debug.LogError("[MeowmentDebugTool] ✗ Canvas恢复失败");
}
isHiding = false;
Debug.Log("[MeowmentDebugTool] 暂时隐藏流程结束");
}
#endregion
}

View File

@ -71,11 +71,11 @@ public static class SampleDebugFunctions
Debug.Log("已触发垃圾回收");
}
[DebugButton("卸载未使用资源", 0.9f, 0.5f, 0.3f)]
[DebugButton("清理未使用资源", 0.9f, 0.5f, 0.3f)]
public static void UnloadUnusedAssets()
{
Resources.UnloadUnusedAssets();
Debug.Log("已卸载未使用的资源");
Debug.Log("已清理未使用的资源");
}
// 场景管理

View File

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