游戏开发进阶新发带你玩转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在不同平台下的路径:
WindowsC:/Users/用户名/AppData/LocalLow/CompanyName/ProductName
Androidandroid/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爬虫进阶,带你玩转短视频

55个必备技巧带你玩转JavaScript

“小票打印”功能来了!酷客多带你玩转小程序

Git 命令太难学?我用一款游戏带你玩转它!

带你玩转模型法线,实验一下大胆的想法(法线贴图 | shader | Unity | python | 爬虫)

带你玩转模型法线,实验一下大胆的想法(法线贴图 | shader | Unity | python | 爬虫)