580 lines
18 KiB
C#
580 lines
18 KiB
C#
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
|
||
}
|
||
}
|