不需要反射,不需要了解 Unity 源码,只需要一点点🤏额外的技巧即可~


<aside>

<aside> 🌟

Achieving a mental-leap request needs a mental-leap solution.

</aside>

</aside>

这是开发过程中面临的一个很现实的需求,对于大量的 lua 代码来说,怎样可以将 lua 打印到控制台的日志,能快速准确的定位到代码行,可以很大的提高我们对日志定位的效率,从而进一步提高 debug 的效率。

我们知道双击控制台日志会跳转到打印此行日志对应的 C# 代码行,而单击日志会跳转到日志对应的上下文(如果有的话)。 lua 侧打印的日志,实际还是会回到 C# 调用 Unity 的日志接口,这时双击日志也仅仅会跳转到 C# 的打印语句(包括主观打印的日志或报错导致的日志)。那么除了单击和双击这两种已经存在的方式,还有第三种方式吗?有!

Unity 日志中可以添加超链接,效果即蓝色文字的部分:点击超链接的效果和双击日志是相同的。

image.png

所以基本思路就是通过向 Lua 日志堆栈信息中添加超链接的方式,将超链接的目标指向 lua 代码。拆解一下这个方案,分成如下几个步骤:

1. 向 Lua 堆栈信息中添加文件来源

这一步其实就是 xLua 中通过添加 CustomLuaLoader 函数并修改其中的文件路径为真实完整的文件路径(相对路径即可),这样无论是在调试还是日志的堆栈信息中都能显示正确的文件路径。

private static LuaEnv _luaEnv;

private void InitEnv()
{
    _luaEnv = new LuaEnv();
    _luaEnv.AddLoader(CustomLuaLoader);
}

// LoadScriptContent 方法为读取文件的函数;
// 由于仅编辑器使用,所以添加 UNITY_EDITOR 判断;
byte[] CustomLuaLoader(ref string fileName)
{
#if UNITY_EDITOR
    //for debug purpose
    byte[] bytes = LoadScriptContent(fileName);
    fileName = $"{fileName.Replace(".", "/")}.lua";
    return bytes;
#else
    return LoadScriptContent(fileName);
#endif
}

这个方法本质上是 lua 代码中的 require 的实现,fileName 为 require 的参数。需要注意的是,lua 代码中使用 require 时,文件相对路径是正确的。因此将 . 替换为 / 并添加 .lua 后缀,即完整的相对路径了。

2. 向日志中添加超链接

在 lua 打印日志时,需要向 C# 传递堆栈信息中包含了上面的文件路径。HTML 的超链接的格式如下:

<a href="filePath" line="lineNo"></a>

其中两个参数代表了点击链接后跳转的 C# 脚本和行数。

如何获得文件路径,需要我们对日志里的堆栈信息字符串格式进行一些处理,并提取出行数和文件路径。需要注意,堆栈信息中可能包含多行文件调用,取决于设置的堆栈深度。

public static string ParseLuaTraceback(string traceback)
{
    if (string.IsNullOrEmpty(traceback))
    {
        return string.Empty;
    }

    StringBuilder sb = new StringBuilder();
    foreach (string line in traceback.Split('\\r', '\\n'))
    {
        if (line.Contains("stack traceback"))
        {
            sb.AppendLine(line);
            continue;
        }
        if (line.Contains("util/log.lua"))
            continue;

        string[] content = line.Split(':');
        if (content.Length <= 1)
        {
            sb.AppendLine(line);
            continue;
        }

        string fileName = content[0].Trim();
        if (fileName.Contains("[") && fileName.Contains("]"))
        {
            string[] str = fileName.Split('\\"');
            if (str.Length != 3)
            {
                sb.AppendLine(fileName);
                continue;
            }
            fileName = str[1];
        }

        string filePath = $"{Application.dataPath}/../../Resources/Lua/{fileName}";
        int lineNumber = content.Length > 2 ? int.Parse(content[1]) : 0;
        string info = content.Length > 2 ? content[2].Trim() : content[1].Trim();
        int guid = RegisterLogMessage($"{filePath}|{lineNumber}");
        sb.AppendLine($"<a href=\\"Assets/Editor/Utils/AssetOpenHandler.cs\\" line=\\"{guid}\\">[{fileName}:{lineNumber}]</a> {info}");
    }

    return sb.ToString();
}

Unity 的代码跳转到接口的回调是 UnityEditor.Callbacks.OnOpenAssetAttribute 标签的函数,其中两个参数一个为文件的 InstanceId,一个是行数,而文件的 InstanceId 指向的是跳转的 C# 脚本,只有一个行数参数,怎么传递 lua 的文件路径和行数呢?我们可以是先将其进行缓存,并分配一个 guid,将 guid 通过行数传递给 C# 回调。