
文章目錄
- 一、前言
- 二、常規日志列印
- 1、列印Hello World
- 2、列印任意型別的資料
- 3、context引數干嘛的
- 4、格式化輸出
- 三、彩色日志列印
- 四、日志存盤與上傳
- 1、列印日志事件
- 2、寫日志到本地檔案
- 3、日志上傳到服務器
- 五、日志開關
- 六、日志雙擊溯源問題
- 方法一、GameLogger編譯為dll
- 方法二、反射攔截,自主跳轉
- 七、專案實戰
- 1、界面制作
- 2、C#代碼
- 2.1、GameLogger.cs代碼
- 2.2、LogUploader.cs代碼
- 2.3 Main.cs代碼
- 3、掛Main腳本
- 4、Web服務器
- 4.1、小皮,Web服務器
- 4.2、PHP腳本:upload_log.php
- 5、運行測驗
- 6、工程原始碼
- 八、完畢
一、前言
嗨,大家好,我是新發,
有鐵粉私信我問我能否寫一篇Unity日志列印相關的文章,

今天,我就來好好講講~
二、常規日志列印
1、列印Hello World
相信很多剛學Unity的同學最早寫的一句代碼就是
Debug.Log("Hello World");
我們可以在Console視窗中看到輸出的日志,視窗下方是對應的呼叫堆疊,
注:
呼叫堆疊可以很好的幫助我們定位問題,特別是報錯的Error日志,

我們還可以輸出警告、錯誤日志,例:
Debug.Log("This is a log message.");
Debug.LogWarning("This is a warning message!");
Debug.LogError("This is an error message!");
如下:

注:這里特別說一下,
Console視窗有個Error Pause按鈕,意思是如果輸出了Error日志,則暫停運行,有時候策劃會跑過來說他的Unity運行游戲的時候突然卡死了,感覺就像發現了什么驚天大BUG,其實是他點了Error Pause,然后游戲中輸出了一句Error日志,
2、列印任意型別的資料
事實上,Debug.Log的引數是object(即System.Object),
// Debug.cs
public static void Log(object message);
我們知道,在C#里面,所有型別都是繼承System.Object的,也就是說,我們可以傳任意型別的引數給Debug.Log,
現在,我來考考你,下面這行代碼會不會報錯?
Debug.Log(null);
答案是不會報錯,它會輸出Null,
現在,我們自定義一個類,比如:
public class TestClass
{
public int a;
public string b;
public bool c;
public TestClass(int a, string b, bool c)
{
this.a = a;
this.b = b;
this.c = c;
}
}
執行下面的代碼,它會輸出什么呢?
Debug.Log(new TestClass(1, "HaHa", true));
答案是:
TestClass
事實上,它是先執行了物件的ToString()方法,然后再輸出日志的,我們override(重寫)類的ToString()方法,就可以自定義輸出啦,比如:
public class TestClass
{
public int a;
public string b;
public bool c;
public TestClass(int a, string b, bool c)
{
this.a = a;
this.b = b;
this.c = c;
}
// 重寫ToString方法
public override string ToString()
{
return string.Format("a:{0}\nb:{1}\nc:{2}", a, b, c);
}
}
再執行下面這行代碼,
Debug.Log(new TestClass(1, "HaHa", true));
它輸出的就是
a:1
b:HaHa
c:True
3、context引數干嘛的
如果你看Debug類的原始碼,就會發現,它有一個接收兩個引數的Debug.Log方法,
// Debug.cs
public static void Log(object message, Object context);
這第二個引數context是干嘛用的呢?
我們來做下實驗,
GameObject go = new GameObject("go");
Debug.Log("Test", go);
效果如下,發現沒有,它可以幫我們定位到物體的實體,

如果你的物體是一個還沒實體化的預設的參考,則它會直接定位到Project視圖中的資源,我們來做下實驗,
NewBehaviourScript.cs腳本代碼如下:
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
public GameObject cubePrefab;
void Start()
{
Debug.Log("Test", cubePrefab);
}
}
掛到Main Camera上,把Cube.prefab預設拖給腳本的cubePrefab成員,如下:

運行,測驗效果如下:

4、格式化輸出
有時候我們要進行格式化輸出,可以使用string.Format進行格式化后再呼叫Debug.Log,例:
int a = 100;
float b = 0.6f;
Debug.Log(string.Format("a is: {0}, b is {1}", a, b));
你也可以直接使用Debug.LogFormat,例:
int a = 100;
float b = 0.6f;
Debug.LogFormat("a is: {0}, b is {1}", a, b);
三、彩色日志列印
我們上面列印出來的日志都是默認的顏色(白色),事實上,我們可以列印出彩色的日志,
格式<color=#rbg顏色值>xxx</color>,例:
Debug.LogFormat("This is <color=#ff0000>{0}</color>", "red");
Debug.LogFormat("This is <color=#00ff00>{0}</color>", "green");
Debug.LogFormat("This is <color=#0000ff>{0}</color>", "blue");
Debug.LogFormat("This is <color=yellow>{0}</color>", "yellow");
效果如下:

注:建議約定好少數幾個顏色即可,不然五顏六色的看的眼花
@_@
四、日志存盤與上傳
實際專案中,我們一般是需要把日志寫成檔案,方便出錯時通過日志來定位問題,Unity提供了一個事件:Application.logMessageReceived,方便我們來監聽日志列印,這樣我們就可以把日志的文本內容寫到檔案里存起來啦~
1、列印日志事件
我們一般在游戲啟動的入口腳本的Awake函式中去監聽Application.logMessageReceived事件,如下:
// 游戲啟動的入口腳本
void Awake()
{
// 監聽日志回呼
Application.logMessageReceived += OnLogCallBack;
}
/// <summary>
/// 列印日志回呼
/// </summary>
/// <param name="condition">日志文本</param>
/// <param name="stackTrace">呼叫堆疊</param>
/// <param name="type">日志型別</param>
private void OnLogCallBack(string condition, string stackTrace, LogType type)
{
// TODO 寫日志到本地檔案
}
2、寫日志到本地檔案
Unity提供了一個可讀寫的路徑給我們訪問:Application.persistentDataPath,我們可以把日志檔案存到這個路徑下,
注:
Application.persistentDataPath在不同平臺下的路徑:
Windows:C:/Users/用戶名/AppData/LocalLow/CompanyName/ProductName
Android:Android/data/包名/files
Mac:/Users/用戶名/Library/Caches/CompanyName/ProductName
iOS:/var/mobile/Containers/Data/Application/APP名稱/Documents
需要注意,iOS的需要越獄并且使用Filza軟體才能查看檔案路徑哦

例:
using System.IO;
using System.Text;
using UnityEngine;
public class Main: MonoBehaviour
{
// 使用StringBuilder來優化字串的重復構造
StringBuilder m_logStr = new StringBuilder();
// 日志檔案存盤位置
string m_logFileSavePath;
void Awake()
{
// 當前時間
var t = System.DateTime.Now.ToString("yyyyMMddhhmmss");
m_logFileSavePath = string.Format("{0}/output_{1}.log", Application.persistentDataPath, t);
Debug.Log(m_logFileSavePath);
Application.logMessageReceived += OnLogCallBack;
Debug.Log("日志存盤測驗");
}
/// <summary>
/// 列印日志回呼
/// </summary>
/// <param name="condition">日志文本</param>
/// <param name="stackTrace">呼叫堆疊</param>
/// <param name="type">日志型別</param>
private void OnLogCallBack(string condition, string stackTrace, LogType type)
{
m_logStr.Append(condition);
m_logStr.Append("\n");
m_logStr.Append(stackTrace);
m_logStr.Append("\n");
if (m_logStr.Length <= 0) return;
if (!File.Exists(m_logFileSavePath))
{
var fs = File.Create(m_logFileSavePath);
fs.Close();
}
using (var sw = File.AppendText(m_logFileSavePath))
{
sw.WriteLine(m_logStr.ToString());
}
m_logStr.Remove(0, m_logStr.Length);
}
}
我們可以在Application.persistentDataPath路徑下看到日志檔案,

3、日志上傳到服務器
實際專案中,我們可能需要把日志上傳到服務端,方便進行查詢定位,
上傳檔案我們可以使用UnityWebRequest來處理,這里需要注意,我們的日志檔案可能很小也可能很大,正常情況下都比較小,但是有時候報錯了是會回圈列印日志的,導致日志檔案特別大,所以我們要考慮到大檔案讀取的情況,否則讀取日志檔案時會很卡,建議使用位元組流讀取,

例:
// 讀取日志檔案的位元組流
byte[] ReadLogFile()
{
byte[] data = null;
using(FileStream fs = File.OpenRead("你的日志檔案路徑"))
{
int index = 0;
long len = fs.Length;
data = new byte[len];
// 根據你的需求進行限流讀取
int offset = data.Length > 1024 ? 1024 : data.Length;
while (index < len)
{
int readByteCnt = fs.Read(data, index, offset);
index += readByteCnt;
long leftByteCnt = len - index;
offset = leftByteCnt > offset ? offset : (int)leftByteCnt;
}
Debug.Log ("讀取完畢");
}
return data;
}
// 將日志位元組流上傳到web服務器
IEnumerator HttpPost(string url, byte[] data)
{
WWWForm form = new WWWForm();
// 塞入描述欄位,欄位名與服務端約定好
form.AddField("desc", "test upload log file");
// 塞入日志位元組流欄位,欄位名與服務端約定好
form.AddBinaryData("logfile", data, "test_log.txt", "application/x-gzip");
// 使用UnityWebRequest
UnityWebRequest request = UnityWebRequest.Post(url, form);
var result = request.SendWebRequest();
while (!result.isDone)
{
yield return null;
//Debug.Log ("上傳進度: " + request.uploadProgress);
}
if (!string.IsNullOrEmpty(request.error))
{
GameLogger.LogError(request.error);
}
else
{
GameLogger.Log("日志上傳完畢, 服務器回傳資訊: " + request.downloadHandler.text);
}
request.Dispose();
}
呼叫:
byte[] data = ReadLogFile();
StartCoroutine(HttpPost("http://你的web服務器", data));
五、日志開關
實際專案中,我們可能需要做日志開關,比如開發階段日志開啟,正式發布后則關閉日志,
Unity并沒有給我們提供一個日志開關的功能,那我們就自己封裝一下吧~
例:
using UnityEngine;
public class GameLogger
{
// 普通除錯日志開關
public static bool s_debugLogEnable = true;
// 警告日志開關
public static bool s_warningLogEnable = true;
// 錯誤日志開關
public static bool s_errorLogEnable = true;
public static void Log(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(message, context);
}
public static void LogWarning(object message, Object context = null)
{
if (!s_warningLogEnable) return;
Debug.LogWarning(message, context);
}
public static void LogError(object message, Object context)
{
if (!s_errorLogEnable) return;
Debug.LogError(message, context);
}
}
我們所有使用Debug列印日志的地方,都是改用GameLogger來列印,這樣就可以統一通過GameLogger來開關日志的列印了~
不過,這里會有一個問題,就是我們在Console日志視窗雙擊日志的時候,它只會跳轉到GameLogger里,而不是跳轉到我們呼叫GameLogger的地方,
比如我們在Test腳本中呼叫GameLogger.Log,如下:
// Test.cs
using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
GameLogger.Log("哈哈哈哈哈");
}
}
看,它是跳到GameLogger里,而不是跳到我們的Test腳本了,這個是顯然的,但我們能不能讓它跳到Test腳本里呢?

下面我就來給大家表演一下!請往下看~
六、日志雙擊溯源問題
要解決上面的問題,有兩種方法:
方法一:把GameLogger編譯成dll放在工程中,把原始碼刪掉;
方法二:通過反射分析日志視窗的堆疊,攔截Unity日志視窗的打開檔案事件,跳轉到我們指定的代碼行處,
下面我來說下具體操作,
方法一、GameLogger編譯為dll
事實上,我們寫的C#代碼都會被Unity編譯成dll放在工程目錄的Library/ScriptAssemblies目錄中,默認是Assembly-CSharp.dll,

我們可以使用ILSpy.exe反編譯一下它,
注:
ILSpy反編譯工具可以從GitHub下載:https://github.com/icsharpcode/ILSpy

不過我們看到,這個dll包含了其他的C#代碼,我們能不能專門只為GameLogger生成一個dll呢?可以滴,只需要把GameLogger.cs單獨放在一個子目錄中,并創建一個Assembly Definition,如下,

把Assembly Definition重命名為GameLogger,如下,

我們再回到Library/ScriptAssemblies目錄中,就可以看到生成了一個GameLogger.dll啦,

把它剪切到Unity工程的Plugins目錄中,把我們的GameLogger.cs腳本刪掉或是移動到工程外備份(Assembly Definition檔案也刪掉),如下:

這樣,就大功告成了,我們測驗一下日志雙擊溯源,如下,可以看到,現在可以正常跳轉到我們想要的地方了,

方法二、反射攔截,自主跳轉
上面我們是把GameLogger.cs編譯成dll然后放回工程中,這種方法比較簡單,下面這個方法稍微比較有難度,看不懂的同學不要緊,就當做漲知識吧~
我們上面看到,在Console日志視窗雙擊日志可以跳到對應的代碼行處,這個邏輯是Unity編輯器幫我們做的,我們能不能找到Console日志視窗本身的代碼,看看它究竟是怎么做的呢?
事實上,我們的Unity編輯器的各個視圖視窗,其實也是使用C#寫出來的,我們可以在Unity引擎的安裝路徑的Editor/Data/Managed目錄中找到UnityEditor.dll,

里面就包含了我們Unity編輯器的各個視圖的代碼,我們可以使用ILSpy.exe反編譯它,找到ConsoleWindow類,如下,

很快,我就找到了當前激活狀態下的日志的代碼,

也就是說,我只要拿到這個m_ActiveText的值就可以知道你雙擊的日志是什么了,
封裝一下介面:
#if UNITY_EDITOR
/// <summary>
/// 獲取當前日志視窗選中的日志的堆疊資訊
/// </summary>
/// <returns></returns>
static string GetStackTrace()
{
// 通過反射獲取ConsoleWindow類
var ConsoleWindowType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.ConsoleWindow");
// 獲取視窗實體
var fieldInfo = ConsoleWindowType.GetField("ms_ConsoleWindow",
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
var consoleInstance = fieldInfo.GetValue(null);
if (consoleInstance != null)
{
if ((object)UnityEditor.EditorWindow.focusedWindow == consoleInstance)
{
// 獲取m_ActiveText成員
fieldInfo = ConsoleWindowType.GetField("m_ActiveText",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic);
// 獲取m_ActiveText的值
string activeText = fieldInfo.GetValue(consoleInstance).ToString();
return activeText;
}
}
return null;
}
#endif
那么接下來就是我怎么攔截打開Unity打開代碼的事件并重新制定跳轉的位置呢?
Unity提供了一個OnOpenAssetAttribute回呼,
詳細可以參見Unity官方手冊:https://docs.unity3d.com/cn/2020.2/ScriptReference/Callbacks.OnOpenAssetAttribute.html

這樣我們就可以通過它來攔截打開代碼的事件了,(return true表示攔截,return false則不攔截)
#if UNITY_EDITOR
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
static bool OnOpenAsset(int instanceID, int line)
{
// TODO 根據日志堆疊分析要打開的代碼,return true;
return false;
}
#endif
現在關鍵是如何跳轉到具體代碼的某一行,我找到了這個介面,

萬事俱備,封裝一下介面:
#if UNITY_EDITOR
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
static bool OnOpenAsset(int instanceID, int line)
{
string stackTrace = GetStackTrace();
if (!string.IsNullOrEmpty(stackTrace) && stackTrace.Contains("GameLogger:Log"))
{
// 使用正則運算式匹配at的哪個腳本的哪一行
var matches = System.Text.RegularExpressions.Regex.Match(stackTrace, @"\(at (.+)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
string pathLine = "";
while (matches.Success)
{
pathLine = matches.Groups[1].Value;
if (!pathLine.Contains("GameLogger.cs"))
{
int splitIndex = pathLine.LastIndexOf(":");
// 腳本路徑
string path = pathLine.Substring(0, splitIndex);
// 行號
line = System.Convert.ToInt32(pathLine.Substring(splitIndex + 1));
string fullPath = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets"));
fullPath = fullPath + path;
// 跳轉到目標代碼的特定行
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(fullPath.Replace('/', '\\'), line);
break;
}
matches = matches.NextMatch();
}
return true;
}
return false;
}
#endif
整合一下,最終GameLogger.cs代碼如下:
using UnityEngine;
public class GameLogger
{
// 普通除錯日志開關
public static bool s_debugLogEnable = true;
// 警告日志開關
public static bool s_warningLogEnable = true;
// 錯誤日志開關
public static bool s_errorLogEnable = true;
public static void Log(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(message, context);
}
public static void LogWarning(object message, Object context = null)
{
if (!s_warningLogEnable) return;
Debug.LogWarning(message, context);
}
public static void LogError(object message, Object context)
{
if (!s_warningLogEnable) return;
Debug.LogError(message, context);
}
#if UNITY_EDITOR
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
static bool OnOpenAsset(int instanceID, int line)
{
string stackTrace = GetStackTrace();
if (!string.IsNullOrEmpty(stackTrace) && stackTrace.Contains("GameLogger:Log"))
{
// 使用正則運算式匹配at的哪個腳本的哪一行
var matches = System.Text.RegularExpressions.Regex.Match(stackTrace, @"\(at (.+)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
string pathLine = "";
while (matches.Success)
{
pathLine = matches.Groups[1].Value;
if (!pathLine.Contains("GameLogger.cs"))
{
int splitIndex = pathLine.LastIndexOf(":");
// 腳本路徑
string path = pathLine.Substring(0, splitIndex);
// 行號
line = System.Convert.ToInt32(pathLine.Substring(splitIndex + 1));
string fullPath = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets"));
fullPath = fullPath + path;
// 跳轉到目標代碼的特定行
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(fullPath.Replace('/', '\\'), line);
break;
}
matches = matches.NextMatch();
}
return true;
}
return false;
}
/// <summary>
/// 獲取當前日志視窗選中的日志的堆疊資訊
/// </summary>
/// <returns></returns>
static string GetStackTrace()
{
// 通過反射獲取ConsoleWindow類
var ConsoleWindowType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.ConsoleWindow");
// 獲取視窗實體
var fieldInfo = ConsoleWindowType.GetField("ms_ConsoleWindow",
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
var consoleInstance = fieldInfo.GetValue(null);
if (consoleInstance != null)
{
if ((object)UnityEditor.EditorWindow.focusedWindow == consoleInstance)
{
// 獲取m_ActiveText成員
fieldInfo = ConsoleWindowType.GetField("m_ActiveText",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic);
// 獲取m_ActiveText的值
string activeText = fieldInfo.GetValue(consoleInstance).ToString();
return activeText;
}
}
return null;
}
#endif
}
我們測驗一下日志雙擊溯源,如下,可以看到,現在可以正常跳轉到我們想要的地方了,

七、專案實戰
寫到這里了,那就順手做個實戰專案吧~
1、界面制作
使用UGUI簡單做下界面,

保存為MainPanel.prefab預設,

2、C#代碼
三個腳本,如下

GameLogger.cs:封裝Debug日志列印,實作日志開關控制、彩色日志列印、日志檔案存盤等功能;
LogUploader.cs:實作日志上傳到服務器的功能;
Main.cs:程式入口腳本,同時實作UI界面互動,
原理我上文都有講,這里就不贅述代碼細節了,可以看注釋,我都寫得比較詳細,
2.1、GameLogger.cs代碼
/// <summary>
/// 封裝Debug日志列印,實作日志開關控制、彩色日志列印、日志檔案存盤等功能
/// 作者:林新發 博客:https://blog.csdn.net/linxinfa
/// </summary>
using UnityEngine;
using System.Text;
using System.IO;
public class GameLogger
{
// 普通除錯日志開關
public static bool s_debugLogEnable = true;
// 警告日志開關
public static bool s_warningLogEnable = true;
// 錯誤日志開關
public static bool s_errorLogEnable = true;
// 使用StringBuilder來優化字串的重復構造
private static StringBuilder s_logStr = new StringBuilder();
// 日志檔案存盤位置
private static string s_logFileSavePath;
/// <summary>
/// 初始化,在游戲啟動的入口腳本的Awake函式中呼叫GameLogger.Init
/// </summary>
public static void Init()
{
// 日期
var t = System.DateTime.Now.ToString("yyyyMMddhhmmss");
s_logFileSavePath = string.Format("{0}/output_{1}.log", Application.persistentDataPath, t);
Application.logMessageReceived += OnLogCallBack;
}
/// <summary>
/// 列印日志回呼
/// </summary>
/// <param name="condition">日志文本</param>
/// <param name="stackTrace">呼叫堆疊</param>
/// <param name="type">日志型別</param>
private static void OnLogCallBack(string condition, string stackTrace, LogType type)
{
s_logStr.Append(condition);
s_logStr.Append("\n");
s_logStr.Append(stackTrace);
s_logStr.Append("\n");
if (s_logStr.Length <= 0) return;
if (!File.Exists(s_logFileSavePath))
{
var fs = File.Create(s_logFileSavePath);
fs.Close();
}
using (var sw = File.AppendText(s_logFileSavePath))
{
sw.WriteLine(s_logStr.ToString());
}
s_logStr.Remove(0, s_logStr.Length);
}
public static void UploadLog(string desc)
{
LogUploader.StartUploadLog(s_logFileSavePath, desc);
}
/// <summary>
/// 普通除錯日志
/// </summary>
public static void Log(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(message, context);
}
/// <summary>
/// 格式化列印日志
/// </summary>
/// <param name="format">例:"a is {0}, b is {1}"</param>
/// <param name="args">可變引數,根據format的格式傳入匹配的引數,例:a, b</param>
public static void LogFormat(string format, params object[] args)
{
if (!s_debugLogEnable) return;
Debug.LogFormat(format, args);
}
/// <summary>
/// 帶顏色的日志
/// </summary>
/// <param name="message"></param>
/// <param name="color">顏色值,例:green, yellow,#ff0000</param>
/// <param name="context">背景關系物件</param>
public static void LogWithColor(object message, string color, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(FmtColor(color, message), context);
}
/// <summary>
/// 紅色日志
/// </summary>
public static void LogRed(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(FmtColor("red", message), context);
}
/// <summary>
/// 綠色日志
/// </summary>
public static void LogGreen(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(FmtColor("green", message), context);
}
/// <summary>
/// 黃色日志
/// </summary>
public static void LogYellow(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(FmtColor("yellow", message), context);
}
/// <summary>
/// 青藍色日志
/// </summary>
public static void LogCyan(object message, Object context = null)
{
if (!s_debugLogEnable) return;
Debug.Log(FmtColor("#00ffff", message), context);
}
/// <summary>
/// 帶顏色的格式化日志列印
/// </summary>
public static void LogFormatWithColor(string format, string color, params object[] args)
{
if (!s_debugLogEnable) return;
Debug.LogFormat((string)FmtColor(color, format), args);
}
/// <summary>
/// 警告日志
/// </summary>
public static void LogWarning(object message, Object context = null)
{
if (!s_warningLogEnable) return;
Debug.LogWarning(message, context);
}
/// <summary>
/// 錯誤日志
/// </summary>
public static void LogError(object message, Object context = null)
{
if (!s_errorLogEnable) return;
Debug.LogError(message, context);
}
/// <summary>
/// 格式化顏色日志
/// </summary>
private static object FmtColor(string color, object obj)
{
if (obj is string)
{
#if !UNITY_EDITOR
return obj;
#else
return FmtColor(color, (string)obj);
#endif
}
else
{
#if !UNITY_EDITOR
return obj;
#else
return string.Format("<color={0}>{1}</color>", color, obj);
#endif
}
}
/// <summary>
/// 格式化顏色日志
/// </summary>
private static object FmtColor(string color, string msg)
{
#if !UNITY_EDITOR
return msg;
#else
int p = msg.IndexOf('\n');
if (p >= 0) p = msg.IndexOf('\n', p + 1);// 可以同時顯示兩行
if (p < 0 || p >= msg.Length - 1) return string.Format("<color={0}>{1}</color>", color, msg);
if (p > 2 && msg[p - 1] == '\r') p--;
return string.Format("<color={0}>{1}</color>{2}", color, msg.Substring(0, p), msg.Substring(p));
#endif
}
#region 解決日志雙擊溯源問題
#if UNITY_EDITOR
[UnityEditor.Callbacks.OnOpenAssetAttribute(0)]
static bool OnOpenAsset(int instanceID, int line)
{
string stackTrace = GetStackTrace();
if (!string.IsNullOrEmpty(stackTrace) && stackTrace.Contains("GameLogger:Log"))
{
// 使用正則運算式匹配at的哪個腳本的哪一行
var matches = System.Text.RegularExpressions.Regex.Match(stackTrace, @"\(at (.+)\)",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
string pathLine = "";
while (matches.Success)
{
pathLine = matches.Groups[1].Value;
if (!pathLine.Contains("GameLogger.cs"))
{
int splitIndex = pathLine.LastIndexOf(":");
// 腳本路徑
string path = pathLine.Substring(0, splitIndex);
// 行號
line = System.Convert.ToInt32(pathLine.Substring(splitIndex + 1));
string fullPath = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Assets"));
fullPath = fullPath + path;
// 跳轉到目標代碼的特定行
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(fullPath.Replace('/', '\\'), line);
break;
}
matches = matches.NextMatch();
}
return true;
}
return false;
}
/// <summary>
/// 獲取當前日志視窗選中的日志的堆疊資訊
/// </summary>
/// <returns></returns>
static string GetStackTrace()
{
// 通過反射獲取ConsoleWindow類
var ConsoleWindowType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.ConsoleWindow");
// 獲取視窗實體
var fieldInfo = ConsoleWindowType.GetField("ms_ConsoleWindow",
System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic);
var consoleInstance = fieldInfo.GetValue(null);
if (consoleInstance != null)
{
if ((object)UnityEditor.EditorWindow.focusedWindow == consoleInstance)
{
// 獲取m_ActiveText成員
fieldInfo = ConsoleWindowType.GetField("m_ActiveText",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic);
// 獲取m_ActiveText的值
string activeText = fieldInfo.GetValue(consoleInstance).ToString();
return activeText;
}
}
return null;
}
#endif
#endregion 解決日志雙擊溯源問題
}
2.2、LogUploader.cs代碼
/// <summary>
/// 實作日志上傳到服務器的功能
/// 作者:林新發 博客:https://blog.csdn.net/linxinfa
/// </summary>
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class LogUploader : MonoBehaviour
{
private static string LOG_UPLOAD_URL = "http://127.0.0.1:7890/upload_log.php";
public static void StartUploadLog(string logFilePath, string desc)
{
var go = new GameObject("LogUploader");
var bhv = go.AddComponent<LogUploader>();
bhv.StartCoroutine(bhv.UploadLog(logFilePath, LOG_UPLOAD_URL, desc));
}
/// <summary>
/// 上報日志到服務端
/// </summary>
/// <param name="url">http介面</param>
/// <param name="desc">描述</param>
private IEnumerator UploadLog(string logFilePath, string url, string desc)
{
var fileName = Path.GetFileName(logFilePath);
var data = ReadLogFile(logFilePath);
WWWForm form = new WWWForm();
// 塞入描述欄位,欄位名與服務端約定好
form.AddField("desc", desc);
// 塞入日志位元組流欄位,欄位名與服務端約定好
form.AddBinaryData("logfile", data, fileName, "application/x-gzip");
// 使用UnityWebRequest
UnityWebRequest request = UnityWebRequest.Post(url, form);
var result = request.SendWebRequest();
while (!result.isDone)
{
yield return null;
//Debug.Log ("上傳進度: " + request.uploadProgress);
}
if (!string.IsNullOrEmpty(request.error))
{
GameLogger.LogError(request.error);
}
else
{
GameLogger.Log("日志上傳完畢, 服務器回傳資訊: " + request.downloadHandler.text);
}
request.Dispose();
}
private byte[] ReadLogFile(string logFilePath)
{
byte[] data = null;
using (FileStream fs = File.OpenRead(logFilePath))
{
int index = 0;
long len = fs.Length;
data = new byte[len];
// 根據你的需求進行限流讀取
int offset = data.Length > 1024 ? 1024 : data.Length;
while (index < len)
{
int readByteCnt = fs.Read(data, index, offset);
index += readByteCnt;
long leftByteCnt = len - index;
offset = leftByteCnt > offset ? offset : (int)leftByteCnt;
}
}
return data;
}
}
2.3 Main.cs代碼
using UnityEngine;
using UnityEngine.UI;
public class Main : MonoBehaviour
{
/// <summary>
/// 日志開關
/// </summary>
public Toggle logEnableTgl;
/// <summary>
/// 列印日志按鈕
/// </summary>
public Button logBtn;
/// <summary>
/// 上傳日志按鈕
/// </summary>
public Button uploadBtn;
/// <summary>
/// 日志文本Text
/// </summary>
public Text logText;
void Awake()
{
GameLogger.Init();
// 監聽日志,輸出到logText中
Application.logMessageReceived += (string condition, string stackTrace, LogType type) =>
{
switch(type)
{
case LogType.Log:
{
if (!GameLogger.s_debugLogEnable) return;
}
break;
case LogType.Warning:
{
if (!GameLogger.s_warningLogEnable) return;
}
break;
case LogType.Error:
{
if (!GameLogger.s_errorLogEnable) return;
}
break;
}
logText.text += condition + "\n";
};
}
private void Start()
{
logText.text = "";
uploadBtn.onClick.AddListener(() =>
{
GameLogger.UploadLog("上傳日志測驗");
});
logBtn.onClick.AddListener(() =>
{
GameLogger.Log("列印一行日志");
});
logEnableTgl.onValueChanged.AddListener((v) =>
{
GameLogger.s_debugLogEnable = v;
});
GameLogger.Log("大家好,我是林新發");
GameLogger.LogCyan("我的CSDN博客:https://blog.csdn.net/linxinfa");
GameLogger.LogYellow("歡迎關注、點贊,感謝支持~");
GameLogger.LogRed("???????????");
}
}
3、掛Main腳本
給MainPanel.prefab預設掛上Main.cs腳本,并賦值UI成員,如下:

4、Web服務器
我們的日志要上傳到Web服務器,所以我們需要搭建一個Web服務器,簡單的做法是使用PHP小皮~
4.1、小皮,Web服務器
關于小皮,可以看我之前寫的這篇文章:https://blog.csdn.net/linxinfa/article/details/103033142
我們設定一下埠號,比如7890,

啟動Apache,

這樣,我們就已經啟動了一個Web服務器了,通過http://127.0.0.1:7890即可訪問
4.2、PHP腳本:upload_log.php
我們打開Web服務器的根目錄,

在根目錄中創建一個upload_log.php,

php代碼如下:
<?php
// 獲取描述
$desc = $_POST["desc"];
// 獲取臨時檔案路徑
$tmp = $_FILES["logfile"]["tmp_name"];
// 檔案保存位置
$savePath = "upload/" . $_FILES["logfile"]["name"];
// 判斷檔案是否已存在
if (file_exists($savePath))
{
// 檔案已存在,洗掉它
unlink($savePath);
}
// 保存檔案到savePath的路徑下
move_uploaded_file($tmp, $savePath);
echo "檔案上傳成功";
?>
我們再創建一個upload檔案夾,用于存放上傳上來的日志,

5、運行測驗
運行Unity,測驗效果如下:

日志成功上傳到了Web服務器的upload目錄中,

日志檔案內容如下:
大家好,我是林新發
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main:Start () (at Assets/Scripts/Main.cs:70)
<color=#00ffff>我的CSDN博客:https://blog.csdn.net/linxinfa</color>
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:LogCyan (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:131)
Main:Start () (at Assets/Scripts/Main.cs:71)
<color=yellow>歡迎關注、點贊,感謝支持~</color>
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:LogYellow (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:122)
Main:Start () (at Assets/Scripts/Main.cs:72)
<color=red>???????????</color>
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:LogRed (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:104)
Main:Start () (at Assets/Scripts/Main.cs:73)
列印一行日志
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main/<>c:<Start>b__5_1 () (at Assets/Scripts/Main.cs:62)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
列印一行日志
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main/<>c:<Start>b__5_1 () (at Assets/Scripts/Main.cs:62)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
列印一行日志
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main/<>c:<Start>b__5_1 () (at Assets/Scripts/Main.cs:62)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
列印一行日志
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main/<>c:<Start>b__5_1 () (at Assets/Scripts/Main.cs:62)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
列印一行日志
UnityEngine.Debug:Log (object,UnityEngine.Object)
GameLogger:Log (object,UnityEngine.Object) (at Assets/Scripts/GameLogger/GameLogger.cs:72)
Main/<>c:<Start>b__5_1 () (at Assets/Scripts/Main.cs:62)
UnityEngine.EventSystems.EventSystem:Update () (at Library/PackageCache/com.unity.ugui@1.0.0/Runtime/EventSystem/EventSystem.cs:501)
6、工程原始碼
本文工程原始碼我一上傳到CODE CHINA,感興趣的同學可自行下載下來學習,
地址:https://codechina.csdn.net/linxinfa/UnityLoggerDemo
注:我使用的Unity版本為:2021.1.7f1c1

八、完畢
好了,沒想到又寫到這么晚,就到這里吧~
我是林新發:https://blog.csdn.net/linxinfa
原創不易,若轉載請注明出處,感謝大家~
喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/291582.html
標籤:其他
上一篇:三子棋游戲【C語言版】
下一篇:目標檢測也就是這么簡單

