目录
一、环境搭建
二、Lua语法
1.输出print、单行注释、多行注释
2.变量
(1)nil
(2)number
(3)string
(3.1)字符串长度
(3.2)字符串拼接
(3.3)多行打印
(3.4)类型转换
(3.5)字符串操作(大小写、反转、查找、截取、重复)
(3.6)字符串与ASCII互转
(4)boolean
(5)function
(5.1)无参无返回值
(5.2)有参无返回值
(5.3)有一个返回值
(5.4)有多个返回值
(5.5)匿名函数
(5.6)可变形参函数
(5.7)获取函数类型
(5.8)函数嵌套
(5.9)函数嵌套-闭包
(6)table
(6.1)一维数组
(6.2)unpack函数
(6.3)二维数组
(6.4)自定义索引
(6.5)迭代器遍历
(6.5.1)ipairs
(6.5.2)pairs
(6.6)字典
(6.7)类
(6.7.1)类函数声明
(6.8)表操作
(6.8.1)插入
(6.8.2)删除
(6.8.3)排序
(6.8.4)拼接
(7)thread
(7.1)协程创建
(7.2)协程运行
(7.3)协程挂起
(7.4)协程状态
3.运算符
(1)算术运算符(+ - * / % ^)
(2)条件运算符(< > <= >= == ~=)
(3)逻辑运算符(and or not,支持短路规则)
4.条件分支语句
(1)单分支
(2)双分支
(3)多分支
5.循环语句
(1)for循环
(1.1)数值for循环
(1.2)泛型for循环
(2)while...do...end循环
(3)repeat...until...循环
6.多脚本执行
(1)本地变量和全局变量
(2)require("脚本名")
(3)package.loaded["脚本名"]
(4)大G表
(5)dofile
7.特殊用法
(1)多变量赋值
(2)多返回值
(3)逻辑与、逻辑或
(4)实现三目运算符
8.元表
(1)设置元表
(2)特定操作
(2.1)__tostring
(2.2)__call
(2.3)运算符重载
(2.4)__index
(2.4.1)__index是表
(2.4.2)__index是方法
(2.5)__newindex
(2.5.1)__newindex是函数
(2.5.1)__newindex是表
(2.6)获取元表
9.面向对象
(1)封装
(2)继承
(3)多态
(4)总结
10.Lua其他库
(1)数学计算
(2)操作系统库-时间
(3)文件操作
(3.1)写文件
(3.2)读文件
(4)其他-路径
11.垃圾回收
三、热更新
1.插件安装
2.创建Lua脚本
3.unity中调用Lua脚本
4.项目升级
(4.1)Lua脚本更改
(4.2)unity中调用Lua
(4.3)效果展示
一、环境搭建
- LuaForWindows:Releases · rjpcomputing/luaforwindows (github.com)
- IDE:运用的是sublime_text
sublime_text快捷方式:
- Ctrl+B:运行
- Ctrl+L:选中当前行
- Ctrl+/:快速注释
- Ctrl+Shift+/:快速块注释
二、Lua语法
1.输出print、单行注释、多行注释
print("hello world")
--单行注释
--[[
多行注释
多行注释
]]
--[[
多行注释
多行注释
--]]
--[[
多行注释
多行注释
]]--
2.变量
- lua中四种简单的变量类型:nil、string、boolean、number。除此之外还有:function、table、userdata、thread。
- 不需要声明变量类型;可通过type函数获取变量类型,type函数返回值是string。
- 变量可随便赋值。Lua 中的变量是动态类型的,这意味着变量没有预定义的类型,类型会在运行时根据赋给变量的值来决定。
- lua中没有赋值过的变量,默认类型是nil。
(1)nil
a=nil
print(a)
(2)number
所有的数值都是number类型。
a=10
print(a)
a=10.5
print(a)
(3)string
在 Lua 中,字符串可以用双引号"
、单引号'
或者长括号[[ ]]
来表示。
a="hello"
print(a)
(3.1)字符串长度
使用#运算符可以获取字符串长度。
a="hello world"
print(#a)
a="一"
print(#a) --3
(3.2)字符串拼接
可以使用..
运算符来拼接字符串。也可以使用string.format来进行拼接。
a="一"
print("一个汉字长度:"..#a) --一个汉字长度是3
print(string.format("今年 %s 岁",'18'))
(3.3)多行打印
在Lua中,可以使用多行字符串来打印多行文本。多行字符串可以通过在字符串的开始和结束使用双方括号[[
和]]
来创建。也可以通过行末尾添加"\n"来进行多行打印。
a=[[1.hello
2.world]]
print(a)
(3.4)类型转换
在Lua中,可以使用 tostring
函数将一个值转换为字符串。这个函数可以接受任何基本类型的值作为参数,并返回该值的字符串表示形式。
a=false
print(tostring(a))
(3.5)字符串操作(大小写、反转、查找、截取、重复)
字符串相关操作,不会改变原字符串。Lua中索引从1开始。
str="YsL"
str1=string.upper(str)
print("原字符串:"..str..",全部大写为:"..str1)
str1=string.lower(str)
print("原字符串:"..str..",全部小写为:"..str1)
str1=string.reverse(str)
print("原字符串:"..str..",反转字符串为:"..str1)
--原字符串:YsL,全部大写为:YSL
--原字符串:YsL,全部小写为:ysl
--原字符串:YsL,反转字符串为:LsY
str="YsL"
print("---------字符串查找")
print(string.find(str,"sL")) --2 3
print("---------字符串截取")
print(string.sub(str,2)) --sL
print(string.sub(str,2,2)) --s
print("---------字符串重复:"..string.rep(str,3)) --字符串重复:YsLYsLYsL
print("---------字符串修改")
print(string.gsub("Session",'s','*'))
--Se**ion 2
string.gsub()返回两个结果,第二个值表示修改了几个字符。
(3.6)字符串与ASCII互转
a=string.byte("Lua",1)
print(a) --76
a=string.char(a)
print(a) --L
(4)boolean
a=false
print(a)
print(type(a)) --boolean
print(type(type(a))) --string
(5)function
在Lua中,函数是一种可以封装代码以便重复使用的基本结构。函数可以接受参数,并且可以返回一个或多个值。Lua不支持重载,但重载后并不报错,默认调用最后声明的函数。
(5.1)无参无返回值
--先声明
function fun1()
print("my fun1")
end
--再调用
fun1()
(5.2)有参无返回值
function fun3(a)
print(a)
end
fun3(1)
fun3("hello world")
fun3(true)
fun3(nil) --nil
fun3() --nil
fun3(1,2) --1 实参和形参个数不匹配,会补空或丢弃
(5.3)有一个返回值
function fun4(a) --直接return即可
return a
end
temp=fun4(1)
print(temp)
(5.4)有多个返回值
function fun5(a) --支持返回多个值
return a,-1
end
temp1,temp2,temp3=fun5(10) --支持多个变量来接取返回值
print(temp1)
print(temp2)
print(temp3) --nil
(5.5)匿名函数
fun2=function()
print("my fun2")
end
fun2()
(5.6)可变形参函数
function fun7(...)
arg={...} --arg表存储参数
for i=1,#arg do
print(arg[i])
end
end
fun7(1,2,3,4,"hello",false)
(5.7)获取函数类型
fun6=function()
print("my fun6")
end
print(type(fun6)) --function
(5.8)函数嵌套
function fun8()
return function()
print("函数嵌套")
end
end
--调用方式1
fun9=fun8()
fun9()
--调用方式2
fun8()()
(5.9)函数嵌套-闭包
在Lua中,函数可以嵌套在其他函数内部,这时候内部的函数可以访问外部函数的局部变量,这种结构称为闭包(closure)。闭包的强大之处在于,即使外部函数的执行已经结束,闭包仍然可以记住外部函数中的变量。
function fun10(x)
return function(y)
print(x+y) --临时变量x的生命周期变长
end
end
close=fun10(1)
close(2)
即使 fun10执行完毕,close依然可以访问x这个变量。
(6)table
表是 Lua 中唯一的数据结构,可以用来表示数组、字典等复杂数据结构。表在 Lua 中是非常强大的,因为它们可以包含所有类型的值。
(6.1)一维数组
a={1,2,3,4,nil,true,"hello",nil} --8个元素 如果数组的最后一个元素是 nil,Lua 通常会将其视为数组的结束,导致长度计算时忽略该 nil 元素。
print("数组a的长度:"..#a) --7 lua5.1.5版本中,nil如果是数组最后一个元素,计算数组长度时则会忽略该元素。用#获计算长度不准确
print(a[0]) --nil lua索引从1开始
print("第一个元素:"..a[1]) --1
print("---------------一维数组遍历")
for i=1,#a do
print(a[i])
end
(6.2)unpack函数
在Lua中,unpack函数是一个非常有用的内置函数,它可以从表中取出一个连续的序列,并返回这些值。这个函数在Lua 5.1中是全局的,但从Lua 5.2开始,它被移到了table库中,即table.unpack
。
a = {1,2,3,4,nil,true,"hello",nil}
print(unpack(a)) --1 2 3 4 nil true hello
使用unpack可以很方便地将表中的值作为多个参数传递给函数。
unpack或table.
unpack接受三个参数:表、起始索引和结束索引。如果不指定起始索引和结束索引,默认情况下它会从第一个元素开始,到表的长度结束。
print(unpack(a,2,4)) --2 3 4
(6.3)二维数组
a={{1,2,3},{4,nil,"hello"}}
print(a[1][3]) --3
print(a[2][3]) --hello
print("---------------二维数组遍历")
for i=1,#a do
b=a[i]
for j=1,#b do
print(b[j])
end
end
(6.4)自定义索引
Lua的表不仅可以使用整数索引(不建议自定义整数索引),还可以使用字符串或其他Lua值(除了nil
)作为键(后续会讲)。
a={[0]=1,2,3,[-1]=4,[4]=5}
print(#a) --2 会去除索引不正常的元素
print(a[0]) --1
for i=1,#a do
print(a[i]) --2 3
end
a={[1]=1,[2]=1,[4]=1,[6]=1}
print(#a) --6
print(unpack(a)) --1 1 nil 1 nil 1
(6.5)迭代器遍历
使用#计算长度并不准确,建议使用迭代器遍历:
- 如果想要处理的是严格意义上的数组(索引连续的列表),使用ipairs更合适,因为它的性能可能会更好一些。
- 如果需要处理的是一个混合了数组和字典风格的表,或者你不确定表的索引是否连续,那么使用pairs会更安全,它能确保遍历到表中的所有元素(nil除外)。
(6.5.1)ipairs
从索引1开始遍历,索引小于0或者断序后无法遍历。
a={[0]=0,1,2,3,nil,4,5,[10]=6,c=7,nil}
for i,k in ipairs(a) do
print("i:"..i.."-k:"..k)
end
-- i:1-k:1
-- i:2-k:2
-- i:3-k:3
(6.5.2)pairs
a={[0]=0,1,2,3,nil,4,5,[10]=6,c=7,nil}
for i,k in pairs(a) do
print("i:"..i.."-k:"..k)
end
--i:1-k:1
--i:2-k:2
--i:3-k:3
--i:5-k:4
--i:6-k:5
--i:0-k:0
--i:10-k:6
--i:c-k:7
(6.6)字典
在Lua中,并没有专门的“字典”类型,但是表(table)结构在Lua中是非常灵活的,通常用来实现类似字典的功能。表可以存储键值对,其中键和值可以是任意类型的数据(除了nil
)。当使用字符串作为键时,表就类似于其他语言中的字典或哈希表。
a={["name"]="Lisa",["age"]=22,["sex"]="girl",["1"]=true,c=56}
print("-------------访问")
print(a["name"])
print(a.c)
print(a.name)--虽然可以通过这种方法得到值,但键不能是数字,如a.1不对
print("-------------修改")
a["age"]=20
print(a["age"])
print("-------------新增")
a["Country"]="TaiLand"
print(a.Country)
print("-------------删除")
a["Country"]=nil
print(a.Country)
print("-------------字典的遍历,使用pairs")
for i,k in pairs(a) do
print(i,k)
end
(6.7)类
Lua中默认没有面向对象,所以不能new。
Student={
age=12,
name="Lisa",
fun0=function(t) --在函数中需要调用表本身的属性和方法时,可以把自己当参数传进来
print("fun0:"..t.name..":"..t.age)
end,
fun1=function()
--print("fun1:"..name..":"..age) --错误
print("fun1:"..Student.name..":"..Student.age) --在函数中调用表本身的属性和方法,需要指明表名称
end
}
--新增变量
Student.sex="girl"
print(Student.age)
print(Student.sex)
Student.fun0(Student)
Student:fun0() --语法糖:冒号调用方法时,会默认把调用者作为第一个参数传入方法中
Student.fun1()
(6.7.1)类函数声明
和普通函数申明的两种方式(普通函数、匿名函数)相同,除此之外,也可以使用:来申明。
- 当声明一个方法时,使用 : 语法允许定义一个需要隐式 self 参数的函数。self 实际上指的是调用方法的对象实例,而不是类名。
- 当调用一个方法时,如果使用 : 语法,Lua会自动将调用该方法的表(对象)作为第一个参数传递给该方法。
Student.fun2=function()
print("fun2")
end
function Student.fun3()
print("fun3")
end
function Student:fun4() --语法糖:冒号声明方法时,会默认把调用者作为第一个参数传入方法中。self表示默认传入的第一个参数,self不是this。(self 实际上指的是调用方法的对象实例,而不是类名)
print("fun4:"..self.name)
end
Student.fun2()
Student.fun3()
Student:fun4()
Student.fun4(Student)
(6.8)表操作
(6.8.1)插入
t1={{name=1,age=10},{name=2,age=11}}
t2={name=3,age=12}
print(#t1) --2
table.insert(t1,t2)
print(#t1) --3
print(t1[3]["age"]) --12
(6.8.2)删除
table.remove(t1) --默认删除最后一个元素
table.remove(t1,1) --第二个参数,指定移除的元素索引
(6.8.3)排序
在Lua中,table.sort
函数用于对表中的元素进行排序。这个函数可以对表中的元素进行原地(in-place)排序,也就是说,排序操作会直接改变原始表,而不是创建一个已排序的新表。
t3={12,53,6,2,4}
table.sort(t3)
print(unpack(t3)) --2 4 6 12 53
默认情况下,table.sort
对表进行升序排序。如果想自定义排序顺序,可以提供一个排序函数作为table.sort
的第二个参数。这个排序函数需要接受两个参数,并返回一个布尔值,表示第一个参数是否应该在排序后位于第二个参数之前。
table.sort(t3, function(a,b)
if a>b then
return true
end
end)
print(unpack(t3)) --53 12 6 4 2
上述代码表示:如果第一个参数大于第二个参数,就返回true,这样table.sort就会把较大的数放在前面。
(6.8.4)拼接
在Lua中,table.concat函数用于将表中的元素连接成一个字符串。你可以指定一个连接符(也称为分隔符),用来在合并时插入元素之间。如果不指定连接符,默认使用空字符串(也就是没有分隔符)。
table.concat还可以接受第二个和第三个可选参数,分别代表起始索引和结束索引,用于指定连接表中的哪一部分。
t4={1,"hello","world"}
str=table.concat( t4, "-", 1, 3 )
print(str) --1-hello-world
(7)thread
Lua中的thread类型是指协同程序(coroutines),它们是由coroutine库提供的。在Lua中,术语"thread"通常指的是这些协同程序,而不是操作系统级别的线程。Lua的协同程序是协作式的,而非抢占式的,这意味着它们不会自行中断以让其他线程运行,而是显式地通过yield和resume来交换控制权。(Lua的线程可以让你编写出看起来像是同时执行的代码,但实际上它们是分时执行的,一个线程让出控制权后,另一个线程才会开始执行。)
Unity 协程是与引擎的事件循环集成在一起的,允许在等待时继续处理其他事件和渲染更新。这与 Lua 中需要显式地调用
coroutine.resume
来恢复协程的执行有很大的不同。在 Unity 中,你只需要启动协程,引擎会在合适的时间点自动恢复它的执行。Lua中使用coroutine.create创建协程,每执行一次coroutine.resume,协程函数会前进至下一个coroutine.yield。
(7.1)协程创建
协程的创建有两种方式:
- coroutine.create():常用方式,返回线程类,协程的本质是线程对象
fun=function()
print("hello 0223")
end
co=coroutine.create(fun)
print(type(co)) --thread
- coroutine.wrap():返回函数类型
fun=function()
print("hello 0223")
end
co1=coroutine.wrap(fun)
print(type(co1))--function
(7.2)协程运行
对应两种创建方式,协程运行也不同:
- coroutine.resume:对应create创建方式
- 按函数方式运行
coroutine.resume(co) --对应create创建方式
co1() --按函数方式运行
(7.3)协程挂起
fun2=function ()
local i=1
while true do
print(i)
i=i+1
--挂起
coroutine.yield(i) --返回i
end
end
co2=coroutine.create(fun2)
temp1,temp2=coroutine.resume(co2)
print(temp1,temp2) --true 2 --true表示协程运行成功,temp2才是返回值i
temp1,temp2=coroutine.resume(co2) --运行一次执行一次
print(temp1,temp2) --true 3
(7.4)协程状态
- coroutine.status(co)返回协同程序co的状态,可能的值有running、suspended、normal和dead。
- coroutine.running() 得到正在运行的协程编号,要在函数内部打印。
coroutine.wrap 返回的是一个函数,而不是协程对象,所以不能对它使用 coroutine.status。因为 coroutine.status 期望它的参数是一个协程,而不是一个函数。
fun=function()
print("hello 0223")
print(coroutine.running())
end
co=coroutine.create(fun)
coroutine.resume(co)
print(coroutine.status(co))
-- hello 0223
-- thread: 00D5E500
-- dead
fun=function()
print("hello 0223")
print(coroutine.running())
coroutine.yield("hello")
end
co=coroutine.create(fun)
coroutine.resume(co)
print(coroutine.status(co))
-- hello 0223
-- thread: 00C9DD90
-- suspended
3.运算符
lua不支持位运算( & |)和三目运算符( ? :),也不支持++、--、 +=、 -=等算术运算符。
(1)算术运算符(+ - * / % ^)
print("加法:"..1+2)
print("123"+1) --字符串进行算术运算,会自动转成number
print("减法:"..1-2)
print("乘法:"..1*2)
print("除法:"..1/2)
print("余数:"..1%2)
print("幂运算:"..3^2)
--加法:3
--124
--减法:-1
--乘法:2
--除法:0.5
--余数:1
--幂运算:9
(2)条件运算符(< > <= >= == ~=)
print("1<2?"..tostring(1<2))
print("1>2?"..tostring(1>2))
print("1==2?"..tostring(1==2))
print("1~=2?"..tostring(1~=2))
--1<2?true
--1>2?false
--1==2?false
--1~=2?true
(3)逻辑运算符(and or not,支持短路规则)
print(true and false)
print(true or false)
print(not false)
--false
--true
--true
4.条件分支语句
Lua 中的条件分支语句主要是通过 if
、then
、elseif
、else
和 end
关键字来实现的。Lua中没有switch。
(1)单分支
a = 10
if a>5 then
print("a>5")
end
(2)双分支
a = 10
if a>5 then
print("a>5")
else
print("false")
end
(3)多分支
a = 10
if a>5 then
print("a>5")
elseif a==5 then --注意:elseif要连着写
print("a=5")
else
print("a<5")
end
5.循环语句
在Lua中,循环语句允许重复执行一段代码多次。Lua提供了几种循环语句,包括for循环、while...do...end循环和repeat...until...循环。
(1)for循环
Lua中的for循环有两种形式:数值for循环和泛型for循环。
(1.1)数值for循环
print("--------------for,默认递增1")
for i=1,5 do
print(i)
end
print("--------------for,自定义增量,递增2")
for i=1,5,2 do
print(i)
end
print("--------------for,自定义增量,递减1")
for i=5,1,-1 do
print(i)
end
(1.2)泛型for循环
用于迭代表(table)和其他迭代器。
for key, value in pairs(mytable) do
print(key, value)
end
(2)while...do...end循环
a=5
while a<10 do
print(a)
a=a+1
end
(3)repeat...until...循环
repeat...until...循环保证至少执行一次,即使条件从一开始就是假的。所以要防止死循环。
a=5
repeat
print(a)
a=a+1
until a>10
6.多脚本执行
(1)本地变量和全局变量
在Lua中,变量可以是全局的或局部的,这取决于它们是如何声明的。
- 全局变量在Lua中不需要明确声明就可以使用。如果你赋值给一个未声明的变量,Lua会自动将其视为全局变量。全局变量在整个Lua程序中都是可见的,除非它们被特定的环境或模块限制。
- 局部变量在Lua中使用关键字
local
进行声明。它们的作用域被限制在了它们被声明的代码块内。这意味着它们只能在定义它们的函数内部、控制结构内或代码块内被访问。
a=10 --全局变量
b="hello" --全局变量
for i=1,5 do
c=i*2 --c也是全局变量
end
print(c) --10
for i=1,5 do
local d=i*2 --局部变量
end
print(d) --nil
(2)require("脚本名")
在Lua中,require函数用于加载和运行库或模块。
- 新建一个test.lua脚本(脚本可以return 一个局部变量)
print("this is test")
testA=10 --全局变量
local x = 1 --局部变量
x=x+1
return x
- 在别的脚本中执行test.lua
require("test") --注意是同一个路径
print(testA) --可调用另一个脚本的全局变量
print(x) --不可调用另一个脚本的局部变量
--this is test
--10
--nil
当使用require来加载一个模块时,Lua会检查该模块是否已经加载过。如果已经加载过,Lua不会再次加载该模块,而是直接返回之前加载的结果。这是通过一个内部的包加载表(package.loaded)来实现的,其中存储了所有已加载模块的状态。如果在上述代码之后仍然require("test") ,则不会输出this is test。
(3)package.loaded["脚本名"]
如果想检查一个模块是否已经被执行过(即已经被加载过),你可以检查 package.loaded
表中对应的条目。如果条目存在并且不是 nil
,则表示该模块已经被加载过了。
print(package.loaded["test"]) --2
package.loaded["test"]=nil --卸载
print(testA) --但是还会输出
执行了print(package.loaded["test"])并且得到了输出 2
,这是因为名为 "
test"
的模块被加载,并且它返回了值:2。
temp=require("test") --可以加local,不加的话会存在_G中
print(temp) --2
print(_G["temp"]) --2
卸载后print(testA)仍然输出10,是因为:在Lua中,全局变量是存储在全局环境_G中的变量,这是一个存储所有全局变量的表。如果你加载并运行了一个Lua脚本,它定义了全局变量,即使之后你卸载了这个脚本,这些全局变量仍然会留在全局环境_G中,除非你显式地删除它们或者重置它们的值。
(4)大G表
在 Lua 中,全局环境是由一个名为 _G
的特殊表来表示的。这个表被称为“全局表”(Global Table),简称“大G表”。它包含了所有的全局变量,你可以通过它访问或修改这些变量。Lua中的全局变量实际上是_G表的键值对。当你在Lua中创建一个全局变量时,实际上是在_G表中添加了一个元素。同样,访问一个全局变量就是在查询_G表。
for i,k in pairs(_G) do
print(i,k)
end
需要注意的是,直接操作 _G
表可能会影响到全局环境的完整性,所以通常建议谨慎使用,尤其是在大型项目或者多人合作的项目中。使用局部变量(local)可以避免不必要的全局污染。
(5)dofile
在Lua编程语言中,dofile 和 require 都是用来加载和执行Lua文件的,但它们之间存在一些重要的区别。
- dofile:这个函数会加载并执行指定的文件,但不会对其进行任何形式的缓存。也就是说,每次调用 dofile 时,Lua都会重新加载和执行该文件。如果你有一个文件需要多次执行,而且每次执行的结果都可能不同,那么 dofile 是一个不错的选择。
- require:这个函数也会加载并执行指定的文件,但它会对文件进行缓存。当你第一次 require 一个文件时,Lua会加载和执行该文件,然后把文件的返回值存储在一个特殊的表中。当你再次 require 同一个文件时,Lua不会重新加载和执行该文件,而是直接返回这个表中的值。这意味着,如果你的文件只需要被执行一次,或者每次执行的结果都应该是相同的,那么 require 是一个更好的选择。
r=dofile("test.lua") --注意需要加后缀
print(r)
--this is test
--2
7.特殊用法
(1)多变量赋值
个数不对应,自动补nil或省略。
a,b,c,d,e=1,"hello",true,nil
print(a,b,c,d,e)
--1 hello true nil nil
(2)多返回值
个数不对应,自动补nil或省略。
function test()
return 10,20,30
end
a,b=test()
print(a,b)
(3)逻辑与、逻辑或
任何类型都能连接。只有nil和false才是假,0是真,"false"也是真。
print(1 and 2) --2 --1满足,判断2,返回2
print(0 and 1) --1 --0满足,判断1,返回1
print(1 or 2) --1 --1满足,返回1(短路原则)
print(nil and 0) --nil
print(false or 1) --1
print(nil or "hello") --hello
print("false" or "1") --false
print("false" and "nil") --nil
(4)实现三目运算符
lua不自持三目运算符。但是利用and和or可以实现三目运算符。
x,y=3,2
res=(x>y) and x or y
print(res) --3
x,y=3,4
res=(x>y) and x or y
print(res) --4
8.元表
在Lua中,元表(Metatables)提供了一种方式来改变表的行为。每个表可以有一个元表,而这个元表中可以包含特殊的键,这些键对应的值是函数或者表,这些函数或表定义了原表在特定操作下的行为。
- 任何表都可以作为另一个表的元表
- 任何表都可以有自己的元表
- 在子表中进行一些特定操作时,会执行元表中的内容
(1)设置元表
meta={} --用于表示元表
mytable={} --用于表示子表
--设置元表
setmetatable(mytable,meta) --参数一:子表,参数二:元表
(2)特定操作
(2.1)__tostring
相当于重写字符串转换方法。当子表要被当作字符串使用时,会默认调用元表中的tostring方法。
meta2={
__tostring=function()
return "hello meta2"
end
}
mytable2={}
setmetatable(mytable2,meta2)
print(mytable2) --hello meta2
meta3={
__tostring=function(t)
return t.name
end
}
mytable3={
name="Lisa"
}
setmetatable(mytable3,meta3)
print(mytable3) --Lisa
(2.2)__call
当子表被当作函数来使用时,会默认调用这个元表中的__call。(默认不允许把普通的表当函数来调用,只有子表的元表声明了__call,子表才可以当函数调用)
当你给一个表设置了元表,并且在这个元表中定义了 __call
函数后,你就可以直接调用这个表,就好像它是一个函数一样。Lua 将自动把表作为第一个参数传递给 __call
函数。
meta4={
__tostring=function(t)
return t.name
end,
__call=function()
print("今天周五")
end
}
mytable4={
name="Linda"
}
setmetatable(mytable4,meta4)
mytable4() --今天周五
meta5={
__tostring=function(t)
return t.name
end,
__call=function(a,b) --当希望传参数时,默认第一个参数是调用者自己,所以,print(a)时,会调用__tostring
print(a,b)
end
}
mytable5={
name="Linda"
}
setmetatable(mytable5,meta5)
mytable5(1) --Linda 1
- 当执行 mytable5(1) 时,Lua 会自动将mytable5作为第一个参数传递给__call 定义的函数,即a为mytable5。__call 被触发
- 根据 __call 方法的定义,它会打印两个参数:a 和 b,即print(a)和print(b)
- 由于 print(a) 被调用,这里的 a 是 mytable5,它会触发 __tostring 元方法,将 mytable5 转换为字符串,即它的 name 属性 "Linda"
- 然后 print(b) 会打印传递的参数 1
(2.3)运算符重载
在Lua中,元表(metatable)允许你改变表(table)的行为,其中之一就是通过元方法(metamethods)来重载运算符。运算符重载可以让你自定义两个表之间的运算行为,比如加法、减法、乘法等。(普通的表之间不支持加减乘除比较等)
- Lua在处理算术运算符的元方法(比如
__add
用于重载加法)时,只需要其中一个操作数有元表并且元表中实现了相应的元方法。Lua会首先检查左操作数(第一个操作数)的元表,如果没有找到对应的元方法,它会检查右操作数(第二个操作数)的元表。 - 比较运算符重载,需要确保两个子表的元表为同一个。
- __add:定义了表相加的行为,(a + b)
- __sub:定义了表相减的行为,(a - b)
- __mul:定义了表相乘的行为,(a * b)
- __div:定义了表相除的行为,(a / b)
- __mod:定义了表进行模运算的行为,(a % b)
- __pow:定义了表的乘幂运算,(a ^ b)
- __unm:定义了表的取负运算,(-a)
- __concat:定义了表的连接运算,(a .. b)
- __len:定义了获取表长度的操作,(#a)
- __eq:定义了表的相等比较,(a == b)
- __lt:定义了表的小于比较,(a < b)
- __le:定义了表的小于等于比较,(a <= b)
meta6={
__add=function(t1,t2) --相当于运算符重载,当子表使用+运算符时,会调用该方法
return t1.total+t2.total
end,
__sub=function(t1,t2) --相当于运算符重载,当子表使用-运算符时,会调用该方法
return t1.total-t2.total
end,
__eq=function (t1,t2)
if(t1.total==t2.total) then
return true
end
return false
end
}
mytable6=
{
total=10
}
setmetatable(mytable6,meta6)
mytable7=
{
total=10
}
print(mytable6+mytable7) --20
print(mytable6-mytable7) ---0
print(mytable6==mytable7)--false --因为不是同一个元表
setmetatable(mytable7,meta6) --设置为同一个元表
print(mytable6==mytable7) --true
(2.4)__index
当你尝试访问一个表中不存在的索引时,如果这个表的元表有__index
元方法,Lua会调用这个方法。
rawget:用于从表中获取一个键对应的值,但不触发任何元方法(metatable中的__index方法)。简单来说,它可以直接从表中获取值,而忽略任何可能已经设置的元方法。
__index
可以是一个函数或者一个表。
(2.4.1)__index是表
如果__index
是一个表,当你尝试访问第一个表中不存在的索引时,Lua会在__index
指定的表中查找这个索引。
mytable8={}
meta8=
{
age=10
}
setmetatable(mytable8,meta8)
print(mytable8.age) --nil
meta8.__index=meta8 --元表补充__index,表指向自己,这种方法需要写到元表外
print(mytable8.age) --10
meta8.__index={age=20} --元表补充__index,这种写法可写在元表内,也可在元表外
print(mytable8.age) --20
print(rawget(mytable8,"age")) --nil
mytable={}
meta=
{
age=10
}
setmetatable(mytable,{__index=meta})
print(mytable.age) --10
(2.4.2)__index是方法
如果__index
是一个函数,当你尝试访问表中不存在的索引时,Lua会调用这个函数。这个函数的第一个参数是表本身,第二个参数是被访问的索引。
Lua 中如果 __index
元方法是一个函数且没有返回值,默认的行为是返回 nil。
mytable={}
meta=
{
age=10
}
setmetatable(mytable,meta)
meta.__index= function()
return 11
end
print(mytable.age) --11
mytable={}
meta=
{
age=10
}
setmetatable(mytable,meta)
meta.__index= function(table,value)
value = 11
return value
end
print(mytable.age) --11
(2.5)__newindex
__newindex:当子表有元表,并且元表中有__newindex,你尝试对子表中不存在的属性赋值时:
- 如果__newindex是一个函数,Lua会调用这个函数,传入子表、键和值作为参数。
- 如果__newindex是一个表,Lua会在这个__newindex表中进行赋值操作,而不是在原始表中。
__newindex
可以用来控制对表的修改,实现只读表,或者在字段被修改时执行特定的行为。
(2.5.1)__newindex是函数
mytable11={}
print("子表地址"..tostring(mytable11))
meta11={
__newindex=function(table,key,value) --参数table为子表
print(table,key,value)
end
}
print("元表地址"..tostring(meta11))
setmetatable(mytable11,meta11)
mytable11.age=20 --对子表mytable11中不存在的属性age赋值
--子表地址table: 00A53478
--元表地址table: 00A531D0
--table: 00A53478 age 20
mytable11={}
print("子表地址"..tostring(mytable11))
meta11={
__newindex=function(table,key,value) --参数table为子表
print(table,key,value)
end
}
print("元表地址"..tostring(meta11))
setmetatable(mytable11,meta11)
mytable11.age=20 --对子表mytable11中不存在的属性age赋值
print(mytable11.age) --nil
print(meta11.age) --nil
--子表地址table: 00F431F0
--元表地址table: 00F43240
--table: 00F431F0 age 20
--nil
--nil
如果你对一个表的不存在的属性进行赋值,并且这个表有一个元表(metatable)且元表中重载了__newindex
方法,那么Lua将调用这个__newindex
方法而不是在原始表中创建这个属性。这意味着,如果__newindex
方法没有显式地在表中创建这个属性,那么即使你尝试设置它,这个属性也不会存在于原始表中。
mytable11={}
print("子表地址"..tostring(mytable11))
meta11={
__newindex=function(table,key,value) --参数table为子表
print(table,key,value)
rawset(table,key,value) --实际在子表中创建属性
end
}
print("元表地址"..tostring(meta11))
setmetatable(mytable11,meta11)
mytable11.age=20 --对子表mytable11中不存在的属性age赋值
print(mytable11.age) --20
print(meta11.age) --nil
--子表地址table: 00A83308
--元表地址table: 00A833D0
--table: 00A83308 age 20
--20
--nil
(2.5.1)__newindex是表
mytable10={}
meta10={
__newindex={}
}
setmetatable(mytable10,meta10)
mytable10.age=20 --对子表mytable10 中不存在的属性age赋值
print(mytable10.age) --nil
print(meta10.age) --nil
print(meta10.__newindex.age) --20
(2.6)获取元表
print(getmetatable(mytable))
9.面向对象
Lua 本身不是一种面向对象编程语言,但它提供了机制,如元表(metatables)和元方法(metamethods),允许开发者模拟面向对象编程的特性,如封装和继承。
(1)封装
--定义一个Object类
Object={}
print("Object类:"..tostring(Object))
Object.id=10
Object.__index=Object
--Object类方法
function Object:Test() --冒号:第一个参数是自己,运用self使用变量值
print("Test function:"..self.id)
end
--Object类构造函数
function Object:new()
--运用元表
local obj={} --创建子表
setmetatable(obj,self)
return obj --返回子表
end
--创建Object子对象
temp=Object:new() --或者temp=Object.new(Object)
print("子对象:"..tostring(temp))
print(temp.id) --10
temp:Test() --Test function:10
--查看temp的元表
print("子对象的元表:"..tostring(getmetatable(temp)))
temp.id=2 --为temp创建了一个id属性,因为元表中没有__newindex。所以之前Object的id不变
print(Object.id) --10
temp:Test() --Test function:2
--Object类:table: 00469648
--子对象:table: 00469698
--10
--Test function:10
--子对象的元表:table: 00469648
--10
--Test function:2
如果Object.__newindex=Object,那么Object.id也会为2。但这通常不是面向对象编程中期望的行为,因为这样会导致所有 Object 类型的实例共享相同的 id 属性值,而不是每个实例拥有自己的 id 值。通常,你会希望每个实例都有自己的属性集,因此,通常会在 __newindex 方法中为实例表设置新的属性,而不是在其元表中设置。
(2)继承
-- 定义一个Object类
Object={}
print("Object类:"..tostring(Object))
Object.id=10
Object.__index=Object
--Object类构造函数
function Object:new()
--运用元表
local obj={} --创建子表
setmetatable(obj,self)
return obj --返回子表
end
--创建Object类方法subClass,参数为子类
function Object:subClass(className)
_G[className]={}
local obj=_G[className] --操作子类,不能写成local obj=className
obj.base=self --定义一个base属性,self为Object
setmetatable(obj,self)
return obj
end
--创建Object子类Person
Object:subClass("Person") --Person的元表是Object,Object设置了__index
print(Person.id) --10
(3)多态
多态性允许我们通过父类的引用来操作不同的子类对象,并且在运行时确定调用哪个类的重写方法。(相同的方法,执行不同的逻辑)
父类= new 子类对象()
--创建一个Object的子类Father
Object:subClass("Father")
Father.__index=Father
Father.PosX=0;
Father.PosY=0
function Father:Move()
self.PosX=self.PosX+1
self.PosY=self.PosY+1
print(self.PosX,self.PosY)
end
print("Father类:"..tostring(Father))
--创建一个Father的子类Son
Father:subClass("Son")
Son.__index=Son
function Son:Move() --重写Move
--想使用base.Move() ,
print("self.base:"..tostring(self.base)) --self的base是Father
self.base.Move(self)----要避免把基类表传入方法中,所以要用. --self是Son的实例
--self.base:Move() --base用:会有问题,会共用posX posY --self.base为father,默认参数也为father,那么就是father的Pos变化
end
print("Son类:"..tostring(Son))
--创建son的实例
local son1=Son:new()
son1:Move()
local son2=Son:new()
son2:Move()
--Father类:table: 00BD96C0
--Son类:table: 00BD9788
--self.base:table: 00BD96C0
--1 1
--self.base:table: 00BD96C0
--1 1
(4)总结
- 封装:父类new函数中创建一个空表,并设置空表的元表为父类。需要运用__index。
- 继承:父类实现一个方法,用于创建子类。函数参数为一个子表,并使用大G表来进行操作该子表。同样需要设置子表的元表为父类。需要运用__index。
- 大坑:一定要注意self是指子类还是父类,可以打印一下地址查看!
10.Lua其他库
Lua 自带的标准库提供了一些基础的功能,除了上文提到的基础库(提供了Lua脚本与Lua解释器交互的基本函数,比如assert, error, ipairs, pairs, pcall, print, select, tonumber, tostring, type, unpack, xpcall等)、包管理(提供了Lua模块加载和构建的功能,主要函数包括require和module)、字符串处理、表处理(如table.insert,table.remove, table.sort等)、还有数学计算、输入/输出(提供了文件操作的函数)以及操作系统库(提供了与操作系统交互的函数)等库。
(1)数学计算
print("绝对值:"..math.abs(-11))
print("弧度转角度:"..math.deg(math.pi))
print("cos90度,参数是弧度:"..math.cos(90*(math.pi/180)))
print("向下取整:"..math.floor(0.56)) --0
print("向上取整:"..math.ceil(0.56)) --1
print("最大值:"..math.max(1,2)) --2
print("最小值:"..math.min(4,5)) --4
--将一个浮点数分解为其整数部分和小数部分
print(math.modf(1.2)) --1 0.2
print("幂运算:"..math.pow(2,5))
print("幂运算:"..2^5) --不利用math库,使用 ^ 运算符通常更简洁
math.randomseed(os.time()) --必须初始化随机种子,否则随机数一定是固定的
print("随机数:"..math.random(0,100))
print("随机数:"..math.random(0,100))
print("开方:"..math.sqrt(4))
(2)操作系统库-时间
print(os.time())
print(os.time({year=2024,month=2,day=26}))
nowTime=os.date("*t")
print(nowTime.year.."/"..nowTime.month.."/"..nowTime.day)
--1711019493
--1708920000
--2024/3/21
(3)文件操作
(3.1)写文件
f=io.open("Lesson_20_IO.txt","w")
f.write(f,"流水从不争先,争的是滔滔不绝。\n")
f.write(f,"不盼刹那绽放,盼的是生生不息。")
io.close(f)
(3.2)读文件
read函数是最常用的读取方法之一。read
函数可以带有不同的参数来指定读取内容的方式:
- "*l"或者"*line": [默认]读取下一行的内容,若为文件尾,则返回nil
- "*n"或者"*number": 读取一个数字
- "a"或者"*all": 从当前位置读取整个文件,若为文件尾,则返回空字串
- number: 读取number个字符
在Lua中,如果你想要将文件的读取光标(读取位置)移动到文件的第一行,你可以使用seek
方法。seek
方法用于设置和获取文件的当前位置,并且有三个选项:"set"
, "cur"
, "end"
。如果你想要将光标移动到文件的开始,你应该使用"set"
模式,并且将偏移量设置为0。
f=io.open("Lesson_20_IO.txt","r")
txt=""
while true do
str=f.read(f)
if str==nil then
break
end
txt=txt..str
print(txt)
end
f:seek("set") --将光标移动到文件的开始,还支持参数:"cur", "end"
txt=f:read(6)
print(txt) --流水
f:seek("cur", 6) -- 将光标向前移动10个字节
txt=f:read(6)
print(txt) --争先
txt=f:read("*number")
print(txt) --nil
io.close(f)
--流水从不争先,争的是滔滔不绝。
--流水从不争先,争的是滔滔不绝。不盼刹那绽放,盼的是生生不息。
--流水
--争先
--nil
(4)其他-路径
print("lua脚本加载路径:"..package.path)
path=package.path..";C:\\"
print("增加一个路径:"..path)
--lua脚本加载路径:;.\?.lua;D:\soft\Lua\5.1\lua\?.lua;D:\soft\Lua\5.1\lua\?\init.lua;D:\soft\Lua\5.1\?.lua;D:\soft\Lua\5.1\?\init.lua;D:\soft\Lua\5.1\lua\?.luac
--增加一个路径:;.\?.lua;D:\soft\Lua\5.1\lua\?.lua;D:\soft\Lua\5.1\lua\?\init.lua;D:\soft\Lua\5.1\?.lua;D:\soft\Lua\5.1\?\init.lua;D:\soft\Lua\5.1\lua\?.luac;C:\
11.垃圾回收
lua中有自动定时进行GC操作。unity中热更新开发,尽量不要去用自动垃圾回收(推荐:切场景或者内存不够用时)
test={id=1024,index=0}
print("获取当前lua占用内存字节数(kb):"..collectgarbage("count"))
test=nil --释放资源
collectgarbage("collect") --进行垃圾回收
print("获取当前lua占用内存字节数(kb):"..collectgarbage("count"))
--获取当前lua占用内存字节数(kb):20.5048828125
--获取当前lua占用内存字节数(kb):19.392578125
三、热更新
1.插件安装
- 在unity资源商店中找到moonsharp 插件,并导入到unity中。
2.创建Lua脚本
创建Lua脚本test.lua,并放入StreamingAssets文件夹下。
function MyLua()
print("hello lua") --lua的print函数在Unity中是无法输出的
Debug.Log("Hello unity,I am Lua.")
return "hello lua"
end
3.unity中调用Lua脚本
下方实例是用来展示:实现Lua调用unity中Debug类,并在unity中成功调用Lua中的方法。
using MoonSharp.Interpreter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LuaManager : MonoBehaviour
{
void Start()
{
//创建Lua脚本解释器
Script luaScript = new Script();
//在Lua中调用Unity提供的Debug功能
UserData.RegisterType<Debug>();
luaScript.Globals["Debug"] = typeof(Debug);
//获取lua脚本位置
string luaPath = System.IO.Path.Combine(Application.streamingAssetsPath, "test.lua");
//读取lua脚本
string luaText = System.IO.File.ReadAllText(luaPath);
//运行lua脚本
DynValue dynValue=luaScript.DoString(luaText);
if(dynValue == null )
{
Debug.Log("test.lua不存在");
return;
}
//获取lua脚本中的函数
DynValue luaFunction = luaScript.Globals.Get("MyLua");
if(luaFunction == null )
{
Debug.Log("MyLua 函数不存在");
return;
}
DynValue result = luaScript.Call(luaFunction);
Debug.Log("unity中运行结果:"+result.ToString());
Debug.Log("unity中运行结果:" + result.ToObject<string>());
}
}
4.项目升级
Lua利用unity中的GameObject类、Vector3类创建一个物体并返回。
(4.1)Lua脚本更改
function MyLua()
go=GameObject("LuaObj")
go.transform.position=Vector3(1,1,1)
print("hello lua") --lua的print函数在Unity中是无法输出的
Debug.Log("Hello unity,I am Lua.")
return go
end
(4.2)unity中调用Lua
private void Test()
{
//获取lua脚本位置,lua脚本可以放在服务器端,通过www下载,下载后再运行Lua脚本
string luaPath = System.IO.Path.Combine(Application.streamingAssetsPath, "test.lua");
//读取lua脚本
string luaText = System.IO.File.ReadAllText(luaPath);
if(luaText=="")
{
return;
}
//创建Lua脚本解释器
Script luaScript = new Script();
//在Lua中调用Unity提供的功能
UserData.RegisterType<Debug>();
UserData.RegisterType<GameObject>();
UserData.RegisterType<Transform>();
UserData.RegisterType<Vector3>();
//将Debug类的类型信息赋值给Lua中的全局变量Debug。这样Lua脚本就可以通过Debug这个全局变量来访问Debug类的静态方法和属性。
luaScript.Globals["Debug"] = typeof(Debug);
//Debug是一个静态类,其方法可以直接在Lua中调用,而GameObject和Vector3是需要实例化的对象,它们在Lua中的使用需要C#层面上的支持和注册。
//创建一个函数委托,这个委托指向一个方法CreateGameObject,这个方法接受一个string参数并返回一个GameObject实例。这样Lua脚本就可以通过调用GameObject("name")来创建一个新的GameObject实例。
//在Lua中创建Unity对象通常需要调用Unity的API函数,而不仅仅是访问类型信息。对于GameObject,你不能直接使用typeof(GameObject),因为GameObject不仅有静态方法,还有实例方法和属性。如果你只是传递类型信息,Lua将不知道如何创建GameObject的实例。
luaScript.Globals["GameObject"] = (System.Func<string, GameObject>)CreateGameObject;
luaScript.Globals["Vector3"] = (System.Func<float, float, float, Vector3>)CreateVector3;
//运行lua脚本
DynValue dynValue = luaScript.DoString(luaText);
if (dynValue == null)
{
Debug.Log("test.lua不存在");
return;
}
DynValue luaFunction = luaScript.Globals.Get("MyLua");
if (luaFunction == null)
{
Debug.Log("MyLua 函数不存在");
return;
}
DynValue result = luaScript.Call(luaFunction);
//返回结果是字符串
//Debug.Log("unity中运行结果:" + result.ToString());
//Debug.Log("unity中运行结果:" + result.ToObject<string>());
//返回结果是物体
GameObject go=result.ToObject<GameObject>();
go.AddComponent<Rigidbody>();
}
static GameObject CreateGameObject(string name)
{
return new GameObject(name);
}
static Vector3 CreateVector3(float x, float y, float z)
{
return new Vector3(x, y, z);
}