热更新介绍 热更新是指游戏或者软件更新时无需重新下载客户端进行安装,而是在应用程序启动的情况下,在内部进行的资源或者代码更新。
热更新的好处:
迅速修复 Bug——避免重新下载安装包,游戏内部更新修复 Bug
提升玩家留存率——避免玩家因为超大的安装包而流失
迅速换“内核”——挂羊头卖狗肉
学习目标:
热更新基础———AssetBundle 文章传送门 : Unity 热更新基础 — AssetBundle
热更新基础—- Lua 语法
热更新方案———xLua
热更新方案———toLua
热更新实践——基于 xLua 的背包系统
Lua 语法 第一个 Lua 程序 注释
输出函数
变量
lua 中所有的变量申明都不需要申明变量类型,会自动的判断类型,类似 C#里面的 var
lua 中的一个变量可以随便赋值 — 自动识别类型
通过 type 函数返回值时 string 我们可以得到变量的类型
四种简单变量类型
nil
:空
number
:数值类型
string
:字符串
boolean
:布尔类型
复杂四种变量类型
function
:函数
table
:表
userdata
:数据结构
thread
:协同程序
type 函数得到变量的类型,返回值是一个 string。
变量注意点
Lua 中变量可以直接使用,不需要声明
Lua 中可以直接使用没有初始化的变量,默认为 nilLua 中不用指明变量类型,可以自动识别
lua 中使用没有声明过的变量,不会报错默认值,是 nil
字符串操作 字符串定义
1 2 3 4 5 6 7 8 str = "双引号字符串" str2 = '单引号字符串' str3 = [[Hello I am Anii ]]
获取字符串的长度 : 一个汉字 3 字节,英文字符 占 1 个长度
1 2 3 s = "abcdefG字符串" print (#s)
字符串多行打印 : lua 中支持转义字符
1 2 3 4 5 6 7 s = [[I am Anii Hello World ]] print (s)print ("123\n234" )
字符串拼接 : 使用 ..
直接拼接字符串
1 print ("Hello" .. "I am " .. "Anii" )
使用 string.format 格式化输出
%d
:与数字拼接
%a
:与任何字符拼接
%s
:与字符配对
1 print (string .format ("我是Anii,今年%d" , 1 ))
别的类型转字符串
使用tostring
1 2 a = true print (tostring (a))
字符串提供的公共方法
小写转大写
1 2 print (string .upper (str))
大写转小写
1 2 print (string .lower (str))
翻转字符串
1 2 print (string .reverse (str))
字符串索引查找
1 2 print (string .find (str, "abc" ))
字符串重复
1 2 print (string .rep ("123" , 3 ))
截取字符串
1 2 print (string .sub (str, 3 , 4 ))
字符串修改
1 2 print (string .gsub (str, "ab" , "**" ))
字符转 ASCII 码
1 2 print (string .byte ("a" , 1 ))
ASCII 码转字符
1 2 print (string .char (string .byte ("a" , 1 )))
运算符 算数运算符
没有自增自减 ++ --
没有复合运算符 += -= /= *= %=
字符串可以进行算数运算符操作,会自动转成 number
+,-,*,/,%
基本上和 C#一致,可以取余小数
^
是取幂运算
>,<,>=,<=
,不等于是 ~=
逻辑与 and
,逻辑或 or
,取非 not
注意:
a and b
:当 a 为 true 时返回 b 的值,否则返回 a 的值
a or b
:当 a 为 true 时返回 a 的值,否则返回 b 的值
真:true
,假 false
不支持位运算符
不支持三目运算符
模拟实现三目运算符
a and b
:当 a 为 true 时返回 b 的值,否则返回 a 的值
a or b
:当 a 为 true 时返回 a 的值,否则返回 b 的值
1 2 3 4 5 print (1 >2 and 1 or 2 )print (1 <2 and nil or 2 )
条件分支语句 注意:
lua 当中,nil
和 false
为假,其他为真(也就是说 0 为真)
单分支
1 2 3 4 5 if a > 5 then print ("123" ) end
双分支
1 2 3 4 5 6 7 if a < 5 then print ("123" ) else print ("123" ) end
多分支
1 2 3 4 5 6 7 a = 9 if a < 5 then print ("123" ) elseif a > 6 then print ("234" ) end
循环语句 注意:循环语句没有 continue,只有 break
while 语句 1 2 3 4 5 6 num = 0 while num < 5 do print (num) num = num + 1 end
repeat until 语句(do… while) 注意:满足条件退出
1 2 3 4 5 6 num = 0 repeat print (num) num = num + 1 until num > 5
for 语句 注意:i 会到达 5,不是 i < 5
第一个参数,下标
第二个参数,下标能到达的最后一个值
第三个参数,步长
1 2 3 4 5 6 7 8 9 10 11 12 for i =2 ,5 do print (i) end for i =1 ,5 ,2 do print (i) end for i =5 ,1 ,-1 do print (i) end
函数(变长,嵌套闭包,多返回值) 函数定义 注意:函数也是一种类型
1 2 3 4 5 function 函数名() end a = function () end
无参数无返回值 1 2 3 4 function F1 () print ("F1函数" ) end F1()
可以使用变量接受函数,有点类似 C#中的 委托和事件
此时,通过变量调用函数
1 2 3 4 F2 = function () print ("F2函数" ) end F2()
有参数 1 2 3 4 5 6 function F3 (a) print (a) end F3(1 ) F3("123" ) F3(true )
如果你传入的参数和函数参数个数不匹配,不会报错只会补空 nil 或者丢弃。
有返回值
多返回值时,在前面申明多个变量来接取即可
如果变量不够,不影响,值接取对应位置的返回值
如果变量多了不对应,直接赋 nil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function F4 (a) return a, "123" , true end temp, temp2, temp3, temp4 = F4("1" ) print (temp)print (temp2)print (temp3)print (temp4)1 123 true nil
函数的类型 函数的类型就是 function
1 2 3 4 5 6 7 F5 = function ( ) print ("123" ) end print (type (F5))function
函数的重载
1 2 3 4 5 6 7 8 9 10 function F6 () print ("Awesome" ) end function F6 (str) print (str) end F6() nil
变长参数 使用 ...
表示
传入的参数就是 ...
,使用一个表接受即可
固定参+可变参,固定参一定在可变参之前
1 2 3 4 5 6 7 8 function F7 ( ... ) arg = {...} for i=1 ,#arg do print (arg [i]) end end F7(1 ,"123" ,true ,4 ,5 ,6 )
函数嵌套 1 2 3 4 5 6 7 8 9 function F8 () return function () print (123 ); end end f9 = F8() f9() 123
闭包 1 2 3 4 5 6 7 8 9 10 11 12 function F9 (x) return function (y) return x + y end end f10 = F9(10 ) print (f10(5 ))15
注意:所有的复杂类型都是 table(表)
table 表实现数组 直接在表中声明元素,数组下标从 1 开始。
一维数组
1 a = {1 ,2 ,nil ,3 ,"1231" ,true ,nil }
二维数组(就是表中表)
1 2 3 4 5 6 7 8 9 10 11 a = {{1 ,2 ,3 ,4 }, {2 ,3 ,4 ,5 }} for i = 1 ,#a do temp = a[i] s = "" for j = 1 , #temp do s = s .. temp[j] .. " " end print (s) end
自定义索引
1 2 3 4 5 6 7 8 9 10 a = {[1 ] = 1 ,[2 ] = 2 ,[5 ] = 4 ,[6 ] = 6 } print (a[1 ])print (a[6 ])print (#a)1 6 2
迭代器遍历(ipairs 和 pairs 区别) 迭代器遍历 迭代器遍历,主要是用来遍历表的
得到长度,其实并不准确,一般不要用#来遍历表
ipairs 迭代器遍历 ipairs 遍历,还是从 1 开始往后遍历的,小于等于 0 的值得不到
只能找到连续索引的键,如果中间断序了 ,它也无法遍历出后面的内容
只能获取到从 1 开始的下标
1 2 3 4 5 6 7 8 a = {[0 ] = 1 , 2 , [-1 ]=3 , 4 , 5 , [5 ] = 6 } s = "" for i, v in ipairs (a) do s = s .. v end print (s)
ipairs 迭代器遍历键 注意:这就相当于 foreach
1 2 3 4 5 6 7 8 s = "" for i in ipairs (t) do s = s .. i end print (s)123
pairs 迭代器遍历 它能够把所有的键都找到,通过键可以得到值
1 2 3 4 5 6 7 8 s = "" for k, v in pairs (t) do s = s .. t[k] end print (s)245136
pairs 迭代器遍历键 1 2 3 4 5 6 7 8 s = "" for i in pairs (t) do s = s .. t[i] end print (s)245136
table 表实现字典 字典的申明 字典是由键值对构成,而 table 可以自定义索引,那么,使用自定义索引即可定义字典
1 2 3 4 5 6 7 8 9 10 a = {["name" ] = "Anii" , ["age" ] = 14 , ["city" ] = "hunan" , ["1" ] = "number" } print (a["name" ])print (a["age" ])print (a["city" ])Anii 14 hunan
虽然可以通过.
成员变量的形式得到值,但是不能是数字
1 2 3 4 5 6 7 8 9 10 11 print (a.name)print (a.age)print (a["1" ])Anii 14 number
字典操作 1 2 3 4 5 6 7 8 9 10 11 12 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)
字典的遍历 如果要模拟字典,遍历一定用pairs
同时遍历键值
1 2 3 4 5 6 7 8 9 10 for k,v in pairs (a) do print (k,v) end 1 numbercity hunan name Anii age 14
遍历值
1 2 3 4 5 6 7 8 9 for k in pairs (a) do print (a[k]) end number hunan Anii 14
table 表实现类(点和冒号的区别,self) Lua 中是默认没有面向对象的,需要我们自己来实现
成员变量,成员函数 其实,就是通过表可以存储任意类型和可以通过直接 .
出自定义索引的特性。
所以此时一个类无需实例化对象了,这个表就是对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Student = { age = 18 , sex = true , Up = function () print (Student.age) print ("我成长了" ) end , Learn = function (t) print (t.sex) print ("好好学习,天天向上" ) end }
在表内部,使用成员,直接使用表名 .
出来即可,如果直接写一个变量,将是一个全局变量。
因为函数也是一种类型,所以如果想定义函数,直接存储函数变量即可
如果想在函数内访问本身,除了使用表名 .
出来,还可以写一个参数,传入自己,再调用
1 2 3 4 5 6 Learn = function (t) print (t.sex) print ("好好学习,天天向上" ) end
成员的使用
成员变量:直接点出来
成员函数,点出来,传入自己,或者使用 冒号 自动传入自己
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Student.Learn(Student) Student:Learn() print (Student.age)Student.name = "Anii" Student.Speak = function () print ("I AM SPEAKING!" ) end true 好好学习,天天向上 true 好好学习,天天向上 18
在表外声明成员函数 lua 中 有一个关键字 self
表示默认传入的第一个参数(类似 python)
1 2 3 4 function Student:Speak2 () print (self .name .. "说话" ) end
C#要是使用类,实例化对象 new,静态直接点
Lua 中类的表现,更像是一个类中有很多静态变量和函数,一个表就是一个对象
1 2 3 4 5 6 print (Student.age)print (Student.name)Student.Up() Student.Speak() Student:Speak2() Student.Speak2(Student)
table 表的公共操作(插入,移除,排序,拼接) 插入 使用 table.insert(arg1, argc2)
函数,将一个元素插入到表后
另一种形式 table.insert(arg1, arg2, argc3)
参数一:插入到哪个表
参数二:插入位置
参数二:插入什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 t1 = { {age = 1 , name = "123" }, {age = 2 , name = "345" } } t2 = {name = "Anii" , sex = true } print (#t1)table .insert (t1, t2);print (#t1)print (t1[1 ])print (t1[2 ])print (t1[3 ])print (t1[3 ].sex)2 3 table : 00 D6A2D0table : 00 D6A2F8table : 00 D6A4D8true
移除 删除指定元素
table.remove
方法,传表进去,会移除最后一个索引的内容
1 2 3 4 5 6 7 8 9 10 11 table .remove (t1)print (#t1)print (t1[1 ].name)print (t1[2 ].name)print (t1[3 ])2 table : 00 C2A348table : 00 C2A028table : 00 C2A050
传两个参数
参数一:是要移除内容的表
参数二:是要移除内容的索引
1 2 3 4 5 6 7 table .remove (t1, 1 )print (t1[1 ].name)print (#t1)true 2
排序 传入要排序的表,默认升序排列,根据内容排序
1 2 3 4 5 6 7 8 9 10 t2 = {name = "Anii" , sex = true , city = "Able" } table .sort (t2)for _,v in pairs (t2) do print (v) end Able Anii true
自定义排序规则:传入两个参数,第一个是用于排序的表,第二个是排序规则函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 t2 = {1 ,2 ,3 ,4 ,5 } table .sort (t2, function (a,b) if a > b then return true end end )for _,v in pairs (t2) do print (v) end 5 4 3 2 1
拼接 连接函数,用于拼接表中元素,返回值是一个字符串
使用 table.concat(tb, ",")
,将第二个参数连接到 tb
表中的每一个元素,返回连接后的字符串
1 2 3 4 5 6 tb = {"123" , "456" , "789" , "10101" } str = table .concat (tb, "," ) print (str)123 ,456 ,789 ,10101
全局变量和本地变量,多脚本执行(require,package,_G) 全局变量 只要是没有 local 修饰的变量,就是全局变量
1 2 3 4 5 6 7 for i = 1 ,2 do c = "Anii" end print (c)Anii
本地变量 也就是局部变量,使用 local
修饰的变量
1 2 3 4 5 6 7 for i = 1 , 10 do local num = 10 end print (num)nil
多脚本执行 关键字 require("脚本名")
, require('脚本名')
类似于引用命名空间
1 2 3 4 5 6 7 8 9 require ('Test' )print (testA)print (testLocalA)Test测试 456 123 nil
Test.lua
注意:本地变量无法在外部脚本获取
1 2 3 4 print ("Test测试" )testA = "123" local testLocalA = "456" print (testLocalA)
脚本卸载 如果是 require 加载执行的脚本,加载一次过后不会再被执行
1 2 3 4 5 6 require ('Test' )require ('Test' )Test测试 456
获取脚本是否加载 返回值是 boolean,意思是,该脚本是否被执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 print ("Test测试" )testA = "123" local testLocalA = "456" print (testLocalA)require ("Test" )print (package .loaded ["Test" ])Test测试 456 true
卸载已经执行过的脚本 1 2 package .loaded ["Test" ] = nil print (package .loaded ["Test" ])
返回局部变量 require 执行一个脚本时 ,可以在脚本最后返回一个外部希望获取的内容
1 2 3 4 5 6 print ("Test测试" )testA = "123" local testLocalA = "456" print (testLocalA)return testLocalA
1 2 3 4 5 local testLA = require ("Test" )print (testLA)456
注意:此时 package.loaded["Test"]
也是返回的此值
大 G 表 _G表
是一个总表(table),将我们声明的所有全局的变量都存储在其中,使用 require
添加的脚本当中的 table,也会在大 G 表当中。
1 2 3 for k,v in pairs (_G ) do print (k,v) end
本地变量,加了 local 的变量不会存到大_G 表中
特殊用法(多变量赋值,三目运算符) 多变量赋值
多变量赋值,如果后面的值不够,会自动补空
1 2 3 4 a,b,c = 1 ,2 print (a)print (b)print (c)
多变量赋值,如果后面的值多了 会自动省略
1 2 3 4 a,b,c = 1 ,2 ,3 ,4 ,5 ,6 print (a)print (b)print (c)
函数多返回值 多返回值时,你用几个变量接,就有几个值
如果少了,就少接几个,如果多了,就自动补空。
1 2 3 4 5 6 7 8 9 10 11 function Test () return 10 ,20 ,30 ,40 end a,b,c = Test() a,b,c,d,e = Test() print (a)print (b)print (c)print (d)print (e)
and,or and or
他们不仅可以连接 boolean
,任何东西都可以用来连接
在 lua 中只有 nil
和 false
才认为是假
“短路”——对于 and 来说, 有假则假 ,对于 or 来说 有真则真
所以 他们只需要判断,第一个是否满足就会停止计算了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 print ( 1 and 2 )print ( 0 and 1 )print ( nil and 1 )print ( false and 2 )print ( true and 3 )print ( true or 1 )print ( false or 1 )print ( nil or 2 )2 1 nil false 3 true 1 2
元表(__index,tostring,newindex) 元表概念 任何表变量都可以作为另一个表变量的元表
任何表变量都可以有自己的元表(父亲)
当我们子表中进行一些特定操作时
会执行元表中的内容
有点类似父类
设置元表 使用 setmetatable(tab1,tab2)
设置元表函数
1 2 3 4 5 6 meta = {} myTable = {} setmetatable (myTable, meta)
特定操作-__tostring 当子表要被当做字符串使用时,会默认调用这个元表中的 tostring 方法
1 2 3 4 5 6 7 8 9 10 11 metaTab2 = { __tostring = function (t) return t.name end } tab2 = { name = "Anii" } setmetatable (tab2, metaTab2)print (tab2)Anii
特定操作-__call 当子表被当做一个函数来使用时,会默认调用元表这个__call 中的内容
当希望传参数时,一定要记住,默认第一个参数,是调用者自己
1 2 3 4 5 6 7 8 9 10 11 12 13 metaTab2 = { __call = function (t, arg) print ("I am Anii" ) print (arg ) end } tab2 = { } setmetatable (tab2, metaTab2)tab2("out arg" ) I am Anii out arg
特定操作 - 运算符重载 当子表使用运算符时,会调用特定名字的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 metaTab3 = { __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 }
设置两个表的元表都为 metaTab3
1 2 3 4 5 6 tab3_1 = { age = 10 ; } setmetatable (tab3_1, metaTab3)tab3_2 = {age = 20 } setmetatable (tab3_2, metaTab3)
使用运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 print (tab3_1 + tab3_2)print (tab3_1 - tab3_2)print (tab3_1 * tab3_2)print (tab3_1 / tab3_2)print (tab3_1 % tab3_2)print (tab3_1 ^ tab3_2)print (tab3_1 .. tab3_2)30 -10 1 2 3 4 567
如果要用条件运算符,来比较两个对象 这两个对象的元表一定要一致 才能准确调用方法
1 2 3 4 5 6 7 8 print (tab3_1 == tab3_2)print (tab3_1 > tab3_2)print (tab3_1 <= tab3_2)true true false
特定操作 - index 和 newIndex __index __index
当子表中找不到某一个属性时 ,会到元表中 __index 指定的表去找属性。
注意:如果父表当中__index
没有,那么会从祖宗表当中查找 __index
1 2 3 4 5 6 7 8 metaTab4 = { age = 1 } metaTab4.__index = metaTab4 tab4 = { } setmetatable (tab4, metaTab4)print (tab4.age)1
__newindex __newindex
:当赋值时,如果赋值一个不存在的索引,那么会把这个值赋值到 newindex 所指的表中,不会修改自己。
__newindex
和 __index
一样,会从元表的元表当中递归赋值
1 2 3 4 5 6 7 8 9 10 11 12 meta7 = {} meta7.__newindex = {} tab7 = {} setmetatable (tab7, meta7)tab7.age = 1 print (tab7.age) print (meta7.__newindex .age) meta7.__newindex = meta7 print (meta7.age)
其他操作 rawget:原始获取 使用此函数获取,会去找自己身上有没有这个变量,而不会使用 __index
1 print (rawget (meta7, "temp" ))
rawset:原始赋值 使用此函数赋值,会去找自己身上有没有这个变量,而不会使用 __newindex
1 2 3 4 tab8 = {} rawset (tab8, "city" , "hunan" )print (tab8.city)
1 2 3 4 5 6 print (meta7)print (getmetatable (tab7))table : 00 BF3138table : 00 BF3138
Lua 面向对象之封装 面向对象类其实都是基于table
来实现
实现 new 函数 主要实现方式:
定义元表的函数 new()
在 new
函数当中定义本地变量 obj
,为一个新空表
在 new
函数当中将 self._index = self
,这样,使得使用空表对象成员时,会从元表当中查找
设置obj
的元表为 self
返回obj
这样, new
函数就实现了,外部只需要通过 冒号
调用此函数,即可创建新对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Object = {} Object.id = 1 Object.time = "" function Object:new () local obj = {} self .__index = self setmetatable (obj, Object) return obj end newObj = Object:new() newObj.time = "2023年8月19日" print (newObj.time )2023 年8 月19 日
实现其他函数 只需要实现元表的函数即可
注意:调用函数,使用 :
调用,才能将自己传入(类似 this 指针)
1 2 3 4 5 6 7 function Object:Print () print ("Hi, I am Anii" ) end newObj:Print() Hi, I am Anii
实现成员变量 给元表声明变量或者直接声明变量,然后新的对象 .
出变量即可修改
注意:调用函数,使用 :
调用,才能将自己传入(类似 this 指针)
此时,谁调用self
,谁就是 self
1 2 3 4 5 6 7 8 9 function Object:PrintDay () print ("Today is " .. self .day) end newObj.day = 10 ; newObj:PrintDay() Today is 10
Lua 面向对象之继承 主要通过元表和大 G 表实现
大 G 表 _G _G
知识点:是总表,所有声明的全局变量,都以键值对的形式存在其中。
我们可以直接通过 _G
表添加变量。
1 2 3 4 5 6 7 8 _G ["a" ] = 1 ;print (a)_G .b = 1 ;print (b)1 1
实现继承的函数 主要实现思路:
通过大 G 表声明一个传入的类名的表(类)
取出此表
将 self.__index
设置为 self
,此时 self 为 Object
定义 base
为 self
将取出的表的元表设置为 self
这样,当使用子类 new
时,没有 new 函数,但是子类元表是 Object,所以从 Object 当中的 __index
查找 new 函数,使用 Object 的 new 函数(即调用父类函数)
1 2 3 4 5 6 7 8 9 10 11 function Object:subClass (className) _G [className] = {} local obj = _G [className] self .__index = self obj.base = self setmetatable (obj, self ) end
注意:这里定义 base,是为了让子类持有父类表,否则在后面实现多态时,无法调用父类方法
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function Object:Test () print (self .id) end 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() 1 100 100 1 200 200
Lua 面向对象之多态 定义 base 属性 在 Object 的继承函数当中定义 base 属性,是为了在调用子类函数时,可以调用父类函数
1 2 3 4 5 6 7 8 9 10 11 function Object:subClass (className) _G [className] = {} local obj = _G [className] self .__index = self obj.base = self setmetatable (obj, self ) end
实现多态 实现多态,就是为子类重新声明一个函数
注意:这里如果要在子类函数当中调用父类函数,需要使用 .
调用,因为使用冒号调用,会传入父类的 self
,会使用类型声明时的源属性,所以使用 .
调用,然后传入自己。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 () self .base.Move(self ) end local player1 = Player:new()player1:Move() player1:Move() local player2 = Player:new()player2:Move() 1 1 2 2 1 1
面向对象总结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 Object = {} function Object:new () local obj = {} self .__index = self setmetatable (obj, self ) return obj end function Object:subClass (className) _G [className] = {} local obj = _G [className] obj.base = self self .__index = self setmetatable (obj, self ) end Object:subClass("GameObject" ) GameObject.posX = 0 GameObject.posY = 0 function GameObject:Move () print ("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)GameObject:subClass("Player" ) function Player:Move () self .base.Move(self ) print ("Player Move" ) end local player1 = Player:new()player1:Move() print ("player1.posX" .. player1.posX)local player2 = Player:new()player2:Move() print ("player2.posX" .. player2.posX)0 GameObject Move 1 GameObject Move Player Move player1.posX1 GameObject Move Player Move player2.posX1
Lua 自带库 时间 1 2 3 4 5 6 7 8 9 10 11 print (os .time ())print (os .time ({year = 2014 , month = 8 , day = 14 }))local nowTime = os .date ("*t" )for k,v in pairs (nowTime) do print (k,v) end print (nowTime.hour)
数学相关 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 ))print (math .min (4 ,5 ))print (math .modf (1.2 ))print (math .pow (2 , 5 ))math .randomseed (os .time ())print (math .random (100 ))print (math .random (100 ))print (math .sqrt (4 ))
路径 可以给 package 加载其他路径
1 2 3 4 5 6 7 8 print (package .path )package .path = package .path .. ";C:\\" print (package .path )for k,v in pairs (_G ) do print (k,v) end
Lua GC
Lua GC 机制分析与理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 test = {id = 1 , name = "123123" } print (collectgarbage ("count" ))test = nil collectgarbage ("collect" )print (collectgarbage ("count" ))
xLua 热更新解决方案 xLua 概述 xLua 学习目标
导入 xLua 框架
C#调用 Lua
Lua 调用 C#
xLua 热补丁
Unity 中的 Lua 热更新的方案的本质
为 Unity 提供 Lua 编程能力
让 C#和 Lua 可以相互调用访问
C# 调用 Lua xLua 导入和 AB 包相关准备
导入 xLua
导入 AssetBundle
导入 ABMgr
导入 Singleton
Lua 解析器 LuaEnv Lua 解析器能够让我们在 Unity 中执行 Lua
一般情况下,保持它的唯一性
1 LuaEnv env = new LuaEnv();
使用 DoString
直接执行 Lua 语言
1 env.DoString("print('你好世界')" );
执行一个 Lua 脚本 Lua 知识点 :多脚本执行 require
注意:默认寻找脚本的路径是在 Resources 下,并且因为在这里,可能是通过 Resources.Load 去加载 Lua 脚本 txt bytes 等等。所以 Lua 脚本后缀要加一个.txt
1 env.DoString("require('Main')" );
env.Tick(); 帮助我们清除 Lua 中我们没有手动释放的对象 (垃圾回收)
帧更新中定时执行或者切场景时执行
env.Dispose(); 销毁 Lua 解析器
文件加载重定向 由于直接使用 DoString,是从 Resources 下加载 Lua 文件,所以我们需要在其他文件目录加载。
AddLoader LuaEnv 提供了一个添加委托的方法,允许我们自定义加载路径
委托类型: delegate byte[] CustomLoader(ref strin filepath)
1 env.AddLoader(MyCustomLoader);
参数为一个委托
注意:customLoaders 为一个 List,所以可以传入多个自定义加载器,他会遍历调用加载器,一个个调用委托
自定义加载器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private byte [] CustomLoader (ref string filePath ){ string path = Application.dataPath + "/Lua/" + filePath + ".lua" ; Debug.Log(path); if (File.Exists(path)) { return File.ReadAllBytes(path); } else { Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath); } return null ; }
但是,此时只是从一个目录当中加载 lua 文件,不是从 AB 包加载文件,下节课将实现从 AB 包当中加载 lua 文件。
Lua 解析器管理器 一般来说,只有在最后打包时,才会将文件后缀改为 txt,平时只需要正常加载就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class LuaMgr : Singleton <LuaMgr >{ public LuaEnv luaEnv; public LuaTable Global { get { return luaEnv.Global; } } public void Init () public void DoString (string str ) public void Tick () public void Dispose () public void DoLuaFile (string fileName ) public byte [] CustomLoader (ref string filePath ) public byte [] CustomABLoader (ref string filepath ) }
全局变量的获取 注意:我们在 lua 文件当中写 require
,也会重定向到我们定义的文件目录当中,这是 xLua 帮我们做的事
获取全局变量 主要是通过大 G 表 _G
获取
在上节课当中已经定义了获取大 G 表的方法,xLua 当中,大 G 表是 LuaTable 类的一个实例
1 2 3 4 5 6 7 8 public TValue Get <TValue >(string key ){ TValue ret; Get(key, out ret); return ret; }
无法通过大 G 表获取本地变量的值
实例
Main.lua
1 2 print ("Main" )require ("Test" )
Test.lua
1 2 3 4 testNum = 1 testBool = true testFloat = 0.1 testString = "123"
UnityC#
1 2 3 4 5 6 7 8 9 LuaMgr.Instance.Init(); LuaMgr.Instance.DoLuaFile("Main" ); print(LuaMgr.Instance.Global.Get<int >("testNum" )); print(LuaMgr.Instance.Global.Get<string >("testString" )); LUA: Main 1 123
设置全局变量的值 1 2 3 4 5 LuaMgr.Instance.Global.Set<string , string >("testString" , "Anii" ); print(LuaMgr.Instance.Global.Get<string >("testString" )); Anii
全局函数的获取 也是通过大 G 表,然后使用 Get
函数获取
自定义委托 注意:使用自定义委托,接收有参有返回值的 lua 函数,需要使用特性 CSharpCallLua
,然后在 Unity 当中的 xLua 重新生成代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public delegate void CustomCall () ;[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 ) ;
无返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 CustomCall call = LuaMgr.GetInstance().Global.Get<CustomCall>("testFun" ); call(); UnityAction ua = LuaMgr.GetInstance().Global.Get<UnityAction>("testFun" ); ua(); Action ac = LuaMgr.GetInstance().Global.Get<Action>("testFun" ); ac(); LuaFunction lf = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun" ); lf.Call();
有返回值 1 2 3 4 5 6 7 8 9 CustomCall2 call2 = LuaMgr.GetInstance().Global.Get<CustomCall2>("testFun2" ); Debug.Log("有参有返回:" + call2(10 )); Func<int , int > sFun = LuaMgr.GetInstance().Global.Get<Func<int , int >>("testFun2" ); Debug.Log("有参有返回:" + sFun(20 )); LuaFunction lf2 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun2" ); Debug.Log("有参有返回:" + lf2.Call(30 )[0 ]);
多返回值 多返回值,可以通过 ref 和 out 参数或者变长参数来接收,将参数作为返回值。
使用 ref 和 out,将需要接收的返回值定义在函数参数当中
使用变长参数,根据实际情况,定义变长参数的类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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); 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 );
LuaFunction 使用LuaFunction
,可以存储任何类型的函数,但是会产生一些垃圾,导致性能问题,官方不推荐使用。
List 和 Dictionary 映射 table 注意:所有通过 Get
方法获取的对象,全是值拷贝(浅拷贝),在 Unity 当中修改其中值,不会修改 lua 当中值。
List 获取指定类型 1 2 3 4 5 testList = {1 ,2 ,3 ,4 ,5 ,6 } List<int > list = LuaMgr.GetInstance().Global.Get<List<int >>("testList" );
获取不同类型 使用 object
1 2 3 4 5 testList2 = {"123" , "123" , true , 1 , 1.2 } List<object > list3 = LuaMgr.GetInstance().Global.Get<List<object >>("testList2" );
Dictionary 获取指定类型 1 2 3 4 5 6 7 8 9 10 testDic = { ["1" ] = 1 , ["2" ] = 2 , ["3" ] = 3 , ["4" ] = 4 } Dictionary<string , int > dic = LuaMgr.GetInstance().Global.Get<Dictionary<string , int >>("testDic" );
获取不同类型 使用 object
1 2 3 4 5 6 7 8 9 10 testDic2 = { ["1" ] = 1 , [true ] = 1 , [false ] = true , ["123" ] = false } Dictionary<object , object > dic3 = LuaMgr.GetInstance().Global.Get<Dictionary<object , object >>("testDic2" );
类映射 table 注意:映射类任然是值拷贝
lua 文件
1 2 3 4 5 6 7 8 9 testClas = { testInt = 2 , testBool = true , testFloat = 1.2 , testString = "123" , testFun = function () print ("123123123" ) end }
类映射 table
在 C#声明一个类,成员变量的名字和类型一定要和 lua 方一致
要映射的只能是 public,private 和 protected 无法赋值
如果变量比 lua 中的少,就会忽略它
如果变量比 lua 中的多,不会赋值,也会忽略
类成员,和上述要求一致就会赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CallLuaInClass { public int testInInt; } public class CallLuaClass { public int testInt; public bool testBool; public float testString; public UnityAction testFun; public CallLuaInClass testInClass; public int i; public void Test () { Debug.Log(testInt); } }
映射,任然是使用 Get 获取
1 CallLuaClass obj = LuaMgr.GetInstance().Global.Get<CallLuaClass>("testClas" );
接口映射 table
接口接收 Lua 表,必须添加特性CSharpCallLua
,清除代码后,重新生成代码。
接口无法定义字段,所以使用属性接收。
接口和类一样,成员多了忽略,少了填默认值。
==接口拷贝是引用拷贝==
Test.lua
1 2 3 4 5 6 7 8 9 testInterface = { testInt = 2 , testBool = true , testFloat = 1.2 , testString = "123" , testFun = function () print ("testFun" ) end }
C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 [CSharpCallLua ] public interface ICallInterface { int testInt { get ; set ; } bool testBool { get ; set ; } float testFloat { get ; set ; } string testString { get ; set ; } UnityAction testFunc { get ; set ; } } public class Lesson8 : MonoBehaviour { void Start () { LuaMgr.Instance.Init(); LuaMgr.Instance.DoLuaFile("Main" ); ICallInterface obj = LuaMgr.Instance.Global.Get<ICallInterface>("testInterface" ); Debug.Log(obj.testInt); Debug.Log(obj.testBool); Debug.Log(obj.testFloat); Debug.Log(obj.testString); obj.testFunc(); obj.testBool = false ; ICallInterface obj2 = LuaMgr.Instance.Global.Get<ICallInterface>("testInterface" ); Debug.Log(obj2.testBool); } }
output
1 2 3 4 5 6 2 True 1.2 123 LUA:testFun False
LuaTable 映射 table 通过类 LuaTable
,获取 lua 文件当中的类,获取到的是引用,所以修改后再次获取也会更改。
获取值:LuaTable.Get(string)
,泛型
设置值:LuaTable.Set(string)
,泛型
获取函数使用 LuaFunction
接收
注意:xLua 不推荐使用 LuaTable 和 LuaFunction,因为会造成更多的性能消耗,并且需要手动 Dispose,否则会造成内存泄露。
Unity 性能优化基础篇——Lua 和 Unity 调用的优化 - 知乎 (zhihu.com)
Test.Lua
1 2 3 4 5 6 7 8 9 testInterface = { testInt = 2 , testBool = true , testFloat = 1.2 , testString = "123" , testFunc = function () print ("testFun" ) end }
C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Lesson9 : MonoBehaviour { void Start () { LuaMgr.Instance.Init(); LuaMgr.Instance.DoLuaFile("Main" ); LuaTable table = LuaMgr.Instance.Global.Get<LuaTable>("testClass" ); Debug.Log(table.Get<int >("testInt" )); Debug.Log(table.Get<bool >("testBool" )); Debug.Log(table.Get<float >("testFloat" )); Debug.Log(table.Get<string >("testString" )); LuaFunction func = table.Get<LuaFunction>("testFun" ); func.Call(); table.Set("testInt" , 55 ); LuaTable table2 = LuaMgr.Instance.Global.Get<LuaTable>("testClass" ); Debug.Log(table2.Get<int >("testInt" )); func.Dispose(); table.Dispose(); table2.Dispose(); } }
output
1 2 3 4 5 6 2 True 1.2 123 LUA:testFun 55
Lua 调用 C# 启动 Lua Lua 不能直接访问 C#,需要 C#先启动一个 lua 脚本,进而调用 C#,所以需要写一个启动类,再在 Lua 当中处理其他逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Main : MonoBehaviour { void Start () { LuaMgr.Instance.Init(); LuaMgr.Instance.DoLuaFile("Main" ); } }
访问类 固定套路:CS.命名空间.类名
如果没有命名空间就不写
实例化对象 lua 中没有 new 关键字,所以我们直接使用,类名()
,就是构造函数
1 2 local obj1 = CS.UnityEngine.GameObject()local obj2 = CS.UnityEngine.GameObject("Anii" )
技巧知识点 为了方便使用,并且节约性能,定义全局变量,存储 C#中的类,相当于取了一个别名
1 2 3 4 5 GameObject = CS.UnityEngine.GameObject local obj3 = GameObject("别名Obj" )Debug = CS.UnityEngine.Debug Vector3 = CS.UnityEngine.Vector3
访问静态方法和变量 直接 类名.xx
1 local obj4 = GameObject.Find("Anii")
访问普通成员 访问字段或者属性,直接 对象.xx
访问成员方法,一定要通过==冒号==访问 对象:xx(args)
1 2 3 Debug.Log(obj4.transform.position) obj4.transform:Translate(Vector3.right)
添加脚本 由于 lua 不支持无参泛型函数,所以使用另外一个 AddComponent 的重载
1 public Component AddComponent (Type componentType )
xLua 提供了一个重要方法,typeof
,可以得到类的 type
1 2 obj5:AddComponent(typeof(CS.LuaCallCSharp))
Lua 使用 C#枚举 1 2 3 4 5 6 7 public enum E_MyEnum{ Idle, Move, Atk }
使用枚举和使用 C#类是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PrimitiveType = CS.UnityEngine.PrimitiveType GameObject = CS.UnityEngine.GameObject local obj = GameObject.CreatePrimitive(PrimitiveType.Cube)E_MyEnum = CS.E_MyEnum local c = E_MyEnum.Idleprint (c)LUA: Idle: 0
枚举转换相关 可以使用 枚举类型名.__CastFrom(argc)
,转换出一个枚举对象
1 2 3 4 5 6 7 8 9 10 11 local a = E_MyEnum.__CastFrom(1 )print (a)local b = E_MyEnum.__CastFrom("Atk" )print (b)LUA: Move: 1 LUA: Atk: 2