• xLua热更新(一)xLua基本使用


    一、什么是xLua

    xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。

    xLua是用来实现Lua代码与C#代码相互调用的插件。我们可以借助这个插件来实现热更新方案。

    那么为什么要选择Lua实现热更新呢?

    这是因为Lua具有轻量、灵活的特点,可以在几乎任何平台上编译、运行。Unity一般使用C#代码编写游戏逻辑。在打包时,C#会先编译成IL(中间语言),存储到dll(动态链接库)中。在游戏运行时,需要通过JIT(即时编译)将IL解释为机器码。在这期间,会开辟一块内存空间,且要求这块空间可读、可写、可执行。但IOS平台是不允许获取具有可执行权限的内存空间的。因此只能进行全量更新。但Lua是使用C写的脚本语言,在运行时读入Lua代码,在解释时直接使用C代码进行解释,不需要开辟特殊的内存空间,执行解释的是C语言编写的虚拟机。(参考自这篇文章

    二、如何使用

    2.1 Hello World

    首先在xLua的GitHub主页下载源码,并引入到Unity中。

    创建一个C#脚本,并编写如下代码。DoString()方法可以执行传入的Lua代码。

    public class HelloWorld : MonoBehaviour
    {
    	private LuaEnv _lua;
    
    	private void Start()
    	{
    		_lua = new LuaEnv();
    		_lua.DoString("print('Hello World')");
    	}
    
    	private void OnDestroy()
    	{
    		_lua.Dispose();
    		_lua = null;
    	}
    }
    

    将脚本挂载到一个游戏物体上,运行游戏。可以在控制台看到输出结果,且输出的字符串带有“Lua:”前缀。

    需要注意的是,一个LuaEnv的实例对应着一个Lua虚拟机,建议全局唯一。

    2.2 加载Lua文件

    DoString()方法中直接写大量的Lua代码是不现实的,我们需要载入外部的Lua文件,并将文件内容传入这个方法中执行。

    首先编写一个简单的Lua脚本,并将脚本放在「Resources」目录下

    a = 2  
    b = 3  
    print("a+b="..a+b)
    

    然后在C#脚本中通过Resources进行载入

    public class LoadLuaFile : MonoBehaviour
    {
    	private void Start()
    	{
    		var lua = Resources.Load<TextAsset>("AddLua");
    		Debug.Log(lua);
    		if (lua != null)
    		{
    			LuaEnv luaEnv = new();
    			luaEnv.DoString(lua.text);
    			luaEnv.Dispose();
    		}
    	}
    }
    

    此时运行游戏我们会发现,控制台输出的是「Null」。这是因为Resources.Load()会默认给文件名后面增加「.txt」后缀。也就是说这个方法只会读取到后缀为「.txt」的文件。

    为了能读取到Lua文件,我们需要将Lua文件的后缀改为「.lua.txt」。在加载时,文件名传入「XXX.lua」,这样就能顺利读取到Lua脚本的内容了。

    我们也可以使用xLua内置的loader进行加载。方法是在luaEnv.DoString()方法中直接传入require语句。require会调一个个的Loader去加载,直到遇到不返回空的Loader。如果全部返回空则会报文件找不到的异常。

    LuaEnv luaEnv = new();  
    luaEnv.DoString("require 'AddLua'");  
    luaEnv.Dispose();
    

    如果lua脚本没有问题,但运行时“unexpected symbol”之类的错误,可以用记事本打开lua脚本,重新保存为UTF-8编码格式。

    2.3 自定义Loader

    某些情况下系统内置的Loader并不能满足我们的需求。比如需要对Lua文件解密,或者Lua文件不在「Resources」目录下等。这时就需要我们自定义Loader。

    要实现自定义Loader也很简单,只需要调用LuaEnv.AddLoader()方法,添加一个自定义Loader即可。该方法需要传入一个委托,委托的参数是加载文件的路径,返回值是文件内容的字节数组。

    public class CustomLoader : MonoBehaviour
    {
    	private void Start()
    	{
    		LuaEnv luaEnv = new();
    		luaEnv.AddLoader(MyLoader);
    		luaEnv.DoString("require '不存在的文件'");
    		luaEnv.Dispose();
    	}
    
    	private byte[] MyLoader(ref string filePath)
    	{
    		string content = "print('Hello World')";
    		return System.Text.Encoding.UTF8.GetBytes(content);
    	}
    }
    

    上面的代码运行结果如下

    要实现加载指定目录的Lua文件,只需要在自定义Loader中通过文件流读取文件即可

    string path = "[指定路径]";  
    return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));
    

    2.4 C#访问Lua

    2.4.1 访问全局变量

    Lua脚本

    str="Hello World"  
    num=12  
    isTrue=true
    

    C#脚本

    LuaEnv luaEnv = new();  
    luaEnv.DoString("require 'CSharpCallLua'");  
    var str = luaEnv.Global.Get<string>("str");  
    var num = luaEnv.Global.Get<int>("num");  
    var isTrue = luaEnv.Global.Get<bool>("isTrue");  
    Debug.Log($"str:{str} num:{num} isTrue:{isTrue}");  
    luaEnv.Dispose();
    

    运行结果

    2.4.2 访问全局table

    映射到class或struct

    Lua脚本

    person = {  
        Name="宇智波佐助",  
        Age=12  
    }
    

    C#脚本

    class Person  
    {  
        public string Name;  
        public int Age;  
    }  
      
    private void CallTableToClass()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'CSharpCallLua'");  
        var person = luaEnv.Global.Get<Person>("person");  
        Debug.Log($"name:{person.Name} age:{person.Age}");  
        luaEnv.Dispose();  
    }
    

    运行结果

    需要注意的是,这个映射过程是值拷贝,如果class比较复杂,代价会比较大。且因为是值拷贝,无论去修改任何一边的字段值,另一边也不会同步修改。

    映射到interface

    Lua脚本

    person = {  
        Name="宇智波佐助",  
        Age=12,  
        Do=function(self,a,b) -- 需要额外定义一个参数self,相当于this  
            print(a+b)  
        end  
    }
    

    C#脚本

    [CSharpCallLua]
    public interface IPerson // 接口必须声明为public
    {  
        string Name { get; set; }  
        int Age { get; set; }  
        void Do(int a,int b);  
    }
    
    private void CallTableToInterface()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'CSharpCallLua'");  
        var person = luaEnv.Global.Get<IPerson>("person");  
        Debug.Log($"name:{person.Name} age:{person.Age}");  
        person.Do(12,3);  
        luaEnv.Dispose();  
    }
    

    运行结果

    这是引用方式的映射,也就是说在C#中更改字段值,对应的Lua表中的值也会跟着修改。另外,如果映射的方法具有参数,则在Lua代码中,需要在最前面额外定义一个接收参数,用来充当this。或者通过如下方式定义方法

    function person:Do(a,b)  
        print(a+b)  
    end
    

    另外在运行时可能会碰到如下问题:

    首先检查映射的接口上有没有加[CSharpCallLua]特性。如果已经添加该特性,且Unity版本是2018以上,那就将「File->Build Settings->Player Settings->Player->Other Settings->Configuration->Api Compatibility Level」中的选项改为「.NET Framework」。具体原因可以查看xLua官方文档中的「faq」文档

    映射到集合

    Lua脚本

    person = {  
        Name="宇智波佐助",  
        Age=12,  
        Do=function(self,a,b)  
            print(a+b)  
        end,  
        1,2,3,  
        "Hello",  
        true  
    }
    

    C#脚本

    private void CallTableToCollection()
    {
    	LuaEnv luaEnv = new();
    	luaEnv.DoString("require 'CSharpCallLua'");
    	var person1 = luaEnv.Global.Get<Dictionary<string,object>>("person");
    	foreach (var e in person1)
    	{
    		Debug.Log($"Key:{e.Key} Value:{e.Value}");
    	}
    	Debug.Log("------------------------------------------------");
    	var person2 = luaEnv.Global.Get<List<object>>("person");
    	foreach (var e in person2)
    	{
    		Debug.Log(e);
    	}
    	luaEnv.Dispose();
    }
    

    运行结果

    可以看到,在table中显式定义了键值的字段都可以正常映射到字典中,而没有定义键值的则会丢失;线性表则与之相反,只会映射没有定义键值的字段。

    映射到LuaTable

    Lua脚本

    person = {  
        Name="宇智波佐助",  
        Age=12,  
        Do=function(self,a,b)  
            print(a+b)  
        end,  
        1,2,3,  
        "Hello",  
        true  
    }
    

    C#脚本

    private void CallTableToLuaTable()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'CSharpCallLua'");  
        var person = luaEnv.Global.Get<LuaTable>("person");  
        Debug.Log($"{person.Get<int,int>(1)}");  
        Debug.Log($"{person.Get<int,int>(2)}");  
        Debug.Log($"{person.Get<int,int>(3)}");  
        Debug.Log($"{person.Get<int,string>(4)}");  
        Debug.Log($"{person.Get<int,bool>(5)}");  
        Debug.Log($"{person.Get<string,string>("Name")}");  
        Debug.Log($"{person.Get<string,int>("Age")}");  
        Debug.Log($"{person.Get<string,object>("Do")}");  
        luaEnv.Dispose();  
    }
    

    运行结果

    LuaTable类是xLua提供的类,它可以把定义了键值和未定义键值的字段全部映射过来。但是性能上要慢很多,且没有类型检查。因而一般很少会使用这种方法。

    2.4.3 访问全局函数

    映射到委托

    对于Lua中无参数和返回值的函数,可以使用C#中的Action接收
    Lua脚本

    function Add1()  
        print("调用了Add")  
    end
    

    C#脚本

    private void CallFunctionToDelegate()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'CSharpCallLua'");  
        Action act = luaEnv.Global.Get<Action>("Add1");  
        act();  
        // 延时调用确保引用被释放  
        StartCoroutine(DisposeLuaEnv(luaEnv));  
    }  
      
    IEnumerator DisposeLuaEnv(LuaEnv luaEnv)  
    {  
        yield return new WaitForSeconds(0.1f);  
        luaEnv.Dispose();  
    }
    

    运行结果

    如果是有返回值和参数的函数,可以自定义一个委托来接收。对于有多个返回值的函数,可以使用out参数接收。
    Lua脚本

    function Add2(a,b)  
        print("调用了Add 结果:"..a+b)  
        return a+b,"Hello",true  
    end
    

    C#脚本

    [CSharpCallLua]  
    private delegate int Add(int a, int b, out string res2, out bool res3);
    
    private void CallFunctionToDelegate()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'CSharpCallLua'");  
        int res1 = add(12,3,out string res2,out bool res3);  
        Debug.Log($"res1:{res1} res2:{res2} res3:{res3}");  
        // 延时调用确保引用被释放
        StartCoroutine(DisposeLuaEnv(luaEnv));
    }
    
    IEnumerator DisposeLuaEnv(LuaEnv luaEnv)  
    {  
        yield return new WaitForSeconds(0.1f);  
        luaEnv.Dispose();  
    }
    

    运行结果

    映射到LuaFunction

    这种方式是将函数映射到xLua提供的LuaFunction类中,写起来比较简单,但是性能要比委托的方式差。
    Lua脚本

    function Add2(a,b)  
        print("调用了Add 结果:"..a+b)  
        return a+b,"Hello",true  
    end
    

    C#脚本

    private void CallFunctionToLuaFunction()
    {
    	LuaEnv luaEnv = new();
    	luaEnv.DoString("require 'CSharpCallLua'");
    	var add = luaEnv.Global.Get<LuaFunction>("Add2");
    	var res = add.Call(12, 3);
    	foreach (var e in res)
    	{
    		Debug.Log(e);
    	}
    	
    	luaEnv.Dispose();
    }
    

    运行结果

    2.5 Lua访问C#

    在Lua中访问C#脚本的成员比较简单,只需要在所有C#相关的代码都加上CS前缀即可
    C#脚本

    private void Start()  
    {  
        LuaEnv luaEnv = new();  
        luaEnv.DoString("require 'LuaCallCSharp'");  
        luaEnv.Dispose();  
    }
    

    Lua脚本

    -- 实例化对象  
    local go = CS.UnityEngine.GameObject("LuaGameObject")  
      
    -- 访问静态属性、方法  
    local deltaTime = CS.UnityEngine.Time.deltaTime  
    local GameObject = CS.UnityEngine.GameObject  
    local camera = GameObject.Find("Main Camera")  
      
    -- 访问成员属性、方法  
    camera.name = "LuaCamera"  
    camera:GetComponent("Camera").clearFlags = CS.UnityEngine.CameraClearFlags.SolidColor
    

    再次强调在Lua中调用成员方法时,要么使用:的方式访问,要么使用.调用并在参数中额外传入对象本身的方式访问。

  • 相关阅读:
    【软件测试】selenium3
    i++ 和 ++i原理
    Mybatis中解决数据库表和实体类字段名不一致的方式
    CVPR2022 | 可精简域适应
    Expression 数学表达式求值
    opencv(4):颜色空间
    Redis 的性能常见问题
    Anaconda配置pip源
    LLM 02-大模型的能力
    redux和Vuex的使用示例
  • 原文地址:https://blog.csdn.net/LWR_Shadow/article/details/127113712