unity日志過濾器

背景:

?之前做游戲的時候和同組的同事聊過說日志過濾盡量不要限制大家怎么使用日志打印的接口,不要加額外的參數,比如多加一個標簽string,或者使用特定的接口,枚舉。最好就是日志大家還是用Debug.Log無感去用,然后通過勾選一些toggle去賽選你感興趣的日志。

原理:

通過分析堆棧,判斷當前日志所屬哪一個模塊,具體屬于哪一個模塊可以通過自定義模塊路徑配置來解決,比如LogFilter.json

如果你的日志來源于路徑manager/net那么該日志屬于網絡模塊,勾選網絡則只打印網絡日志,當然可以同時勾選多個日志,這取決于你上面配置了多少json數組。另外當你勾選了一些toggle之后如果希望保存,比如你只關心你的模塊但是不想每次都去勾選一堆的toggle,那么點擊Save tag可以保存你的修改 ,對應的配置路徑在

其json格式為:

運行截圖:

核心類:

#if DebugMod
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

using HotFix.Manager;
using HotFix.UtilTool;
using Newtonsoft.Json;
using UnityEngine;

namespace HotFix.Helper.LogHelper
{

? ? public class LogHelper : SingletonMono<LogHelper>
{
public class Log
{
public enum _LogType
{
Assert = LogType.Assert,
Error = LogType.Error,
Exception = LogType.Exception,
Log = LogType.Log,
Warning = LogType.Warning,
}

? ? ? ??
public _LogType logType;
public string condition;
public string stacktrace;
public string time;
public string moduleName;

? ? ? ? ? ? public override string ToString()
{
return time + condition + "\n" + stacktrace;
}


public Log CreateCopy()
{
return (Log)this.MemberwiseClone();
}
public float GetMemoryUsage()
{
return (float)(sizeof(int) +
sizeof(_LogType) +
condition.Length * sizeof(char) +
stacktrace.Length * sizeof(char) +
sizeof(int));
}
}

? ? ? ? public List<Log> logs = new();
public Dictionary<string, string> fileModule = new();

? ? ? ? private Dictionary<string, HashSet<string>> customModuleMap;
public Dictionary<string, HashSet<string>> CustomModuleMap
{
get
{
#if UNITY_EDITOR
if(customModuleMap == null)
{
var sourceFile = proRoot + "/environment/LogFilter.json";
if (File.Exists(sourceFile))
{
if(File.Exists(persistentDataPath + "/LogFilter.json"))
{
File.Delete(persistentDataPath + "/LogFilter.json");
}
File.Copy(sourceFile, persistentDataPath + "/LogFilter.json");
}
}

#endif
if(customModuleMap == null && File.Exists(persistentDataPath + "/LogFilter.json"))
{
customModuleMap = JsonConvert.DeserializeObject<Dictionary<string, HashSet<string>>>(File.ReadAllText(persistentDataPath + "/LogFilter.json"));

}
return customModuleMap;
}

}
public Regex pattern = new Regex(@"\bat Assets/([^:]+)\.cs:\d+\b", RegexOptions.Compiled);
public string persistentDataPath = "";
public string proRoot;
public override void Init()
{
#if UNITY_EDITOR // debug模式不監聽 因為需要正則處理堆棧信息?
Application.logMessageReceivedThreaded -= CaptureLogThread;
Application.logMessageReceivedThreaded += CaptureLogThread;
#endif
persistentDataPath = Application.persistentDataPath;
proRoot = Environment.CurrentDirectory;
}
string GetModuleName(string path, int level = 2)
{
if(CustomModuleMap != null && CustomModuleMap.Count > 0)
{
string matchKey = customModuleMap.FirstOrDefault(pair => pair.Value.Any(value => path.StartsWith(value))).Key;
if(matchKey != null)
{
return matchKey;
}

? ? ? ? ? ? }
return "other";
//int index = 0;
//int lastIndex = 0;
//int runLevel = 0;
//for (int i = path.Length - 1; i >= 0; --i)
//{
// ? ?if (path[i] == '/')
// ? ?{
// ? ? ? ?lastIndex = index;
// ? ? ? ?index = i;
// ? ? ? ?++runLevel;
// ? ? ? ?if (runLevel == level)
// ? ? ? ?{
// ? ? ? ? ? ?break;
// ? ? ? ?}
// ? ?}
//}
//int length = Math.Abs(index - lastIndex) - 1;
//length = Math.Max(0, length);
//var moduleName = path.Substring(index + 1, length);
//return moduleName;
}
void CaptureLogThread(string condition, string stacktrace, LogType type)
{

var arr = stacktrace.Split("\n");
string dir = null;
string csName = "";
string moduleName = "";
//逐行分析 日志?
for (int i = 0; i < arr.Length; i++)
{
//有些日志屬于自定義日志 或者封裝的日志接口 這種需要過濾堆棧?
if (arr[i].StartsWith("HotFix.UtilTool.CommonUtils"))
{
continue;
}
if (pattern.IsMatch(arr[i]))
{
var group = pattern.Match(arr[i]);
dir = group.Groups[1].Value;
csName = dir.Substring(dir.LastIndexOf("/") + 1);
lock(fileModule)
{
if (!fileModule.ContainsKey(csName))
{
moduleName = GetModuleName(dir,2);
fileModule[csName] = moduleName;
}
else
{
moduleName = fileModule[csName];
}
}
Log log = new Log() { condition = condition, stacktrace = stacktrace, logType = (Log._LogType)type ,moduleName = moduleName,time = $"[{DateTime.Now.ToString("HH:mm:ss:fff")}]" };
lock (logs)?
{
logs.Add(log);
}
break;
}
}

}

? ? ? ??
}
}
#endif

核心gui類:

部分接口請參考github實現。

#if DebugMod
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using HotFix.Helper.LogHelper;
using Newtonsoft.Json;
using HotFix.UtilTool;
using System.IO;
using System.Text;


//作為loghelper的顯示窗口 不打算做到包體內部
// 先在編輯器上測試?

public class LogHelperWindow : EditorWindow
{

public class ShowControlInfo
{
public ShowControlInfo()
{
isShowLog = false;
Color = new Color(1, 1, 1, 1);
showDebugTrace = false;
}
public bool showDebugTrace; // 是否顯示堆棧?

public int color_r = 255;
public int color_g = 255;
public int color_b = 255;
[JsonIgnore]
public Color _color;
[JsonIgnore]
private bool isColorCtor = false;
[JsonIgnore]
public Color Color
{
set
{
_color = value;
color_r = UnityEngine.Mathf.CeilToInt( _color.r * 255);
color_g = UnityEngine.Mathf.CeilToInt(_color.g * 255);
color_b = UnityEngine.Mathf.CeilToInt(_color.b * 255);
}
get
{
if(!isColorCtor)
{
isColorCtor = true;
_color = new Color(color_r / 255f, color_g / 255f, color_b / 255f, 1f);
}
return _color;
}
}
public bool isShowLog;
}
// 滾動視圖的位置
private Vector2 scrollPosition;
private bool isShow = false;
public Dictionary<string, ShowControlInfo> allTags = new Dictionary<string, ShowControlInfo>();
private bool isAllSelected = false; // 全選狀態
private Vector2 tagScrollPosition; // 標簽滾動視圖位置

private bool isInited = false;
private GUIStyle customLogStyle;
// 繪制錯誤圖標
private Texture2D errorIcon;?
[InitializeOnLoadMethod]
static void Initialize()
{
if( EditorPrefs.GetBool("customlog"))
{
// 訂閱播放模式狀態改變事件
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}


}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode)
{
// 當進入播放模式時,打開自定義控制臺窗口
ShowWindow();
}
else if(state == PlayModeStateChange.ExitingPlayMode)
{
CloseWindow();
}
}

? ? public static void ShowWindow()
{
// 獲取或創建自定義控制臺窗口實例
LogHelperWindow window = GetWindow<LogHelperWindow>(false,"Custom Console",true);
window.Show();
window.OnShow();
}

? ? public static void CloseWindow()
{
LogHelperWindow window = GetWindow<LogHelperWindow>(false, "Custom Console", true);
window.OnRelease();
}
private void OnShow()
{
isShow = true;
}
private void OnRelease()
{
isShow = false;
isInited = false;
}

? ? // 繪制窗口內容
private void OnGUI()
{
if (!Application.isPlaying || !isShow) return;

? ? ? ? if(!isInited)
{
isAllSelected = false;
isInited = true;
errorIcon = EditorGUIUtility.Load("icons/d_console.erroricon.sml.png") as Texture2D;
if (errorIcon == null)
{
errorIcon = new Texture2D(16, 16);
Color[] pixels = new Color[16 * 16];
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = Color.red; // 紅色感嘆號
}
errorIcon.SetPixels(pixels);
errorIcon.Apply();
}

customLogStyle = new GUIStyle(EditorStyles.label);
// 設置選中時的背景顏色
Texture2D selectedBackground = new Texture2D(1, 1);
selectedBackground.SetPixel(0, 0, Color.blue); // 這里將選中背景顏色設為紅色
selectedBackground.Apply();
//customLogStyle.normal.background = selectedBackground;
customLogStyle.onFocused.background = selectedBackground;

? ? ? ? ? ? ReadTagConfig();
}


// --- 整體橫向布局 ----
EditorGUILayout.BeginHorizontal(GUILayout.ExpandHeight(true));

? ? ? ? // ----------- 左側:日志滾動區(自動填充剩余空間) ------------
scrollPosition = EditorGUILayout.BeginScrollView(
scrollPosition,
GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)
);
{

var logs = LogHelper.Instance.logs;
foreach (var item in LogHelper.Instance.fileModule.Values)
{
if (!allTags.ContainsKey(item))
allTags.Add(item, new ShowControlInfo());
}
Color consoleColor = GUI.color;
foreach (var log in logs)
{
var tagInfo = allTags[log.moduleName];
if (!tagInfo.isShowLog) continue;

? ? ? ? ? ? ? ? GUI.color = tagInfo.Color;
EditorGUILayout.BeginVertical("box");
string content = "";
if (tagInfo.showDebugTrace)
{
content = $"{log.time}{log.condition}\n{log.stacktrace}";
}
else
{
content = $"{log.time}{log.condition}";
}

? ? ? ? ? ? ? ? if(log.logType == LogHelper.Log._LogType.Error)
{
EditorGUILayout.BeginHorizontal();
{
GUILayout.Label(new GUIContent(errorIcon), GUILayout.Width(16), GUILayout.Height(16)); // 顯示圖標

? ? ? ? ? ? ? ? ? ? ? ? var height = Mathf.Max(20, GUI.skin.label.CalcHeight(new GUIContent(content), position.width - 240)); //留出右側寬度
EditorGUILayout.SelectableLabel(content, customLogStyle, GUILayout.Height(height));
EditorGUILayout.EndHorizontal();
}

}
else
{
var height = Mathf.Max(20, GUI.skin.label.CalcHeight(new GUIContent(content), position.width - 240)); //留出右側寬度
EditorGUILayout.SelectableLabel(content, customLogStyle, GUILayout.Height(height));
}


EditorGUILayout.EndVertical();
}
GUI.color = consoleColor;
EditorGUILayout.EndScrollView();
}

? ? ? ? // ----------- 右側:標簽篩選區(固定寬度,豎直鋪滿) ------------
EditorGUILayout.BeginVertical("box", GUILayout.Width(400), GUILayout.ExpandHeight(true));
{
EditorGUILayout.LabelField("標簽篩選", EditorStyles.boldLabel);

? ? ? ? ? ? // 標簽滾動部分-豎直鋪滿
tagScrollPosition = EditorGUILayout.BeginScrollView(tagScrollPosition, GUILayout.ExpandHeight(true));
{
foreach (var tag in allTags)
{
GUILayout.BeginHorizontal();
{
bool newValue = GUILayout.Toggle(tag.Value.isShowLog,tag.Key);
if (newValue != tag.Value.isShowLog)
allTags[tag.Key].isShowLog = newValue;

? ? ? ? ? ? ? ? ? ? ? ? GUILayout.Space(5);

? ? ? ? ? ? ? ? ? ? ? ? bool showTrace = GUILayout.Toggle(tag.Value.showDebugTrace,"堆棧" );
if (showTrace != tag.Value.showDebugTrace)
allTags[tag.Key].showDebugTrace = showTrace;
GUILayout.Space(5);
GUILayout.Label("顏色", GUILayout.Width(40));
Color newColor = EditorGUILayout.ColorField(tag.Value.Color, GUILayout.Width(60));
if (newColor != tag.Value.Color)
tag.Value.Color = newColor;

? ? ? ? ? ? ? ? ? ? ? ? GUILayout.EndHorizontal();
}

}
}
EditorGUILayout.EndScrollView();

? ? ? ? ? ? // 全選/取消全選
EditorGUILayout.Space();
if (GUILayout.Button(isAllSelected ? "取消全選" : "全選"))
{
isAllSelected = !isAllSelected;
var keys = new List<string>(allTags.Keys);
foreach (var key in keys)
allTags[key].isShowLog = isAllSelected;
}
}
EditorGUILayout.EndVertical();

? ? ? ? // --- End橫向大布局
EditorGUILayout.EndHorizontal();

? ? ? ? // ------ 下方操作按鈕區(單獨一行) ------
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("save tag"))
SaveLogTag();
if (GUILayout.Button("copy log"))
CopyLog();
if (GUILayout.Button("Clear Logs"))
ClearLogs();
EditorGUILayout.EndHorizontal();
}
private void ReadTagConfig()
{
allTags?.Clear();
var path = tagPath;
path = CommonUtils.GetLinuxPath(path);
var content = File.ReadAllText(path);
allTags = JsonConvert.DeserializeObject<Dictionary<string, ShowControlInfo>>(content);
}
void CopyLog()
{
StringBuilder sb = new();
var logs = LogHelper.Instance.logs;
foreach(var log in logs)
{
if(allTags[log.moduleName].isShowLog)
{
sb.AppendLine(log.ToString());
}
}

? ? ? ? GUIUtility.systemCopyBuffer = sb.ToString();

? ? }

? ? private string tagPath = System.Environment.CurrentDirectory + "/environment/log.json";

private void SaveLogTag()
{
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented, // 格式化輸出,使生成的 JSON 有縮進
};
var json = JsonConvert.SerializeObject(allTags, settings);
var path = tagPath;
path = CommonUtils.GetLinuxPath(path);
EasyUseEditorFuns.CreateDir(path);
File.WriteAllText(path, json);
this.ShowNotification(new GUIContent($"保存日志tag成功{path}"));
} ? ?


// 清空日志的方法
private void ClearLogs()
{
LogHelper.Instance.logs?.Clear();
}
}
#endif

提示:

不一定所有人都用得上這個功能,在接入的時候需要考慮開關,比如EditorPrefs 存儲一個開關的key。例如參考以下截圖:

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/91570.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/91570.shtml
英文地址,請注明出處:http://en.pswp.cn/web/91570.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenGL Camera

一. lookAt函數的參數含義glm::mat4 view glm::lookAt(cameraPos, // 相機在世界坐標系中的位置&#xff08;任意值&#xff09;cameraPos cameraFront, // 相機看向的目標點&#xff08;位置朝向&#xff09;cameraUp // 相機的"上方向"&#xff08;通…

Android RTMP推送|輕量級RTSP服務同屏實踐:屏幕+音頻+錄像全鏈路落地方案

一、背景&#xff1a;從“移動終端”到“遠程協作節點”&#xff0c;同屏音頻錄像為何成剛需&#xff1f; 在數字化辦公、智慧醫療與遠程教育等快速發展的推動下&#xff0c;手機作為隨身終端&#xff0c;已不再只是“內容接收者”&#xff0c;而逐步成為遠程信息發布與可視化…

NLP 和 LLM 區別、對比 和關系

理解自然語言處理(NLP)和大語言模型(LLM)的區別、對比和關系對于把握現代人工智能的發展非常重要。以下是清晰的分析: 核心定義 NLP (Natural Language Processing - 自然語言處理): 是什么: 一個廣闊的計算機科學和人工智能子領域,致力于讓計算機能夠理解、解釋、操作…

Altium 移除在原理圖之外的元器件

Altium新手&#xff0c;最近在畫原理圖的時候&#xff0c;遇見了這種不小心拖到界面外的元器件&#xff0c;發現拖不回來了了&#xff0c;查閱了一下&#xff0c;總結在這里 官方推薦的方法----------------使用“SCH List”面板刪除 鏈接&#xff1a;如何移除在原理圖之外的元…

【Linux我做主】細說環境變量

Linux環境變量Linux環境變量github地址前言1. 基本概念環境變量的本質2. 認識常見的環境變量PATH查看PATH修改PATHHOMESHELL其他常見環境變量PWD與OLDPWDLOGNAME與USERSSH_TTY由環境變量理解權限使用系統調用獲取環境變量理解權限3. 總結什么是環境變量3. 命令行參數和環境變量…

leecode-15 三數之和

我的解法&#xff08;不是完全解309/314&#xff09;我的思路是定義一個left和一個right&#xff0c;然后在向集合里去查詢&#xff0c;看看有沒有除了nums[left]&#xff0c;和nums[right]的第三個元素&#xff0c;把這個問題轉換為一個遍歷查找問題 利用List.contains()方法來…

精通分類:解析Scikit-learn中的KNN、樸素貝葉斯與決策樹(含隨機森林)

在機器學習領域&#xff0c;分類任務占據核心地位。Scikit-learn作為Python的機器學習利器&#xff0c;提供了豐富高效的分類算法。現在進行初步探討三種經典算法&#xff1a;K最近鄰&#xff08;KNN&#xff09;、樸素貝葉斯&#xff08;Naive Bayes&#xff09;和決策樹&…

Galaxea機器人由星海圖人工智能科技有限公司研發的高性能仿人形機器人

Galaxea機器人是由星海圖人工智能科技有限公司研發的高性能仿人形機器人&#xff0c;具有多種型號&#xff0c;包括Galaxea R1和Galaxea R1 Pro。以下是關于Galaxea機器人的詳細介紹&#xff1a; GitHub官網 產品特點 高自由度設計&#xff1a;Galaxea R1是一款全尺寸仿人型機…

python基礎:用戶輸入和 while 循環

一、input() 函數的工作原理input() 函數讓程序暫停運行&#xff0c;等待用戶輸入一些文本。獲取用戶輸入后&#xff0c;Python 將其賦給一個變量&#xff0c;以便使用。message input("Tell me something, and I will repeat it back to you: ") print(message) 結…

開啟云服務器mysql本地連接(is not allowed to connect to this mysql server)

is not allowed to connect tothis mmysql server 阿里云上安裝的mysql&#xff0c;發現用本地電腦的navicat鏈接不上。通過了解知道了原因&#xff0c;小二在此寫了一篇&#xff0c;省的以后自己在碰到。 錯誤如圖。 aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTU4MTU1My8…

電腦的時間同步電池壞掉了,每次開機都要調整時間

電腦的時間同步的電池沒電了&#xff0c;每天開機時間都不對&#xff0c;要打開時間同步按鈕來設置時間解決方案1.找到這個設置并打開&#xff0c;實際上&#xff0c;要打開這個界面&#xff0c;時間才會同步&#xff0c;可能是我的電腦原因&#xff0c;所以我沒辦法打開這個就…

mycat在游戲中的使用場景(郵件表,mysql集群,而不是郵件服)

其實還有一種是SharingJDBC&#xff0c;而且之間在B站的同學也是說用這個&#xff0c;但是我們目前項目郵件中用的卻是: mycat&#xff0c;為什么呢&#xff1f;mycat其實是中間件&#xff0c;是需要獨立部署的&#xff0c;是數據庫服務器這塊的代理&#xff0c;在應用層的話很…

TP-Link Archer C50路由器曝安全漏洞,硬編碼DES密鑰可解密敏感配置

漏洞概述CERT協調中心&#xff08;CERT/CC&#xff09;發布安全公告&#xff0c;披露TP-Link Archer C50路由器存在編號為CVE-2025-6982的漏洞。該漏洞源于路由器固件中使用了硬編碼的DES&#xff08;數據加密標準&#xff09;解密密鑰&#xff0c;這一設計缺陷使大量家庭和小型…

番茄項目3:完成了項目的數據庫設計

今天抽了會時間設計了下表結構&#xff0c;并選定的使用的數據庫&#xff0c;經過調查&#xff0c;我決定還是把數據存在數據庫中&#xff0c;因為寫SQL是我擅長的。 最終我選擇使用python自帶的sqlite來實現這個工具&#xff0c;具體建表語句如下&#xff1a; 基于AI生成&…

11、read_object_model_3d 讀取點云

個人理解 read_object_model_3d 這個Halcon算子中的xyz_map_width這個參數設置的目的就是,把讀取的點云數據中每一個點的XYZ坐標,生成一個對應的二維圖像,其中圖像中的坐標值就對應每一個點的索引坐標,而圖像中的灰度值就對應xyz坐標??(因為得到的是三通道圖像)!!并且根…

【人工智能-17】機器學習:KNN算法、模型選擇和調優、樸素貝葉斯分類

上一期【人工智能-16】機器學習&#xff1a;概念、工具介紹、數據集、特征工程 文章目錄一 、KNN算法1. 應用理由2. 原理核心&#xff1a;距離度量 多數投票/平均3. 優點和缺點二、模型選擇和調優1.使用理由2.原理核心&#xff1a;數據劃分與性能平均3.超參數搜索4. 應用場景總…

關于繼承的一些知識(C++)

當我們想要設計幾個類分別記錄老師&#xff0c;學生的個人信息時會發現&#xff0c;像姓名、地址、身份證號、電話等等記錄基礎信息的成員變量是都具有的&#xff0c;重復定義會顯得冗余&#xff0c;但同時它們兩者又具有不同的記錄信息的成員變量&#xff0c;像學生需要記錄學…

永磁同步電機無速度算法--脈振方波注入法

一、原理介紹為了實現表貼式永磁電機的低速運行&#xff0c;研究一種基于高頻方波測試信號注入的無位置零低速傳感器控制策略。選取注入到觀測直軸的脈振高頻方波信號&#xff0c; 該信號注入方案可以有效避免旋轉信號注入法在轉子交軸分量引起轉矩脈動&#xff0c; 提高系統的…

VSCode Python 與 C++ 聯合調試配置指南

VSCode Python 與 C 聯合調試配置指南 為了實現 Python 與 C 的聯合調試&#xff0c;需要正確配置 launch.json 文件&#xff0c;具體配置如下&#xff1a; {// IntelliSense 支持查看屬性描述// 更多信息請參考: https://go.microsoft.com/fwlink/?linkid830387"version…

stm32和freeRtos的can總線

STM32內置bxCAN外設&#xff08;CAN控制器、拓展CAN&#xff09;&#xff0c;支持CAN2.0A和2.0B(全部的CAN)&#xff0c;可以自動發送CAN報文和按照過濾器自動接收指定CAN報文&#xff0c;程序只需處理報文數據而無需關注總線的電平細節波特率最高可達1兆位/秒&#xff0c;高速…