MeowmentDebugTool/Packages/com.bywaystudios.meowmentdebugtool/Runtime/ConsoleModule.cs
zhang hongbo cf352f2a22 11
2026-04-06 10:03:28 +08:00

730 lines
24 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 TMP_InputField textFilterInputField;
private string textFilter = "";
private TMP_Text infoFilterLabel;
private TMP_Text warningFilterLabel;
private TMP_Text errorFilterLabel;
private TMP_Text fatalFilterLabel;
// 日志项预制件
private GameObject logItemPrefab;
// 日志数据 - 分离的缓存池
private Queue<LogNode> infoLogNodes = new Queue<LogNode>();
private Queue<LogNode> warningLogNodes = new Queue<LogNode>();
private Queue<LogNode> errorLogNodes = new Queue<LogNode>();
private Queue<LogNode> fatalLogNodes = new Queue<LogNode>();
private LogNode selectedNode = null;
// 日志计数
private int infoCount = 0;
private int warningCount = 0;
private int errorCount = 0;
private int fatalCount = 0;
// 设置
private bool lockScroll = true;
private int maxInfoLines = 50;
private int maxWarningLines = 50;
private int maxErrorLines = 50;
private int maxFatalLines = 50;
private bool infoFilter = false; // 默认不显示Info
private bool warningFilter = false; // 默认不显示Warning
private bool errorFilter = true;
private bool fatalFilter = true;
// 颜色设置
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;
private bool hasRegisteredLogCallback = false;
private bool uiInitialized = false;
private int lastDisplayedInfoCount = -1;
private int lastDisplayedWarningCount = -1;
private int lastDisplayedErrorCount = -1;
private int lastDisplayedFatalCount = -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, TMP_InputField textFilterInput, GameObject logItemPrefabObj)
{
consolePage = page;
logScrollRect = logScroll;
logContent = logContentTransform;
detailScrollRect = detailScroll;
detailText = detail;
clearButton = clearBtn;
lockScrollToggle = lockToggle;
infoFilterToggle = infoToggle;
warningFilterToggle = warningToggle;
errorFilterToggle = errorToggle;
fatalFilterToggle = fatalToggle;
textFilterInputField = textFilterInput;
logItemPrefab = logItemPrefabObj;
}
#endregion
#region IDebugModule
public void Initialize()
{
Debug.Log("[ConsoleModule] 初始化控制台模块...");
EnsureLogCaptureRegistered();
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;
}
// 刷新已有日志项
if (uiInitialized)
{
RefreshLogDisplay();
}
}
public void EnsurePageViewInitialized()
{
EnsureLogCaptureRegistered();
if (uiInitialized)
{
return;
}
// 检查必要的组件
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!");
if (clearButton != null)
{
clearButton.onClick.RemoveListener(ClearAllLogs);
clearButton.onClick.AddListener(ClearAllLogs);
}
if (lockScrollToggle != null)
{
lockScrollToggle.onValueChanged.RemoveListener(OnLockScrollChanged);
lockScrollToggle.isOn = lockScroll;
lockScrollToggle.onValueChanged.AddListener(OnLockScrollChanged);
}
if (infoFilterToggle != null)
{
infoFilterToggle.onValueChanged.RemoveListener(OnInfoFilterChanged);
infoFilterToggle.isOn = infoFilter;
infoFilterToggle.onValueChanged.AddListener(OnInfoFilterChanged);
infoFilterLabel = infoFilterToggle.GetComponentInChildren<TMP_Text>(true);
}
if (warningFilterToggle != null)
{
warningFilterToggle.onValueChanged.RemoveListener(OnWarningFilterChanged);
warningFilterToggle.isOn = warningFilter;
warningFilterToggle.onValueChanged.AddListener(OnWarningFilterChanged);
warningFilterLabel = warningFilterToggle.GetComponentInChildren<TMP_Text>(true);
}
if (errorFilterToggle != null)
{
errorFilterToggle.onValueChanged.RemoveListener(OnErrorFilterChanged);
errorFilterToggle.isOn = errorFilter;
errorFilterToggle.onValueChanged.AddListener(OnErrorFilterChanged);
errorFilterLabel = errorFilterToggle.GetComponentInChildren<TMP_Text>(true);
}
if (fatalFilterToggle != null)
{
fatalFilterToggle.onValueChanged.RemoveListener(OnFatalFilterChanged);
fatalFilterToggle.isOn = fatalFilter;
fatalFilterToggle.onValueChanged.AddListener(OnFatalFilterChanged);
fatalFilterLabel = fatalFilterToggle.GetComponentInChildren<TMP_Text>(true);
}
if (textFilterInputField != null)
{
textFilterInputField.onValueChanged.RemoveListener(OnTextFilterChanged);
textFilterInputField.text = textFilter;
textFilterInputField.onValueChanged.AddListener(OnTextFilterChanged);
}
if (detailText != null)
detailText.text = "点击日志查看详细信息...";
uiInitialized = true;
needRefresh = true;
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 SetMaxLogLines(int maxInfo, int maxWarning, int maxError, int maxFatal)
{
maxInfoLines = Mathf.Max(1, maxInfo);
maxWarningLines = Mathf.Max(1, maxWarning);
maxErrorLines = Mathf.Max(1, maxError);
maxFatalLines = Mathf.Max(1, maxFatal);
Debug.Log($"[ConsoleModule] 设置缓存池大小 - Info:{maxInfoLines}, Warning:{maxWarningLines}, Error:{maxErrorLines}, Fatal:{maxFatalLines}");
}
/// <summary>
/// 获取各类型日志的最大缓存数量
/// </summary>
public void GetMaxLogLines(out int maxInfo, out int maxWarning, out int maxError, out int maxFatal)
{
maxInfo = maxInfoLines;
maxWarning = maxWarningLines;
maxError = maxErrorLines;
maxFatal = maxFatalLines;
}
/// <summary>
/// 更新(每帧调用)
/// </summary>
public void Update()
{
if (!uiInitialized)
{
return;
}
// 如果页面未激活,跳过更新
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;
}
}
/// <summary>
/// 销毁
/// </summary>
public void Shutdown()
{
if (hasRegisteredLogCallback)
{
Application.logMessageReceived -= OnLogMessageReceived;
hasRegisteredLogCallback = false;
}
ClearAllLogs();
}
#endregion
#region
private void EnsureLogCaptureRegistered()
{
if (hasRegisteredLogCallback)
{
return;
}
Application.logMessageReceived += OnLogMessageReceived;
hasRegisteredLogCallback = true;
}
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);
// 根据类型添加到对应的缓存池
Queue<LogNode> targetQueue = null;
int maxLines = 0;
switch (logType)
{
case LogType.Log:
targetQueue = infoLogNodes;
maxLines = maxInfoLines;
break;
case LogType.Warning:
targetQueue = warningLogNodes;
maxLines = maxWarningLines;
break;
case LogType.Error:
targetQueue = errorLogNodes;
maxLines = maxErrorLines;
break;
case LogType.Exception:
targetQueue = fatalLogNodes;
maxLines = maxFatalLines;
break;
}
if (targetQueue != null)
{
targetQueue.Enqueue(logNode);
// 限制该类型的最大行数
while (targetQueue.Count > maxLines)
{
targetQueue.Dequeue();
}
}
// 标记需要刷新而不是立即刷新避免rebuild loop
needRefresh = true;
}
private void RefreshLogDisplay()
{
if (!uiInitialized)
{
needRefresh = true;
return;
}
// 如果页面未激活,延迟刷新到下次页面激活时
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性能优化只为需要显示的日志创建LogItem
int index = 0;
int displayCount = 0;
// 合并所有缓存池的日志,按时间排序
List<LogNode> allLogs = new List<LogNode>();
allLogs.AddRange(infoLogNodes);
allLogs.AddRange(warningLogNodes);
allLogs.AddRange(errorLogNodes);
allLogs.AddRange(fatalLogNodes);
allLogs.Sort((a, b) => a.LogTime.CompareTo(b.LogTime));
foreach (LogNode logNode in allLogs)
{
// 根据过滤器判断是否显示如果不显示则跳过创建LogItem
if (!ShouldShowLog(logNode))
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++;
}
UpdateFilterToggleText();
}
private bool ShouldShowLog(LogNode logNode)
{
// 首先检查类型筛选
bool typeMatch = false;
switch (logNode.LogType)
{
case LogType.Log:
typeMatch = infoFilter;
break;
case LogType.Warning:
typeMatch = warningFilter;
break;
case LogType.Error:
typeMatch = errorFilter;
break;
case LogType.Exception:
typeMatch = fatalFilter;
break;
default:
typeMatch = true;
break;
}
if (!typeMatch)
return false;
// 然后检查文字筛选
if (!string.IsNullOrEmpty(textFilter))
{
// 如果日志消息不包含筛选文字,则不显示
if (!logNode.LogMessage.Contains(textFilter))
return false;
}
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 = infoLogNodes.Count;
warningCount = warningLogNodes.Count;
errorCount = errorLogNodes.Count;
fatalCount = fatalLogNodes.Count;
}
private void UpdateFilterToggleText()
{
if (!uiInitialized)
{
return;
}
RefreshCount();
UpdateFilterLabel(infoFilterLabel, "Info", infoCount, ref lastDisplayedInfoCount);
UpdateFilterLabel(warningFilterLabel, "Warning", warningCount, ref lastDisplayedWarningCount);
UpdateFilterLabel(errorFilterLabel, "Error", errorCount, ref lastDisplayedErrorCount);
UpdateFilterLabel(fatalFilterLabel, "Fatal", fatalCount, ref lastDisplayedFatalCount);
}
private void UpdateFilterLabel(TMP_Text label, string prefix, int count, ref int lastDisplayedCount)
{
if (label == null)
{
return;
}
if (lastDisplayedCount == count)
{
return;
}
label.text = $"{prefix} ({count})";
lastDisplayedCount = count;
}
private void ClearAllLogs()
{
infoLogNodes.Clear();
warningLogNodes.Clear();
errorLogNodes.Clear();
fatalLogNodes.Clear();
selectedNode = null;
lastDisplayedInfoCount = -1;
lastDisplayedWarningCount = -1;
lastDisplayedErrorCount = -1;
lastDisplayedFatalCount = -1;
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;
}
private void OnTextFilterChanged(string value)
{
textFilter = value;
needRefresh = true;
}
#endregion
}
}