628 lines
21 KiB
C#
628 lines
21 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 全局日志GUI管理器
|
||
/// 用于捕获和显示Unity中所有的日志输出
|
||
/// </summary>
|
||
public class LogGUI : MonoBehaviour
|
||
{
|
||
[Header("日志显示设置")]
|
||
[SerializeField] private bool showLogs = true; // 是否显示日志界面
|
||
[SerializeField] private int maxLogCount = 200; // 最大日志数量
|
||
[SerializeField] private float logWindowWidth = 500f; // 日志窗口宽度
|
||
[SerializeField] private float logWindowHeight = 400f; // 日志窗口高度
|
||
[SerializeField] private bool autoScroll = true; // 是否自动滚动到底部
|
||
|
||
[Header("日志过滤设置")]
|
||
[SerializeField] private bool showLogMessages = true; // 是否显示普通日志
|
||
[SerializeField] private bool showWarnings = true; // 是否显示警告日志
|
||
[SerializeField] private bool showErrors = true; // 是否显示错误日志
|
||
[SerializeField] private string filterKeyword = ""; // 关键词过滤
|
||
|
||
[Header("字体设置")]
|
||
[SerializeField] private Font chineseFont; // 中文字体资源
|
||
[SerializeField] private Font fallbackFont; // 备用字体资源
|
||
|
||
private List<LogEntry> logEntries = new List<LogEntry>(); // 日志条目列表
|
||
private Vector2 scrollPosition; // 滚动位置
|
||
private bool showScrollView = true; // 是否显示滚动视图
|
||
private bool showFilterPanel = false; // 是否显示过滤面板
|
||
private bool isMinimized = false; // 是否处于最小化状态
|
||
private Font currentFont; // 当前使用的字体
|
||
private bool isWebGL = false; // 是否为WebGL环境
|
||
|
||
// 触摸滚动相关变量
|
||
private bool isDragging = false; // 是否正在拖拽滚动
|
||
private Vector2 lastTouchPosition; // 上次触摸位置
|
||
private Rect scrollViewRect; // 滚动视图的矩形区域
|
||
private bool scrollViewRectInitialized = false; // 滚动视图矩形是否已初始化
|
||
|
||
/// <summary>
|
||
/// 日志条目数据结构
|
||
/// </summary>
|
||
[System.Serializable]
|
||
public class LogEntry
|
||
{
|
||
public string message; // 日志消息
|
||
public string stackTrace; // 堆栈跟踪
|
||
public LogType logType; // 日志类型
|
||
public string timestamp; // 时间戳
|
||
public int frameCount; // 帧数
|
||
|
||
public LogEntry(string msg, string stack, LogType type)
|
||
{
|
||
message = msg;
|
||
stackTrace = stack;
|
||
logType = type;
|
||
timestamp = System.DateTime.Now.ToString("HH:mm:ss");
|
||
frameCount = Time.frameCount;
|
||
}
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
// 确保只有一个实例
|
||
if (FindObjectsOfType<LogGUI>().Length > 1)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
// 设置为不销毁,确保日志系统持续运行
|
||
DontDestroyOnLoad(gameObject);
|
||
|
||
// 检测运行环境
|
||
DetectRuntimeEnvironment();
|
||
|
||
// 初始化字体设置
|
||
InitializeFonts();
|
||
|
||
// 注册日志接收事件
|
||
Application.logMessageReceived += HandleLog;
|
||
|
||
Debug.Log("全局日志系统已启动");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测运行环境
|
||
/// </summary>
|
||
private void DetectRuntimeEnvironment()
|
||
{
|
||
// 检测是否为WebGL环境
|
||
isWebGL = Application.platform == RuntimePlatform.WebGLPlayer;
|
||
|
||
if (isWebGL)
|
||
{
|
||
Debug.Log("检测到WebGL环境,将应用特殊字体设置");
|
||
}
|
||
else
|
||
{
|
||
Debug.Log("检测到非WebGL环境");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化字体设置
|
||
/// </summary>
|
||
private void InitializeFonts()
|
||
{
|
||
// 优先使用指定的中文字体
|
||
if (chineseFont != null)
|
||
{
|
||
currentFont = chineseFont;
|
||
Debug.Log("使用指定的中文字体");
|
||
}
|
||
// 如果没有指定中文字体,尝试使用备用字体
|
||
else if (fallbackFont != null)
|
||
{
|
||
currentFont = fallbackFont;
|
||
Debug.Log("使用备用字体");
|
||
}
|
||
// 如果都没有指定,使用系统默认字体
|
||
else
|
||
{
|
||
// 注意:不能在Awake中直接访问GUI.skin.font
|
||
// 将在OnGUI中设置
|
||
Debug.Log("将使用系统默认字体");
|
||
}
|
||
|
||
// 验证字体是否可用
|
||
ValidateFont();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证字体是否可用
|
||
/// </summary>
|
||
private void ValidateFont()
|
||
{
|
||
if (currentFont != null)
|
||
{
|
||
Debug.Log($"字体验证: {currentFont.name} - 可用");
|
||
|
||
// 在WebGL环境下,标记需要预加载字体
|
||
if (isWebGL)
|
||
{
|
||
StartCoroutine(PreloadFontForWebGL());
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("没有可用的字体,将使用系统默认字体");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// WebGL环境下预加载字体
|
||
/// </summary>
|
||
private System.Collections.IEnumerator PreloadFontForWebGL()
|
||
{
|
||
if (currentFont == null) yield break;
|
||
|
||
// 等待一帧,确保字体资源加载完成
|
||
yield return null;
|
||
|
||
// 标记字体已预加载,将在OnGUI中应用
|
||
Debug.Log("WebGL环境下字体预加载完成,将在OnGUI中应用");
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
// 取消注册日志接收事件
|
||
Application.logMessageReceived -= HandleLog;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理Unity日志消息
|
||
/// </summary>
|
||
/// <param name="logString">日志字符串</param>
|
||
/// <param name="stackTrace">堆栈跟踪</param>
|
||
/// <param name="type">日志类型</param>
|
||
private void HandleLog(string logString, string stackTrace, LogType type)
|
||
{
|
||
// 创建新的日志条目
|
||
LogEntry entry = new LogEntry(logString, stackTrace, type);
|
||
|
||
// 添加到日志列表
|
||
logEntries.Add(entry);
|
||
|
||
// 限制日志数量,防止内存占用过多
|
||
if (logEntries.Count > maxLogCount)
|
||
{
|
||
logEntries.RemoveAt(0);
|
||
}
|
||
|
||
// 自动滚动到底部
|
||
if (autoScroll)
|
||
{
|
||
scrollPosition.y = float.MaxValue;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空所有日志
|
||
/// </summary>
|
||
private void ClearLogs()
|
||
{
|
||
logEntries.Clear();
|
||
scrollPosition = Vector2.zero;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出日志到文件
|
||
/// </summary>
|
||
private void ExportLogs()
|
||
{
|
||
string logContent = "";
|
||
foreach (var entry in logEntries)
|
||
{
|
||
logContent += $"[{entry.timestamp}] [{entry.logType}] {entry.message}\n";
|
||
if (!string.IsNullOrEmpty(entry.stackTrace))
|
||
{
|
||
logContent += $"堆栈: {entry.stackTrace}\n";
|
||
}
|
||
logContent += "\n";
|
||
}
|
||
|
||
// 保存到文件
|
||
string fileName = $"Logs_{System.DateTime.Now:yyyyMMdd_HHmmss}.txt";
|
||
string filePath = System.IO.Path.Combine(Application.persistentDataPath, fileName);
|
||
|
||
try
|
||
{
|
||
System.IO.File.WriteAllText(filePath, logContent);
|
||
Debug.Log($"日志已导出到: {filePath}");
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogError($"导出日志失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取过滤后的日志列表
|
||
/// </summary>
|
||
private List<LogEntry> GetFilteredLogs()
|
||
{
|
||
List<LogEntry> filtered = new List<LogEntry>();
|
||
|
||
foreach (var entry in logEntries)
|
||
{
|
||
// 类型过滤
|
||
if (!showLogMessages && entry.logType == LogType.Log) continue;
|
||
if (!showWarnings && entry.logType == LogType.Warning) continue;
|
||
if (!showErrors && entry.logType == LogType.Error) continue;
|
||
|
||
// 关键词过滤
|
||
if (!string.IsNullOrEmpty(filterKeyword) &&
|
||
!entry.message.ToLower().Contains(filterKeyword.ToLower()))
|
||
continue;
|
||
|
||
filtered.Add(entry);
|
||
}
|
||
|
||
return filtered;
|
||
}
|
||
|
||
/// <summary>
|
||
/// GUI绘制方法
|
||
/// </summary>
|
||
private void OnGUI()
|
||
{
|
||
// 只在需要显示日志时绘制
|
||
if (!showLogs) return;
|
||
|
||
// 确保字体设置正确(在OnGUI中安全调用)
|
||
EnsureFontIsSet();
|
||
|
||
// 设置日志窗口位置(右上角)
|
||
float windowX = Screen.width - logWindowWidth - 20f;
|
||
float windowY = 20f;
|
||
|
||
// 如果是最小化状态,只显示一个小的X按钮
|
||
if (isMinimized)
|
||
{
|
||
DrawMinimizedWindow(windowX, windowY);
|
||
return;
|
||
}
|
||
|
||
// 绘制完整日志窗口
|
||
GUILayout.BeginArea(new Rect(windowX, windowY, logWindowWidth, logWindowHeight));
|
||
|
||
// 绘制标题栏和控制按钮
|
||
GUILayout.BeginHorizontal();
|
||
GUILayout.Label($"实时日志输出 ({logEntries.Count})", GUI.skin.box, GUILayout.ExpandWidth(true));
|
||
|
||
// 过滤面板按钮
|
||
if (GUILayout.Button("过滤", GUILayout.Width(50)))
|
||
{
|
||
showFilterPanel = !showFilterPanel;
|
||
}
|
||
|
||
// 清空日志按钮
|
||
if (GUILayout.Button("清空", GUILayout.Width(50)))
|
||
{
|
||
ClearLogs();
|
||
}
|
||
|
||
// // 导出日志按钮
|
||
// if (GUILayout.Button("导出", GUILayout.Width(50)))
|
||
// {
|
||
// ExportLogs();
|
||
// }
|
||
//
|
||
// 最小化按钮
|
||
if (GUILayout.Button("最小化", GUILayout.Width(50)))
|
||
{
|
||
isMinimized = true;
|
||
}
|
||
|
||
|
||
GUILayout.EndHorizontal();
|
||
|
||
// 绘制过滤面板
|
||
if (showFilterPanel)
|
||
{
|
||
GUILayout.BeginVertical(GUI.skin.box);
|
||
GUILayout.Label("日志过滤设置", GUI.skin.label);
|
||
|
||
GUILayout.BeginHorizontal();
|
||
showLogMessages = GUILayout.Toggle(showLogMessages, "信息");
|
||
showWarnings = GUILayout.Toggle(showWarnings, "警告");
|
||
showErrors = GUILayout.Toggle(showErrors, "错误");
|
||
GUILayout.EndHorizontal();
|
||
|
||
GUILayout.BeginHorizontal();
|
||
GUILayout.Label("关键词:", GUILayout.Width(50));
|
||
filterKeyword = GUILayout.TextField(filterKeyword, GUILayout.ExpandWidth(true));
|
||
GUILayout.EndHorizontal();
|
||
|
||
GUILayout.EndVertical();
|
||
}
|
||
|
||
// 绘制日志内容区域
|
||
if (showScrollView)
|
||
{
|
||
// 处理触摸滚动输入(在绘制滚动视图之前)
|
||
HandleTouchScrollInput(windowX, windowY);
|
||
|
||
// 滚动视图
|
||
scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUI.skin.box);
|
||
|
||
// 记录滚动视图的矩形区域(用于触摸检测)
|
||
if (Event.current.type == EventType.Repaint)
|
||
{
|
||
// 在重绘时更新滚动视图矩形区域
|
||
UpdateScrollViewRect(windowX, windowY);
|
||
}
|
||
|
||
// 获取过滤后的日志
|
||
var filteredLogs = GetFilteredLogs();
|
||
|
||
// 显示过滤后的日志消息
|
||
foreach (var entry in filteredLogs)
|
||
{
|
||
// 根据日志类型设置不同的颜色
|
||
Color originalColor = GUI.color;
|
||
|
||
switch (entry.logType)
|
||
{
|
||
case LogType.Error:
|
||
GUI.color = Color.red;
|
||
break;
|
||
case LogType.Warning:
|
||
GUI.color = Color.yellow;
|
||
break;
|
||
case LogType.Log:
|
||
GUI.color = Color.white;
|
||
break;
|
||
default:
|
||
GUI.color = Color.gray;
|
||
break;
|
||
}
|
||
|
||
// 绘制日志消息
|
||
GUILayout.Label($"[{entry.timestamp}] {entry.message}", GUI.skin.label);
|
||
|
||
// 恢复原始颜色
|
||
GUI.color = originalColor;
|
||
}
|
||
|
||
GUILayout.EndScrollView();
|
||
}
|
||
else
|
||
{
|
||
// 显示日志统计信息
|
||
var filteredLogs = GetFilteredLogs();
|
||
GUILayout.Label($"总日志数: {logEntries.Count}", GUI.skin.label);
|
||
GUILayout.Label($"过滤后: {filteredLogs.Count}", GUI.skin.label);
|
||
GUILayout.Label("日志内容已隐藏", GUI.skin.label);
|
||
}
|
||
|
||
GUILayout.EndArea();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保字体设置正确(在OnGUI中安全调用)
|
||
/// </summary>
|
||
private void EnsureFontIsSet()
|
||
{
|
||
// 如果没有设置字体,尝试获取系统默认字体
|
||
if (currentFont == null)
|
||
{
|
||
currentFont = GUI.skin.font;
|
||
Debug.Log("已获取系统默认字体");
|
||
}
|
||
|
||
// 确保字体设置正确
|
||
if (currentFont != null && GUI.skin.font != currentFont)
|
||
{
|
||
GUI.skin.font = currentFont;
|
||
|
||
// 在WebGL环境下记录字体设置
|
||
if (isWebGL)
|
||
{
|
||
Debug.Log($"WebGL环境下字体设置完成: {currentFont.name}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制最小化状态的窗口
|
||
/// </summary>
|
||
/// <param name="windowX">窗口X坐标</param>
|
||
/// <param name="windowY">窗口Y坐标</param>
|
||
private void DrawMinimizedWindow(float windowX, float windowY)
|
||
{
|
||
// 最小化状态下只显示一个小的X按钮
|
||
float buttonSize = 30f;
|
||
float buttonX = windowX + logWindowWidth - buttonSize;
|
||
float buttonY = windowY;
|
||
|
||
// 绘制最小化状态的X按钮
|
||
if (GUI.Button(new Rect(buttonX, buttonY, buttonSize, buttonSize), "□"))
|
||
{
|
||
// 点击X按钮还原窗口
|
||
isMinimized = false;
|
||
}
|
||
|
||
// 可选:显示日志数量提示
|
||
if (logEntries.Count > 0)
|
||
{
|
||
string logCountText = $"{logEntries.Count}";
|
||
GUI.Label(new Rect(buttonX - 40, buttonY + 5, 35, 20), logCountText, GUI.skin.label);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新滚动视图矩形区域
|
||
/// </summary>
|
||
/// <param name="windowX">窗口X坐标</param>
|
||
/// <param name="windowY">窗口Y坐标</param>
|
||
private void UpdateScrollViewRect(float windowX, float windowY)
|
||
{
|
||
// 计算滚动视图的实际位置和大小
|
||
// 需要考虑标题栏、过滤面板等的高度
|
||
float headerHeight = 30f; // 标题栏高度(按钮行)
|
||
float filterPanelHeight = showFilterPanel ? 100f : 0f; // 过滤面板高度(如果显示)
|
||
float scrollViewY = windowY + headerHeight + filterPanelHeight;
|
||
float scrollViewHeight = logWindowHeight - headerHeight - filterPanelHeight;
|
||
|
||
scrollViewRect = new Rect(windowX, scrollViewY, logWindowWidth, scrollViewHeight);
|
||
scrollViewRectInitialized = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算滚动范围的最大值
|
||
/// </summary>
|
||
/// <returns>最大滚动值</returns>
|
||
private float CalculateMaxScrollPosition()
|
||
{
|
||
var filteredLogs = GetFilteredLogs();
|
||
if (filteredLogs.Count == 0) return 0f;
|
||
|
||
// 估算每个日志条目的高度(根据字体大小和行数)
|
||
float estimatedLineHeight = 20f; // 每行大约20像素
|
||
float estimatedLogHeight = estimatedLineHeight * 1.5f; // 每个日志条目约1.5行
|
||
|
||
// 计算总内容高度
|
||
float totalContentHeight = filteredLogs.Count * estimatedLogHeight;
|
||
|
||
// 计算可视区域高度
|
||
float visibleHeight = scrollViewRect.height > 0 ? scrollViewRect.height : logWindowHeight;
|
||
|
||
// 最大滚动值 = 总内容高度 - 可视区域高度
|
||
float maxScroll = Mathf.Max(0, totalContentHeight - visibleHeight);
|
||
|
||
return maxScroll;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理触摸滚动输入
|
||
/// </summary>
|
||
/// <param name="windowX">窗口X坐标</param>
|
||
/// <param name="windowY">窗口Y坐标</param>
|
||
private void HandleTouchScrollInput(float windowX, float windowY)
|
||
{
|
||
// 如果滚动视图矩形未初始化,先更新它
|
||
if (!scrollViewRectInitialized)
|
||
{
|
||
UpdateScrollViewRect(windowX, windowY);
|
||
}
|
||
|
||
// 计算最大滚动值
|
||
float maxScroll = CalculateMaxScrollPosition();
|
||
|
||
Event currentEvent = Event.current;
|
||
Vector2 touchPosition = Vector2.zero;
|
||
|
||
// 检测触摸输入(移动设备)
|
||
if (Input.touchCount > 0)
|
||
{
|
||
Touch touch = Input.GetTouch(0);
|
||
touchPosition = touch.position;
|
||
// Unity 的触摸坐标 Y 轴是反向的,需要转换(GUI坐标系从左上角开始)
|
||
touchPosition.y = Screen.height - touchPosition.y;
|
||
|
||
// 处理触摸的各个阶段
|
||
switch (touch.phase)
|
||
{
|
||
case TouchPhase.Began:
|
||
// 检查触摸是否在滚动视图区域内
|
||
if (scrollViewRect.Contains(touchPosition))
|
||
{
|
||
isDragging = true;
|
||
lastTouchPosition = touchPosition;
|
||
// 禁用自动滚动,因为用户正在手动滚动
|
||
autoScroll = false;
|
||
}
|
||
break;
|
||
|
||
case TouchPhase.Moved:
|
||
if (isDragging)
|
||
{
|
||
// 计算触摸移动距离(向下滑动时delta.y为正,需要向上滚动)
|
||
Vector2 delta = lastTouchPosition - touchPosition;
|
||
// 更新滚动位置(向下滑动增加滚动值)
|
||
scrollPosition.y += delta.y;
|
||
// 限制滚动范围(0 到 maxScroll)
|
||
scrollPosition.y = Mathf.Clamp(scrollPosition.y, 0f, maxScroll);
|
||
lastTouchPosition = touchPosition;
|
||
}
|
||
break;
|
||
|
||
case TouchPhase.Ended:
|
||
case TouchPhase.Canceled:
|
||
if (isDragging)
|
||
{
|
||
isDragging = false;
|
||
lastTouchPosition = Vector2.zero;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
// 检测鼠标输入(编辑器或桌面端,也支持鼠标拖拽)
|
||
else if (currentEvent != null)
|
||
{
|
||
touchPosition = currentEvent.mousePosition;
|
||
|
||
switch (currentEvent.type)
|
||
{
|
||
case EventType.MouseDown:
|
||
if (currentEvent.button == 0 && scrollViewRect.Contains(touchPosition))
|
||
{
|
||
isDragging = true;
|
||
lastTouchPosition = touchPosition;
|
||
autoScroll = false;
|
||
currentEvent.Use(); // 标记事件已使用
|
||
}
|
||
break;
|
||
|
||
case EventType.MouseDrag:
|
||
if (isDragging && currentEvent.button == 0)
|
||
{
|
||
Vector2 delta = lastTouchPosition - touchPosition;
|
||
scrollPosition.y += delta.y;
|
||
scrollPosition.y = Mathf.Clamp(scrollPosition.y, 0f, maxScroll);
|
||
lastTouchPosition = touchPosition;
|
||
currentEvent.Use(); // 标记事件已使用
|
||
}
|
||
break;
|
||
|
||
case EventType.MouseUp:
|
||
if (isDragging && currentEvent.button == 0)
|
||
{
|
||
isDragging = false;
|
||
lastTouchPosition = Vector2.zero;
|
||
currentEvent.Use(); // 标记事件已使用
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 键盘快捷键
|
||
/// </summary>
|
||
private void Update()
|
||
{
|
||
// F1键切换日志显示
|
||
if (Input.GetKeyDown(KeyCode.F1))
|
||
{
|
||
showLogs = !showLogs;
|
||
}
|
||
|
||
// F2键清空日志
|
||
if (Input.GetKeyDown(KeyCode.F2))
|
||
{
|
||
ClearLogs();
|
||
}
|
||
|
||
// F3键切换最小化状态
|
||
if (Input.GetKeyDown(KeyCode.F3))
|
||
{
|
||
isMinimized = !isMinimized;
|
||
}
|
||
}
|
||
}
|