zoukankan      html  css  js  c++  java
  • 热更新解决方案--xlua学习笔记

    一.热更新方案简介

      在Unity游戏工程中,C#代码(编译型语言)资源和Resources文件夹下的资源打包后都不可以更改,因此这部分内容不能进行热更新,而lua代码(解释型语言)逻辑不需要进行预编译再运行,可以在游戏运行过程中进行修改,AB包资源也可以在游戏运行过程中下载解压缩并使用其中的资源。因此客户端可以在启动时检验服务器端的AB包资源是否有更新,如果有更新先下载更新,将lua代码资源和其他更新资源打包为AB包放在服务器端,客户端下载后直接在运行过程中解压缩并使用更新资源,实现了客户端不中断运行即完成更新的目的,也就是热更新。

    二.xlua热更新方案简介

      xlua框架提供了C#和lua相互调用的功能及Hotfix热补丁的功能,主要目的是方便我们将纯C#工程在不重做的情况下改造成具备热更新功能的工程。

    三.准备工作--说明:使用的Unity版本为2019.4.18f1c1,导入的xlua为2021年4月4日从GitHub上直接clone的工程文件,没有下载release版本。

      1.xlua框架导入

        在GitHub上搜索xlua,找到腾讯的xlua项目,下载项目的压缩包。

        下载完成后解压,发现下载的是一个Unity工程文件:

        在工程文件中,核心代码是Assets目录下的Plugins和XLua这两个文件夹中的内容,将其复制到自己的工程文件中即可。

        将这两个文件夹复制到自己的工程中后稍等一会儿,就会发现在菜单栏中Windows菜单选项左侧出现了XLua菜单选项,没有报错的话说明成功导入。

      2.AB包工具导入

        在Unity中通过PackageManager导入AB包工具,导入方法详见:热更新基础--AssetBundle学习笔记

      3.AB包管理器

        为了方便加载AB包,我们可以制作一个AB包的管理器脚本,脚本详见:热更新基础--AssetBundle学习笔记

    四.C#调用lua

      1.lua解析器

        void Start()
        {
            //Lua解析器,能够在Unity中执行Lua
            LuaEnv env = new LuaEnv();
    
            //执行单行Lua语言,使用DoString成员方法
            env.DoString("print('hello world!')");
    
            //执行lua脚本,一般都是直接调用lua语言的require关键字执行lua脚本
            //默认寻找脚本的路径是在Resources下
            //lua后缀Unity不能识别,需要将lua文件添加上.txt以便Unity识别
            env.DoString("require('Main')");
    
            //清除lua中没有手动释放的对象,相当于垃圾回收,一般在帧更新中定时执行或者切换场景时执行
            env.Tick();
    
            //销毁lua解析器,但是一般不会销毁,因为最好保持解析器的唯一性以节约性能
            env.Dispose();
        }

      2.lua文件加载重定向,即更改使用require关键字加载lua文件时寻找lua文件的位置(默认lua脚本在Resources下才能识别,这和热更新的目的冲突)

        void Start()
        {
            LuaEnv env = new LuaEnv();
    
            //使用AddLoader方法添加重定向,即自定义文件加载的规则
            //参数为一个委托,这个委托有一个ref参数,自动执行传入require执行的脚本文件名,在委托中拼接好完整的路径;委托的返回值为lua文件转化出的字节数组
            //添加委托后,委托在执行require语句时自动执行
            env.AddLoader(MyCustomLoader);
    
            //使用require语句执行lua文件,会自动先调用添加的重定向方法寻找lua文件,如果找不到再到默认路径Resources下寻找
            env.DoString("require('Main')");
        }
    
        /// <summary>
        /// 重定向方法
        /// </summary>
        /// <param name="filePath">文件名</param>
        /// <returns></returns>
        private byte[] MyCustomLoader(ref string filePath)
        {
            //拼接完整的lua文件所在路径
            string path = Application.dataPath + "/Lua/" + filePath + ".lua";
            Debug.Log(path);
            //判断文件是否存在,存在返回读取的文件字节数组,不存在打印提醒信息,返回null
            if (File.Exists(path))
            {
                return File.ReadAllBytes(path);
            }
            else
            {
                Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath);
            }
            return null;
        }

      3.lua解析器管理器:对lua解析器的进一步封装以方便使用(4.16更新,增加了只需要直接输入lua文件名即可从Lua文件夹中的子文件夹中读取lua文件的功能)

    /// <summary>
    /// lua管理器,对lua解析器的进一步封装,保证lua解析器的唯一性
    /// </summary>
    public class LuaManager
    {
        //单例模块
        private static LuaManager instance;
        public static LuaManager Instance
        {
            get
            {
                if (instance == null)
                    instance = new LuaManager();
                return instance;
            }
        }
        private LuaManager()
        {
            //在构造方法中就为唯一的lua解析器赋值
            luaEnv = new LuaEnv();
            //加载lua脚本重定向
            //重定向到lua文件夹下
            luaEnv.AddLoader((ref string filePath) =>
            {
                //用于存储读取结果的数组
                byte[] fileContent = null;
                //拼接lua文件夹的位置
                string path = Application.dataPath + "/Lua/";
                //调用自定义方法读取lua文件
                if(TryGetLoadFile(path,ref fileContent,filePath))
                    return fileContent;
                return null;
            });
            //重定向加载AB包中的lua脚本
            luaEnv.AddLoader((ref string filePath) =>
            {
                /*//加载AB包
                string path = Application.streamingAssetsPath + "/lua";
                AssetBundle bundle = AssetBundle.LoadFromFile(path);
    
                //加载lua文件,返回
                TextAsset texts = bundle.LoadAsset<TextAsset>(filePath + ".lua");
                //返回加载到的lua文件的byte数组
                return texts.bytes;*/
    
                /*//使用AB包管理器加载,异步加载,这段代码会始终返回空,因为还没等异步加载完成已经返回了,lua也不适合异步加载,不要使用
                byte[] luaBytes = null;
                AssetBundleManager.Instance.LoadResAsync<TextAsset>("lua", filePath + ".lua", (lua) =>
                {
                     if (lua != null)
                         luaBytes = lua.bytes;
                     else
                         Debug.Log("重定向失败,从AB包加载lua文件失败");
                 });
                return luaBytes;*/
    
                //使用AB包管理器加载,同步加载
                TextAsset texts = AssetBundleManager.Instance.LoadRes<TextAsset>("lua", filePath + ".lua");
                if (texts != null)
                    return texts.bytes;
                else
                    Debug.Log("从AB包加载lua文件失败,文件名是" + filePath);
    
                return null;
    
            });
        }
    
        //持有一个唯一的lua解析器
        private LuaEnv luaEnv;
    
        //luaEnv中的大G表,提供给外部调用
        public LuaTable Global
        {
            get
            {
                //校验一下instance是否是null,避免dispose后无法获取的情况出现
                if (instance == null)
                    instance = new LuaManager();
                return luaEnv.Global;
            }
        }
    
        /// <summary>
        /// 执行单句lua代码
        /// </summary>
        /// <param name="luaCodeString"></param>
        public void DoString(string luaCodeString)
        {
            luaEnv.DoString(luaCodeString);
        }
        /// <summary>
        /// 执行lua文件的代码,直接提供文件名即可执行文件,不需要再书写lua的require语句,在方法内部拼接lua语句
        /// </summary>
        /// <param name="fileName">lua文件名</param>
        public void DoLuaFile(string fileName)
        {
            luaEnv.DoString("require('" + fileName + "')");
        }
        /// <summary>
        /// 释放解析器
        /// </summary>
        public void Tick()
        {
            luaEnv.Tick();
        }
        /// <summary>
        /// 销毁解析器
        /// </summary>
        public void Dispose()
        {
            luaEnv.Dispose();
            //销毁解析器后将lua解析器对象和单例变量都置空,下次调用时会自动调用构造函数创建lua解析器,以免报空
            luaEnv = null;
            instance = null;
        }
    
        /// <summary>
        /// 用于重定向读取lua文件的方法,使用递归读取Lua文件夹下的子文件夹
        /// </summary>
        /// <param name="path">文件夹路径</param>
        /// <param name="fileContent">存储读取结果的数组</param>
        /// <param name="filePath">lua文件的名称</param>
        /// <returns></returns>
        private bool TryGetLoadFile(string path,ref byte[] fileContent,string filePath){
            //读取当前文件夹下的文件夹
            string[] directories = Directory.GetDirectories(path);
            //有文件夹的情况下遍历这些文件夹依次递归
            if(directories.Length > 0)
                for(int i = 0;i < directories.Length;i++)
                    //递归,如果读取到了文件,则返回
                    if(TryGetLoadFile(directories[i],ref fileContent,filePath))
                        return true;
            
            //递归在子文件夹下读取到了文件,下面代码不会执行,如果没有递归读取到,再执行下面的代码
            //下面的代码获取当前文件夹下的文件并读取文件
    
            //拼接完整的lua文件所在路径
            path = path + "/" + filePath + ".lua";
            //判断文件是否存在,存在返回读取的文件字节数组,不存在打印提醒信息,返回null
            if (File.Exists(path))
            {
                fileContent = File.ReadAllBytes(path);
                Debug.Log("Lua文件夹重定向成功,文件路径为" + path);
                return true;
            }
            else
            {
                Debug.Log("Lua文件夹重定向失败,文件路径为" + path);
                return false;
            }
        }
    }

      4.访问变量

    void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //使用_G表获取luaenv中的global变量值
            Debug.Log(LuaManager.Instance.Global.Get<int>("testNumber"));
            Debug.Log(LuaManager.Instance.Global.Get<bool>("testBool"));
            Debug.Log(LuaManager.Instance.Global.Get<float>("testFloat"));
    
            //使用_G表修改luaenv中的global变量值
            LuaManager.Instance.Global.Set("testBool",false);
            Debug.Log(LuaManager.Instance.Global.Get<bool>("testBool"));
    
            //不能直接获取和设置本地变量
        }

      5.访问函数,使用委托接收

    //自定义委托,对于有参数和返回值的委托,必须加上特性[CSharpCallLua],否则无法处理,无参无返回值的委托不需要
    //特性起作用还需要在Unity中生成脚本
    [CSharpCallLua]
    public delegate void CustomCall(int a);
    //自定义含有out或者ref参数的委托用于接收多返回值函数,out和ref根据需要选择,都可以用于接收多返回值
    [CSharpCallLua]
    public delegate int CustomCall2(out int a, out int b);
    [CSharpCallLua]
    public delegate void CustomCall3(params int[] args);
        void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //获取函数,使用委托存储
            UnityAction npnr = LuaManager.Instance.Global.Get<UnityAction>("NoParamNoReturn");
            npnr();
    
            //xlua提供了获取函数的方法,但是不推荐使用,推荐使用Unity或者C#提供的委托或者自定义委托存储
            LuaFunction luaFun = LuaManager.Instance.Global.Get<LuaFunction>("NoParamNoReturn");
            luaFun.Call();
    
            //有参无返回值
            //使用自定义的委托需要声明特性且在Unity中生成代码
            CustomCall hpnr = LuaManager.Instance.Global.Get<CustomCall>("HaveParamNoReturn");
            hpnr(2);
    
            //使用C#提供的委托存储,不用声明特性
            Action<int> hpnr2 = LuaManager.Instance.Global.Get<Action<int>>("HaveParamNoReturn");
            hpnr2(2);
    
            //多返回值
            //不能使用系统自带的委托,多返回值需要自定义委托
            CustomCall2 mr = LuaManager.Instance.Global.Get<CustomCall2>("ManyReturns");
            int m;
            int n;
            int p = mr(out m, out n);
            Debug.Log(m + "-" + n + "-" + p);
    
            //变长参数
            CustomCall3 vp = LuaManager.Instance.Global.Get<CustomCall3>("VariableParams");
            vp(1, 2, 3, 4, 5);
        }

      6.表映射为List或者Dictionary

        void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //得到List
            List<int> list = LuaManager.Instance.Global.Get<List<int>>("ListTable");
            foreach (int i in list)
            {
                Debug.Log(i);
            }
    
            //得到Dictionary
            Dictionary<string, int> dic = LuaManager.Instance.Global.Get<Dictionary<string, int>>("DictionaryTable");
            foreach (KeyValuePair<string,int> pair in dic)
            {
                Debug.Log(pair.Key + ":" + pair.Value);
            }
        }

      7.表映射到类对象

    /// <summary>
    /// 声明一个类来和lua中的类进行映射,变量名称必须和lua中的对应类一致,但是不必一一对应,映射时会自动丢弃找不到的内容
    /// </summary>
    public class CallLuaClass
    {
        public string name;
        public int age;
        public int sex;
        public UnityAction eat;
    }
        void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //获得类对象
            CallLuaClass clc = LuaManager.Instance.Global.Get<CallLuaClass>("testClass");
            Debug.Log(clc.name + "-" + clc.age + "-" + clc.sex);
            clc.eat();
        }

      8.表映射到接口

    /// <summary>
    /// 使用一个接口接收表的映射,但是接口中的变量不允许被赋值,这里使用属性
    /// 必须加上特性[CSharpCallLua]
    /// </summary>
    [CSharpCallLua]
    public interface ICSharpCallLua
    {
        string name { get; set; }
        int age { get; set; }
        int sex { get; set; }
        Action eat { get; set; }
    }
        void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //得到接口对象
            ICSharpCallLua icscl = LuaManager.Instance.Global.Get<ICSharpCallLua>("testClass");
            Debug.Log(icscl.name + "-" + icscl.age + "-" + icscl.sex);
            icscl.eat();
        }

      注意:之前实现的所有拷贝都是引用拷贝,也就是c#中的拷贝值发生改变,lua代码不受影响,但是接口的拷贝是引用拷贝,也就是改变C#中的拷贝的值,lua中的值也发生了改变。

      9.映射到luaTable类

        void Start()
        {
            LuaManager.Instance.DoLuaFile("Main");
    
            //得到LuaTable,对应lua中的table。
            //本质上Global也是LuaTable类型的变量,使用方法和之前通过Global获取各种变量函数等方法相同
            //不推荐使用LuaTable和LuaFunction,效率低
            //LuaTable的拷贝是引用拷贝
            LuaTable table = LuaManager.Instance.Global.Get<LuaTable>("testClass");
            Debug.Log(table.Get<int>("age"));
            Debug.Log(table.Get<string>("name"));
            Debug.Log(table.Get<int>("sex"));
            table.Get<LuaFunction>("eat").Call();
        }

      LuaTable类对应Lua中的表,本质上Global变量也是LuaTable类型,所以LuaTable的使用方法和之前讲的通过Global获取各种变量的方法相同。LuaTable和LuaFunction使用后记得调用dispose方法释放垃圾,否则容易造成内存泄漏。

    五.lua调用C#

      1.在Unity中无法直接运行lua,因此需要使用C#脚本作为lua脚本的主入口启动lua脚本的运行,接下来都不再赘述这一步骤,所有的lua代码也都在这个特定的lua脚本中编写。

    public class Main : MonoBehaviour
    {
        private void Start()
        {
            //在这个脚本中启动特定的lua脚本,接下来的lua代码都在这个脚本中编写
            LuaManager.Instance.DoLuaFile("Main");
        }
    }

      Main.lua这个脚本作为lua脚本的入口,接下来再在这个Main.lua脚本中调用其他脚本。

    require("CallClass")

      2.创建类对象

    --lua中调用C#脚本
    
    --创建类对象
    --Unity中的类如GameObject、Transform等类都存储在CS表中
    --使用CS.命名空间.类名的方式调用Unity中的类
    local obj1 = CS.UnityEngine.GameObject("使用lua创建的第一个空物体")

    --lua中调用C#脚本
    
    --创建类对象
    --Unity中的类如GameObject、Transform等类都存储在CS表中
    --使用CS.命名空间.类名的方式调用Unity中的类
    --每次都写命名空间太麻烦,可以定义全局变量先把类存储起来,也能节约性能
    GameObject = CS.UnityEngine.GameObject
    local obj = GameObject("movin")
    
    --使用点来调用静态方法
    local obj2 = GameObject.Find("movin")
    
    --使用.来调用对象中的成员变量
    Log = CS.UnityEngine.Debug.Log
    Log(obj.transform.position)
    
    Vector3 = CS.UnityEngine.Vector3
    --使用对象中的成员方法必须使用:调用
    obj.transform:Translate(Vector3.right)
    Log(obj.transform.position)
    
    --自定义类的调用
    --直接使用CS点的方式调用
    local customClass = CS.CustomClass()
    customClass.name = "movin"
    customClass:Eat()
    
    --有命名空间的类再点一层
    local customClassInNamespace = CS.CustomNamespace.CustomClassInNamespace()
    customClassInNamespace.name = "movin2"
    customClassInNamespace:Eat()
    
    --继承了mono的类不能new出来,只能获取组件
    --xlua提供了typeof的方法得到类的Type
    --自定义的脚本组件直接用CS点出来即可
    obj:AddComponent(typeof(CS.Main))
    --系统自带的脚本一般在UnityEngine命名空间下
    obj:AddComponent(typeof(CS.UnityEngine.Rigidbody))
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public string name;
        public void Eat()
        {
            Debug.Log(name + "在吃饭");
        }
    }
    
    /// <summary>
    /// 自定义类,包裹在命名空间中
    /// </summary>
    namespace CustomNamespace
    {
        public class CustomClassInNamespace
        {
            public string name;
            public void Eat()
            {
                Debug.Log(name + "在吃饭");
            }
        }
    }

      3.使用枚举

    --调用枚举
    
    --调用Unity提供的枚举
    --Unity提供的枚举一般在UnityEngine中
    
    PrimitiveType = CS.UnityEngine.PrimitiveType
    GameObject = CS.UnityEngine.GameObject
    
    local obj = GameObject.CreatePrimitive(PrimitiveType.Cube)
    
    --调用自定义的枚举
    E_CustomEnum = CS.E_CustomEnum
    
    Log = CS.UnityEngine.Debug.Log
    Log(E_CustomEnum.Idle)
    
    --使用_CastFrom方法进行枚举类型转换,可以从数字转换成枚举或者字符串转换成枚举
    Log(E_CustomEnum.__CastFrom(1))
    Log(E_CustomEnum.__CastFrom("Atk"))

      4.使用List和Dictionary

    local CustomClass = CS.CustomClass
    local Log = CS.UnityEngine.Debug.Log
    
    --调用数组,使用C#的数组相关API,不要使用lua的方法
    obj = CustomClass();
    Log(obj.array.Length)
    
    --遍历数组,注意从0到length-1,按照C#的下标遍历不是lua的
    for i=0,obj.array.Length-1 do
        Log(obj.array[i])
    end
    
    Log("******************")
    --创建数组,利用数组类Array的CreateInstance静态方法创建数组
    --参数意思:创建数组中存储元素的类型、创建的数组的长度
    local arr = CS.System.Array.CreateInstance(typeof(CS.System.Int32),5)
    Log(arr.Length)
    Log(arr[1])
    
    Log("******************")
    --调用List,调用成员方法用:
    obj.list:Add('M')
    for i = 0,obj.list.Count-1 do
        Log(obj.list[i])
    end
    
    Log("******************")
    --创建List
    --老版,方法很麻烦
    local list2 = CS.System.Collections.Generic["List`1[System.String]"]()
    list2:Add("abcde")
    Log(list2[0])
    --新版 版本>v2.1.12  先创建一个类,再实例化出来list
    local List_String = CS.System.Collections.Generic.List(CS.System.String)
    local list3 = List_String()
    list3:Add("aaaaaaaaaa")
    Log(list3[0])
    
    Log("******************")
    --调用dic
    obj.dic:Add(1,"abc")
    obj.dic:Add(2,"def")
    --遍历
    for k,v in pairs(obj.dic) do
        Log(k.."--"..v)
    end
    
    Log("******************")
    --创建dic
    local Dic_String_Vector3 = CS.System.Collections.Generic.Dictionary(CS.System.String,CS.UnityEngine.Vector3)
    local dic2 = Dic_String_Vector3()
    dic2:Add("abc",CS.UnityEngine.Vector3.right)
    dic2:Add("def",CS.UnityEngine.Vector3.up)
    
    Log(dic2["abc"])  --在lua中创建的字典使用这种方式得不到值,这句代码打印出的结果是空值
    Log(dic2:get_Item("abc"))  --在lua中自己创建的字典使用get_Item方法得到值
    dic2:set_Item("abc",CS.UnityEngine.Vector3.left)  --同样地,通过set_Item方法设置字典地值
    Log(dic2:get_Item("abc"))
    print(dic2:TryGetValue("abc"))  --也可以通过TryGetValue方法获取值
    
    for k,v in pairs(dic2) do
        print(k,v)
    end
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public string[] array = { "a","b","c","d","e","f","g","h" };
        public List<char> list = new List<char>{ 'A','B','C','D','E','F','G','H','I','J' };
        public Dictionary<int, string> dic = new Dictionary<int, string>();
    }

      5.使用C#拓展方法

    CustomClass = CS.CustomClass
    
    --使用成员方法
    local customClass = CustomClass()
    customClass.name = "movin"
    customClass:Eat()
    
    --使用拓展方法,拓展方法一定是静态方法,但是调用时和成员方法一样的调用方式
    --在定义拓展方法的工具类前一定加上特性[LuaCallCSharp],并且生成代码
    customClass:Move()
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public string name;
        public void Eat()
        {
            Debug.Log(name + "在吃饭");
        }
    }
    /// <summary>
    /// 工具类,定义拓展方法
    /// </summary>
    [LuaCallCSharp]
    public static class Tools
    {
        public static void Move(this CustomClass cc)
        {
            Debug.Log(cc.name + "在移动");
        }
    }

      

      建议:要在lua中使用的C#类都可以加上[LuaCallCSharp]特性,这样预先将代码生成,可以提高Lua访问C#类的性能。

      6.使用含有ref和out参数的函数

    CustomClass = CS.CustomClass
    local obj = CustomClass()
    
    
    --ref参数,使用多返回值形式接收
    --如果函数有返回值,这个返回值是多返回值的第一个
    --参数数量不够,会默认使用默认值补位
    local a,b,c = obj:RefFun(1,0,0,1)
    print(a,b,c)
    
    --out参数,还是以多返回值的形式接收
    --out参数不需要传递值
    local a,b,c = obj:OutFun(23,24)
    print(a,b,c)
    
    --综合来看
    --从返回值上看,ref和out都会以多返回值的形式返回,原来如果有返回值的话原来的返回值是多返回值中的第一个
    --从参数看,ref参数需要传递,out参数不需要传递
    local a,b,c = obj:RefOutFun(12,23)
    print(a,b,c)
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public int RefFun(int a ,ref int b,ref int c,int d)
        {
            b = a + d;
            c = a - d;
            return 100;
        }
        public int OutFun(int a,out int b,out int c,int d)
        {
            b = a;
            c = d;
            return 200;
        }
        public int RefOutFun(int a,out int b,ref int c)
        {
            b = a * 10;
            c = a * 20;
            return 200;
        }
    }

      7.使用重载函数

    CustomClass = CS.CustomClass
    local customClass = CustomClass()
    
    --使用重载函数
    --lua支持调用C#的重载函数
    --lua中的数值类型只有number,所以对C#中多精度的重载函数支持不好,使用时可能出现问题
    --如第四个重载函数调用结果为0(应该是11.4),所以应避免这种情况
    print(customClass:Calc())
    print(customClass:Calc(1))
    print(customClass:Calc(2,3))
    print(customClass:Calc(1.4))
    
    --解决重载函数含糊的问题(效率低,仅作了解)
    --运用反射
    local m1 = typeof(CustomClass):GetMethod("Calc",{typeof(CS.System.Int32)})
    local m2 = typeof(CustomClass):GetMethod("Calc",{typeof(CS.System.Single)})
    --通过xlua提供的tofunction方法将反射得到的方法信息转化为函数
    local f1 = xlua.tofunction(m1)
    local f2 = xlua.tofunction(m2)
    --再次调用函数,非静态方法需要传入对象
    print(f1(customClass,10))
    print(f2(customClass,1.4))
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public int Calc()
        {
            return 100;
        }
        public int Calc(int a,int b)
        {
            return a + b;
        }
        public int Calc(int a)
        {
            return a;
        }
        public float Calc(float a)
        {
            return a + 10;
        }
    }

      8.委托和事件

    local customClass = CS.CustomClass()
    
    --委托中存储的是函数,声明函数存储到委托中
    local fun = function()
        print("函数fun")
    end
    
    --委托中第一次添加函数使用=添加
    customClass.action = fun
    --委托中第二次添加函数使用+=,lua中不支持+=运算符,需要分开写
    customClass.action = customClass.action + fun
    --委托中也可以添加匿名函数
    customClass.action = customClass.action + function()
        print("临时函数")
    end
    
    --使用点调用委托还是冒号调用委托都可以调用,最好使用冒号
    customClass:action()
    
    print("********************")
    
    --事件和委托的使用方法不一致(事件不能在外部调用)
    --使用冒号添加和删除函数,第一个参数传入加号或者减号字符串,表示添加还是修改函数
    --事件也可以添加匿名函数
    customClass:eventAction("+",fun)
    --事件不能直接调用,必须在C#中提供调用事件的方法,这里已经提供了DoEvent方法执行事件
    customClass:DoEvent()
    --同样地,事件不能直接清空,需要在C#中提供对应地方法
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public UnityAction action;
        public event UnityAction eventAction;
        public void DoEvent()
        {
            if (eventAction != null)
                eventAction();
        }
    }

      9.特殊问题

    local customClass = CS.CustomClass()
    
    --特殊问题一:得到二维数组指定位置元素的值
    --获取二维数组的长度
    print("行:"..customClass.array:GetLength(0))
    print("行:"..customClass.array:GetLength(1))
    
    --不能通过C#的索引访问元素(array[0,0]或array[0][0])
    --使用数组提供的成员方法GetValue访问元素
    print(customClass.array:GetValue(0,0))
    
    --遍历
    for i=0,customClass.array:GetLength(0)-1 do
        for j=0,customClass.array:GetLength(1)-1 do
            print(customClass.array:GetValue(i,j))
        end
    end
    
    
    print("***********************")
    
    --特殊问题二:lua中空值nil和C#中空值null的比较
    
    --往场景对象上添加一个脚本,存在就不加,不存在再加
    GameObject = CS.UnityEngine.GameObject
    Rigidbody = CS.UnityEngine.Rigidbody
    
    local obj = GameObject("测试nil和null")
    local rigidbody = obj:GetComponent(typeof(Rigidbody))
    print(rigidbody)
    --校验空值,看是否需要添加脚本
    --nil和null并不相同,在lua中不能使用==进行判空,一定要使用Equals方法进行判断
    --这里如果rigidbody为空,可能报错,所以可以自己提供一个判空函数进行判空
    --这里为了笔记方便将函数定义在这里,这个全局函数最好定义在lua脚本启动的主函数Main中
    function IsNull(obj)
        if obj == nil or obj:Equals(nil) then
            return true
        end
        return false
    end
    --使用自定义的判空函数进行判断
    if IsNull(rigidbody) then
        rigidbody = obj:AddComponent(typeof(Rigidbody))
    end
    print(rigidbody)
    
    print("***********************")
    
    --特殊问题三:让lua和系统类型能够相互访问
    
    --对于自定义的类型,可以添加CSharpCallLua和LuaCallCSharp这两个特性使Lua和自定义类型能相互访问,但是对于系统类或第三方代码库,这种方式并不适用
    --为系统类或者第三方代码库加上这两个特性的写法比较固定,详情见C#代码
    /// <summary>
    /// 自定义类
    /// </summary>
    public class CustomClass
    {
        public int[,] array = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
    
        //实现为系统类添加[CSharpCallLua]和[LuaCallCSharp]特性
        [CSharpCallLua]
        public static List<Type> csharpCallLuaList = new List<Type>()
        {
            //将需要添加特性的类放入list中
            typeof(UnityAction<float>),
        };
        [LuaCallCSharp]
        public static List<Type> luaCallCsharpList = new List<Type>()
        {
            typeof(GameObject),
        };
    }

       10.使用协程

    --xlua提供了一个工具表,要使用协程必须先调用这个工具表
    util = require("xlua.util")
    
    GameObject = CS.UnityEngine.GameObject
    WaitForSeconds = CS.UnityEngine.WaitForSeconds
    
    local obj = GameObject("Coroutine")
    local mono = obj:AddComponent(typeof(CS.LuaCallCSharp))
    
    --被开启的协程函数
    fun = function()
        local a = 1
        while true do
            --lua中不能直接使用C#中的yield return返回
            --使用lua中的协程返回方法
            coroutine.yield(WaitForSeconds(1)) 
            print(a)
            a = a + 1
            if a>10 then 
                --协程的关闭,必须要将开启的协程存储起来
                mono:StopCoroutine(startedCoroutine)
            end
        end
    end
    
    --启动协程
    --写法固定,必须使用固定表的cs_generate方法把xlua方法处理成mono能够使用的协程方法
    startedCoroutine = mono:StartCoroutine(util.cs_generator(fun))

      11.使用泛型函数

        lua中没有泛型语法,对于C#中的泛型方法,可以直接传递参数(因为lua中不需要声明类型),但是这种写法并不是所有的泛型方法都支持,xlua只支持有约束且泛型作为参数的泛型函数,其他泛型函数不支持。如果要在lua中调用泛型函数,可以使用特定的语法。

    local tank = CS.UnityEngine.GameObject.Find("Tank")
    
    --xlua提供了得到泛型函数的方法get_generic_method,参数第一个为类名,第二个为方法名
    local addComponentFunc = xlua.get_generic_method(CS.UnityEngine.GameObject,"AddComponent")
    --接着调用这个泛型方法,参数为泛型的类,得到一个新方法
    local addComponentFunc2 = addComponentFunc(CS.MonoForLua)
    --调用,第一个参数是调用的对象,如果有其他参数在后面传递
    addComponentFunc2(tank)

        使用限制:打包时如果使用mono打包,这种方式支持使用;如果使用il2cpp打包,泛型参数需要是引用类型或者是在C#中已经调用过的值类型。

  • 相关阅读:
    面试题:最小的K个数
    面试题:反转链表
    面试题:二进制中1的个数
    数据结构之二叉树
    Serializable-源码分析
    归并、希尔、快速排序
    栈与队列
    简单排序(冒泡、选择、插入)
    Shiro 框架认证和授权
    mybatis select查询使用返回结果为resultMap
  • 原文地址:https://www.cnblogs.com/movin2333/p/14617291.html
Copyright © 2011-2022 走看看