AssetBundle AB包

AB包就是用于特定平台的资产压缩包。它的作用是:拆分资源、缩包体;支持热更新。

Asset Bundle Browser 打包AB包

打包AB包可以用到Unity中提供的打包工具:Asset Bundle Browser。(需要在Window - Package Manager - Asset Bundle Browser中安装)

打包方法

  • 在Project窗口中选择要打包的内容,在Inspector窗口下方的AssetBundle取一个包名
  • 在AssetBundle的Build页签中选择Build Target打包平台、OutputPath输出路径、建议选择Clear Folders和Copy to StreamingAssets;高级设置中压缩方式选择LZ4 (用什么解压什么,内存占用低)
  • Build

打包好后可以在与Asset同级的AssetBundle文件夹下找到打包好的文件。

使用方法

  1. 同步加载
  • 加载AB包:
1
2
// 参数:AB包的路径
AssetBundle ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "model");
  • 加载AB包中的资源:
1
ab.LoadAsset<GameObject>("Cube")
  1. 异步加载
1
2
3
4
5
6
7
8
9
10
IEnumerator LoadABResourcesAsync(string ABName, string resName)
{
// 加载AB包
AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetPath + "/" + "model");
yield return abcr;
// 加载资源
AssetBundleRequest abq = abcr.assetBundle.LoadAsseyAsync(resName, typeof(Sprite));
yield return abq;
img.sprite = abq.asset as Sprite;
}

注意:同一个AB包不能重复加载

  1. 卸载AB包
1
2
// 参数:是否卸载通过AB包加载的资源
AssetBundle.UnloadAllAssetBundles(false);

另外,如果一个AB包中的资源用到了其他AB包中的资源,需要加载用到资源的那个AB包。其中,可以通过主包得到依赖信息:

  1. 加载主包
1
AssetBundle abMain = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "PC");
  1. 加载主包中的固定文件
1
AssetBundleManifest abManifest = abMain.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
  1. 从固定文件中得到依赖信息的名字
1
string[] strs = abManifest.GetAllDependencies("model");

Lua语法

Lua 代码文件的后缀名为.lua

注释

  • 单行注释
1
-- 单行注释
  • 多行注释
1
2
3
--[[

--]]

变量

在默认情况下,变量总是认为是全局的。用 local 显式声明为局部变量。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil

多变量赋值

1
a, b, c = 1, 2, "123"

如果后面的值不够,会自动补空;如果后面的值多了,会自动省略。

数据类型

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:falsetrue
number 所有的数值都是number。表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function C Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。
注意:Lua中的变量可以随便赋值。

我们可以使用 type 函数测试给定变量或者值的类型:

1
2
3
4
5
6
7
print(type("Hello world"))      --> string  
print(type(10.4*3))             --> number
print(type(print))              --> function
print(type(type))               --> function
print(type(true))               --> boolean
print(type(nil))                --> nil
print(type(type(X)))            --> string

nil 空

nil 类型表示一种没有任何有效值,它只有一个值 —— nil,例如打印一个没有赋值的变量,便会输出一个 nil 值:

1
2
print(type(a))
nil

对于全局变量和 tablenil 还有一个”删除”作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉

nil 作比较时应该加上双引号 "

1
2
3
4
5
6
type(X)  
nil
type(X)==nil
false
type(X)=="nil"
true

type(X)==nil 结果为 false 的原因是 type(X) 实质是返回的 "nil" 字符串,是一个 string 类型。

boolean 布尔

boolean 类型只有两个可选值:true(真) 和 false(假),Luafalsenil 看作是 false,其他的都为 true数字 0 也是 true

number 数字

Lua 默认只有一种 number 类型 —— double(双精度)类型

string 字符串

字符串由一对双引号或单引号来表示。

1
2
string1 = "this is string1"
string2 = 'this is string2'

也可以用 2 个方括号 “[[]]” 来表示”一块”字符串。

1
2
3
4
5
6
7
8
9
html = [[  
<html>
<head></head>
<body>
    <a href="http://www.runoob.com/">菜鸟教程</a>
</body>
</html>
]]
print(html)

以下代码执行结果为:

1
2
3
4
5
6
<html>
<head></head>
<body>
<a href="http://www.runoob.com/">菜鸟教程</a>
</body>
</html>

字符串操作

  • 获取字符串长度
1
2
s = "aBcdEfG"
print(#s)

注意:一个汉字占3个长度

  • 字符串拼接
1
print("123" .. "456")

也可以这样:

1
print(string.format("我是小草, 我今年%d岁了", 18))
符号 含义
%d 与数字拼接
%a 与任何字符拼接
%s 与字符配对
  • 小写转大写
1
string.upper(str)
  • 大写转小写
1
string.lower(str)
  • 翻转字符串
1
string.reverse(str)
  • 字符串索引查找(返回起始位置和结束位置)
1
2
str = "abCdefg"
string.find(str, "Cde")

结果为:

1
3 5
  • 截取字符串
1
string.sub(str, 3)

从第3个位置向后截取。

还有一个重载:

1
string.sub(str, 3, 4)

截取这两个位置之间的字符。

  • 重复整个字符串
1
string.rep(str, 2)
  • 字符串修改(将指定的字符修改)
1
string.gsub(str, "Cd", "**")
  • 字符转ASCII码
1
2
3
// 参数1:字符串
// 参数2:要转为ASCII码的字符
string.byte("Lua", 1)
  • ASCII码转字符
1
string.byte(76)

结果为:L

function 函数

基本语法

基本语法:

1
2
3
function 函数名()

end

或者这样写:

1
2
3
F1 = function()

end

参数与返回值

  • 无参数无返回值
1
2
3
function F1()

end

注意:必须声明后才能调用。因为Lua是从上到下依次执行。

  • 有参数
1
2
3
function F1(a)

end

参数可以传任意类型。

  • 变长参数
1
2
3
function F1(...)
arg = {...} // 变长参数要用一个表存起来
end
  • 有返回值
1
2
3
function F1(a)
return a
end

多返回值时,在前面申明多个变量来接取即可。如果变量不够,只接取对应位置的返回值;如果变量多了,直接赋值nil

注意:Lua不支持重载,默认调用申明的最后一个函数

函数的嵌套

函数也支持嵌套:

1
2
3
4
5
function F1()
return function()
print("F2调用")
end
end

函数的闭包

闭包的含义:在A函数里面返回一个B函数,改变了A函数中变量的生命周期。

函数的闭包:

1
2
3
4
5
function F1(x)
return function(y)
return x + y
end
end
1
2
f2 = F1(10)
print(f2(5))

输出:

1
15 --x = 10,y = 5

table 表

数组

Lua中数组的申明可以是不同类型:

1
a = {1, 2, 3, "123", true}

注意:Lua中索引从1开始

  • 获取数组长度
1
#a

打印长度时如果nil在最后,则被忽略

  • 数组的遍历
1
2
3
for i = 1, #a do

end
迭代器遍历

ipairs

通过这种遍历,是从1开始往后遍历的,小于等于0和索引中断的都不能遍历得到

1
2
3
for i, k in ipairs(a) do
print(i.."_"..k)
end

其中,i是键,k是值,a是表

pairs

这种遍历可得到任意键

1
2
3
for i, k in pairs(a) do
print(i.."_"..k)
end

其中,i是键,k是值,a是表

也可以单独遍历键

二维数组

1
a = {{1, 2, 3},{4, 5, 6}}

得到里面的元素:

1
print(a[1][2]) // 结果为:2
  • 二维数组的遍历
1
2
3
4
5
6
for i = 1, #a do
b = a[i]
for j = 1, #b do
print(b[j])
end
end

自定义索引

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

可以自定义索引,不过使用索引时只会用没有自定义索引的值。比如以上的这个,长度为3a[1] = 2

字典

  • 申明:
1
a = {["name"] = "小草", ["age"] = 18}
  • 获得值:
1
a["name"]

或者:

1
a.name // .后面不能是数字
  • 修改值直接用=赋新值即可

  • 添加值直接用表名["新键名"]即可

  • 删除

1
a["name"] = nil
  • 遍历
1
2
3
for k, v in pairs(a) do

end

类和结构体

注意:Lua中默认没有面向对象,需要我们自己实现

  • 在内部申明:
1
2
3
4
5
6
7
Student = {
age = 1,
sex = true,
Up = function()
print("我成长了")
end
}

想要在表内部函数中调用表本身的属性和方法,一定要指明表本身的属性或方法:

1
2
3
4
5
6
7
8
Student = {
age = 1,
sex = true,
Up = function(t)
print(t.age)
print("我成长了")
end
}

调用时:

1
Student.Up(Student)

上面这句话也等价于:

1
Student:Up()

其中,:的含义是默认把调用者作为第一个参数传入方法中。

  • 申明类后再在外部申明:
1
Student.name = "小草"
1
2
3
Student.Speak = function()
print("你好")
end

另外,Lua中有一个关键字self表示默认传入的第一个参数。

1
2
3
function Student:Speak()
print(self.name .. ":你好")
end

大G表

_G是一个总表,它将我们声明的所有全局变量都存储在其中

表的公共操作

接下来用这两张表实例:

1
2
3
t1 = { {age = 1, name = "小草"}, {age = 2, name = "小花"} }

t2 = { name = "小树", sex = true }
  • 将一个表插入另一张表的后面
1
table.insert(t1, t2)
  • 删除表中指定元素
1
table.remove(表名, 元素位置)

第二个参数是可选参数,不传默认移除最后一个元素,传数字n表示删除下标位n的元素。

  • 升序排序
1
table.sort(t2)
  • 降序排序
1
2
3
4
table.sort(t2, function(a, b)
if a > b then
return true
end)
  • 拼接
1
table.concat(t1, ",")

第二个参数表示用于拼接的字符,返回值是一个字符串。注意:这个方法只适用于表中全是数字的情况。

元表

元表是一张普通 table,用来给另一个表(或用户数据)定义元方法,修改它的运算、访问、赋值行为,实现面向对象、重载运算符、自定义读写逻辑。

  • 设置元表
1
setmetatable(子表, 元表)
  • 得到元表
1
getmetatable(子表)
  • __tostring
1
2
3
4
5
6
7
8
9
meta = {
__tostring = function()
return "转为字符串"
end
}
myTable = {}
setmetatable(myTable, meta)

print(myTable)

当子表要被当做一个字符串使用时,默认调用元表中的__tostring方法

传入的参数默认是myTable这个表本身

  • __call
1
2
3
4
5
6
7
8
9
meta = {
__call = function()
print("作为函数")
end
}
myTable = {}
setmetatable(myTable, meta)

myTable()

当子表要被当做一个函数使用时,默认调用元表中的__call方法

传入的第一个参数默认是myTable这个表本身

  • 运算符重载
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
meta = {
-- 相当于+运算符重载。其中,t1、t2分别代表传入的表1、表2
__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 t1.age * t2.age
end,
-- 运算符/
__div = function(t1, t2)
return t1.age / t2.age
end,
-- 运算符%
__mod = function(t1, t2)
return t1.age % t2.age
end,
-- 运算符^
__pow = function(t1, t2)
return t1.age ^ t2.age
end,
-- 运算符==(要用条件运算符比较,二者的元表必须一致)
__eq = function(t1, t2)
return 0
end,
-- 运算符<
__lt = function(t1, t2)
return 0
end,
-- 运算符<=
__le = function(t1, t2)
return 0
end,
-- 运算符..
__concat = function(t1, t2)
return ""
end
}
myTable = {age = 1}
setmetatable(myTable, meta)
myTable1 = {age = 2}

print(myTable + myTable1)
  • __index__newindex
1
2
3
4
5
6
7
8
meta = {
age = 1
}
meta.__index = {age = 2}
myTable = {}
setmetatable(myTable, meta)

print(myTable.age)

当子表中找不到某一个属性时,会到元表中__index指定的表去找索引(建议把__index写在外部来避免问题)

打印结果是

1
2

另外,如果想找自己身上的变量,可以用下面这个方法:

1
rawget(myTabel, "age")

当赋值时,如果赋值一个不存在的索引,那么会把这个值赋值到newindex所指的表中,不会修改自己

1
2
3
4
5
6
7
8
meta = {}
meta.__newindex = {}
myTable = {}
setmetatable(myTable, meta)
myTable.age = 1

print(myTable.age)
print(meta.__newindex.age)

打印结果是

1
2
nil
1

同样的,如果想设置自己身上的变量,可以用下面这个方法:

1
rawset(myTable, "age", 6)

运算符

  • 算术运算符
    • C#的区别:没有++--+=-=/=*=%=;有^幂运算
    • 特点:字符串可以进行算术运算符操作,会自动转成number
  • 条件运算符
    • 不等于:~=
  • 逻辑运算符
    • andornot
  • 位运算符(不支持)
  • 三目运算符(不支持)

注意:

andor不仅可以连接boolean,任何东西都可以用来连接。在Lua中,只有nilfalse才认为是假。

另外,还存在“短路”的使用。and有假则假;or有真则真。

1
print(1 and 2)

在这个例子中,1为真,所以需要判断2是否为假,返回值就是2

1
print(true or 1)

在这个例子中,true为真,所以返回结果就是true

我们还可以用andor来模拟三目运算符:

1
2
3
x = 1
y = 2
(x > y) and x or y

选择结构

  • if-else结构:
1
2
3
4
5
if 条件 then
print("满足条件")
else
print("不满足条件")
end

另外,多分支写作elseif

  • Lua中没有switch-case结构
  • 不支持三目运算符

循环结构

  • while
1
2
3
while 条件 do

end
  • repeat-until
1
2
3
repeat

until 结束条件
  • for
1
2
3
for i = 1, 5 do

end

意思是i1开始,默认+1,到5结束。

1
2
3
for i = 1, 5, 2 do

end

第三个数字是增量。

多脚本执行和卸载

执行

在一个脚本中相执行另一个脚本,可以在这个脚本中这样写:

1
require("脚本名")

可以在这个脚本中使用另一个脚本里面的全局变量。

要想使用里面的本地变量可以在要被加载的脚本中返回需要的变量。

注意:如果是require加载执行的脚本,加载一次后不会再被执行

下面这句代码返回值是boolean,可以通过这个判断脚本有没有被执行:

1
package.loaded["脚本名"]

卸载

卸载:

1
package.loaded["脚本名"] = nil

协程

创建、运行和挂起

创建协程有两种方式,不同的方式对应不同的运行方式:

create创建协程

  • create创建协程,返回值是一个线程
1
co = coroutine.create(函数)
  • 运行:
1
coroutine.resume(co)

运行结果就是123,相当于执行了这个函数。

  • 挂起
1
coroutine.yield()

第一个返回值:协程是否启动成功
第二个返回值:yield里面的返回值

wrap创建协程

  • wrap创建协程,返回值是一个函数
1
co2 = coroutine.wrap(函数)
  • 运行:
1
co2()
  • 挂起
1
coroutine.yield()

返回值就是传入的参数

状态

得到协程的状态:

1
coroutine.status(协程对象)

可能得到的返回值:

  • dead 结束
  • suspended 暂停
  • running 进行中

得到当前正在运行的协程的线程号:

1
coroutine.running()

面向对象

Lua中比较复杂的数据结构都是通过table来实现的。

封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Object = {}
Object.id = 1

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

-- 实例化方法
function Object : new()
local obj = {}
self.__index = self
setmetatable(obj, self)
return obj
end

-- 实例化一个对象
local myObj = Object:new()
print(myObj) -- 结果是一张表
print(myObj.id) -- 1
myObj:Test() -- 1

其中,:会自动将调用这个函数的对象作为第一个参数传入。

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object = {}
Object.id = 1

function Object : subClass(className)
_G[className] = {}
local obj = _G[className]
self.__index = self
setmetatable(obj, self)
end

Object:subClass("Person")

-- 实例化一个对象
local p1 = Person:new() -- 实际上调用的是Object里的new()

p1的元表是PersonPerson的元表是Object。找不到的属性会依次往上找。

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Object = {}

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()
-- 重写方法
end

-- 实例化对象
local p1 = Player:new()
p1:Move()

不过实现多态有一个问题,那就是没有办法通过base.保留父类的方法,因此,我们需要实现base的功能:

我们可以在subClass()中加上:

1
2
3
4
5
6
7
8
function Object : subClass(className)
_G[className] = {}
local obj = _G[className]
self.__index = self
-- 子类定义一个base属性,base属性代表父类
obj.base = self
setmetatable(obj, self)
end

然后我们就可以使用了:

1
2
3
function Player : Move()
self.base.Move(self)
end

自带库

时间戳

  • 系统时间
1
os.time()
  • 根据年月日得到时间
1
os.time({year = 2026, month = 7, day = 17})
  • 得到年月日
1
os.date("*t")

数学运算

  • 绝对值
1
math.abs(-11)
  • 弧度转角度
1
math.deg(math.pi)
  • 向下取整
1
math.floor(2.6)
  • 向上取整
1
math.ceil(5.2)
  • 小数分离
1
math.modf(1.2)
  • 随机数(先设置随机数种子)
1
2
math.randomseed(os.time())
math.random(!00)

路径

  • Lua脚本加载路径
1
package.path

垃圾回收

  • 获取当前Lua占用内存数、K字节(用返回值*1024就可以得到具体的内存占用字节数)
1
collectgarbage("count")
  • 垃圾回收
1
collectgarbage("collect")