游戏开发进阶新发带你玩转Unity日志打印技巧(彩色日志 | 日志存储与上传 | 日志开关 | 日志双击溯源)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发进阶新发带你玩转Unity日志打印技巧(彩色日志 | 日志存储与上传 | 日志开关 | 日志双击溯源)相关的知识,希望对你有一定的参考价值。
文章目录
一、前言
嗨,大家好,我是新发。
有铁粉私信我问我能否写一篇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
[Unity以上是关于游戏开发进阶新发带你玩转Unity日志打印技巧(彩色日志 | 日志存储与上传 | 日志开关 | 日志双击溯源)的主要内容,如果未能解决你的问题,请参考以下文章
大家沉迷短视频无法自拔?Python爬虫进阶,带你玩转短视频