zoukankan      html  css  js  c++  java
  • Unity热更新 xLua

    xLua是Unity3D下Lua编程解决方案,自2016年初推广以来,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。现在,腾讯已经将xLua开源到GitHub。

    2016年12月末,xLua刚刚实现新的突破:全平台支持用Lua修复C#代码bug。

    目前Unity下的Lua热更新方案大多都是要求要热更新的部分一开始就要用Lua语言实现,不足之处在于:

    1. 接入成本高,有的项目已经用C#写完了,这时要接入需要把需要热更的地方用Lua重新实现;
    2. 即使一开始就接入了,也存在同时用两种语言开发难度较大的问题;
    3. Lua性能不如C#;

    xLua热补丁技术支持在运行时把一个C#实现(函数,操作符,属性,事件,或者整个类)替换成Lua实现,意味着你可以:

    1. 平时用C#开发;
    2. 运行也是C#,性能秒杀Lua;
    3. 有bug的地方下发个Lua脚本fix了,下次整体更新时可以把Lua的实现换回正确的C#实现,更新时甚至可以做到不重启游戏; 这个新特性iOS,Android,Window,Mac都测试通过了,目前在做一些易用性优化。

    xLua插件下载地址:https://github.com/Tencent/xLua

     

    xLua的使用

    创建工程并导入xLua插件

    通过xLua插件运行lua程序

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class MyHelloWorld : MonoBehaviour {
    
    	void Start () {
            // 创建lua环境
            LuaEnv luaenv = new LuaEnv();
            // 运行Lua代码
            luaenv.DoString("print('Hello World')");
            // 关闭Lua环境
            luaenv.Dispose();
    	}
    }
    

    可以看到,输出了打印,前缀有Lua的标识表示这是由Lua中的方法执行的

    反过来,也可以使用lua调用C#中的程序

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class MyHelloWorld : MonoBehaviour {
    
    	void Start () {
            // 创建lua环境
            LuaEnv luaenv = new LuaEnv();
            // 运行Lua代码
            //luaenv.DoString("print('Hello World')");
            luaenv.DoString("CS.UnityEngine.Debug.Log('Hello World')");
            // 关闭Lua环境
            luaenv.Dispose();
    	}
    }
    

    这个时候,打印前就没有Lua标识符了,表示这是由C#中代码执行的

    上面是C#和Lua之间的简单调用,但是在实际工作中,我们不可能这么写。我们的做法是写好Lua文件后,在C#中加载这个文件,然后使用其中的函数功能。

    首先我们创建好一个Lua文件,然后在C#中加载后使用

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class MyHello : MonoBehaviour {
    
    	void Start () {
            TextAsset t = Resources.Load<TextAsset>("helloworld.lua");
    
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString(t.ToString());
            luaenv.Dispose();
    	}
    }
    

    注意:在加载的时候,我们使用的是TextAsset文本格式,它默认识别的后缀为.txt,所以我们上面创建的lua文件后缀不是.lua,但是为了让我们方便的看出它是一个lua文件,所以取名的时候使用.lua.txt。

    除了上面的加载方法外,更常用的方法是使用require加载

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class MyHello : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'helloworld'");
            luaenv.Dispose();
    	}
    }
    

    require实际上是调一个个的loader去加载,有一个成功则不再往下尝试,全部失败则报文件找不到。目前Lua除了原生的loader外,还添加了从Resources加载的loader,需要注意的是Resources只支持有限的后缀,放在Resources下的lua文件需要加上.txt后缀。

    自定义loader

    我们发现上面的lua文件都是放在Resources文件夹下,因为原生的loader会在这个下面去加载。在我们的项目中,可能我们的lua文件放在自定义的文件夹下,这个时候就需要我们自定义loader,在xLua加自定义loader是很简单的,只涉及到一个接口:

    public delegate byte[] CustomLoader(ref string filepath);

    public void LuaEnv.AddLoader(CustomLoader loader)

    通过AddLoader可以注册个回调,该回调参数是字符串,lua代码里头调用require时,参数将会透传给回调,回调中就可以根据这个参数去加载指定文件,如果需要支持调试,需要把filepath修改为真实路径传出。该回调返回值是一个byte数组,如果为空表示该loader找不到,否则则为lua文件的内容。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    using System.IO;
    
    public class CreateNewLoader : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            // 自定义loader
            luaenv.AddLoader(MyLoader);
            luaenv.DoString("require 'newloaderText'");
            luaenv.Dispose();
    	}
    	
        private byte[] MyLoader(ref string filePath)
        {
            string absPath = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
            return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
        }
    }
    

    上面代码中我们定义的lua文件为“newloaderText.lua”,该文件位于“StreamingAssets”文件夹下,该文件夹与Assets文件夹同级,所以在后面设置路径的时候使用系统自带的函数“Application.streamingAssetsPath”可以找到该文件夹。当然,我们也可以自定义文件夹的位置,后面的路径改一下就行。

    上面的执行过程,注册回调后,调用require的时候,将“newloaderText”传递给回调函数"MyLoader",在此回调函数中我们加载到指定文件然后传回来使用。

    C#访问Lua   获取全局变量

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
            // 获取lua中的全局变量
            int num = luaenv.Global.Get<int>("num");
            string name = luaenv.Global.Get<string>("name");
            bool isPause = luaenv.Global.Get<bool>("isPause");
            Debug.Log("num:" + num);
            Debug.Log("name:" + name);
            Debug.Log("isPause:" + isPause);
            luaenv.Dispose();
    	}
    }
    

    使用函数LuaEnv.Global就能访问,其中,luaenv.Global.Get<int>("num")中,<int>指的是要转换成的类型,"num"是在lua中定义的变量名

    C#访问Lua   获取全局table

    • 映射到普通class或struct:定义一个class或者struct,有对应于table的字段的public属性,而且有无参数构造函数即可,比如对于{f1 = 100, f2 = 100}可以定义一个包含public int f1;public int f2;的class。这种方式下xLua会帮你new一个实例,并把对应的字段赋值过去。table的属性可以多于或者少于class的属性。可以嵌套其它复杂类型。

    注意:lua的table中的字段名和C#的class中的字段名要一一对应(名字也要相同),否则取不到值。此种方式为值拷贝,修改class的字段值不会同步到table,反过来也不会。使用此种方式,不能访问lua的函数。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
            // 获取lua中的全局table
            Person p = luaenv.Global.Get<Person>("Person");
            Debug.Log("name:" + p.name);
            Debug.Log("age:" + p.age);
            luaenv.Dispose();
    	}
    
        class Person
        {
            public string name;
            public int age;
        }
    }
    
    • 映射到interface:这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),代码生成器会生成这个interface的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段。甚至可以通过interface的方法访问lua的函数

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
    
            // 获取lua中的全局table(映射到interface)
            Person_1 p1 = luaenv.Global.Get<Person_1>("Person");
            Debug.Log("name:" + p1.name);
            Debug.Log("age:" + p1.age);
            p1.eat("apple");
            luaenv.Dispose();
    	}
    
        [CSharpCallLua]
        interface Person_1
        {
            string name { get;set;}
            int age { get; set; }
            void eat(string str);
        }
    }
    

    注意:在lua中定义函数的时候,第一个参数是arg,需要写上,名字随意取都行,这里写的self。在C#中定义接口的时候,要加上标签[CSharpCallLua]

    • 映射到Dictionary<>,List<>

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
    
            // 获取lua中的全局table(通过Dictionary)
            Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("Person");
            foreach(string key in dict.Keys)
            {
                print("key:" + key + "   value:" + dict[key]);
            }
            luaenv.Dispose();
    	}
    }
    

    注意:映射到Dictionary<>的时候,只映射了Lua中键值对的形式,普通的值没有映射过来

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
    
            // 获取lua中的全局table(通过List)
            List<object> list = luaenv.Global.Get<List<object>>("Person");
            foreach(object o in list)
            {
                print(o);
            }
            luaenv.Dispose();
    	}
    }
    

     

    注意:映射到List<>的时候,只映射了Lua中值的形式,键值对的形式没有映射过来 

    映射到LuaTable类:这种方式不常用,也不建议使用

     

    C#访问Lua   获取全局函数

    • 映射到delegate:这种是建议的方式,性能好很多,而且类型安全。缺点是要生成代码(如果没生成代码会抛InvalidCastException异常)。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class CSharpCallLua : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'CSharpCallLua'");
    
            // 访问lua中的全局函数(映射到delegate)
            Add add = luaenv.Global.Get<Add>("add");
            int res1 = 0; int res2 = 0;
            int res = add(3, 4, out res1, out res2);
            print("res:" + res);
            print("res1:" + res1);
            print("res2:" + res2);
            add = null;
            luaenv.Dispose();
    	}
    
        [CSharpCallLua]
        delegate int Add(int a, int b, out int res1, out int res2);
    }
    

    注意:使用delegate需要添加特性[CSharpCallLua],如果lua中函数返回多值,在C#中只能接收一个值,其它值从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数。

    • 映射到LuaFunction:这个性能不好,不建议使用

    Lua访问C#

    在C#这样new一个对象:

    var newGameObj = new UnityEngine.GameObject();

    对应到Lua是这样:

    local newGameObj = CS.UnityEngine.GameObject()

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using XLua;
    
    public class LuaCallCSharp : MonoBehaviour {
    
    	void Start () {
            LuaEnv luaenv = new LuaEnv();
            luaenv.DoString("require 'LuaCallCS'");
            luaenv.Dispose();
    	}
    }
    

    Lua访问C#静态属性和方法

    如果需要经常访问的类,可以先用局部变量引用后访问,除了减少敲代码的时间,还能提高性能

     

    Lua访问C#成员属性和方法

    读成员属性

    testobj.DMF

    写成员属性

    testobj.DMF = 1024

     

  • 相关阅读:
    MRF能量优化
    Django:model中的ForeignKey理解
    Django:常见的orm操作
    Django:在模板中获取当前url信息
    Django:haystack全文检索详细教程
    Django:全文检索功能可参考博客
    看电影学英语
    Markdown中怎么上传图片
    Mosquitto的安装、配置、测试
    Django:评论文章后局部刷新评论区
  • 原文地址:https://www.cnblogs.com/lmx282110xxx/p/10798660.html
Copyright © 2011-2022 走看看