xLua学习笔记——Hotfix热补丁(叁)

目录

目录

五、Hotfix 热补丁

(一)热补丁的概念

热补丁是指在不重启设备的情况下,可以对设备当前软件版本的缺陷进行修复。

Hotfix 是基于 xLua 框架实现的一种代码热更新技术,专门用于在游戏已发布后,无需重新打包或通过应用商店审核,就能动态修复 C# 代码中的 BUG 或更新逻辑。

通俗理解就是将 C# 中的方法替换成 Lua 等脚本语言中的函数来执行。

(二)准备工作

在进行热补丁之前,我们需要准备一些东西,才能进行热补丁

1、现在打包的界面中,在 Scripting Define Symbols 这个位置,添加 HOTFIX_ENABLE 这个宏,表示启用热补丁,之后从菜单栏的 Xlua 中就会多一个 Hotfix Inject In Editor 按钮

image-20260130232754550

2、在 Unity 顶部菜单栏中找到 “XLua/Generate Code” 按钮并单击

3、还记得之前概述提到得 Tools 文件夹吗?现在需要确保 xLua 的 Tools 工具放进 Unity 工程中,Tools 文件夹不要放在 Assets 文件夹里,而是放在和 Assets 同级的文件夹中。

4、注入,点击 Hotfix 注入按钮,如果提示注入完成即可

image-20260130232808974

(三)热补丁基础用法

我们先在 C# 定义要使用热补丁的类,在类上边加上 [Hotfix] 特性,当然每次更改后需要重新点击注入按钮

1、C# 代码如下:

[Hotfix]
public class HotfixTest
{
    public HotfixTest()
    {
        Debug.Log("HotfixTest构造函数");
    }
 
    ~HotfixTest()
    {
        Debug.Log("析构");
    }
 
    public void Speak(string str)
    {
        Debug.Log(str);
    }
}

2、单函数替换

Lua 使用热补丁很简单,使用 xlua.hotfix(class, [method_name], fix) 这个方法即可,第一个参数是类名,第二个参数是方法名,第三个参数是要替换的函数,我们先替换 Speak 方法

Lua 代码如下:

注意 Speak 是成员方法哦,因此要加个 self 参数

xlua.hotfix(CS.HotfixTest,"Speak",function(self,str)
	print(str)
end)

这样,我们在 C# 中调用的 Speak 方法,都会被替换成 Lua 的这个方法

3、多函数替换

有时我们希望一次性替换多个函数,而不是一个个替换。使用的方法还是xlua.hotfix,不过参数不太一样了,我们用一张表表示所有要替换的函数。下面举个例子,我们一次性替换 C# 的 3 个函数吧:

Lua 代码如下:

xlua.hotfix(CS.HotfixTest,{
	[".ctor"] = function()
		print("lua热补丁构造函数")
	end,
	Speak = function(self,str)
		print("你好!"..str)
	end,
	Finalize = function()
		print("lua热补丁析构函数")
	end
})

可以看到,构造函数的替换和析构函数的替换比较特殊,必须得用特定的名称,构造函数就是 [".ctor"],析构函数就是 Finalize

(四)协程函数替换

1、C# 代码如下:

IEnumerator TestCoroutine()
{
    while (true)
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("协程启动一次");
    }
}

2、Lua 代码如下:

替换的方法就是用一个 Lua 函数替换 C# 协程函数,在这个 Lua 函数里再返回一个处理过的 Lua 协程。就相当于包了一层

util = require("xlua.util") -- 获取util工具
xlua.hotfix(CS.HotFixMain,{
	TestCoroutine = function(self)
		-- 替换c#协程:返回一个xlua处理过的lua协程函数
		return util.cs_generator(function()
			while true do
				coroutine.yield(CS.UnityEngine.WaitForSeconds(1))
				print("Lua热补丁协程")
			end
		end)
	end
})

(五)索引器和属性替换

1、C# 代码如下:

[Hotfix]
public class HotfixTest
{
    public int[] array = new int[3] { 1, 2, 3 };
    // 属性
    public int Age
    {
        get
        {
            return 0;
        }
        set
        {
            Debug.Log(value);
        }
    }
    // 索引器
    public int this[int index]
    {
        get
        {
            if (index < 0 || index >= array.Length)
                return 0;
            return array[index];
        }
        set
        {
            array[index] = value;
        }
    }
}

2、Lua 代码如下:

属性的 get 和 set 方法的替换就是在属性名前面加上 set_get_

索引器的 get 和 set 固定是 get_Itemset_Item

xlua.hotfix(CS.HotFixMain,{
	set_Age = function(self,value)
		print("lua重定向设置属性"..value)
	end,
	get_Age = function(self)
		return 10
	end,
	set_Item = function(self,index,value)
		print("lua重定向设置索引器".." "..index.." "..value)
	end,
	get_Item = function(self,index)
		return 10
	end
})

当然,你可能会问如果属性名也是 Item 会怎么样,其实当你属性改成 Item 时,C# 里就先会冲突,因为索引器默认名字就是 Item,如果通过一些方法将 C# 属性改成了 Item,还是需要将 Lua 内的代码修改一番,比较麻烦,因此一般不会把属性命名为 Item

(六)事件操作替换

我们可以对事件操作进行函数替换,当监听到事件的 + 操作或 - 操作时,就会调用 Lua 替换的方法,主要是为了修复事件的 bug 或者增加一些额外功能

1、C# 代码如下:

[Hotfix]
public class HotfixTest
{
    public event UnityAction myEvent;
}

在Unity主入口的类中,创建一个方法 Test (),并在主函数中调用事件,即添加和删除事件的逻辑:

HotfixTest test = new HotfixTest();
test.myEvent += Test;
test.myEvent -= Test;

2、Lua 代码如下:

只需要使用 add_事件名remove_事件名,即可对其进行替换,我们可以记录下添加 / 删除的委托或者弄些其他逻辑

xlua.hotfix(CS.HotFixMain,{
	-- add_事件名 事件加操作
	-- remove_事件名,事件减操作
	add_myEvent = function(self,delegate)
		print(delegate)
		print("添加事件")
	end,
	remove_myEvent = function(self,delegate)
		print(delegate)
		print("移除事件")
	end
})

(七)泛型类替换

我们声明一个泛型类,就可以开始啦

1、C# 代码如下:

[Hotfix]
public class HotfixTest2<T>
{
    public void Test(T str)
    {
        Debug.Log(str);
    }
}

2、Lua 代码如下:

只需要类的括号内带上泛型的类型即可

xlua.hotfix(CS.HotfixTest2(CS.System.Int32),{
	Test = function(self,str)
		print("lua中的泛型类int")
	end
})

六、其他知识点

(一)XLua 的配置

有时我们无法直接给一个类型打标签,比如系统 api,没源码的库,或者实例化的泛化类型,这时你可以在一个静态类里声明一个静态字段,如静态列表,然后为这字段加上标签,这样列表中的内容都会被打上这个特性的标签,我们举例子来说明一下,当然具体使用可参照 Doc 下《XLua 的配置.doc》

先声明一个静态类,里面我们放上各种各样的静态列表,用于配置:

public static class GenConfig
{
    // 配置相关
}

下面就来配置我们之前学过的 C# 调用 Lua,Lua 调用 C# 和热补丁相关的

1、[CSharpCallLua]

一般只需要配置要使用的委托接口即可

//C#静态调用Lua的配置(包括事件的原型),仅可以配delegate,interface
[CSharpCallLua]
public static List<Type> CSharpCallLua = new List<Type>() 
{
    typeof(Action),
    typeof(Func<double, double, double>),
    typeof(Action<string>),
    typeof(Action<double>),
    typeof(Action<bool>),
    typeof(UnityEngine.Events.UnityAction),
    typeof(System.Collections.IEnumerator)
    // 其他配置...
};

2、[LuaCallCSharp]

一般配置 C# 的标准库 或者 Unity 的 API

//lua中要使用到C#库的配置,比如C#标准库,或者Unity API,第三方库等
[LuaCallCSharp]
public static List<Type> LuaCallCSharp = new List<Type>() 
{
    typeof(System.Object),
    typeof(UnityEngine.Object),
    typeof(Vector2),
    typeof(Vector3),
    typeof(Light),
    typeof(Mathf)
    // 其他配置...
};

3、[Hotfix]

热补丁也可以通过静态列表的方式进行配置

[Hotfix]
public static List<Type> Hotfix = new List<Type>()
{
    typeof(HotFixSubClass), // 热补丁的类
    typeof(GenericClass<>), // 热补丁泛型类
    // 其他要热补丁的类...
};

当然,还有其他 xlua 的特性,比如 [BlackList] 黑名单等等,这里只举了常用的,其他要用到的时候再去查

(二)Lua 一键转换工具

我们之前说过,AB 包加载时,无法识别.lua 文件,因此需要添加后缀 txt 转换成文本文件。但是每次修改时手动修改过于麻烦,因此需要一个自动转 txt 的工具,代码如下:

该代码的功能是将 Lua 文件夹下的 .lua 文件复制到 LuaTxt 文件夹下,并统一添加 .txt 后缀,再自动分类到 ab 包的 lua 分类中

public class LuaCopyEditor : Editor
{
    [MenuItem("XLua/Copy Lua To Txt")]
    public static void CopyLuaToTxt()
    {
        // 原始文件夹
        string path = Application.dataPath + "/Lua/";
        if (!Directory.Exists(path))
            return;
        string[] oldFiles = Directory.GetFiles(path, "*.lua");
        // 目标文件夹
        string newPath = Application.dataPath + "/LuaTxt/";
        if (!Directory.Exists(newPath))
            Directory.CreateDirectory(newPath);
        else
        {
            // 删除目标文件夹中旧的文件
            string[] files = Directory.GetFiles(newPath, "*.txt");
            foreach (string file in files)
            {
                File.Delete(file);
            }
        }
        // 将修改后的文件复制到新文件夹
        List<string> newFiles = new List<string>();
        foreach (string oldFile in oldFiles)
        {
            string newFile = newPath + oldFile.Substring(oldFile.LastIndexOf("/") + 1) + ".txt";
            newFiles.Add(newFile);
            File.Copy(oldFile, newFile);
        }
 
        // 刷新
        AssetDatabase.Refresh();
 
        // 自动归类到AB包
        foreach (string newFile in newFiles)
        {
            // 该路径必须是相对与Assets的
            AssetImporter importer = AssetImporter.GetAtPath(newFile.Substring(newFile.IndexOf("Assets")));
            if (importer != null)
            {
                importer.assetBundleName = "lua";
            }
        }
        
        AssetDatabase.Refresh();
    }
}