using System.Collections.Generic; using UnityEngine; /// /// 全局日志GUI管理器 /// 用于捕获和显示Unity中所有的日志输出 /// 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 logEntries = new List(); // 日志条目列表 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; // 滚动视图矩形是否已初始化 /// /// 日志条目数据结构 /// [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().Length > 1) { Destroy(gameObject); return; } // 设置为不销毁,确保日志系统持续运行 DontDestroyOnLoad(gameObject); // 检测运行环境 DetectRuntimeEnvironment(); // 初始化字体设置 InitializeFonts(); // 注册日志接收事件 Application.logMessageReceived += HandleLog; Debug.Log("全局日志系统已启动"); } /// /// 检测运行环境 /// private void DetectRuntimeEnvironment() { // 检测是否为WebGL环境 isWebGL = Application.platform == RuntimePlatform.WebGLPlayer; if (isWebGL) { Debug.Log("检测到WebGL环境,将应用特殊字体设置"); } else { Debug.Log("检测到非WebGL环境"); } } /// /// 初始化字体设置 /// 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(); } /// /// 验证字体是否可用 /// private void ValidateFont() { if (currentFont != null) { Debug.Log($"字体验证: {currentFont.name} - 可用"); // 在WebGL环境下,标记需要预加载字体 if (isWebGL) { StartCoroutine(PreloadFontForWebGL()); } } else { Debug.LogWarning("没有可用的字体,将使用系统默认字体"); } } /// /// WebGL环境下预加载字体 /// private System.Collections.IEnumerator PreloadFontForWebGL() { if (currentFont == null) yield break; // 等待一帧,确保字体资源加载完成 yield return null; // 标记字体已预加载,将在OnGUI中应用 Debug.Log("WebGL环境下字体预加载完成,将在OnGUI中应用"); } private void OnDestroy() { // 取消注册日志接收事件 Application.logMessageReceived -= HandleLog; } /// /// 处理Unity日志消息 /// /// 日志字符串 /// 堆栈跟踪 /// 日志类型 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; } } /// /// 清空所有日志 /// private void ClearLogs() { logEntries.Clear(); scrollPosition = Vector2.zero; } /// /// 导出日志到文件 /// 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}"); } } /// /// 获取过滤后的日志列表 /// private List GetFilteredLogs() { List filtered = new List(); 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; } /// /// GUI绘制方法 /// 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(); } /// /// 确保字体设置正确(在OnGUI中安全调用) /// 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}"); } } } /// /// 绘制最小化状态的窗口 /// /// 窗口X坐标 /// 窗口Y坐标 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); } } /// /// 更新滚动视图矩形区域 /// /// 窗口X坐标 /// 窗口Y坐标 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; } /// /// 计算滚动范围的最大值 /// /// 最大滚动值 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; } /// /// 处理触摸滚动输入 /// /// 窗口X坐标 /// 窗口Y坐标 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; } } } /// /// 键盘快捷键 /// 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; } } }