【第四章自定义编辑器窗口_Game窗口中的GUI_运行时控制台窗口(10/12)】
4.3.1 运行时控制台窗口
书上了是讲一个实现了一个简易控制台小案例,这个控制台可以运行在编辑器和非编辑器下,个人感觉新东西不多,基本都是前面涉及过的东西,就是日志获取方面的知识。这个只能在非编辑器使用,当你在编辑器模式下,点击挂载该脚本的物体,会疯狂报错,原因就是Unity 的 IMGUI(OnGUI) 在 Inspector 选中物体时也会执行,而例子代码在 OnGUI() 里 没有检查是否在 Game 视图,导致 Editor 环境下布局错乱。有一些方法似乎可以避免,有兴趣的可以研究怎么避免。
// 当Unity引擎产生任何日志(包括Debug.Log/LogWarning/LogError等)时
// 将自动触发OnLogMessageReceived方法
// 参数说明:
// condition: 日志消息内容
// stackTrace: 调用堆栈信息
// type: 日志类型(Log/Warning/Error等)
Application.logMessageReceived += OnLogMessageReceived;
先看效果
代码
using System;
using UnityEngine;
using System.Collections.Generic;/// <summary>
/// 控制台GUI窗口组件
/// </summary>
public class ConsoleGUIWindow : MonoBehaviour
{[SerializeField]private WorkingType workingType = WorkingType.ALWAYS_OPEN; // 工作模式// 窗口尺寸定义private Rect expandRect; // 展开状态窗口private Rect retractRect; // 收起状态窗口private Rect dragableRect; // 可拖拽区域private bool isExpand; // 当前是否展开private int fps; // 当前帧率private float lastShowFPSTime; // 上次更新FPS的时间private Color fpsColor = Color.white; // FPS显示颜色// 日志存储private readonly List<ConsoleItem> logs = new List<ConsoleItem>();// 滚动视图位置private Vector2 listScroll; // 日志列表滚动条private Vector2 detailScroll; // 详情滚动条// 日志计数private int infoCount; // 普通日志数量private int warnCount; // 警告日志数量private int errorCount; // 错误日志数量// 显示过滤开关[SerializeField] private bool showInfo = true; // 显示普通日志[SerializeField] private bool showWarn = true; // 显示警告日志[SerializeField] private bool showError = true; // 显示错误日志private ConsoleItem currentSelected; // 当前选中的日志项[SerializeField] private bool showTime = true; // 是否显示时间[SerializeField] private int maxCacheCount = 100; // 最大缓存日志数量private string searchContent; // 搜索关键词void Awake(){// 注册全局日志回调处理器// 当Unity引擎产生任何日志(包括Debug.Log/LogWarning/LogError等)时// 将自动触发OnLogMessageReceived方法// 参数说明:// condition: 日志消息内容// stackTrace: 调用堆栈信息// type: 日志类型(Log/Warning/Error等)Application.logMessageReceived += OnLogMessageReceived;}void Start(){// 根据工作模式初始化switch (workingType){case WorkingType.ALWAYS_OPEN:enabled = true;break;case WorkingType.ONLY_OPEN_WHEN_DEVELOPMENT_BUILD:enabled = Debug.isDebugBuild; // 仅在开发构建启用break;case WorkingType.ONLY_OPEN_IN_EDITOR:enabled = Application.isEditor; // 仅在编辑器启用break;case WorkingType.ALWAYS_CLOSE:enabled = false;break;}// 初始化窗口尺寸expandRect = new Rect(Screen.width * .7f, 0f,Screen.width * .3f, Screen.height * .5f);retractRect = new Rect(Screen.width - 100f, 0f, 100f, 60f);dragableRect = new Rect(0, 0, Screen.width * .3f, 20f);}void OnDestroy(){// 注销日志回调Application.logMessageReceived -= OnLogMessageReceived;}/// <summary>/// 日志接收处理/// </summary>/// <param name="condition"></param>/// <param name="stackTrace"></param>/// <param name="logType"></param>void OnLogMessageReceived(string condition, string stackTrace, LogType logType){if (!enabled)return;var item = new ConsoleItem(DateTime.Now, logType, condition, stackTrace);logs.Add(item);// 精确统计日志类型switch (logType){case LogType.Log:infoCount++;break;case LogType.Warning:warnCount++;break;case LogType.Error:case LogType.Assert:case LogType.Exception:errorCount++;break;}// 清理旧日志if (logs.Count > maxCacheCount){var removed = logs[0];switch (removed.type){case LogType.Log:infoCount--;break;case LogType.Warning:warnCount--;break;case LogType.Error:case LogType.Assert:case LogType.Exception:errorCount--;break;}if (currentSelected == removed)currentSelected = null;logs.RemoveAt(0);}}void OnGUI(){// 只在启用时渲染GUIif (!enabled)return;// 根据展开状态渲染不同窗口if (isExpand){expandRect = GUI.Window(0, expandRect, OnExpandGUI, "Console");//限制范围expandRect.x = Mathf.Clamp(expandRect.x, 0, Screen.width * .7f);expandRect.y = Mathf.Clamp(expandRect.y, 0, Screen.height * .5f);dragableRect = new Rect(0, 0, Screen.width * .3f, 20f);}else{retractRect = GUI.Window(0, retractRect, OnRetractGUI, "Console");//限制范围retractRect.x = Mathf.Clamp(retractRect.x, 0, Screen.width - 100f);retractRect.y = Mathf.Clamp(retractRect.y, 0, Screen.height - 60f);dragableRect = new Rect(0, 0, 100f, 20f);}// FPS计算(每秒更新)if (Time.realtimeSinceStartup - lastShowFPSTime >= 1){fps = Mathf.RoundToInt(1f / Time.unscaledDeltaTime);lastShowFPSTime = Time.realtimeSinceStartup;// 根据错误状态设置颜色fpsColor = errorCount > 0 ? Color.red: warnCount > 0 ? Color.yellow : Color.white;}// 调试按钮:强制添加测试日志if (GUI.Button(new Rect(10, 10, 150, 30), "Add Test Log")){Debug.Log("Test console message");Debug.LogWarning("Test warning message");Debug.LogError("Test error message");}}// 收起状态GUIvoid OnRetractGUI(int windowId){//Unity中,窗口的绘制和事件处理(包括拖拽)都是在回调函数中进行的。GUI.DragWindow(dragableRect); // 可拖拽区域GUI.contentColor = fpsColor; //设置文本颜色,用来绘制// FPS按钮(点击展开窗口)if (GUILayout.Button($"FPS:{fps}", GUILayout.Height(30f)))isExpand = true;GUI.contentColor = Color.white; // 重置颜色,避免后续文本渲染都是这个颜色}/// <summary>/// 展开状态GUI/// </summary>/// <param name="windowId"></param>void OnExpandGUI(int windowId){GUI.DragWindow(dragableRect);GUI.contentColor = fpsColor;// FPS按钮(点击收起窗口)if (GUILayout.Button($"FPS:{fps}", GUILayout.Height(20f)))isExpand = false;// 渲染各区域OnTopGUI();OnListGUI();OnDetailGUI();}/// <summary>/// 顶部控制区/// </summary>void OnTopGUI(){GUILayout.BeginHorizontal();// 清空按钮if (GUILayout.Button("Clear", GUILayout.Width(50f))){logs.Clear();infoCount = warnCount = errorCount = 0;currentSelected = null;}// 时间显示开关showTime = GUILayout.Toggle(showTime, "ShowTime", GUILayout.Width(80f));// 搜索框searchContent = GUILayout.TextField(searchContent, GUILayout.ExpandWidth(true));// 日志类型过滤开关GUI.contentColor = showInfo ? Color.white : Color.grey;showInfo = GUILayout.Toggle(showInfo, $"Info [{infoCount}]", GUILayout.Width(60f));GUI.contentColor = showWarn ? Color.white : Color.grey;showWarn = GUILayout.Toggle(showWarn, $"Warn [{warnCount}]", GUILayout.Width(65f));GUI.contentColor = showError ? Color.white : Color.grey;showError = GUILayout.Toggle(showError, $"Error [{errorCount}]", GUILayout.Width(65f));// 添加重置过滤按钮,重置所有过滤和搜索内容,显示出所有日志if (GUILayout.Button("Reset", GUILayout.Width(50f))){showInfo = showWarn = showError = true;searchContent = "";}GUI.contentColor = Color.white;GUILayout.EndHorizontal();}/// <summary>/// 日志列表区/// </summary>void OnListGUI(){// 修复:使用最小高度确保可见GUILayout.BeginVertical("Box", GUILayout.MinHeight(100));listScroll = GUILayout.BeginScrollView(listScroll);// 显示日志计数调试信息GUILayout.Label($"Total logs: {logs.Count} | Visible: {infoCount + warnCount + errorCount}");// 倒序显示最新日志for (int i = logs.Count - 1; i >= 0; i--){var temp = logs[i];// 关键词过滤if (!string.IsNullOrEmpty(searchContent) &&!temp.message.ToLower().Contains(searchContent.ToLower()))continue;// 类型过滤bool show = false;switch (temp.type){case LogType.Log when showInfo:show = true;break;case LogType.Warning when showWarn:show = true;GUI.contentColor = Color.yellow;break;case LogType.Error:case LogType.Assert:case LogType.Exception when showError:show = true;GUI.contentColor = Color.red;break;}// 渲染可点击的日志项if (show){if (GUILayout.Toggle(currentSelected == temp,showTime ? temp.brief : temp.message))currentSelected = temp;}GUI.contentColor = Color.white;}// 添加提示信息if (logs.Count == 0){GUILayout.Label("No logs available");}else if (infoCount + warnCount + errorCount == 0){GUILayout.Label("All logs filtered out");}GUILayout.EndScrollView();GUILayout.EndVertical();}/// <summary>/// 日志详情区/// </summary>void OnDetailGUI(){GUILayout.BeginVertical("Box", GUILayout.ExpandHeight(true));detailScroll = GUILayout.BeginScrollView(detailScroll);// 显示选中日志的完整信息if (currentSelected != null){GUILayout.Label(currentSelected.detail);}else{GUILayout.Label("Select a log to view details");}GUILayout.EndScrollView();GUILayout.FlexibleSpace();// 复制按钮(仅在选中日志时可用)GUI.enabled = currentSelected != null;if (GUILayout.Button("Copy", GUILayout.Height(20f))){GUIUtility.systemCopyBuffer = currentSelected?.detail ?? "";}GUI.enabled = true;GUILayout.EndVertical();}
}// 工作模式枚举
public enum WorkingType
{ALWAYS_OPEN, // 始终开启ONLY_OPEN_WHEN_DEVELOPMENT_BUILD, // 仅开发构建开启ONLY_OPEN_IN_EDITOR, // 仅编辑器开启ALWAYS_CLOSE // 始终关闭
}// 日志项数据结构
public class ConsoleItem
{public LogType type; // 日志类型public DateTime time; // 记录时间public string message; // 日志内容public string stackTrace; // 调用堆栈public string brief; // 简略信息(含时间)public string detail; // 完整信息(含堆栈)public ConsoleItem(DateTime time, LogType type, string message, string stackTrace){this.type = type;this.time = time;this.message = message;this.stackTrace = stackTrace;// 修复时间格式brief = $"[{time:HH:mm:ss}] {message}";detail = $"[{time:HH:mm:ss.fff}] {message}\n{stackTrace}";}
}