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

580 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.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
}
}