第1章: Lua热更新方案概述

热更新解决方案

什么是热更新

(1)游戏上线后,在运营过过程中,如果需要更换UI显示,或者修改游戏的逻辑行为。传统的更新模式下,需要重新打包游戏,让玩家重新下载包体,造成用户体验不佳的情况。
 (2)热更新允许在不重新下载游戏客户端的情况下,更新游戏内容。

第2章: AssetBundle

需要注意的地方

AssetBundle在高版本unity中已经被集成到了Addressables当中(可以理解为优化版本的AssetBundle,更加智能和方便管理更新),本文讲解的AssetBundle依然是单纯的老版本下存粹版本的AssetBundle,用来理解AssetBundle的原理和应对某些上古项目。

如何安装老版本的AB包

1.先打开目录中的Packages文件夹,删除里面的manifest.json文件,如果不删除,会报错,这是Unity的包管理文件,删除了会重新生成。
2.打开Package Manager,在左上角+那里通过git url添加:
https://github.com/Unity-Technologies/AssetBundles-Browser.git
随后便会安装成功。

AB包理论基础

AB包资源打包

AB包打包只需要在编辑器里面进行实现即可
打包页面:

参数解析:

打包完毕后,如果勾选了Copy to StreamingAssets,你会发现Streaming里面出现了许多的包
这就是AB包打包出来的结构,里面有一个主包(存储所有包之间的依赖关系,用来方便获取配置文件构建包之间的依赖)。以及一大堆AB资源包(用来获取游戏资源)。

可参考资料:
(23条消息) AB包资源打包-AssetBundles-Browser-master(工具)_我和BUG只能活一个的博客-CSDN博客

(23条消息) Unity中的AssetBundle_unity assetbundle_流浪打工人的博客-CSDN博客

AB包资源加载

同步加载

加载AB包中所需要的资源,需要用代码实现。
根据打包时候对应资源打进的包名,获取对应的AB包:
AssetBundle ab=AssetBundle.LoadFromFile(string 包地址);
获取AB包里面相应的物体,便得到了游戏对象
只用名字去加载,会出现同名不同类型资源分不清的物体,所以建议用下面两种方法去获得:
1.游戏对象类型 gam=AB.LoadAsset(String “对象名”) as 游戏对象类型;
2.游戏对象类型 gam=AB.LoadAsset<游戏对象类型>(String “对象名”);

对象gam此时被加载到内存当中,接下来就可以进行实例化等操作。

异步加载

//异步加载——>协程
        StartCoroutine(LoadABRes("icon", "Sphere"));

  IEnumerator LoadABRes(string ABname,string resName)
    {
    //下面一句使用特殊的对象 AssetBundleCreateRequest去接收,用来让yield return识别完成程度
        AssetBundleCreateRequest ab = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + ABname);
        yield return ab;//包加载后继续

  //下面一句使用特殊的对象 AssetBundleRequest去接收,用来让yield return识别完成程度
        AssetBundleRequest abq = ab.assetBundle.LoadAssetAsync(resName, typeof(GameObject));
        yield return abq;//对象加载后继续
        GameObject obj = abq.asset as GameObject;//转换对象为对应游戏物体类型
        Instantiate(obj);//实例化对象
    }

AB包卸载

需要强调的是,AB包如果多次加载同一个包就会报错,所以,有时候会碰到需要卸载AB包的情况:

  • 卸载所有加载的AB包,如果参数为true,则会把所有通过AB包加载的资源也卸载,所以一般都是用false
 AssetBundle.UnloadAllAssetBundles(false);
  • 对单个包卸载 参数作用和全部卸载一样 不建议填true
ab.Unload(false);

AB包依赖

获取包依赖

例如导入一个预制体的时候,预制体可能有图片,如果只把这个预制体打进AB包,那这个预制体的图片就没有打入AB包内,因此,要额外将图片导入AB包。如果图片导入的不是同一个包而是另一个包,那么,这两个AB包就有依赖关系。依赖关系为:放有预制体的AB包依赖放有其他组件的AB包。

那么,获取依赖就是一个问题,我们无法指望把所有包的依赖记得清清楚楚,所以我们可以使用主包信息中携带的依赖关系去进行有依赖关系的对象的实例化,代码如下:

  //加载主AB包(不是ab文件夹,是ab文件!)
        AssetBundle main = AssetBundle.LoadFromFile("主AB包‘路径’(无后缀名)");
 
        //获取AB包的配置文件,这里为固定写法获取配置文件,没必要深究
        AssetBundleManifest manifest = main.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
 
        //分析预制体所在AB包,依赖哪些AB包
        //需要注意的是,这里只需要包名,而不是路径!
        string [] deps= manifest.GetAllDependencies("预制体AB包‘名’(无后缀名)");
       
        
        //加载依赖的AB包
        for(int i=0;i<deps.Length;i++)
        {
            AssetBundle.LoadFromFile("存放所有文件夹的路径/" + deps[i]);
        }
 
        //加载预制体所在的AB包
        AssetBundle imageab= AssetBundle.LoadFromFile("预制体AB包路径(无后缀名)");
 
 
        //加载预制体(内部资源)
        GameObject prefab = imageab.LoadAsset<GameObject>("预制体名字");
 
        //实例化
        GameObject gob = Instantiate(prefab);
        //设置父物体
        gob.transform.SetParent(GameObject.Find("/Canvas").transform);

注意

我们只能获取整个包和其他包的依赖关系,而不能获取包中物体和其他包的依赖关系。
这会导致大部分情况下我们会加载多余的依赖包,这是无法避免的,因为配置文件只记载了包间关系,而没有包中物体的依赖关系。

参考资料:
(23条消息) unity|加载AB包|有依赖关系的AB包_unity加载ab包_Jooth的博客-CSDN博客

AB包资源管理器

由于ab包依赖和卸载的相关问题,使用单例类写一个AB包管理器是一个很好的使用方式,下面就将管理器代码贴上。

构建单例类SingletonAutoMono:

//C#中 泛型知识点  
//设计模式 单例模式的知识点  
//继承这种自动创建的 单例模式基类 不需要我们手动去拖 或者 api去加了  
//想用他 直接 GetInstance就行了  
public class SingletonAutoMono<T> : MonoBehaviour where T : MonoBehaviour  
{  
  private static T instance;  
  
  public static T GetInstance()  
 {  if( instance == null )  
 {  GameObject obj = new GameObject();  
  //设置对象的名字为脚本名  
  obj.name = typeof(T).ToString();  
  //让这个单例模式对象 过场景 不移除  
  //因为 单例模式对象 往往 是存在整个程序生命周期中的  
  DontDestroyOnLoad(obj);  
  instance = obj.AddComponent<T>();  
 }  return instance;  
 }  
}

AB包管理类ABmgr:
拥有4种主要函数:
1.同步加载AB包资源(三种重载): LoadRes()
2.异步加载AB包资源(三种重载):LoadResAsync()
3.卸载对应AB包:UnLoadAB()
4.清空所有AB包:ClearAB()

//继承SingletonAutoMono<T>构建单例类ABMgr
public class ABMgr : SingletonAutoMono<ABMgr>
{
    //主包
    private AssetBundle mainAB = null;
    //主包依赖获取配置文件
    private AssetBundleManifest manifest = null;

    //选择存储 AB包的容器
    //AB包不能够重复加载 否则会报错
    //字典知识 用来存储 AB包对象
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl
    {
        get
        {
            return Application.streamingAssetsPath + "/";
        }
    }

    /// <summary>
    /// 主包名 根据平台不同 报名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
//注意,老版本这里是PC,2021以上有可能默认打包名是StandaloneWindows
            return "pc";
#endif
        }
    }

    /// <summary>
    /// 加载主包 和 配置文件
    /// 因为加载所有包是 都得判断 通过它才能得到依赖信息
    /// 所以写一个方法
    /// </summary>
    private void LoadMainAB()
    {
        if( mainAB == null )
        {
        mainAB = AssetBundle.LoadFromFile( PathUrl + MainName);
            //获取配置文件
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 加载指定包的依赖包
    /// </summary>
    /// <param name="abName"></param>
    private void LoadDependencies(string abName)
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                abDic.Add(strs[i], ab);
            }
        }
    }

    /// <summary>
    /// 泛型资源同步加载
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <returns></returns>
    public T LoadRes<T>(string abName, string resName) where T:Object
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if ( !abDic.ContainsKey(abName) )
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        T obj = abDic[abName].LoadAsset<T>(resName);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// Type同步加载指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="type"></param>
    /// <returns></returns>
    public Object LoadRes(string abName, string resName, System.Type type) 
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        Object obj = abDic[abName].LoadAsset(resName, type);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// 名字 同步加载指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <returns></returns>
    public Object LoadRes(string abName, string resName)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        Object obj = abDic[abName].LoadAsset(resName);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// 泛型异步加载资源
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T:Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack));
    }
    //正儿八经的 协程函数
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset) as T);
        else
            callBack(abq.asset as T);
    }

    /// <summary>
    /// Type异步加载资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="type"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName, type);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset));
        else
            callBack(abq.asset);
    }

    /// <summary>
    /// 名字 异步加载 指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, callBack));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callBack)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset));
        else
            callBack(abq.asset);
    }

    //卸载AB包的方法
    public void UnLoadAB(string name)
    {
        if( abDic.ContainsKey(name) )
        {
            abDic[name].Unload(false);
            abDic.Remove(name);
        }
    }

    //清空AB包的方法
    public void ClearAB()
    {
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        //卸载主包
        mainAB = null;
    }
}

第3章: Lua语法

环境搭建

Lua需要按照Lua编程包后进行使用
安装地址:Releases · rjpcomputing/luaforwindows (github.com)

本博客使用SubLime这个轻量化编程器来充当lua编程器。

导入——语法大纲

打印函数和注释

注意:lua在句子末尾可以加分号,也可以不加。

知识点

--单行注释 print打印函数 
--lua语句 可以省略分号
print("你好世界");
print("唐老狮欢迎你")

--[[
多行
注释
]]

--[[
第二种
多行
注释
]]--

--[[
第三种
多行
注释
--]]

变量

大纲

知识点

rint("**********变量************")
--lua当中的简单变量类型
-- nil number string boolean
--lua中所有的变量申明 都不需要申明变量类型 他会自动的判断类型
--类似C# 里面的 var
--lua中的一个变量 可以**随便赋值** ——自动识别类型
--通过 type 函数 返回值时string 我们可以得到变量的类型

--lua中使用没有声明过的变量 
--不会报错 默认值 是nil
print(b)

--nil 类似 C#中的null
print("**********nil************")
a = nil
print(a)
print(type(a))
print(type(type(a)))
--number 所有的数值都是number
print("**********number************")
a = 1
print(a)
print(type(a))
a = 1.2
print(a)
print(type(a))
print("**********string************")
a = "12312"
print(a)
print(type(a))
--字符串的声明 使用单引号或者双引号包裹
--lua里 没有char
a = '123'
print(a)
print(type(a))
print("**********boolean************")
a = true
print(a)
a = false
print(a)
print(type(a))

--复杂数据类型
--函数 function
--表 table
--数据结构 userdata
--协同程序 thread(线程)

字符串操作

大纲

知识点

print("**********字符串************")
str = "双引号字符串"
str2 = '单引号字符串'

--获取字符串的长度
print("**********字符串长度************")
s = "aBcdEfG字符串"
--一个汉字占3个长度
--英文字符 占1个长度
print(#s)

print("**********字符串多行打印************")
--lua中也是支持转义字符的
print("123\n123")

s = [[我是
唐
老师
]]
print(s)

print("**********字符串拼接************")
--字符串拼接 通过..
print( "123" .. "456" )
s1 = 111
s2 = 111
print(s1 .. s2)

print(string.format("我是唐老狮,我今年%d岁了", 18))
--%d :与数字拼接
--%a:与任何字符拼接
--%s:与字符配对
--.......
print("**********别的类型转字符串************")
a = true
print(tostring(a))

print("**********字符串提供的公共方法************")
str = "abCdefgCd"
--小写转大写的方法
print(string.upper(str))
--大写转小写
print(string.lower(str))
--翻转字符串
print(string.reverse(str))
--字符串索引查找
print(string.find(str, "Cde"))
--截取字符串
print(string.sub(str, 3, 4))
--字符串重复
print(string.rep(str, 2))
--字符串修改
print(string.gsub(str, "Cd", "**"))

--字符转 ASCII码,第二个参数指位置
a = string.byte("Lua", 1)
print(a)
--ASCII码 转字符 输出L
print(string.char(a))

注意

1.lua下标从1开始,而不是0。
2.lua的返回值可以直接是多个

运算符

大纲

知识点

print("**********运算符************")
print("**********算数运算符************")
-- + - * / % ^
-- 没有自增自减 ++ --
-- 没有复合运算符 += -= /= *= %=
--字符串如果不用..进行连接,就可以直接和数字进行算数运算符操作 会自动把字符串转成number
print("加法运算" .. 1 + 2)
a = 1
b = 2

a = a + b
a = a + 1
print(a + b)
print("123.4" + 1)

print("减法运算" .. 1 - 2)
print("123.4" - 1)

print("乘法运算" .. 1 * 2)
print("123.4" * 2)

print("除法运算" .. 1 / 2)
print("123.4" / 2)

print("取余运算" .. 1 % 2)
print("123.4" % 2)

--^ lua中 该符号 是幂运算
print("幂运算" .. 2 ^ 5)
print("123.4" ^ 2)

print("**********条件运算符************")
-- > < >= <= == ~=
print(3>1)
print(3<1)
print(3>=1)
print(3<=1)
print(3==1)
--不等于 是 ~=
print(3~=1)

print("**********逻辑运算符************")
--lua的逻辑运算符和C#完全不一样
--and(&&)  or(||)  not(!)   lua中 
--也遵循逻辑运算的 “短路” 规则,即and种前者为false,直接不执行后者内容。
print( true and false)
print( true and true)
print( false and true)

print( true or false)
print( false or false)

print( not true)

print("**********位运算符************")
-- & | 不支持位运算符 需要我们自己实现

print("**********三目运算符************")
-- ? :  lua中 也不支持 三目运算

条件分支语句

大纲

知识点

print("**********条件分支语句************")
a = 9
--if 条件 then.....end
--单分支
if a > 5 then
	print("123")
end

--双分支
-- if 条件 then.....else.....end
if a < 5 then
	print("123")
else
	print("321")
end

--多分支
-- if 条件 then.....elseif 条件 then....elseif 条件 then....else.....end
if a < 5 then
	print("123")
--lua中 elseif 一定是连这些 否则报错
elseif a == 6 then
	print("6")
elseif a == 7 then
	print("7")
elseif a == 8 then
	print("8")
elseif a == 9 then
	print("9")
else
	print("other")
end


if a >= 3 and a <= 9 then
	print("3到9之间")
end

--lua中没有switch语法  需要自己实现

循环语句

大纲

知识点

print("**********循环语句************")

print("**********while语句************")
num = 0
--while 条件 do ..... end
while num < 5 do
	print(num)
	num = num + 1
end
print("**********do while语句************")
num = 0
--repeat ..... until 条件 (注意:条件是结束条件)
repeat
	print(num)
	num = num + 1
until num > 5 --满足条件跳出 结束条件

print("**********for语句************")

for i =2,5 do --默认递增 i会默认+1
	print(i)
end

for i =1,5,2 do --如果要自定义增量 直接逗号后面写
	print(i)
end

for i =5,1,-1 do --如果要自定义增量 直接逗号后面写
	print(i)
end

函数(变长,嵌套闭包,多返回值)

大纲

知识点

print("**********函数************")
--function 函数名()
--end

--a = function()
--end
print("**********无参数无返回值************")
function F1()
	print("F1函数")
end
F1()
--有点类似 C#中的 委托和事件
F2 = function()
	print("F2函数")
end
F2()

print("**********有参数************")
function F3(a)
	print(a)
end
F3(1)
F3("123")
F3(true)
--如果你传入的参数 和函数参数个数不匹配
--不会报错 只会补空nil 或者 丢弃
F3()
F3(1,2,3)
print("**********有返回值************")
function F4(a)
	return a, "123", true
end
--多返回值时 在前面申明多个变量来接取即可
--如果变量不够 不影响 值接取对应位置的返回值
--如果变量多了 lua也不会报错 而是直接赋nil
temp, temp2, temp3, temp4 = F4("1")
print(temp)
print(temp2)
print(temp3)
print(temp4)

print("**********函数的类型************")
--函数类型 就是 function
F5 = function( )
	print("123")
end
print(type(F5))

print("**********函数的重载************")
--函数名相同 参数类型不同 或者参数个数不同
--lua中 函数不支持重载 
--默认调用最后一个声明的函数
function F6()
	print("唐老狮帅帅的")
end
function F6(str)
	print(str)
end

F6()

print("**********变长参数************")
function F7( ... )
	--变长参数使用 用一个表存起来 再用
	arg = {...}
	for i=1,#arg do
		print(arg[i])
	end
end
F7(1,"123",true,4,5,6)

print("**********函数嵌套************")
function F8()
	return function()
		print(123);
	end
end
f9 = F8()
f9()

--闭包
function F9(x)
	--改变传入参数的生命周期
	return function(y)
		return x + y
	end
end

f10 = F9(10)
print(f10(5))

table表实现数组

大纲

知识点

print("**********复杂数据类型 talbe************")
--所有的复杂类型都是table(表)
print("**********数组************")
a = {1,2,nil,3,"1231",true,nil}
--lua中 索引从1开始
print(a[1])
print(a[5])
print(a[6])
print(a[7])
--#是通用的获取长度的关键字
--在打印长度的时候 空被忽略
--如果表中(数组中)某一位变成nil 会影响#获取的长度
print(#a)
print("**********二维数组************")
aa={{1,2,3}{4,5,6}}
print(aa[1][2]) --结果为2
print("**********自定义索引************")
aaa={[1]=1,[2]=2,3,[4]=5}
--并不建议为自定义索引赋多个值或者不连续的数组,否则会有一些令人意外的事情发生
--如上文aaa[2]被赋予了2个值,这并不会让aaa[2]本身有了两个值,
--而是2会被赋值给aaa[2],3会被lua自动找一个赋值aaa
--然后你就会惊恐的发现,aaa[1]=3,而我们找不到1这个值了,[1]=1大概率是被覆盖了

注意

1.lua所有复杂类型都是表
2.如果数组中混入了nil,可能会导致#获取的长度不准确,这是由于不同的长度算法不同导致的。
参考:lua table 长度解析 | 菜鸟教程 (runoob.com)
(24条消息) lua中#取table长度的一些坑以及如何改良_lua #tab 复杂度_我虽浪迹天涯的博客-CSDN博客
3.除非可以保证数组的连续性,否则不建议使用#来遍历表,而是使用迭代器遍历,下一个知识点会讲解迭代器遍历。

迭代器遍历(ipairs和pairs区别)

大纲

知识点

print("**********迭代器遍历************")
--迭代器遍历 主要是用来遍历表的
--#得到长度 其实并不准确 一般不要用#来遍历表

a = {[0] = 1, 2, [-1]=3, 4, 5, [5] = 6}

print("**********ipairs迭代器遍历************")
--ipairs
--ipairs遍历 还是 从1开始往后遍历的 小于等于0的值得不到
--只能找到连续索引的 键 如果中间断序了 它也无法遍历出后面的内容
for i,k in ipairs(a) do
	print("ipairs遍历键值"..i.."_"..k)
end
print("**********ipairs迭代器遍历键************")
for i in ipairs(a) do
	print("ipairs遍历键"..i)
end

print("**********pairs迭代器遍历************")
--它能够把所有的键都找到 通过键可以得到值
for i,v in pairs(a) do
	print("pairs遍历键值"..i.."_"..v)
end

print("**********pairs迭代器遍历键************")
for i in pairs(a) do
	print("pairs遍历键"..i)
end

table表实现字典

大纲

知识点

print("**********复杂数据类型——表2************")
print("**********字典************")
print("**********字典的申明************")
--字典是由键值对构成 
a = {["name"] = "唐老湿", ["age"] = 14, ["1"] = 5}
--访问当个变量 用中括号填键 来访问
print(a["name"])
print(a["age"])
print(a["1"])
--还可以类似.成员变量的形式得到值
print(a.name)
print(a.age)
--虽然可以通过.成员变量的形式得到值 但是不能是数字
print(a["1"])
--修改
a["name"] = "TLS";
print(a["name"])
print(a.name)
--新增
a["sex"] = false
print(a["sex"])
print(a.sex)
--删除
a["sex"] = nil
print(a["sex"])
print(a.sex)
print("**********字典的遍历************")
--如果要模拟字典 遍历一定用pairs
for k,v in pairs(a) do
	--print可以传多个参数 一样可以打印出来,并且会自动空格
	print(k,v)
end

for k in pairs(a) do
	print(k)
	print(a[k])
end

--只获得值的一种常见写法(实际上_就是键的变量名,只不过这么写容易看出来是被忽略了)
for _,v in pairs(a) do
	print(_, v)
end

table表实现类(点和冒号的区别,self)

大纲

知识点

print("**********类和结构体************")

--Lua中是默认没有面向对象的 需要我们自己来实现
--成员变量 成员函数。。。。
Student = { 
	--年龄
	age = 1, 
	--性别
	sex = true,
	--成长函数
	Up = function()
		--这样写 这个age 和表中的age没有任何关系 它是一个全局变量
		--print(age)

		--想要在表内部函数中 调用表本身的属性或者方法
		--一定要指定是谁的 所以要使用 表名.属性 或 表名.方法
		print(Student.age)
		print("我成长了")
	end,
	--学习函数
	Learn = function(t)
		--第二种 能够在函数内部调用自己属性或者方法的 方法
		--把自己作为一个参数传进来 在内部 访问
		print(t.sex)
		print("好好学习,天天向上")
	end
}

--Lua中 .和冒号的区别
Student.Learn(Student)
--冒号调用方法 会默认把调用者(即student) 作为第一个参数传入方法中
Student:Learn()

--申明表过后 在表外去申明表有的变量和方法
Student.name = "唐老狮"
Student.Speak = function()
	print("说话")
end
--函数的第三种申明方式
function Student:Speak2()
	--lua中 有一个关键字 self 表示 默认传入的第一个参数
	print(self.name .. "说话")
end

--C#要是使用类 实例化对象new 静态直接点
--Lua中类的表现 更像是一个类中有很多 静态变量和函数
print(Student.age)
print(Student.name)
Student.Up()
Student.Speak()
Student:Speak2()
Student.Speak2(Student)

注意

1.lua的类和方法都是类似静态的存在,因为没有new,所以全局只有一个,甚至还有先后顺序的要求。

2.请搞清楚冒号和self的用途:
如果你试图构造一个调用表内变量的表内函数,如知识点里面提到的方式,你可以使用冒号或者点号两种方法
冒号和self是搭配使用的
冒号只是一种语法机制,提供的是便利性,并没有引入任何新的东西。使用冒号完成的事情,都可以使用点语法来完成。
两者有什么区别呢?

一. lua 为冒号提供了独有的指令:self,self.变量名可引用所在的表内的局部变量。
二. 从用法上来说,使用点语法实现需要传入表对象,而冒号就显得较为简洁了。

参考资料:(24条消息) Lua中self 、自索引及其面向对象应用代码示例_lua self_基层搬砖的Panda的博客-CSDN博客

3.表构造器内的函数声明和构造器外的函数声明是有所不同的。
表构造器里面定义一个变量,必须有=赋值这种行为,否则会报语法错误。表本身是无法通过function 函数名()直接识别函数名的,因为这样的写法会被解释为调用函数,而不是定义函数。
这导致了,你如果想要使用冒号定义函数,除非你用方括号来定义变量,否则 你只能在表外定义冒号函数。

4.可以在表构造器下面继续添加定义表内部的变量,包括函数。

table表的公共操作(插入,移除,排序,拼接)

大纲

知识点

print("**********表的公共操作************")
--表中 table提供的一些公共方法的讲解

t1 = { {age = 1, name = "123"}, {age = 2, name = "345"} }

t2 = {name = "唐老狮", sex = true}
print("**********插入************")
--插入
print(#t1)
table.insert(t1, t2);
print(#t1)
print(t1[1])
print(t1[2])
print(t1[3])
print(t1[3].sex)
print("**********移除************")
--删除指定元素
--remove方法 传表进去 会移除最后一个索引的内容
table.remove(t1)
print(#t1)
print(t1[1].name)
print(t1[2].name)
print(t1[3])

--remove方法 传两个参数 第一个参数 是要移除内容的表
--第二个参数 是要移除内容的索引
table.remove(t1, 1)
print(t1[1].name)
print(#t1)
print("**********排序************")
t2 = {5,2,7,9,5}
--传入要排序的表 默认 降序排列
table.sort(t2)
for _,v in pairs(t2) do
	print(v)
end
print("**********降序************")
--传入两个参数 第一个是用于排序的表
--第二个是 排序规则函数
--类似C#的实现,可以自行去对比
table.sort(t2, function(a,b)
	if a > b then
		return true
	end
end)
for _,v in pairs(t2) do
	print(v)
end

print("**********拼接************")
tb = {"123", "456", "789", "10101"}
--连接函数 用于拼接表中元素 返回值 是一个字符串
str = table.concat(tb, ",")
print(str)

多脚本执行(require,package,_G)

大纲

知识点

为了演示,本次分为主要脚本和被主要脚本调用的外部脚本。

主要脚本:

print("**********多脚本执行************")
print("**********全局变量和本地变量************")
--全局变量
a = 1
b = "123"
for i = 1,2 do
	c = "唐老狮"
end
print(c)
--本地(局部)变量的关键字 local
for i = 1,2 do
	local d = "唐老狮"
	print("循环中的d"..d)
end
print(d)
fun = function()
	local tt = "123123123"
end
fun()
print(tt)
local tt2 = "555"
print(tt2)

print("**********多脚本执行************")
--关键字 require("脚本名") require('脚本名')
require('Test')
print(testA)
print(testLocalA)

print("**********脚本卸载************")
--如果是require加载执行的脚本 加载一次过后不会再被执行
require("Test")
--package.loaded["脚本名"]
--返回值是boolean 意思是 该脚本是否被执行
print(package.loaded["Test"])
--卸载已经执行过的脚本
package.loaded["Test"] = nil
print(package.loaded["Test"])

--require 执行一个脚本时  可以再脚本最后返回一个外部希望获取的内容
local testLA = require("Test")
print(testLA)

print("**********大G表************")
--_G表是一个总表(table) 他将我们申明的所有全局的变量都存储在其中
for k,v in pairs(_G) do
	print(k,v)
end
--本地变量 加了local的变量时不会存到大_G表中

外部脚本:

print("Test测试")
testA = "123"
local testLocalA = "456"
print(testLocalA)

--与主要脚本的local testLA = require("Test")关联
--通过返回值,让主要脚本得到了本来获得不到的本地变量print(testLA)
return testLocalA

特殊用法(多变量赋值,三目运算符)

大纲

知识点

print("**********特殊用法************")
print("**********多变量赋值************")
local a,b,c = 1,2,"123"
print(a)
print(b)
print(c)
--多变量赋值 如果后面的值不够 会自动补空
a,b,c = 1,2 
print(a)
print(b)
print(c) --nil
--多变量赋值 如果后面的值多了 会自动省略
a,b,c = 1,2,3,4,5,6
print(a)
print(b)
print(c)
print("**********多返回值************")
function Test()
	return 10,20,30,40
end
--多返回值时 你用几个变量接 就有几个值
--如果少了 就少接几个 如果多了 就自动补空
a,b,c = Test()
print(a)
print(b)
print(c)

a,b,c,d,e = Test()
print(a)
print(b)
print(c)
print(d)
print(e)--nil

print("**********and or************")
--逻辑与 逻辑或
-- and or 他们不仅可以连接 boolean 任何东西都可以用来连接
-- 在lua中 只有 nil 和 false 才认为是假
-- "短路"——对于and来说  有假则假  对于or来说 有真则真
-- 所以 他们只需要判断 第一个 是否满足 就会停止计算了
print( 1 and 2 )  --返回2
print( 0 and 1)   --返回1
print( nil and 1)  --返回nil
print( false and 2)  --返回false
print( true and 3)   --返回3

print( true or 1 )   --返回true
print( false or 1)   --返回1
print( nil or 2)     --返回2
--lua不支持三目运算符 
x = 1
y = 2
-- ? :
local res = (x>y) and x or y
print(res)

--(x>y) and x ——> x
-- x or y —— > x

--(x>y) and x ——> (x>y)
-- (x>y) or y ——> y


注意

lua的and和or是不同于普通语言只能返回ture和false的,而是可以直接返回两个比较变量的其中一个。(可以结合本节知识点里面我标注的返回值来进行理解)
规律为:
1.a and b:a为true时,返回b,否则返回a.

2.a or b:a为false时,返回b,否则返回a.

正因为如此,我们有了很多有意思的用法。
比如,尽管lua不支持原生的三目运算符,但是我们却可以直接构造一个三目运算符

 (比较条件) and 变量1 or 变量2

参考资料:(24条消息) Lua中and和or的用法和记忆方法_lua or_星际行走的博客-CSDN博客

协同程序(协程)

大纲

知识点

print("**********协同程序************")

print("**********协程的创建************")
--常用方式
--coroutine.create(),返回的是Thread
fun = function()
	print(123)
end
co = coroutine.create(fun)
--协程的本质是一个线程对象
print(co)
print(type(co))

--coroutine.wrap(),返回的是函数function
co2 = coroutine.wrap(fun)
print(co2)
print(type(co2))

print("**********协程的运行************")
--第一种方式 对应的 是通过 create创建的协程
coroutine.resume(co)
--第二种方式 直接用,因为返回的本来就是一个函数
co2()

print("**********协程的挂起************")
fun2 = function( )
	local i = 1
	while true do
		print(i)
		i = i + 1
		--协程的挂起函数
		print(coroutine.status(co3))
		print(coroutine.running())
		-- 这里可以返回出去函数,随后此协程被挂起
		coroutine.yield(i)
	end
end

co3 = coroutine.create(fun2)
--默认第一个返回值 是 协程是否启动成功
--第二个返回值 yield里面的返回值
isOk, tempI = coroutine.resume(co3)
print(isOk,tempI)
isOk, tempI = coroutine.resume(co3)
print(isOk,tempI)
isOk, tempI = coroutine.resume(co3)
print(isOk,tempI)

co4 = coroutine.wrap(fun2)
--这种方式的协程调用 也可以有返回值 只是没有默认第一个返回值了
print("返回值"..co4())
print("返回值"..co4())
print("返回值"..co4())

print("**********协程的状态************")
--coroutine.status(协程对象)
--dead 结束
--suspended 暂停
--running 进行中
print(coroutine.status(co3))
print(coroutine.status(co))

--这个函数可以得到当前正在 运行的协程的线程号
print(coroutine.running())

元表(__index,tostring,newindex)

大纲

知识点

print("**********元表************")
print("**********元表概念************")
--任何表变量都可以作为另一个表变量的元表
--任何表变量都可以有自己的元表(爸爸)
--当我们子表中进行一些特定操作时(不过lua官方里面并没有这种称呼)
--会执行元表中的内容
print("**********设置元表************")
meta = {}
myTable = {}
--设置元表函数
--第一个参数 子表
--第二个参数 元表(爸爸)
setmetatable(myTable, meta)

print("**********特定操作************")
print("**********特定操作-__tostring************")
meta2 = {
	--当子表要被当做字符串使用时 会默认调用这个元表中的tostring方法
	__tostring = function(t)
		return t.name
	end
}
myTable2 = {
	name = "唐老狮2"
}
--设置元表函数
--第一个参数 子表
--第二个参数 元表(爸爸)
setmetatable(myTable2, meta2)

print(myTable2)

print("**********特定操作-__call************")
meta3 = {
	--当子表要被当做字符串使用时 会默认调用这个元表中的tostring方法
	__tostring = function(t)
		return t.name
	end,
	--当子表被当做一个函数来使用时 会默认调用这个__call中的内容
	--当希望传参数时 一定要记住 默认第一个参数 是调用者自己
	__call = function(a, b)
		print(a)
		print(b)
		print("唐老狮好爱你")
	end
}
myTable3 = {
	name = "唐老狮2"
}
--设置元表函数
--第一个参数 子表
--第二个参数 元表(爸爸)
setmetatable(myTable3, meta3)
--把子表当做函数使用 就会调用元表的 __call方法
myTable3(1)

print("**********特定操作-运算符重载************")

meta4 = {
	--相当于运算符重载 当子表使用+运算符时 会调用该方法
	--运算符+
	__add = function(t1, t2)
		return t1.age + t2.age
	end,
	--运算符-
	__sub = function(t1, t2)
		return t1.age - t2.age
	end,
	--运算符*
	__mul = function(t1, t2)
		return 1
	end,
	--运算符/
	__div = function(t1, t2)
		return 2
	end,
	--运算符%
	__mod = function(t1, t2)
		return 3
	end,
	--运算符^
	__pow = function(t1, t2)
		return 4
	end,
	--运算符==
	__eq = function(t1, t2)
		return true
	end,
	--运算符<
	__lt = function(t1, t2)
		return true
	end,
	--运算符<=
	__le = function(t1, t2)
		return false
	end,
	--运算符..
	__concat = function(t1, t2)
		return "567"
	end

}
myTable4 = {age = 1}
setmetatable(myTable4, meta4)
myTable5 = {age = 2}
setmetatable(myTable5, meta4)

print(myTable4 + myTable5)
print(myTable4 - myTable5)
print(myTable4 * myTable5)
print(myTable4 / myTable5)
print(myTable4 % myTable5)
print(myTable4 ^ myTable5)

--如果要用条件运算符 来比较两个对象
--这两个对象的元表一定要一致 才能准确调用方法
print(myTable4 == myTable5)
print(myTable4 > myTable5)
print(myTable4 <= myTable5)

print(myTable4 .. myTable5)

print("**********特定操作-__index和__newIndex************")

meta6Father = {
	age = 1
}
meta6Father.__index = meta6Father

meta6 = {
	--age = 1
}
--__index的赋值 写在表外面来初始化
meta6.__index = meta6
--meta6.__index = {age = 2}

myTable6 = {}
setmetatable(meta6, meta6Father)
setmetatable(myTable6, meta6)
--得到元表的方法
print(getmetatable(myTable6))

--__index 当子表中 找不到某一个属性时 
--会到元表中 __index指定的表去找属性
print(myTable6.age)
--rawget 当我们使用它时,不再管元表。 会去找自己身上有没有这个变量
--myTable6.age = 1
print(rawget(myTable6, "age"))

--newIndex 当赋值时,如果赋值一个不存在的索引
--那么会把这个值赋值到newindex所指的表中 不会修改自己
meta7 = {}
meta7.__newindex = {}
myTable7 = {}
setmetatable(myTable7, meta7)
myTable7.age = 1
print(myTable7.age)
--rawset 该方法 会忽略newindex的设置 只会改自己的变量
rawset(myTable7, "age", 2)
print(myTable7.age)

注意

1.元表(metatable)是 Lua 中一种特殊的表,它允许我们改变一个表的行为。lua为常用的每个行为提供了对应元方法供我们书写。


2.你或许会有疑问,为什么运算符重载和tostring可以不指定元方法就自动的调用元表呢?
tostring本身就是一种运算符重载,应该一视同仁。还有就是,运算符重载本身就是一种元方法。


3.通常,我们把运算符重载直接写在元表内部,其他元方法则必须得写到外部,如指定__index之类的。


4.初学者经常会犯的错误
一.认为设置完毕元表后,子表会自动获取到自己没有而元表里面有的值,这是错误的!请你不要认为子表和元表有什么所谓的变量互通关系!你更应该把元表当作子表用来解读元方法的导航,或者官方一点的说法,元表规范了绑定的子表的行为。
因为设置元表后子表只是懂得去读元表里面的元方法了,得通过解读元表里面的元方法__index才能最终确定元表要找的值或者表!假如__index对应的是其他的表而不是元表,即使元表有对应的值,子表也获取不到!

二.直接写成 子表.__index=元表 ,这是错误的!
因为__index等所有的元方法都是直接赋值到元表中的!因为设置元表后子表只是懂得去读元表里面的元方法了!

这两个错误都可以通过看注意4加深理解。


5.子表与元方法的执行顺序(以触发了子表找不到的值为例子):
1.子类找不到某个函数或者变量
2.查询自身有没有元表,无则返回nil,有则查询元表
3.查询元表有没有元方法__index,无则返回nil,有则查看__index对应的表继续查询对应的表
4.查询对应的表有没有对应数据,有则返回值。无则继续进入步骤3。

建议参考资料:(24条消息) Lua元表_lua原表_宝贝琳琳果冻的博客-CSDN博客
(24条消息) lua面向对象-----继承的实现_lua 继承_攻城狮少年的博客-CSDN博客

(24条消息) 【Lua基础系列】__index元方法___index lua_Lampard猿奋的博客-CSDN博客

Lua面向对象——封装,继承,多态

大纲

知识点

print("**********面向对象************")
print("**********封装************")
--面向对象 类 其实都是基于 table来实现
--元表相关的知识点
Object = {}
Object.id = 1

function Object:Test()
	print(self.id)
end

--冒号 是会自动将调用这个函数的对象 作为第一个参数传入的写法
function Object:new()
	--self 代表的是 我们默认传入的第一个参数
	--对象就是变量 返回一个新的变量
	--返回出去的内容 本质上就是表对象
	--必须使用本地变量,否则会变成全局变量共用一表
	local obj = {}
	--元表知识 __index 当找自己的变量 找不到时 就会去找元表当中__index指向的内容
	self.__index = self
	setmetatable(obj, self)
	return obj
end

--local myObj = Object:new()
--print(myObj)
--print(myObj.id)
--myObj:Test()
--对空表中 申明一个新的属性 教做id
--myObj.id = 2
--myObj:Test()

print("**********继承************")
--C# class 类名 : 继承类
--写一个用于继承的方法
function Object:subClass(className)
	-- _G知识点 是总表 所有声明的全局标量 都以键值对的形式存在其中
	_G[className] = {}
	--写相关继承的规则
	--用到元表
	local obj = _G[className]
	self.__index = self
	--子类 定义个base属性 base属性代表父类,用于多态
	obj.base = self
	setmetatable(obj, self)
end
--print(_G)
--_G["a"] = 1
--_G.b = "123"
--print(a)
--print(b)

Object:subClass("Person")

local p1 = Person:new()
print(p1.id)
p1.id = 100
print(p1.id)
p1:Test()

Object:subClass("Monster")
local m1 = Monster:new()
print(m1.id)
m1.id = 200
print(m1.id)
m1:Test()

print("**********多态************")
--相同行为 不同表象 就是多态
--相同方法 不同执行逻辑 就是多态
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
function GameObject:Move()
	self.posX = self.posX + 1
	self.posY = self.posY + 1
	print(self.posX)
	print(self.posY)
end

GameObject:subClass("Player")
function Player:Move()
--此函数用来实现多态

	--有坑的写法:会导致子类共用 GameObject 表的move
   --self.base:Move()

	--base 指的是 GameObject 表(类)
	--这种方式调用 相当于是把基类表 作为第一个参数传入了方法中
	--避免把基类表 传入到方法中 这样相当于就是公用一张表的属性了
	--我们如果要执行父类逻辑 我们不要直接使用冒号调用
	--要通过.调用 然后自己传入第一个参数 self


	--这里的self要理解清楚逻辑,我们拿下文的一个子类p1.move()举例子
	--如果没有后面的self参数,前面的self虽然也是指P1
	--但是p1.base,指的就是p1的父类player,
	--然后由于player也没有base
	--所以最终指向player的父类,GameObject
	--最终直接在父类里面运行父类本身的move
	--但是我们放弃了冒号,使用点号手动的传入self
	--move方法里面的self就会被替换为p1的内容
	--相当于只修改p1的move,实现真正的多态

	--但是,需要强调的是,这里面依然有一个恐怖的隐患
	--那就是,如果你修改父类object的posx和posy
	--由于p1初始化move的时候没有这两个值,会从父类拿。
	--那就会导致父类影响子类
	--解决办法就是直接给子类赋值好初始值,去除父类干扰。
	--当然,父类Gameobject本来就是我们用来表现继承的一个功能性的表

--类似C#中base的存在,即调用父类函数。
	self.base.Move(self)
end

local p1 = Player:new()
p1:Move()
p1:Move()

注意

1.请注意查看本节的知识点内容,你需要搞懂:
一.封装和继承的不同—注意2有详解
二.多态里面的关于点号和冒号调用self的坑—注意3有详解
三.需要强调,lua不是一种面向对象语言,我们只不过是使用了它的语法模拟和构造出来了面向对象的功能来方便我们的使用,但是依然会有许多功能上的缺失,毕竟这不是原生的用法。

2.封装和继承的不同

问:这两者之间的区别仅仅在于操作的方式不同,实际上都是声明一个子表,只不过一个返回出来,一个声明指定名字的子表,不返回,那么作用是不是重复了呢?我可以在外部用new方法得到相同的目的


回答:
进行new的时候,子表和元表的关系,类似于C#里面实例化对象和类的关系,而不是C#继承里面的子类和父类的关系

subClass方法的主要目的是要取创建一个新表,让该表和基类模拟建立继承关系
这个继承关系的建立,是建立在元表和__index的用法上的
这个新表 作为之后new的对象找不到属性的方法时 查找调用的目标
也就是说,你可以理解当new一个对象时,我们去获取某个方法和变量,在没有自己声明之前 是从subClass这个表中取获取的

但是 new的对象只要对变量和方法赋值了,那么之后再访问访问的就是自己刚才声明的内容,不在是subClass中的内容了
new的时候是本地变量,继承的时候是通过_G表创建的全局变量。
subClass的目的是建立一个继承关系
new是在新创建一个对象
我们通过subClass建立了继承关系后,才去new对象
subClass这里模拟的继承的关系 实际上是创建了作为基础信息的table
当我们new一个新对象时,该对象中找不到的信息 其实都是通过 subClass这里创建的基础table去获取的
综上所述,subClass创建出来的表更多的是用来进行new对象的工具或者说模拟关系的桥梁,对这个表的修改大部分也是用来实现多态等方法的,而不是直接使用它。

3.多态里面的关于点号和冒号调用self的坑
请先去我的知识点里面查看相关注释,有详细的分析。

有一个前置的知识点是,lua里面可以使用冒号来自动为self赋值使用冒号的调用者。
但是,对于冒号函数,我们依然可以使用 调用者.函数名(另一个调用者)的点号用法来进行调用,另一个调用者这个参数就会被传入原方法并赋值给self
如:

text={name="text"}
--为text创建一个获取自己名字的冒号函数
function text:myname()
    print(self.name)
end

--注意,此表和text没有任何关系
text2={name="text2"}
--虽然t1方法是冒号方法,依然可以使用点号调用方法并传入参数,修改self
text.myname(text2)
--输出结果是text2

这就是lua动态语言的好处了,由于没有类型的桎梏,即使两个表没任何关系,依然可以随意的混合调用彼此的变量和函数,非常的灵活,但是不理解的话也非常的绕。

回到问题上,关于那两个self,非常容易让人绕进去,实际上,第一个self的存在是用来定位函数位置的,第二个self则是作为参数指定我们想要修改的对象的(即子类p1们)。
想明白了这些,这个写法的疑惑点就迎刃而解。

可参考资料:(24条消息) 【Lua基础系列】__index元方法___index lua_Lampard猿奋的博客-CSDN博客

在Lua中实现“继承”遇到的坑 - 知乎 (zhihu.com)

面向对象总结

总结代码

--面向对象实现 
--万物之父 所有对象的基类 Object
--封装
Object = {}
--实例化方法
function Object:new()
	local obj = {}
	--给空对象设置元表 以及 __index
	self.__index = self
	setmetatable(obj, self)
	return obj
end
--继承
function Object:subClass(className)
	--根据名字生成一张表 就是一个类
	_G[className] = {}
	local obj = _G[className]
	--设置自己的“父类”
	obj.base = self
	--给子类设置元表 以及 __index
	self.__index = self
	setmetatable(obj, self)
end

--申明一个新的类
Object:subClass("GameObject")
--成员变量
GameObject.posX = 0
GameObject.posY = 0
--成员方法
function GameObject:Move()
	self.posX = self.posX + 1
	self.posY = self.posY + 1
end

--实例化对象使用
local obj = GameObject:new()
print(obj.posX)
obj:Move()
print(obj.posX)

local obj2 = GameObject:new()
print(obj2.posX)
obj2:Move()
print(obj2.posX)

--申明一个新的类 Player 继承 GameObject
GameObject:subClass("Player")
--多态 重写了 GameObject的Move方法
function Player:Move()
	--base调用父类方法 用.自己传第一个参数
	self.base.Move(self)
end
print("****")
--实例化Player对象
local p1 = Player:new()
print(p1.posX)
p1:Move()
print(p1.posX)

local p2 = Player:new()
print(p2.posX)
p2:Move()
print(p2.posX)

自带库

大纲

知识点

print("**********自带库************")
--string
--table
print("**********时间************")
--os.time()
--得到是当前时间的距离1970.1.1.08:00时间的秒数
--无参默认是当前系统时间
print(os.time())
--自己传入参数 得到时间
print(os.time({year = 2014, month = 8, day = 14}))

--os.date("*t")
--获得一张表,表里面包含了可视化的年月日等详细信息
local nowTime = os.date("*t")
for k,v in pairs(nowTime) do
	print(k,v)
end
print(nowTime.hour)

print("**********数学运算************")
--math
--绝对值
print(math.abs(-11))
--弧度转角度
print(math.deg(math.pi))
--三角函数 传弧度
print(math.cos(math.pi))

--向下向上取整
print(math.floor(2.6))
print(math.ceil(5.2))

--获取一堆数字里面的最大或者最小值
print(math.max(1,2,3))
print(math.min(4,5,2,3,1))

--小数分离 分成整数部分和小数部分
print(math.modf(1.2))
--幂运算
print(math.pow(2, 5))

--随机数
--lua里面需要先设置随机数种子
--否则随机数会固定
math.randomseed(os.time())
print(math.random(100))
print(math.random(100))
--开方
print(math.sqrt(4))

print("**********路径************")
--lua脚本加载路径
print(package.path)
--路径可以改变
package.path = package.path .. ";C:\\"
print(package.path)


--可以用大G表去获取所有的其他库
for k,v in pairs(_G) do
	print(k,v)
end

垃圾回收

大纲

知识点

print("**********垃圾回收************")

test = {id = 1, name = "123123"}
--垃圾回收关键字
--collectgarbage
--获取当前lua占用内存数 K字节 用返回值*1024 就可以得到具体的内存占用字节数
print(collectgarbage("count"))
--lua中的机制和C#中垃圾回收机制很类似 解除羁绊 就是变垃圾
test = nil
--进行垃圾回收 理解有点像C#的 GC
collectgarbage("collect")

print(collectgarbage("count"))

--lua中 有开启自动定时进行GC的方法
--但是在Unity中热更新开发中,我们尽量不要去用自动垃圾回收
--会容易引起无法预测的卡顿

第4章: xLua热更新解决方案

导入

概述

第1节: 环境准备

xLua导入和AB包相关准备

本章节需要的前置包:

刚需:xlua框架与AB包(或Addressables)

可选:自动单例化组件,AB包管理类

xlua的导入

xlua并不是package,只需要放到asset文件夹即可。
xlua github地址:GitHub - Tencent/xLua: xLua is a lua programming solution for C# ( Unity, .Net, Mono) , it supports android, ios, windows, linux, osx, etc.

第2节: C#调用Lua

Lua解析器

大纲

知识点

//Lua解析器 能够让我们在Unity中执行Lua  
//一般情况下 保持它的唯一性  
LuaEnv env = new LuaEnv();  
  
//执行Lua语言  
env.DoString("print('你好世界')");  
  
//执行一个Lua脚本 Lua知识点 :多脚本执行 require//默认寻找脚本的路径 是在 Resources下 并且 因为在这里  
//估计是通过 Resources.Load去加载Lua脚本 txt bytes等等  
//所以Lua脚本 后缀要加一个txt  
env.DoString("require('Main')");  
  
//帮助我们清楚Lua中我们没有手动释放的对象 垃圾回收  
//帧更新中定时执行 或者 切场景时执行  
env.Tick();  
  
//销毁Lua解析器 env.Dispose();

文件加载重定向

大纲

知识点

LuaEnv env = new LuaEnv();  
  
//xlua提供的一个 路径重定向 的方法  
//允许我们自定义 加载 Lua文件的规则  
//当我们执行Lua语言 require 时 相当于执行一个lua脚本  
//它就会 执行 我们自定义传入的这个函数  
env.AddLoader(MyCustomLoader);  
//最终我们其实 会去AB包中加载 lua文件   
env.DoString("require('Main')");
-------------------------------------------------------
//这个委托是用于路径重定向方法的参数  
//每当require指向的时候,就会自动执行这个参数  
//本函数的参数filePath会被自动赋值lua的require()里面的参数对象字符串  
//而返回值 byte[]会被作为require()的真正参数地址  
private byte[] MyCustomLoader(ref string filePath)  
{  
  //通过函数中的逻辑 去加载 Lua文件   
 //传入的参数 是 require执行的lua脚本文件名  
  //拼接一个Lua文件所在路径  
  string path = Application.dataPath + "/Lua/" + filePath + ".lua";  
  Debug.Log(path);  
  
  //有路径 就去加载文件   
 //File知识点 C#提供的文件读写的类  
  //判断文件是否存在  
  if ( File.Exists(path) )  
 {  //该函数可以把字符串转换为字节数组  
  return File.ReadAllBytes(path);  
 }  else  
  {  
  Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath);  
 }  
  return null;  
}

如上,虽然Unity本身无法识别lua文件,但是通过重定向,我们就可以指定路径甚至指定文件名,没必要每次都加txt后缀了。
而且还拥有了抛出异常信息的能力。

注意


我们来理一理当进行了上文的env.DoString(“require(‘Main’)”); 发生了什么事情。

如上图,当执行了require后,xlua发挥功能,开始自动的检测env有没有进行过重定向,如env.AddLoader(MyCustomLoader);

这里需要强调的是,AddLoader方法是可以有多个参数的,如果为第一个返回值为null,AddLoader会试着去找下一个参数。即多个委托中只要有一个正常的返回值byte[]就会修改require()里面的参数为该路径,如果返回值均为null就会使用默认路径,即 Resources里面的同名文件。

这是一个很好的重定向形式,我们借此可以在委托内部对文件名进行一些条件限制,不同情况查找不同路径里面的文件夹。

未解决的问题

我们不会在实际游戏发布后使用Resources进行热更新(因为发布后不可修改),而是使用AB包,下一节会讲解如何处理这种情况。

Lua解析器管理器

大纲

lua解析器管理器代码

鉴于解析器必须是唯一的,我们不妨写成一个单例方便全局统一管理。

/// <summary>  
/// Lua管理器  
/// 提供 lua解析器  
/// 保证解析器的唯一性  
/// </summary>  
public class LuaMgr : BaseManager<LuaMgr>  
{  
  //执行Lua语言的函数  
  //释放垃圾  
  //销毁  
  //重定向  
  private LuaEnv luaEnv;  
  
  
  /// <summary>  
 /// 得到Lua中的_G  
 /// </summary>  public LuaTable Global  
 { get {  return luaEnv.Global;  
  }  
 }  
  
  /// <summary>  
 /// 初始化解析器  
  /// </summary>  
  public void Init()  
 {  //已经初始化了 别初始化 直接返回  
  if (luaEnv != null)  
  return;  
  //初始化  
  luaEnv = new LuaEnv();  
  //加载lua脚本 重定向  
  luaEnv.AddLoader(MyCustomLoader);  
  luaEnv.AddLoader(MyCustomABLoader);  
 }  
  //自动执行  
  private byte[] MyCustomLoader(ref string filePath)  
 {  //通过函数中的逻辑 去加载 Lua文件   
 //传入的参数 是 require执行的lua脚本文件名  
  //拼接一个Lua文件所在路径  
  string path = Application.dataPath + "/Lua/" + filePath + ".lua";  
  
  //有路径 就去加载文件   
 //File知识点 C#提供的文件读写的类  
  //判断文件是否存在  
  if (File.Exists(path))  
 {  return File.ReadAllBytes(path);  
 }  else  
  {  
  Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath);  
 }  
  
  return null;  
 }  
  
  //Lua脚本会放在AB包   
 //最终我们会通过加载AB包再加载其中的Lua脚本资源 来执行它  
  //重定向加载AB包中的LUa脚本  
  private byte[] MyCustomABLoader(ref string filePath)  
 {  //Debug.Log("进入AB包加载 重定向函数");  
 ////从AB包中加载lua文件  
  ////加载AB包  
  //string path = Application.streamingAssetsPath + "/lua";  
 //AssetBundle ab = AssetBundle.LoadFromFile(path);  
 ////加载Lua文件 返回  
  //TextAsset tx = ab.LoadAsset<TextAsset>(filePath + ".lua");  
 ////加载Lua文件 byte数组  
  //return tx.bytes;  
  
 //通过我们的AB包管理器 加载的lua脚本资源  
  TextAsset lua = ABMgr.GetInstance().LoadRes<TextAsset>("lua", filePath + ".lua");  
  if (lua != null)  
  return lua.bytes;  
  else  
  Debug.Log("MyCustomABLoader重定向失败,文件名为:" + filePath);  
  
  return null;  
 }  
  
  /// <summary>  
 /// 传入lua文件名 执行lua脚本  
  /// </summary>  
 /// <param name="fileName"></param>  public void DoLuaFile(string fileName)  
 {  string str = string.Format("require('{0}')", fileName);  
  DoString(str);  
 }  
  /// <summary>  
 /// 执行Lua语言  
  /// </summary>  
 /// <param name="str"></param>  public void DoString(string str)  
 {  if(luaEnv == null)  
 {  Debug.Log("解析器未初始化");  
  return;  
 }  luaEnv.DoString(str);  
 }  
  /// <summary>  
 /// 释放lua 垃圾  
  /// </summary>  
  public void Tick()  
 {  if (luaEnv == null)  
 {  Debug.Log("解析器未初始化");  
  return;  
 }  luaEnv.Tick();  
 }  
  /// <summary>  
 /// 销毁解析器  
  /// </summary>  
  public void Dispose()  
 {  if (luaEnv == null)  
 {  Debug.Log("解析器未初始化");  
  return;  
 }  luaEnv.Dispose();  
  luaEnv = null;  
 }}

使用例:

//初始化解析器  
LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");

注意

1.由于AB包我们几乎只有在项目完成的时候才会构建,所以我们平时建议使用 luaEnv.AddLoader(MyCustomLoader) :即asset里面的lua文件夹的lua文件来进行测试,AB打包完成后再进行 luaEnv.AddLoader(MyCustomABLoader); 获取AB里面的lua文件。
这也是为什么上文代码里面给AddLoader先后赋值了两次参数的理由。

2.AB包里面只能识别txt的字符串,导致我们打包AB包的时候依然要给lua脚本加上TXT后缀,但是后面实践案例的时候会有解决办法。

全局变量的获取

大纲

知识点

unity部分

//使用lua管理器进行初始化与运行脚本
LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");  
  
//int local = LuaMgr.GetInstance().Global.Get<int>("testLocal");  
//Debug.Log("testLocal:" + local);  
  
//使用lua解析器luaenv中的 Global属性 int i = LuaMgr.GetInstance().Global.Get<int>("testNumber");  
Debug.Log("testNumber:" + i);  
i = 10;  
//改值  
LuaMgr.GetInstance().Global.Set("testNumber", 55);  
//值拷贝 不会影响原来Lua中的值  
int i2 = LuaMgr.GetInstance().Global.Get<int>("testNumber");  
Debug.Log("testNumber_i2:" + i2);  
  
bool b = LuaMgr.GetInstance().Global.Get<bool>("testBool");  
Debug.Log("testBool:" + b);  
  
float f = LuaMgr.GetInstance().Global.Get<float>("testFloat");  
Debug.Log("testFloat:" + f);  
  
double d = LuaMgr.GetInstance().Global.Get<double>("testFloat");  
Debug.Log("testFloat_double:" + d);  
  
string s = LuaMgr.GetInstance().Global.Get<string>("testString");  
Debug.Log("testString:" + s);

Lua部分
Main脚本(Unity代码部分调用的脚本):

--在lua脚本里面调用其他脚本,和使用xlua进行DoString方法是一样的,经过xlua的重定向才真正执行
require("Text")

Text脚本:

print("Test.Lua")
testNumber = 1
testBool = true
testFloat = 1.2
testString = "123"

--我们通过C# 没办直接获取本地局部变量
local testLocal = 10

注意

1.xlua无法获取lua脚本的局部变量,以及得通过方法泛型来获取对应的lua变量。
2.如果在lua里面直接用print输出变量,在Unity里面控制台输出的是问号。

全局函数的获取

大纲

知识点

Unity部分:

//无参无返回值的委托  
public delegate void CustomCall();  
  
//有参有返回 的委托  
//该特性是在XLua命名空间中的  
//加了过后 要在编辑器里 生成 Lua代码  
[CSharpCallLua]  
public delegate int CustomCall2(int a);  
  
[CSharpCallLua]  
public delegate int CustomCall3(int a, out int b, out bool c, out string d, out int e);  
[CSharpCallLua]  
public delegate int CustomCall4(int a, ref int b, ref bool c, ref string d, ref int e);  
  
[CSharpCallLua]  
public delegate void CustomCall5(string a, params int[] args);//变长参数的类型 是根据实际情况来定的  
  
public class Lesson5_CallFunction : MonoBehaviour  
{  
  // Start is called before the first frame update  
  void Start()  
  {  
  LuaMgr.GetInstance().Init();  
  
  LuaMgr.GetInstance().DoLuaFile("Main");  
  
  //无参无返回的获取  
  //委托  
  CustomCall call = LuaMgr.GetInstance().Global.Get<CustomCall>("testFun");  
 call();  //Unity自带委托  
  UnityAction ua = LuaMgr.GetInstance().Global.Get<UnityAction>("testFun");  
 ua();  //C#提供的委托  
  Action ac = LuaMgr.GetInstance().Global.Get<Action>("testFun");  
 ac();  //Xlua提供的一种 获取函数的方式 少用  
  LuaFunction lf = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun");  
 lf.Call();  
  
  //有参有返回  
  CustomCall2 call2 = LuaMgr.GetInstance().Global.Get<CustomCall2>("testFun2");  
  Debug.Log("有参有返回:" + call2(10));  
  //C#自带的泛型委托 方便我们使用  
  Func<int, int> sFun = LuaMgr.GetInstance().Global.Get<Func<int, int>>("testFun2");  
  Debug.Log("有参有返回:" + sFun(20));  
  //Xlua提供的  
  LuaFunction lf2 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun2");  
  Debug.Log("有参有返回:" + lf2.Call(30)[0]);  
  
  //多返回值  
  //使用 out 和 ref 来接收  
  CustomCall3 call3 = LuaMgr.GetInstance().Global.Get<CustomCall3>("testFun3");  
  int b;  
  bool c;  
  string d;  
  int e;  
  Debug.Log("第一个返回值:" + call3(100, out b, out c, out d, out e));  
  Debug.Log(b + "_" + c + "_" + d + "_" + e);  
  
  CustomCall4 call4 = LuaMgr.GetInstance().Global.Get<CustomCall4>("testFun3");  
  int b1 = 0;  
  bool c1 = true;  
  string d1 = "";  
  int e1 = 0;  
  Debug.Log("第一个返回值:" + call4(200, ref b1, ref c1, ref d1, ref e1));  
  Debug.Log(b1 + "_" + c1 + "_" + d1 + "_" + e1);  
  //Xlua  
  LuaFunction lf3 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun3");  
  object[] objs = lf3.Call(1000);  
  for( int i = 0; i < objs.Length; ++i )  
 {  Debug.Log("第" + i + "个返回值是:" + objs[i]);  
 }  
  //变长参数  
  CustomCall5 call5 = LuaMgr.GetInstance().Global.Get<CustomCall5>("testFun4");  
 call5("123", 1, 2, 3, 4, 5, 566, 7, 7, 8, 9, 99);  
  
  LuaFunction lf4 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun4");  
 lf4.Call("456", 6, 7, 8, 99, 1);  
 }

lua部分:
Text脚本(与unity部分一一对应):

--无参无返回
function testFun()
	print("无参无返回?")
end

--有参有返回
testFun2 = function(a)
	print("有参有返回")
	print(a+1)
	return a + 1
end

--多返回
testFun3 = function(a)
	print("多返回值")
	print(a)
	return 1, 2, false, "123", a
end

--变长参数
testFun4 = function(a, ...)
	print("变长参数")
	print(a)
	arg = {...}
	for k,v in pairs(arg) do
		print(k,v)
	end
end

注意

1.除了无参无返回值之外,其他函数形式如果用自定义委托来装的话,xlua是无法直接识别的,你需要把这个自定义委托在xlua内部进行构建
你需要进行两个步骤
一.在自定义委托处加上注解[CSharpCallLua] ,此注解来源就是xlua。
二.在页面中通过xlua选项进行此委托的构建,xlua会在文件夹里面生成代码来解释这个自定义委托的存在,构建就成功了。(构建按钮如下图,如果报错可能需要先进行clear再按构建)


2.由于Unity没有多返回值,你只能使用自定义函数或者xlua提供的 LuaFunction。
其中自定义函数是用ref和out实现的。
格式为:默认返回值为第一个返回值,不带out或ref的参数为普通参数。带out或ref的按顺序为第二个开始的其他返回值

如:
C#

public delegate int nucall(int a,
int b,
int c,
out bool return1,
out int return2);

lua

testFun3 = function(a,b,c)
	print(a)
	print(b)
	print(c)
	return 123, false,12032323
end

对应关系:
C#->lua :
a->a (参数)
b->b(参数)
c->c (参数)
int->123 (C#委托的默认返回值对应lua第一个返回值)
retunr1->false (第二个返回值)
retunr2->12032323 (第三个返回值)


3.为什么不用xlua提供的LuaFunction?明明它看起来更方便,而且适用于所有的函数获取情况?

因为LuaFunction会产生垃圾,太多次使用会导致卡顿,所以不建议使用。

List和Dictionary映射table

大纲

知识点

Unity部分:

LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");  
  
//同一类型List
//值拷贝 深拷贝 不会改变lua中的内容   
List<int> list = LuaMgr.GetInstance().Global.Get<List<int>>("testList");  
Debug.Log("*******************List************************");  
for ( int i = 0; i <list.Count; ++i )  
{  
  Debug.Log(list[i]);  
}  
 

  
//不指定类型 object
List<object> list3 = LuaMgr.GetInstance().Global.Get<List<object>>("testList2");  
Debug.Log("*******************List object************************");  
for (int i = 0; i < list3.Count; ++i)  
{  
  Debug.Log(list3[i]);  
}  
  
Debug.Log("*******************Dictionary************************");  
Dictionary<string, int> dic = LuaMgr.GetInstance().Global.Get<Dictionary<string, int>>("testDic");  
foreach (string item in dic.Keys)  
{  
  Debug.Log(item + "_" + dic[item]);  
}  
dic["1"] = 100000;  


  
Debug.Log("*******************Dictionary object************************");  
Dictionary<object, object> dic3 = LuaMgr.GetInstance().Global.Get<Dictionary<object, object>>("testDic2");  
foreach (object item in dic3.Keys)  
{  
  Debug.Log(item + "_" + dic3[item]);  
}

lua部分:
Text脚本


--List
testList = {1,2,3,4,5,6}
testList2 = {"123", "123", true, 1, 1.2}

--Dictionary
testDic = {
	["1"] = 1,
	["2"] = 2,
	["3"] = 3,
	["4"] = 4
}

testDic2 = {
	["1"] = 1,
	[true] = 1,
	[false] = true,
	["123"] = false
}

注意

lua的变量和数组类型都是遵循值类型的拷贝方式。

类映射table

大纲

知识点

Unity部分:

C#调用lua

LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");  
  
CallLuaClass obj = LuaMgr.GetInstance().Global.Get<CallLuaClass>("testClas");  
Debug.Log(obj.testInt);  
Debug.Log(obj.testBool);  
//Debug.Log(obj.testFloat);  
Debug.Log(obj.testString);  
Debug.Log(obj.i);  
Debug.Log("嵌套:" + obj.testInClass.testInInt);  
obj.testFun();  
//值拷贝 改变了它 不会改变Lua表里的内容  
obj.testInt = 100;  
CallLuaClass obj2 = LuaMgr.GetInstance().Global.Get<CallLuaClass>("testClas");  
Debug.Log(obj2.testInt);

两个用来获取lua类的C#类

public class CallLuaClass  
{  
  //在这个类中去声明成员变量  
  //名字一定要和 Lua那边的一样  
  //公共 私有和保护 没办法赋值  
  //这个自定义中的 变量 可以更多也可以更少  
  //如果变量比 lua中的少 就会忽略它  
  //如果变量比 lua中的多 不会赋值 也会忽略  
  public int testInt;  
  public bool testBool;  
  //public float testFloat;  
  public float testString;  
  public UnityAction testFun;  
  
  public CallLuaInClass testInClass;  
  
  public int i;  
  
  public void Test()  
 {  Debug.Log(testInt);  
 }}  
  
  //被嵌套进CallLuaClass中
public class CallLuaInClass  
{  
  public int testInInt;  
}

lua部分

--lua当中的一个自定义类
testClas = {
	testInt = 2,
	testBool = true,
	testFloat = 1.2,
	testString = "123",
	testFun = function()
		print("123123123")
	end
	textInClass={
	testInint=5
}
}

注意

为C#获取lua的类为值拷贝。

接口映射table

大纲

知识点

Unity部分:
C#获取Lua:

LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");  
  
ICSharpCallInterface obj = LuaMgr.GetInstance().Global.Get<ICSharpCallInterface>("testClas");  
Debug.Log(obj.testInt);  
Debug.Log(obj.testBool);  
Debug.Log("新加的:" + obj.testFloat222);  
Debug.Log(obj.testString);  
obj.testFun();  
  
//接口拷贝 是引用拷贝 改了值 lua表中的值也变了  
obj.testInt = 10000;  
ICSharpCallInterface obj2 = LuaMgr.GetInstance().Global.Get<ICSharpCallInterface>("testClas");  
Debug.Log(obj2.testInt);

C#中的接口

//接口中是不允许有成员变量的 //我们用属性来接受  
//接口和类规则一样 其中的属性多了少了 不影响结果 无非就是忽略他们  
//嵌套几乎和类一样 无非 是要遵循接口的规则  
[CSharpCallLua]
public interface ICSharpCallInterface  
{  
  int testInt  
  {  
  get;  
  set;  
 }  
  bool testBool  
  {  
  get;  
  set;  
 }  
  ////float testFloat  
 ////{ ////    get; ////    set; ////}  
  string testString  
  {  
  get;  
  set;  
 }  
  UnityAction testFun  
  {  
  get;  
  set;  
 }  
  float testFloat222  
  {  
  get;  
  set;  
 }}

Lua部分:

--lua当中的一个自定义接口
testClas = {
	testInt = 2,
	testBool = true,
	testFloat = 1.2,
	testString = "123",

}

注意

1.C#的接口是无法直接放变量的,但是却可以使用属性来替代。并且获取Lua对象

2.尽管接口和类获取Lua对象的方法非常类似,但是有两个巨大的差别:
一.接口需要加上注解[CSharpCallLua],类不需要
二.类获得的拷贝是值拷贝(深拷贝),而接口获得的拷贝是引用拷贝(浅拷贝),改变C#中接口对象的值会直接改变lua的值!

LuaTable映射table

大纲

知识点

Unity部分:
C#调用Lua:

LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("Main");  
  
//不建议使用LuaTable和LuaFunction 效率低  
//引用对象  
LuaTable table = LuaMgr.GetInstance().Global.Get<LuaTable>("testClas");  
Debug.Log(table.Get<int>("testInt"));  
Debug.Log(table.Get<bool>("testBool"));  
Debug.Log(table.Get<float>("testFloat"));  
Debug.Log(table.Get<string>("testString"));  
  
table.Get<LuaFunction>("testFun").Call();  
//改  引用  
table.Set("testInt", 55);  
Debug.Log(table.Get<int>("testInt"));  
LuaTable table2 = LuaMgr.GetInstance().Global.Get<LuaTable>("testClas");  
Debug.Log(table2.Get<int>("testInt"));  
  
  //清理垃圾,否则会一直占用内存。
table.Dispose();  
table2.Dispose();

Lua:

testClas = {
	testInt = 2,
	testBool = true,
	testFloat = 1.2,
	testString = "123",
	testFun = function(a)

		print(a)
	end
}

注意

1.LuaTable也是引用类型,因为只能用set修改值,修改后Lua脚本的内容也会变。

2.使用LuaTable装载后,执行函数使用的是函数方法Call(),获得(get)和设置(set)变量也是。

3.LuaTable和Luafunction类似的用法,尽管非常方便,但是不建议使用,会产生垃圾,如果不用Dispose清理垃圾就会永久的占用内存。

第3节: Lua调用C#

Lua使用C#类

大纲

知识点

前提:先使用C#调用Lua

由于文件肯定是先打开Unity游戏才能触发Lua文件,我们需要先在C#中调用写好的Lua脚本,在通过这个脚本反过来用Lua调用C#。
本节皆为如此,之后不再复述。

C#:

//初始化预制的Lua解析器单例,调用lua脚本main
LuaMgr.GetInstance().Init();  
LuaMgr.GetInstance().DoLuaFile("main");
调用C#自身的类

Lua:

--lua中使用C#的类非常简单
--固定套路
--CS.命名空间.类名
--Unity的类 比如 GameObject Transform等等 —— CS.UnityEngine.类名
--CS.UnityEngine.GameObject

--通过C#中的类 实例化一个对象 lua中没有new 所以我们直接 类名括号就是实例化对象
--默认调用的 相当于就是无参构造
local obj1 = CS.UnityEngine.GameObject()
local obj2 = CS.UnityEngine.GameObject("唐老狮")

--为了方便使用 并且节约性能 定义全局变量存储 C#中的类
--相当于取了一个别名
GameObject = CS.UnityEngine.GameObject
local obj3 = GameObject("唐老狮好爱同学们")

--类中的静态对象 可以直接使用.来调用
local obj4 = GameObject.Find("唐老狮")

--得到对象中的成员变量  直接对象 . 即可
print(obj4.transform.position)
Debug = CS.UnityEngine.Debug
Debug.Log(obj4.transform.position)

Vector3 = CS.UnityEngine.Vector3
--如果使用对象中的 成员方法!!!!一定要加:
obj4.transform:Translate(Vector3.right)
Debug.Log(obj4.transform.position)
调用自定义类

Lua:

--自定义类 使用方法 相同  只是命名空间不同而已
local t = CS.Test()
t:Speak("test说话")

local t2 = CS.MrTang.Test2()
t2:Speak("test2说话")

C#:

//无命名空间的类
public class Text
{
	public void Speak(String str)
		{
		Debug.Log("Test1:"+str)
		}
}

//有命名空间的类
nameSpace MrTang
{
	public class Text2
	{
			public void Speak(String str)
			{
			Debug.Log("Test2"+str)
			}
	}
}
调用继承了Mono的类(即游戏物体挂载的脚本)

Lua:

--继承了Mono的类
--继承了Mono的类 是不能直接new 
local obj5 = GameObject("加脚本测试")
--通过GameObject的 AddComponent添加脚本
--xlua提供了一个重要方法 typeof 可以得到类的Type
--xlua中不支持 无参泛型函数  所以 我们要使用另一个重载
obj5:AddComponent(typeof(CS.LuaCallCSharp))

注意

xlua里面lua调用C#时用的冒号和Lua本体里面的冒号,含义是有所不同的。

在Lua本体里面,冒号是一种语法糖,用来表示调用一个对象的方法,相当于传递了一个self参数,而且冒号和点号是可以随时互相替代的。例如:

obj:foo (x) -- 等价于 obj.foo (obj, x)

在xlua里面,冒号是一种约定,用来表示调用一个C#对象的非静态方法,相当于绑定了一个实例,冒号和点号被xlua人为约定为了两种不同的功能是不能互相替代的。例如:

local a = CS.A () -- 创建一个C#类A的实例
a:Test1 () -- 等价于 a.Test1 (a) 或者 CS.A.Test1 (a),调用C#类A的非静态方法Test1

这样做的原因是为了区分C#类的静态方法和非静态方法,以及避免在lua中使用new关键字。


总结:xlua里面lua调用C#的点号用来new对象或者调用静态变量,冒号用来调用C#对象的非静态方法。这是xlua的一种设计约定,为了让lua和C#之间的交互更加方便和自然。

Lua使用C#枚举

大纲

知识点

Lua:

print("*********Lua调用C#枚举相关知识点***********")

--枚举调用 
--调用Unity当中的枚举
--枚举的调用规则 和 类的调用规则是一样的
--CS.命名空间.枚举名.枚举成员
--也支持取别名 
PrimitiveType = CS.UnityEngine.PrimitiveType
GameObject = CS.UnityEngine.GameObject

local obj = GameObject.CreatePrimitive(PrimitiveType.Cube)

--自定义枚举 使用方法一样 只是注意命名空间即可
E_MyEnum =  CS.E_MyEnum

local c = E_MyEnum.Idle
print(c)
--枚举转换相关
--数值转枚举
local a = E_MyEnum.__CastFrom(1)
print(a)
--字符串转枚举
local b = E_MyEnum.__CastFrom("Atk")
print(b)

C#:

public enum E_MyEnum  
  {  
  Idle,Move,Atk,  
  }

注意

由于Lua里面没有类型,也就没有强转的写法,xlua提供了__CastFrom方法给枚举进行转换使用。

Lua使用C#数组、list、Dictionary

大纲

知识点

Lua调用C#:

print("*********Lua调用C# 数组相关知识点***********")

local obj = CS.Lesson3()

--Lua使用C#数组相关知识
--长度 userdata
--C#怎么用 lua就怎么用 不能使用#去获取长度
print(obj.array.Length)

--访问元素
print(obj.array[0])

--遍历要注意 虽然lua中索引从1开始
--但是数组是C#那边的规则 所以 还是得按C#的来
--注意最大值 一定要减1
for i=0,obj.array.Length-1 do
	print(obj.array[i])
end

--Lua中创建一个C#的数组 Lua中表示数组和List可以用表 
--但是我要使用C#中自带的[]呢???
--创建C#中的数组 使用 Array类中的静态方法即可
local array2 = CS.System.Array.CreateInstance(typeof(CS.System.Int32), 10)
print(array2.Length)
print(array2[0])
print(array2[1])
print(array2)

print("*********Lua调用C# list相关知识点***********")
--调用成员方法 用冒号!!!!!!
obj.list:Add(1)
obj.list:Add(2)
obj.list:Add(3)
--长度
print(obj.list.Count)
--遍历
for i=0,obj.list.Count - 1 do
	print(obj.list[i])
end
print(obj.list)

--在Lua中创建一个List对象
--老版本,太过繁琐不建议使用
local list2 = CS.System.Collections.Generic["List`1[System.String]"]()
print(list2)
list2:Add("123")
print(list2[0])

--新版本 >v2.1.12
--相当于得到了一个 List<string> 的一个类别名 需要再实例化
--敲定泛型类型然后即可实例化
local List_String = CS.System.Collections.Generic.List(CS.System.String)
local list3 = List_String()
list3:Add("5555555")
print(list3[0])

print("*********Lua调用C# dictionary相关知识点***********")
--使用和C#一致
obj.dic:Add(1, "123")
print(obj.dic[1])

--遍历
for k,v in pairs(obj.dic) do
	print(k,v)
end

--在Lua中创建一个字典对象
--相当于得到了一个 Dictionary<string, Vector3> 的一个类别名 需要再实例化
local Dic_String_Vector3 = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.UnityEngine.Vector3)
local dic2 = Dic_String_Vector3()
dic2:Add("123", CS.UnityEngine.Vector3.right)
for i,v in pairs(dic2) do
	print(i,v)
end
--在Lua中创建的字典 直接通过键中括号得 得不到 是nil
print(dic2["123"])

--如果要通过键获取值 要通过这个固定方法
print(dic2:get_Item("123"))
--设置值
dic2:set_Item("123", nil)
print(dic2:get_Item("123"))

--或者用这种方法获取
print(dic2:TryGetValue("123"))

C#:

public int[] array = new int[5]{1,2,3,20,23};  
public List<int> list= new List<int>();  
public Dictionary<int, string> dic = new Dictionary<int, string>();

Lua使用C#拓展方法

大纲

知识点

Lua:

print("*********Lua调用C# 拓展方法相关知识点***********")

Lesson4 = CS.Lesson4
--使用静态方法
--CS.命名空间.类名.静态方法名()
Lesson4.Eat()

--成员方法 实例化出来用
local obj = Lesson4()
--成员方法 一定用冒号
obj:Speak("唐老狮哈哈哈哈哈")

--使用拓展方法 和使用成员方法 一致
--要调用 C#中某个类的拓展方法 那一定要在拓展方法的静态类前面加上LuaCallCSharp特性
obj:Move()

C#:

//想要在Lua中使用拓展方法,一定要在工具类前面加上注解  
//建议 Lua中要使用的C#类 都加上此特性然后进行编译,可以提高性能  
//如果不加该特性 除了拓展方法对应的类  其他类的使用虽然不会报错  
//但是lua是通过反射的机制去调用的C#类 效率较低  
[LuaCallCSharp]  
public static class Tools  
{  
  //Lesson4的拓展方法  
  public static void Move(this Lesson4 obj)  
 {  Debug.Log(obj.name+"移动");  
 }}  
  
public class Lesson4  
{  
  public string name = "唐老师";  
  
  public void Speak(string str)  
 {  Debug.Log(str);  
 }  
  public static void Eat()  
 {  Debug.Log("吃东西");  
 }}

注意

1.拓展方法既可以当作静态方法也可以当实例方法,回忆详情可以查看C#核心

2.加上[LuaCallCSharp] 并提前进行比编译,可以提升Lua访问C#类的性能,所以建议只要LUa中要使用的C#类都加上该特性

Lua使用C# ref和out函数

大纲

知识点

Lua:

print("*********Lua调用C# ref方法相关知识点***********")

Lesson5 = CS.Lesson5

local obj = Lesson5()

--ref参数 会以多返回值的形式返回给lua
--如果函数存在返回值 那么第一个值 就是该返回值
--之后的返回值 就是ref的结果 从左到右一一对应
--ref参数 需要传入一个默认值 占位置
--a 相当于 函数返回值
--b 第一个ref
--c 第二个ref
local a,b,c = obj:RefFun(1, 0, 0, 1)
print(a)
print(b)
print(c)
print("*********Lua调用C# out方法相关知识点***********")
--out参数 会以多返回值的形式返回给lua
--如果函数存在返回值 那么第一个值 就是该返回值
--之后的返回值 就是out的结果 从左到右一一对应
--out参数 不需要传占位置的值
local a,b,c = obj:OutFun(20,30)
print(a)
print(b)
print(c)


--混合使用时  综合上面的规则
--ref需占位 out不用传
--第一个是函数的返回值  之后 从左到右依次对应ref或者out
local a,b,c = obj:RefOutFun(20,1)
print(a)--300
print(b)--200
print(c)--400

C#:

public class Lesson5  
{  
  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 300;  
 }  
}

Lua使用C#重载函数

大纲

知识点

Lua:

print("*********Lua调用C# 重载函数相关知识点***********")

local obj = CS.Lesson6()

--虽然Lua自己不支持写重载函数
--但是Lua支持调用C#中的重载函数  
print(obj:Calc())
print(obj:Calc(15, 1))

--Lua虽然支持调用C#重载函数
--但是因为Lua中的数值类型 只有Number
--对C#中多精度的重载函数支持不好 傻傻分不清
--在使用时 可能出现意想不到的问题
print(obj:Calc(10))
print(obj:Calc(10.2))


--解决重载函数含糊的问题
--xlua提供了解决方案 反射机制 
--这种方法只做了解 尽量别用
--Type是反射的关键类
--得到指定函数的相关信息
local m1 = typeof(CS.Lesson6):GetMethod("Calc", {typeof(CS.System.Int32)})
local m2 = typeof(CS.Lesson6):GetMethod("Calc", {typeof(CS.System.Single)})

--通过xlua提供的一个方法 把它转成lua函数来使用
--一般我们转一次 然后重复使用
local f1 = xlua.tofunction(m1)
local f2 = xlua.tofunction(m2)
--成员方法 第一个参数传对象
--静态方法 不用传对象
print(f1(obj, 10))
print(f2(obj, 10.2))

C#:

public class Lesson6  
{  
  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 ;  
 }
 }

注意

尽管lua不支持重载,但是xlua支持。
不过lua对C#的重载有局限,那就是因为lua没有类型,导致无法识别同参数数量不同参数类型的重载。
知识点内也有解决办法,但是干脆不建议使用,效率太差。

Lua使用C#委托和事件

大纲

知识点

Lua:

print("*********Lua调用C# 委托相关知识点***********")

local obj = CS.Lesson7()

--委托是用来装函数的
--使用C#中的委托 就是用来装lua函数的
local fun = function( )
	print("Lua函数Fun")
end

--Lua中没有复合运算符 不能+=
--如果第一次往委托中加函数 因为是nil 不能直接+
--所以第一次 要先等=
print("*********开始加函数***********")
obj.del = fun
--obj.del = obj.del + fun
obj.del = obj.del + fun
--不建议这样写 最好最好还是 先声明函数再加
obj.del = obj.del + function( )
	print("临时申明的函数")
end
--委托执行
obj.del()
print("*********开始减函数***********")
obj.del = obj.del - fun
obj.del = obj.del - fun
--委托执行
obj.del()
print("*********清空***********")
--清空所有存储的函数
obj.del = nil
--清空过后得先等
obj.del = fun
--调用
obj.del()

print("*********Lua调用C# 事件相关知识点***********")
local fun2 = function()
	print("事件加的函数")
end
print("*********事件加函数***********")
--事件加减函数  和 委托非常不一样
--lua中使用C#事件 加函数 
--有点类似使用成员方 冒号事件名("+", 函数变量)
obj:eventAction("+", fun2)
--最好最好不要这样写
obj:eventAction("+", function()
	print("事件加的匿名函数")
end)

obj:DoEvent()
print("*********事件减函数***********")
obj:eventAction("-", fun2)
obj:DoEvent()

print("*********事件清除***********")
--清事件 不能直接设空
--所以我们得在C#的类里面设置好清除方法ClaerEvent
--直接调用方法清理
obj:ClaerEvent()
obj:DoEvent()

C#:

public class Lesson7  
{  
  public UnityAction del;  
  public event UnityAction eventAction;  
  public void DoEvent()  
 {  if (eventAction != null)  
 {  eventAction();  
 } }  
  public void ClaerEvent()  
 {  eventAction = null;  
 }  
}

注意

比较委托和事件,这两种在lua里面的使用差别很大。
委托更接近C#原版的使用,事件则得调用一堆xlua提供的API。

Lua使用C#二维数组

大纲

知识点

Lua:

print("*********Lua调用C# 二维数组相关知识点***********")

local obj = CS.Lesson8()

--获取长度
print("行:" .. obj.array:GetLength(0))
print("列:" .. obj.array:GetLength(1))

--获取元素
--不能通过[0,0]或者[0][0]访问元素 会报错
print(obj.array:GetValue(0,0))
print(obj.array:GetValue(1,0))
print("********************")
for i=0,obj.array:GetLength(0)-1 do
	for j=0,obj.array:GetLength(1)-1 do
		print(obj.array:GetValue(i,j))
	end
end

C#:

public class Lesson8  
{  
  public int[,] array = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };  
}

注意

lua不支持二维数组获取C#,所以只能用API来进行获取。

Lua使用C#的null和nil比较

大纲

知识点

Lua中判空的使用:

print("*********Lua调用C# nil和null比较的相关知识点***********")

--往场景对象上添加一个脚本 如果存在就不加 如果不存在再加
GameObject = CS.UnityEngine.GameObject
Rigidbody = CS.UnityEngine.Rigidbody

local obj = GameObject("测试加脚本")
--得到身上的刚体组件  如果没有 就加 有就不管
local rig = obj:GetComponent(typeof(Rigidbody))
print(rig)
--判断空
--nil和null 没法进行==比较
--第一种方法
--if rig:Equals(nil) then
--if IsNull(rig) then
if rig:IsNull() then
	print("123")
	rig = obj:AddComponent(typeof(Rigidbody))
end
print(rig)


两种实现lua对C#对象判空的方法:
Lua实现:

function IsNull(obj)
	if obj==nil or obj:Equals(nil) then
		return true
	end
	return false
end	

Unity实现(写一个Object扩展方法):

public static class Lesson9  
{  
  //扩展一个为Object判空的方法,主要是给lua用  
  public static bool IsNull(this Object obj)  
 {  return obj == null;  
 }}

Lua和系统类及委托相互使用

大纲

知识点

lua:

GameObject = CS.UnityEngine.GameObject
UI = CS.UnityEngine.UI

local slider = GameObject.Find("Slider")
print(slider)
local sliderScript = slider:GetComponent(typeof(UI.Slider))
print(sliderScript)

--这里等于把lua的函数作为参数传给了C#的方法进行执行
--而这个参数在Unity中是一个委托
--C#获取到lua函数后,
--C#需要提前使用[CsharpCallLua]对其要装的委托进行预制
--但是这是一个系统类,我们无法直接加
--得通过特殊方法(请看下文的C#代码)
sliderScript.onValueChanged:AddListener(function(f)
	print(f)
end)

C#:
为系统委托加上特性(这是一个固定写法):

public  static  class Lesson10  
{  
 [CSharpCallLua]  
  public static List<Type> csharpCallLuaList = new List<Type>()  
  {  
typeof(UnityAction<float>)  
 };}

Lua使用C#协程

大纲

知识点

Lua:

print("*********Lua调用C# 协程相关知识点***********")
--xlua提供的一个工具表
--一定是要通过require调用之后 才能用
util = require("xlua.util")
--C#中协程启动都是通过继承了Mono的类 通过里面的启动函数StartCoroutine

GameObject = CS.UnityEngine.GameObject
WaitForSeconds = CS.UnityEngine.WaitForSeconds
--在场景中新建一个空物体  然后挂一个脚本上去 脚本继承mono使用它来开启协程
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
			--停止协程和C#当中一样
			mono:StopCoroutine(b)
		end
	end
end
--我们不能直接将 lua函数传入到开启协程中!!!!!
--如果要把lua函数当做协程函数传入
--必须 先调用 xlua.util中的cs_generator(lua函数)
b = mono:StartCoroutine(util.cs_generator(fun))

Lua使用C#泛型函数

大纲

知识点

Lua:

print("*********Lua调用C# 泛型函数相关知识点***********")

local obj = CS.Lesson12()

local child = CS.Lesson12.TestChild()
local father = CS.Lesson12.TestFather()

--支持有约束有参数的泛型函数
obj:TestFun1(child, father)
obj:TestFun1(father, child)

--lua中不支持 没有约束的泛型函数
--obj:TestFun2(child)

--lua中不支持 有约束 但是没有参数的泛型函数
--obj:TestFun3()

--lua中不支持 非class的约束
--obj:TestFun4(child)

--补充知识 让上面 不支持使用的泛型函数 变得能用
--但是有一定的使用限制
--Mono打包 这种方式支持使用
--il2cpp打包  如果泛型参数是引用类型才可以使用
--il2cpp打包  如果泛型参数是值类型,除非C#那边已经调用过了 同类型的泛型参数 lua中才能够被使用

--得到通用函数  
--设置泛型类型再使用
--xlua.get_generic_method(类, "函数名")
local testFun2 = xlua.get_generic_method(CS.Lesson12, "TestFun2")
local testFun2_R = testFun2(CS.System.Int32)
--调用
--成员方法  第一个参数 传调用函数的对象
--静态方法 不用传
testFun2_R(obj, 1) 

C#:

public class Lesson12  
{  
  
  public interface ITest  
  {  
  
 }  
  public class TestFather  
  {  
  
 }  
  public class TestChild : TestFather, ITest  
  {  
  
 }  
  public void TestFun<T>(T a, T b) where T : TestFather  
  {  
  Debug.Log("有参数有约束的泛型方法");  
 }  
  public void TestFun2<T>(T a, T b)  
 {  Debug.Log("有参数,没有约束");  
 }  
  public void TestFun3<T>() where T : TestFather  
  {  
  Debug.Log("有约束,但是没有参数的泛型函数");  
 }  
  public void TestFun4<T>(T a) where T : ITest  
  {  
  Debug.Log("有约束有参数,但是约束不是类");  
 }}

注意

1.为什么对约束有强要求?其实很容易理解,lua本身没有类型,你得给Lua安排明白参数类型才可以使用泛型。
2.补充方法不建议使用,因为对性能有损耗。

第4节: Hotfix热补丁

第一个热补丁

大纲

知识点

Lua(热更新):

print("*********第一个热补丁***********")

--直接写好代码 运行 是会报错的
--我们必须做4个非常重要的操作
--1.加特性
--2.加宏 第一次开发热补丁需要加
--3.生成代码
--4.hotfix 注入  --注入时可能报错 提示你要引入Tools

--热补丁的缺点:只要我们修改了热补丁类的代码,我们就需要重新执行第4步!!!
--需要重新点击 注入

--lua当中 热补丁代码固定写法
--xlua.hotfix(类, "函数名", lua函数)

--成员函数 第一个参数 self
xlua.hotfix(CS.HotfixMain, "Add", function(self, a, b)
	return a + b
end)

--静态函数 不用传第一个参数
xlua.hotfix(CS.HotfixMain, "Speak", function(a)
	print(a)
end)

C#(需要被热更新的两个方法):

public int Add(int a, int b)  
{  
  return 0;  
}  
  
public static void Speak(string str)  
{  
  Debug.Log("哈哈哈哈");  
}

请注意,上面代码完成后,如果是第一次开启热更新,还有一些步骤。

如何实现热更新

1.为需要热更新的类加上注解[XLua.Hotfix]。

2.如果是第一次进行热补丁开发,需要加宏 File->BuildSetting->OtherSetting->Scripting Define Symbols添加
HOTFIX_ENABLE
如果添加成功 窗口Xlua选项下 会多一个选项

并且,你还需要在工程文件里面加上tool文件夹,这个文件夹存在于下载xlua时的文件包里面
警告:路径不可以有中文!!否则会找不到Tools包。

3.Xlua/Generate Code 生成代码,随后hotfix inject in editor 注入热补丁,即可实现热更新。

多函数替换(构造、析构)

大纲

知识点

Lua:

print("*********多函数替换***********")

--lua当中 热补丁代码固定写法
--xlua.hotfix(类, "函数名", lua函数)

--xlua.hotfix(类, {函数名 = 函数, 函数名 = 函数....})
xlua.hotfix(CS.HotfixMain, {
	Update = function(self)
		print(os.time())
	end,
	Add = function(self, a, b )
		return a + b
	end,
	Speak = function(a)
		print(a)
	end
})

xlua.hotfix(CS.HotfixTest, {
	--构造函数 热补丁固定写法[".ctor"]!!!!
	--他们和别的函数不同 不是替换 是先调用原逻辑 再调用lua逻辑
	[".ctor"] = function()
		print("Lua热补丁构造函数")
	end,
	Speak = function(self,a)
		print("唐老狮说" .. a)
	end,
	--析构函数固定写法Finalize
	Finalize = function()
		
	end
})

C#:
被热更新的C#的两个类:

[Hotfix()]  
public class HotfixTest  
{  
  public HotfixTest()  
 {  Debug.Log("构造函数");  
 }  
  public void Speak(string str)  
 {  Debug.Log(str);  
 }  
  //析构函数  
  ~HotfixTest()  
 {     }  
}  
 
[XLua.Hotfix]  
public class Main : MonoBehaviour  
{  
  HotfixTest hotTest;  
  void Start()  
 {  
  LuaMgr.GetInstance().Init();  
  LuaMgr.GetInstance().DoLuaFile("main");  
  
  print(Add(1,3));   
          
  Speak("wsw");  
  
  //没有继承Mono的class  
  hotTest = new HotfixTest();  
  hotTest.Speak("测试");  
 }  
  
  void Update()  
 {     }  
  
  public int Add(int a, int b)  
 { 
  return 0;  
 }  
  public static void Speak(string str)  
 {  
 Debug.Log("哈哈哈哈");  
 }  
}

注意

构造函数和析构函数热更新是在保留原先代码的基础上更新。

协程函数替换

知识点

Lua:

print("*********协程函数替换***********")

--xlua.hotfix(类, {函数名 = 函数, 函数名 = 函数....})
--要在lua中配合C#协程函数  那么必使用它
util = require("xlua.util")
xlua.hotfix(CS.HotfixMain, {
	TestCoroutine = function(self)
		--和之前lua执行协程不同,这里进行热更新需要套娃。
		--被return的这个函数里面的参数才是热更新对象
		--返回一个正儿八经的 xlua处理过的lua协程函数
		--return的内容是一个固定写法
		return util.cs_generator(function()
			while true do
				coroutine.yield(CS.UnityEngine.WaitForSeconds(1))
				print("Lua打补丁后的协程函数")
			end
		end)
	end
}

)


--如果我们为打了Hotfix特性的C#类新加了函数内容
--不能只注入  必须要先生成代码 再注入 不然注入会报错

C#:

[XLua.Hotfix]  
public class Main : MonoBehaviour  
{  
  void Start()  
 {  LuaMgr.GetInstance().Init();  
  LuaMgr.GetInstance().DoLuaFile("text");  
  StartCoroutine(TestCoroutine());  
  
  
 }  
  void Update()  
 {     }  
  
  
  
  
  IEnumerator TestCoroutine()  
 {  while (true)  
 {  yield return new WaitForSeconds(1f);  
  Debug.Log("C#协程打印一次");  
 } }  
}

注意

我使用时发生了报错,原因是没有CsharpCallLua,把IEnumerator加入CSharpCallLua的列表中即可解决,但是官方文档没有提到这个事情,原因未知。

public  static  class Lesson10
 {  
	[CSharpCallLua]    
	 public static List<Type> csharpCallLuaList = new List<Type>()    
	 { 
	 typeof(IEnumerator)    
	 };
 }

索引器和属性替换

大纲

知识点

Lua:

print("*********属性和索引器替换***********")

xlua.hotfix(CS.HotfixMain, {
	--如果是属性进行热补丁重定向
	--set_属性名 是设置属性 的方法
	--get_属性名 是得到属性 的方法
	set_Age = function(self, v)
		print("Lua重定向的属性"..v)
	end,
	get_Age = function(self)
		return 10;
	end,
	--索引器固定写法
	--set_Item 通说索引器设置
	--get_Item 通过索引器获取
	set_Item = function(self, index, v )
		print("Lua重定向索引器,索引:"..index .."值" ..v)
	end,
	get_Item = function(self, index)
		print("Lua重定向索引器")
		return 999
	end
})

C#:
被热更新的属性和索引:

public int[] array = new int[] { 1, 2, 3 };  
public int Age  
{  
  get  
  {  
  return 0;  
  }  
  set  
  {  
  Debug.Log(value);  
  }  
  
}  
  
public int this[int index]  
{  
  get  
  {  
  if (index >= array.Length || index < 0)  
 {  Debug.Log("索引越界");  
  return 0;  
 }  
  return array[index];  
 }  
  set  
  {  
  if (index >= array.Length || index < 0)  
 {  Debug.Log("索引越界");  
  return;  
 }  array[index] = value;  
 }}

事件操作替换

大纲

知识点

Lua:

print("*********事件加减替换***********")


xlua.hotfix(CS.HotfixMain, {
	--add_事件名 代表着事件加操作
	--remove_事件名 减操作
	add_myEvent = function(self, del)
		print(del)
		print("添加事件函数")
		--不要会去尝试使用lua使用C#事件的方法去添加
		--在事件加减的重定向lua函数中
		--千万不要把传入的委托往事件里存
		--否则会死循环
		--会把传入的 函数 存在lua中!!!!!
		--如错误代码写法:self:myEvent("+", del) 

	end,
	remove_myEvent = function(self, del )
		print(del)
		print("移除事件函数")
	end
})

C#:


public event UnityAction myEvent;
void Start()  
  {  
  LuaMgr.GetInstance().Init();  
  LuaMgr.GetInstance().DoLuaFile("text");  
  myEvent += text;  
  myEvent -= text;  
  }

注意

注意:如果要重定向事件加减到Lua中来
一定不要使用C#的事件加减了 self:myEvent("+/-", 传入函数)
因为会造成死循环
如果想重定向 肯定是希望把逻辑让lua处理
所以一定是把传入的函数 存到lua里的容器中

泛型类替换

大纲

知识点

Lua:

print("*********泛型类的替换***********")  
  
--泛型类 T是可以变化 那lua中应该如何替换呢?  
--lua中的替换 要一个类型一个类型的来  
  
xlua.hotfix(CS.HotfixTest2(CS.System.String), {  
  Test = function(self, str)  
  print("lua中打的补丁:"..str)  
  end  
})  
  
xlua.hotfix(CS.HotfixTest2(CS.System.Int32), {  
  Test = function(self, str)  
  print("lua中打的补丁:"..str)  
  end  
})

C#:
被替换的泛型类

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

注意

因为泛型可变
所以我们要指定某一个类型来进行替换
你想替换几个类型 就写几个补丁

第5章: xLua实践—背包功能

概述

第1节: 准备工作

前置步骤

步骤


各步骤在上文均有提到。
所需文件接在我 Lua背包工程准备资源 整合包内部

可参考文献:热更新实践–xlua实现背包面板逻辑学习笔记 - 码农教程 (manongjc.com)

第2节: Lua编程器环境搭建

注意

虽然唐老师使用的是VSCode插件,而本文博主依然使用Rider,所以会有大纲对不上的情况。
不过两款编辑器涉及到lua断点的插件都是同一款插件EmmyLua,但是使用和安装都是有所不同的,因此本博主写了一篇讲解Rider中EmmyLua的安装和使用过程。

大纲(不对应,为VSCode安装)

安装和使用Rider中的lua断点

Unity中Rider调试Lua断点的安装和使用 - 张先生的小屋 (klned.com)

第3节: 拼面板

面板拼凑

注意

永远记得一开始就调整Canves的分辨率适配。

第4节: 常用别名准备

常用类别名准备

代码

Lua:
脚本InitClass:

--常用别名都在这里定位  
--准备我们自己之前导入的脚本  
--面向对象相关  
require("Object")  
--字符串拆分  
require("SplitTools")  
--json解析  
Json =require("JsonUtility")  
  
--Unity相关  
GameObject=CS.UnityEngine.GameObject  
Resources=CS.UnityEngine.Resources  
Transform=CS.UnityEngine.Transform  
RectTransform=CS.UnityEngine.RectTransform  
--图像对象类  
SpriteAtlas=CS.UnityEngine.U2D.SpriteAtlas  
  
Vector3=CS.UnityEngine.Vector3  
Vector2=CS.UnityEngine.Vector2  
  
--UI相关  
UI=CS.UnityEngine.UI  
Image=UI.Image  
Text=UI.Text  
Button=UI.Button  
Toggle=UI.Toggle  
ScrollRect=UI.ScrollRect  
  
--自己写的C#脚本相关  
--直接得到AB包资源管理器的 单例对象  
ABMgr=CS.ABMgr.GetInstance()

注意

1.如果你在Rider里面直接创建Lua脚本,记得在右下角的文件编码(一般是UTF8)中选择选项,Remove BOM(即取消文本头部隐藏字符),否则可能会导致编译失败。

第5节: 数据准备

数据准备

大纲

准备步骤

1.exel写完数据,随后用Bejson网站转换为json文件:

[  
{"id":1,"name":"弓箭","icon":"Icon_2","type":1,"tips":"威力巨大的武器"},  
{"id":2,"name":"手套","icon":"Icon_1","type":1,"tips":"一副手套"},  
{"id":3,"name":"蓝药","icon":"Icon_3","type":2,"tips":"加魔法的药水"},  
{"id":4,"name":"红药","icon":"Icon_4","type":2,"tips":"加血量的药水"},  
{"id":5,"name":"红宝石","icon":"Icon_5","type":3,"tips":"合成高级道具的必备之物"},  
{"id":6,"name":"蓝宝石","icon":"Icon_6","type":3,"tips":"合成高级道具的必备之物"}  
]

2.将装备图标打入图集,随后,将图集(UI包) json文件(json包) 和3个UI预制体(UI包)打包进入AB包。

3.使用Lua进行读取:

一.前置:常用名脚本,会对应其他脚本里面的简写:

--常用别名都在这里定位  
--准备我们自己之前导入的脚本  
--面向对象相关  
require("Object")  
--字符串拆分  
require("SplitTools")  
--json解析  
Json =require("JsonUtility")  
  
--Unity相关  
GameObject=CS.UnityEngine.GameObject  
Resources=CS.UnityEngine.Resources  
Transform=CS.UnityEngine.Transform  
RectTransform=CS.UnityEngine.RectTransform  
TextAsset=CS.UnityEngine.TextAsset  
--图像对象类  
SpriteAtlas=CS.UnityEngine.U2D.SpriteAtlas  
  
Vector3=CS.UnityEngine.Vector3  
Vector2=CS.UnityEngine.Vector2  
  
--UI相关  
UI=CS.UnityEngine.UI  
Image=UI.Image  
Text=UI.Text  
Button=UI.Button  
Toggle=UI.Toggle  
ScrollRect=UI.ScrollRect  
  
--自己写的C#脚本相关  
--直接得到AB包资源管理器的 单例对象  
ABMgr=CS.ABMgr.GetInstance()

二.存储所有道具信息的ItemData脚本:

--先把json表从AB包加载出来  
--加载json文件里的文本对象  
local txt=ABMgr:LoadRes("json","ItemData",typeof(TextAsset))  
--获取文本信息,进行解析  
local itemList=json.decode(txt.text);  
print(itemList[2].name)  
  
--这样加载出来的就是一个表,类似数组的存在,表里面拥有了Json道具的信息  
--但是我们的id应该以我们写的id变量为准而不是先后顺序为准  
--而且我们也希望用字典形式更方便的查找数据,而不是数组  
--为此,我们重新用一张新表转存,并使用全局变量,让任何地方都可以使用。  
  
--一张用来存储道具信息的表  
--键值对形式,键是道具ID 值是道具吧的一行信息  
ItemData={}  
for _,value in pairs(itemList) do  
  ItemData[value.id]=value  
end  
  
for index,value in pairs(ItemData) do  
  print(index,value.name)  
end

三:存储和初始化玩家拥有的背包信息的PlayerData脚本:

PlayerData={}  
--我们目前只做背包功能 所以只需要它的道具信息即可  
  
--装备信息  
PlayerData.equips={}  
--道具信息  
PlayerData.items={}  
--宝石信息  
PlayerData.gems={}  
  
--为玩家数据写初始化方法,以后直接改这里的数据来源即可  
function PlayerData:Init()  
  --对玩家来说  
  --道具信息 无论是本地 还是服务器,肯定不会把所有的道具信息存进去  
  --只存道具ID和道具数量,然后再对照道具总表ItemData去读取  
  table.insert(self.equips,{id=1,num=1})  
  table.insert(self.equips,{id=2,num=2})  
  table.insert(self.items,{id=3,num=50})  
  table.insert(self.items,{id=4,num=20})  
  
  table.insert(self.gems,{id=5,num=99})  
  table.insert(self.gems,{id=6,num=81})  
end --初始化玩家背包数据  
PlayerData:Init()  
  
--测试:读取装备栏第一件物品的名字和数量  
print(ItemData[PlayerData.equips[1].id].name , PlayerData.equips[1].num

第6节: 纯Lua逻辑处理(结合第7节)

导入

本节会初步完成3个面板的Lua代码写法,但是并没有使用面向对象方法,第七节会使用面向对象代码进行优化

面板逻辑

大纲


请注意,该大纲和第7节共用。
itemBag到第7节进行面向对象优化后才会使用
本节将使用Lua的原生暴力法——local+数组来实现小格子UI的创建

知识点

接下来我们将在Lua中实现对UI对象的控制,请结合第三节大纲中的3个UI面板进行参考。

MainPanel(主页面板,用来打开装备面板):

--只要是一个新的对象(面板)我们那就新建一张表  
MainPanel = {}  
  
--不是必须写 因为lua的特性 不存在声明变量的概念  
--这样写的目的 是当别人看这个lua代码时  知道这个表(对象)有什么变量很重要  
--关联的面板对象  
MainPanel.panelObj = nil  
--对应的面板控件  
MainPanel.btnRole = nil  
MainPanel.btnSkill = nil  
  
--需要做 实例化面板对象  
--为这个面板 处理对应的逻辑 比如按钮点击等等  
  
--初始化该面板 实例化对象 控件事件监听  
function MainPanel:Init()  
  --面板对象没有实例化过 才去实例化处理  
  if self.panelObj == nil then  
  --1.实例化面板对象 ABMgr + 设置父对象  
  self.panelObj = ABMgr:LoadRes("ui", "MainPanel", typeof(GameObject))  
  self.panelObj.transform:SetParent(Canvas, false)  
  --2.找到对应控件   
 --找到子对象  再找到身上挂在的 想要的脚本  
  self.btnRole = self.panelObj.transform:Find("btnRole"):GetComponent(typeof(Button))  
  print(self.btnRole)  
  --3.为控件加上事件监听  进行点击等等的逻辑处理  
  --如果直接.传入自己的函数 那么在函数内部 没有办法用self获取内容  
  --self.btnRole.onClick:AddListener(self.BtnRoleClick)  
  self.btnRole.onClick:AddListener(function()  
  self:BtnRoleClick()  
  end)  
  end  
end  
  
function MainPanel:ShowMe()  
  self:Init()  
  self.panelObj:SetActive(true)  
end  
  
function MainPanel:HideMe()  
  self.panelObj:SetActive(false)  
end  
  
function MainPanel:BtnRoleClick()  
  --print(123123)  
 --print(self.panelObj) --等我们写了背包面板   
 --在这显示我们的 背包面板  
  BagPanel:ShowMe()  
end

BgaPanel(装备面板,包含格子面板的实现):

--一个面板对应一个表  
BagPanel = {}  
--"成员变量"  
--面板对象  
BagPanel.panelObj = nil  
--各个控件  
BagPanel.btnClose = nil  
BagPanel.togEquip = nil  
BagPanel.togItem = nil  
BagPanel.togGem = nil  
BagPanel.svBag = nil  
BagPanel.Content = nil  
--用来存储当前 显示的格子  
BagPanel.items = {}  
--用来存储当前显示的页签 类型 避免重复刷新  
BagPanel.nowType = -1  
  
--"成员方法"  
--初始化方法  
function BagPanel:Init()  
  
  if self.panelObj == nil then  
  --实例化面板对象  
  self.panelObj = ABMgr:LoadRes("ui", "BagPanel", typeof(GameObject))  
  self.panelObj.transform:SetParent(Canvas, false)  
  --找控件  
  --关闭按钮  
  self.btnClose = self.panelObj.transform:Find("btnClose"):GetComponent(typeof(Button))  
  --找3个toggle  
  local group = self.panelObj.transform:Find("Group")  
  self.togEquip = group:Find("togEquip"):GetComponent(typeof(Toggle))  
  self.togItem = group:Find("togItem"):GetComponent(typeof(Toggle))  
  self.togGem = group:Find("togGem"):GetComponent(typeof(Toggle))  
  --sv相关  
  self.svBag = self.panelObj.transform:Find("svBag"):GetComponent(typeof(ScrollRect))  
  self.Content = self.svBag.transform:Find("Viewport"):Find("Content")  
  --加事件  
  --关闭按钮  
  self.btnClose.onClick:AddListener(function()  
  self:HideMe()  
  end)  
  --单选框事件  
  --切页签  
  --toggle 对应委托 是 UnityAction<bool> --请注意,这里的委托们是有参数的    
 --相当于传入了一个来着Lua的匿名带参函数给C#执行    
 --所以我们需要去C#用静态法给此参数加上CsharpCallLua才可以用    
 self.togEquip.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(1)  
  end  
 end)  
  self.togItem.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(2)  
  end  
 end)  
  self.togGem.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(3)  
  end  
 end)  
  end  
  
end  
--显示隐藏  
function BagPanel:ShowMe()  
  self:Init()  
  self.panelObj:SetActive(true)  
  --第一次打开是 更新数据  
  if self.nowType == -1 then  
  self:ChangeType(1)  
  end  
end  
function BagPanel:HideMe()  
  self.panelObj:SetActive(false)  
end  
  
--逻辑处理函数 用来切页签的  
--type 1装备 2道具 3宝石  
function BagPanel:ChangeType(type)  
  --判断如果已经是该页签 就别更新了  
  if self.nowType == type then  
 return end  --切页  根据玩家信息 来进行格子创建  
  
  --更新之前 把老的格子删掉 BagPanel.items  for i = 1, #self.items do  
  --销毁格子对象  
  GameObject.Destroy(self.items[i].obj)  
  end  
  self.items = {}  
  
  --再根据当前选择的类型 来创建新的格子 BagPanel.items --要根据 传入的 type 来选择 显示的数据  
  local nowItems = nil  
  if type == 1 then  
  nowItems = PlayerData.equips  
    elseif type == 2 then  
  nowItems = PlayerData.items  
    else  
  nowItems = PlayerData.gems  
    end  
  
  --创建格子  
  for i = 1, #nowItems do  
  --有格子资源 在这 加载格子资源 实例化 改变图片 和 文本 以及位置即可  
  local grid = {}  
  --用一张新表 代表 格子对象 里面的属性 存储对应想要的信息  
  grid.obj = ABMgr:LoadRes("ui", "ItemGrid");  
  --设置父对象  
  grid.obj.transform:SetParent(self.Content, false)  
  --继续设置他的位置  
  grid.obj.transform.localPosition = Vector3((i-1)%4 * 175, math.floor((i-1)/4)*175, 0)  
  --找控件  
  grid.imgIcon = grid.obj.transform:Find("imgIcon"):GetComponent(typeof(Image))  
 grid.Text = grid.obj .transform:Find("Text"):GetComponent(typeof(Text))  
  --设置它的图标  
  --通过 道具ID 去读取 道具配置表 得到 图标信息  
  local data = ItemData[nowItems[i].id]  
  --想要的是data中的 图标信息  
  --根据名字 先加载图集 再加载图集中的 图标信息  
  local strs = string.split(data.icon, "_")  
  --加载图集  
  local spriteAtlas = ABMgr:LoadRes("ui", strs[1], typeof(SpriteAtlas))  
  --加载图标  
  grid.imgIcon.sprite = spriteAtlas:GetSprite(strs[2])  
  --设置它的数量  
  grid.Text.text = nowItems[i].num  
  
        --把它存起来  
  table.insert(self.items, grid)  
  end  
end

注意

本节中我们实现了3个UI控件的显示,但是有一个问题,那就是我们使用了Lua的暴力方法直接实现了这个效果,还将道具面板和格子写到了一起,并且没有对格子实现面向对象,拥有过高的耦合性且不利于未来的维护,因此第7节我们将进行面向对象优化,新增格子脚本ItemBag,将其解耦。

第7节: 用面向对象思想优化(结合第6节)

对格子和背包面板的脚本进行切分优化

大纲

知识点

优化后改进的BagPanel脚本:

--一个面板对应一个表  
BagPanel = {}  
--"成员变量"  
--面板对象  
BagPanel.panelObj = nil  
--各个控件  
BagPanel.btnClose = nil  
BagPanel.togEquip = nil  
BagPanel.togItem = nil  
BagPanel.togGem = nil  
BagPanel.svBag = nil  
BagPanel.Content = nil  
--用来存储当前 显示的格子  
BagPanel.items = {}  
--用来存储当前显示的页签 类型 避免重复刷新  
BagPanel.nowType = -1  
  
--"成员方法"  
--初始化方法  
function BagPanel:Init()  
  
  if self.panelObj == nil then  
  --实例化面板对象  
  self.panelObj = ABMgr:LoadRes("ui", "BagPanel", typeof(GameObject))  
  self.panelObj.transform:SetParent(Canvas, false)  
  --找控件  
  --关闭按钮  
  self.btnClose = self.panelObj.transform:Find("btnClose"):GetComponent(typeof(Button))  
  --找3个toggle  
  local group = self.panelObj.transform:Find("Group")  
  self.togEquip = group:Find("togEquip"):GetComponent(typeof(Toggle))  
  self.togItem = group:Find("togItem"):GetComponent(typeof(Toggle))  
  self.togGem = group:Find("togGem"):GetComponent(typeof(Toggle))  
  --sv相关  
  self.svBag = self.panelObj.transform:Find("svBag"):GetComponent(typeof(ScrollRect))  
  self.Content = self.svBag.transform:Find("Viewport"):Find("Content")  
  --加事件  
  --关闭按钮  
  self.btnClose.onClick:AddListener(function()  
  self:HideMe()  
  end)  
  --单选框事件  
  --切页签  
  --toggle 对应委托 是 UnityAction<bool>  self.togEquip.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(1)  
  end  
 end)  
  self.togItem.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(2)  
  end  
 end)  
  self.togGem.onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(3)  
  end  
 end)  
  end  
  end  
--显示隐藏  
function BagPanel:ShowMe()  
  self:Init()  
  self.panelObj:SetActive(true)  
  --第一次打开是 更新数据  
  if self.nowType == -1 then  
  self:ChangeType(1)  
  end  
end  
function BagPanel:HideMe()  
  self.panelObj:SetActive(false)  
end  
  
--逻辑处理函数 用来切页签的  
--type 1装备 2道具 3宝石  
function BagPanel:ChangeType(type)  
  --判断如果已经是该页签 就别更新了  
  if self.nowType == type then  
 return end  --切页  根据玩家信息 来进行格子创建  
  
  --更新之前 把老的格子删掉 BagPanel.items  for i = 1, #self.items do  
  --销毁格子对象  
  self.items[i]:Destroy()  
  end  
  self.items = {}  
  
  --再根据当前选择的类型 来创建新的格子 BagPanel.items --要根据 传入的 type 来选择 显示的数据  
  local nowItems = nil  
  if type == 1 then  
  nowItems = PlayerData.equips  
    elseif type == 2 then  
  nowItems = PlayerData.items  
    else  
  nowItems = PlayerData.gems  
    end  
  
  --创建格子  
  for i = 1, #nowItems do  
  --根据数据 创建一个格子对象  
  local grid = ItemGrid:new()  
  --要实例化对象 设置位置  
  grid:Init(self.Content, (i-1)%4*175, math.floor((i-1)/4)*175)  
  --初始化它的信息 数量和图标  
  grid:InitData(nowItems[i])      
 --把它存起来  
  table.insert(self.items, grid)  
  end  
end

优化后多出来的格子ItemGrid脚本:

--用到之前讲过的知识 Object--生成一个table 集成Object 主要目的是要它里面实现的 继承方法subClass和new  
Object:subClass("ItemGrid")  
--“成员变量”  
ItemGrid.obj = nil  
ItemGrid.imgIcon = nil  
ItemGrid.Text = nil  
--成员函数  
--实例化格子对象  
function ItemGrid:Init(father, posX, posY)  
  --实例化格子对象  
  self.obj = ABMgr:LoadRes("ui", "ItemGrid");  
  --设置父对象  
  self.obj.transform:SetParent(father, false)  
  --继续设置他的位置  
  self.obj.transform.localPosition = Vector3(posX, posY, 0)  
  --找控件  
  self.imgIcon = self.obj.transform:Find("imgIcon"):GetComponent(typeof(Image))  
  self.Text = self.obj .transform:Find("Text"):GetComponent(typeof(Text))  
end  
  
--初始化格子信息  
--data 是外面传入的 道具信息 里面包含了 id和num  
function ItemGrid:InitData(data)  
  --通过 道具ID 去读取 道具配置表 得到 图标信息  
  local itemData = ItemData[data.id]  
  --想要的是data中的 图标信息  
  --根据名字 先加载图集 再加载图集中的 图标信息  
  local strs = string.split(itemData.icon, "_")  
  --加载图集  
  local spriteAtlas = ABMgr:LoadRes("ui", strs[1], typeof(SpriteAtlas))  
  --加载图标  
  self.imgIcon.sprite = spriteAtlas:GetSprite(strs[2])  
  --设置它的数量  
  self.Text.text = data.num  
end  
  
--加自己的逻辑  
function ItemGrid:Destroy()  
  GameObject.Destroy(self.obj)  
  self.obj = nil  
end

利用面板基类,对所有UI面板进行父子类对象化

大纲

面板基类

前置Object
用来实现Lua的对象化:

--面向对象实现 --万物之父 所有对象的基类 Object--封装  
Object = {}  
--实例化方法  
function Object:new()  
  local obj = {}  
  --给空对象设置元表 以及 __index  self.__index = self  
  setmetatable(obj, self)  
  return obj  
end  
--继承  
function Object:subClass(className)  
  --根据名字生成一张表 就是一个类  
  _G[className] = {}  
  local obj = _G[className]  
  --设置自己的“父类”  
  obj.base = self  
  --给子类设置元表 以及 __index  self.__index = self  
  setmetatable(obj, self)  
end

面板基类BasePanel:

--利用面向对象  
Object:subClass("BasePanel")  
  
BasePanel.panelObj = nil  
--相当于模拟一个字典 键为 控件名 值为控件本身  
BasePanel.controls = {}  
--事件监听标识  
BasePanel.isInitEvent = false  
  
function BasePanel:Init(name)  
  if self.panelObj == nil then  
  --公共的实例化对象的方法  
  self.panelObj = ABMgr:LoadRes("ui", name, typeof(GameObject))  
  self.panelObj.transform:SetParent(Canvas, false)  
  --GetComponentsInChildren  
 --找到所有UI控件 存起来(UIBehaviour是所有UI控件的父类)  
  print(self.panelObj)  
  local allControls = self.panelObj:GetComponentsInChildren(typeof(UIBehaviour))  
  --如果存入一些对于我们来说没用UI控件   
 --为了避免 找各种无用控件 我们定一个规则 拼面板时 控件命名一定按规范来  
  --Button btn名字  
  --Toggle tog名字  
  --Image img名字  
  --ScrollRect sv名字  
  for i = 0, allControls.Length-1 do  
 local controlName = allControls[i].name  
            --按照名字的规则 去找控件 必须满足命名规则 才存起来  
  if string.find(controlName, "btn") ~= nil or   
 string.find(controlName, "tog") ~= nil or   
 string.find(controlName, "img") ~= nil or   
 string.find(controlName, "sv") ~= nil or  
  string.find(controlName, "txt") ~= nil then  
  --一个物体上是有可能有多个符合条件的控件的  
  --为了避免出现一个对象上 连续挂载多个UI控件 出现覆盖的问题   
 --我们都存到一个容器中 相当于像列表数组的形式(即一个对象键存多个控件)  
  --最终存储形式   
 --{ btnRole = { Image = 控件, Button = 控件 }, --  togItem = { Toggle = 控件} }  
                 --为了让我们在得的时候 能够 确定得的控件类型 所以我们需要存储类型  
  --用[控件名表][脚本名表]来存  
  --也许看起来类似是二维数组,但是lua是没有二维数组的  
  --本质上,只是[控件]表里面套娃了一个[脚本]表,[脚本]里面的值才是控件对象  
  --利用反射 Type 得到 控件的类名   
 local typeName = allControls[i]:GetType().Name  
                if self.controls[controlName] ~= nil then  
  --通过自定义索引的形式 去加一个新的 “成员变量”  
  self.controls[controlName][typeName] = allControls[i]  
  else  
  self.controls[controlName] = {[typeName] = allControls[i]}  
  end  
 end end endend  
  
--得到控件 根据 控件依附对象的名字 和 控件的类型字符串名字 Button Image Togglefunction BasePanel:GetControl(name, typeName)  
  if self.controls[name] ~= nil then  
 local sameNameControls = self.controls[name]  
  if sameNameControls[typeName] ~= nil then  
 return sameNameControls[typeName]  
  end  
 end return nil  
end  
  
function BasePanel:ShowMe(name)  
  self:Init(name)  
  self.panelObj:SetActive(true)  
end  
  
function BasePanel:HideMe()  
  self.panelObj:SetActive(false)  
end

面板基类的使用

主面板MainPanel:

--只要是一个新的对象(面板)我们那就新建一张表  
BasePanel:subClass("MainPanel")  
  
--需要做 实例化面板对象  
--为这个面板 处理对应的逻辑 比如按钮点击等等  
  
--初始化该面板 实例化对象 控件事件监听  
function MainPanel:Init(name)  
  self.base.Init(self, name)  
  --为了只添加一次事件监听  
  if self.isInitEvent == false then  
  print(self:GetControl("btnRole", "Image"))  
  self:GetControl("btnRole", "Button").onClick:AddListener(function()  
  self:BtnRoleClick()  
  end)  
  
  self.isInitEvent = true  
  end  
  end  
  
function MainPanel:BtnRoleClick()  
  --print(123123)  
 --print(self.panelObj) --等我们写了背包面板   
 --在这显示我们的 背包面板  
  BagPanel:ShowMe("BagPanel")  
end

背包面板:

--一个面板对应一个表  
BasePanel:subClass("BagPanel")  
  
BagPanel.Content = nil  
--用来存储当前 显示的格子  
BagPanel.items = {}  
--用来存储当前显示的页签 类型 避免重复刷新  
BagPanel.nowType = -1  
  
--"成员方法"  
--初始化方法  
function BagPanel:Init(name)  
  self.base.Init(self, name)  
  
  if self.isInitEvent == false then  
  --找到没有挂在UI控件的 对象 还是需要手动去找  
  self.Content = self:GetControl("svBag", "ScrollRect").transform:Find("Viewport"):Find("Content")  
  --加事件  
  --关闭按钮  
  self:GetControl("btnClose", "Button").onClick:AddListener(function()  
  self:HideMe()  
  end)  
  --单选框事件  
  --切页签  
  --toggle 对应委托 是 UnityAction<bool>  self:GetControl("togEquip", "Toggle").onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(1)  
  end  
 end)  
  self:GetControl("togItem", "Toggle").onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(2)  
  end  
 end)  
  self:GetControl("togGem", "Toggle").onValueChanged:AddListener(function(value)  
  if value == true then  
  self:ChangeType(3)  
  end  
 end)  
  
  self.isInitEvent = true  
  end  
end  
--显示隐藏  
function BagPanel:ShowMe(name)  
  self.base.ShowMe(self, name)  
  --第一次打开是 更新数据  
  if self.nowType == -1 then  
  self:ChangeType(1)  
  end  
end  
  
--逻辑处理函数 用来切页签的  
--type 1装备 2道具 3宝石  
function BagPanel:ChangeType(type)  
  --判断如果已经是该页签 就别更新了  
  if self.nowType == type then  
 return end  --切页  根据玩家信息 来进行格子创建  
  
  --更新之前 把老的格子删掉 BagPanel.items  for i = 1, #self.items do  
  --销毁格子对象  
  self.items[i]:Destroy()  
  end  
  self.items = {}  
  
  --再根据当前选择的类型 来创建新的格子 BagPanel.items --要根据 传入的 type 来选择 显示的数据  
  local nowItems = nil  
  if type == 1 then  
  nowItems = PlayerData.equips  
    elseif type == 2 then  
  nowItems = PlayerData.items  
    else  
  nowItems = PlayerData.gems  
    end  
  
  --创建格子  
  for i = 1, #nowItems do  
  --根据数据 创建一个格子对象  
  local grid = ItemGrid:new()  
  --要实例化对象 设置位置  
  grid:Init(self.Content, (i-1)%4*175, math.floor((i-1)/4)*175)  
  --初始化它的信息 数量和图标  
  grid:InitData(nowItems[i])      
 --把它存起来  
  table.insert(self.items, grid)  
  end  
end

总结

使用自制的lua面向对象,可以节省大量的代码量
我们也由此在lua中实现了类似C#UI控件管理的效果

第6章:Lua迁移小工具开发

Lua迁移小工具开发

大纲

目的

开发阶段的时候,我们大概率会直接使用和调用工程里面Lua文件,发布的时候才会打包到AB包里面。
而且由于AB包不认Lua文件,我们还得给原来的Lua文件一个一个加上txt后缀,十分的繁琐。

因此,我们可以开发一个小工具来辅助实现这个过程。

代码

迁移工具C#脚本LuaCopyEditor :

//先继承Editor类
public class LuaCopyEditor : Editor  
{  
 [MenuItem("XLua/自动生成txt后缀的Lua")]  
  public static void CopyLuaToTxt()  
 {  //首先要找到 我们的所有Lua文件  
  string path = Application.dataPath + "/Lua/";  
  //判断路径是否存在  
  if( !Directory.Exists(path) )  
  return;  
  //得到每一个lua文件的路径 才能进行迁移拷贝  
  string[] strs = Directory.GetFiles(path, "*.lua");  
  
  //然后把Lua文件拷贝到一个新的文件夹中  
  //首先定一个新路径  
  string newPath = Application.dataPath + "/LuaTxt/";  
  
  //为了避免一些被删除的lua文件 不再使用 我们应该先清空目标路径  
  
  //判断新路径文件夹 是否存在  
  if( !Directory.Exists(newPath) )  
  Directory.CreateDirectory(newPath);  
  else  
  {  
  //得到该路径中 所有后缀.txt的文件 把他们全部删除了  
  string[] oldFileStrs = Directory.GetFiles(newPath, "*.txt");  
  for (int i = 0; i < oldFileStrs.Length; i++)  
 {  File.Delete(oldFileStrs[i]);  
 } }  
 
 //数组原来记录所有txt的lua文件路径
 //方便最后AB包的修改
  List<string> newFileNames = new List<string>();  

//用来得到新的文件路径
  string fileName;  
  for(int i = 0; i < strs.Length; ++i)  
 { 
  //得到新的文件路径 用于拷贝  
  fileName = newPath + strs[i].Substring(strs[i].LastIndexOf("/")+1) + ".txt";  
 newFileNames.Add(fileName);  
  File.Copy(strs[i], fileName);  
 }  
  AssetDatabase.Refresh();  
  
  //刷新过后再来改AB包 因为 如果不刷新 第一次改变 会没用  
  for (int i = 0; i < newFileNames.Count; i++)  
 {  
 //Unity API  
 //此API传入的路径 必须是 相对Assets文件夹的 Assets/..../.... 
 //但是我们之前得到的是电脑的完整路径
 //所以路径又得再改变一次
  AssetImporter importer = AssetImporter.GetAtPath( newFileNames[i].Substring(newFileNames[i].IndexOf("Assets")));  
  if(importer != null)  
 importer.assetBundleName = "lua";  
 } }}

一个坑

打AB包的时候会和Xlua有一定冲突 会报错
所以打包的时候 先清xlua代码 再打包
打包完毕过后再重新生成xlua代码

原因不明,可能和xlua的源码有关