WX-Game1/Assets/Scripts/LogGUI.cs

628 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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