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;
}
}
}