Resources资源加载

同步和异步

同步就是我等你啊,异步就是我不等你啊。

同步:排队等饭的苦逼打工人:你去餐厅点了一份炒饭,必须 站在柜台前干等,直到厨师做好饭递给你,中间不能做其他事。

异步:边等外卖边刷剧的聪明人:你在外卖平台下单后,不用干等,直接回家刷剧。外卖到了,骑手打电话通知你取餐。

Resources同步加载

加载预制体对象

// Resource.Load的本质,只是将这个对象数据配置在内存中,但是并没有创建
GameObject go = Resources.Load<GameObject>("预制体文件资源");
Instantiate(go);

加载各种资源

Resources.Load<AudioClip>("音频文件路径");
Resources.Load<TextAsset>("文本文件路径");
Resources.Load<Sprite>("图片文件路径");
Resources.Load<RuntimeAnimatorController>("状态机文件资源");

Resources异步加载

配合协程进行异步加载

public void AsyncLoad()
{
    // 使用协程加载资源
    StartCoroutine(Load());
}

private IEnumerator Load()
{
    ResourceRequest rr = Resources.LoadAsync<TextAsset>("文本资源路径");
    while(!rr.isDone)
    {
        yield return null;
    }
    //asset 是资源对象 加载完毕过后 就能够得到它
    string content = (rr.asset as TextAsset).text;
    print(content);
}

Resources资源卸载

重复加载载不会浪费内存

当一个资源被加载后,将会把它放入内存进行缓存。如果再一次使用,则会从内存中查找。

但是会浪费性能(每次加载都会去内存中查找并取出,始终伴随一些性能消耗)

卸载指定资源

仅适用于卸载非GameObject和Component的资源,如纹理、材质、音频等。

必须保证该资源未被任何活跃对象引用,否则调用无效。

因为精准卸载指定资源,所以会立刻生效。无显著性能开销。

Resouces.UnLoadAsset(指定资源);

卸载当前场景未被引用的资源

遍历所有加载的资源,然后检查是否还有被任何对象引用,如果没有,则卸载它们。

因为会遍历所有资源,所以开销很大。不建议使用。

资源加载后会被缓存到内存中,即使不再使用,但它可能依然在内存中,如果游戏或应用中有大量的资源加载和卸载操作,可能会导致内存占用过高。
而Resources.UnloadUnusedAssets()就是删除缓存在内存中的资源,从而减少内存的占用。


// 使用示例
public class ResourceManager : MonoBehaviour
{
    void Start()
    {
        // 在游戏启动时或某个特定时刻调用
        StartCoroutine(UnloadUnusedAssetsRoutine());
    }

    IEnumerator UnloadUnusedAssetsRoutine()
    {
        yield return Resources.UnloadUnusedAssets(); // 卸载未使用的资源
        Debug.Log("Unused assets have been unloaded.");
    }
}

AB包

AB包的定义

AB可以对Unity中的资源(场景、模型、贴图等)进行打包为独立文件,供运行时按需进行加载

主资源包文件

实际打包的资源(如预制体、贴图、音频等)

清单文件

AB包内的资源列表和对应资源的Hash值

主清单文件

全局记录所有AB包的依赖关系

AB包的作用

减少初始安装包体积,支持动态加载,实现热更新

Asset Bundle Browser

常用参数

BuildTarget:目标平台

OutPutPath:目标输出路径

如果添的是:ABPackage/PC
实际上就是:项目根目录/Assets/ABPackage/PC

Clear Folders:是否清空文件夹,重新打包

Copy To StreamingAssets:是否拷贝到StreamingAssets文件夹中

Compression:压缩方式

NoCompression: 不压缩、解压快,包大(不推荐)
LZMA:压缩最小,解压慢。缺点:用一个资源压解压所有
LZ4:压缩(相对于LZMA大),用什么解压什么,内存占用低(建议)

构建AB的步骤

  1. 打AB包

  2. 通过BuildPipeline.BuildAssetBundles构建AB包到指定目录下

  3. 加载和卸载AB资源

    1. 先加载AB包 AssetBundle.LoadFromFile

    2. 在从AB包加载对应的资源  ab.LoadAsset 

构建AB包信息API

BuildPipeline.BuildAssetBundles

将项目资源打包为AB包,返回一个记录了所有生成AssetBundle的元数据信息

参数一:生成AB包文件的输出路径(路径存在且具有写入权限)

参数二:构建选项BuildAssetBundleOptions

  • None:默认选项LZMA压缩,增量构建。(增量构建:如果资源修改,只会构建修改的资源,原来已经构建的资源不会重新构建。LZMA压缩:有着很高的压缩比)

参数三:BuildTarget指定目标平台(不同平台的 AssetBundle 不兼容)

// 示例代码
namespace DYFramework.Examples.Editor
{
    public class AbBuild
    {

        [MenuItem("DYFramework/Example/1. 构建AB包")]
        static void Build()
        {
            if (!Directory.Exists(Application.streamingAssetsPath))
            {
                Directory.CreateDirectory(Application.streamingAssetsPath);
            }

            BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath,
                BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
        }
    }
}

AB包的加载

本地同步加载

// 从本地路径加载AB包
AssetBundle localBundle = AssetBundle.LoadFromFile("Assets/AssetBundles/characters.assetbundle");
// 加载资源
GameObject playerPrefab = localBundle.LoadAsset<GameObject>("Player");
Instantiate(playerPrefab);

远程异步加载

IEnumerator LoadRemoteBundle() {
    string url = "http://your-server.com/characters.assetbundle";
    UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url);
    yield return request.SendWebRequest();

    if (request.result == UnityWebRequest.Result.Success) {
        AssetBundle remoteBundle = DownloadHandlerAssetBundle.GetContent(request);
        GameObject enemyPrefab = remoteBundle.LoadAsset<GameObject>("Enemy");
        Instantiate(enemyPrefab);
    } else {
        Debug.LogError("加载失败: " + request.error);
    }
}

带缓存的加载

AB的卸载

1. 当为true时,卸载AssetBundle及其所有已加载资源。如果场景中有对象引用了这些资源,会导致引用丢失(如材质变粉、模型消失)

2. 当为false是,仅卸载AssetBundle文件的内存镜像,不卸载已加载的资源。资源实例会留在内存中,需要手动管理。

// 示例代码
AssetBundle bundle = AssetBundle.LoadFromFile("characters.assetbundle");
GameObject obj = bundle.LoadAsset<GameObject>("Player");
Instantiate(obj);

// 卸载AB包及所有资源(即使资源已被实例化)
bundle.Unload(true);
// 此时obj可能变为"Missing"(若场景中仍有引用)

bundle.Unload(false);
// 已实例化的GameObject仍存在,但无法通过bundle再次加载

AB包的依赖

依赖加载的定义

当不同的AB包之间共享资源时,比如材质、纹理或脚本,这些共享资源会被打包到一个公共的AB包中,而其他使用这些资源的AB包则会依赖这个公共包。例如,如果“角色”AB包和“场景”AB包都使用了同一个材质,那么这个材质可能被打包到“shared_materials”AB包中,这样角色和场景AB包在加载时都需要先加载这个共享包。如果不处理依赖关系,可能会导致资源丢失,比如角色模型显示为紫色(材质丢失)。

当打包AB包时,Unity会自动分析资源之间的引用关系,并生成依赖信息。这些信息存储在AB包的清单文件(.manifest)中,以及主清单文件(AssetBundleManifest)中。主清单文件记录了所有AB包之间的依赖关系,可以通过它来查询某个AB包依赖的其他包。

依赖加载的流程

1. 加载主清单文件
AssetBundle manifestBundle = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
AssetBundleManifest manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

2. 获得依赖链
string targetBundle = "characters";
string[] dependencies = manifest.GetAllDependencies(targetBundle);  // 获得characters的AB包中所有依赖包

3. 按顺序加载依赖包
foreach (string depBundle in dependencies) {
    AssetBundle.LoadFromFile($"AssetBundles/{depBundle}");
}

4. 加载目标依赖包
AssetBundle mainBundle = AssetBundle.LoadFromFile("AssetBundles/characters");
GameObject playerPrefab = mainBundle.LoadAsset<GameObject>("Player");

AB包资源加载管理器策略

详细见DYFramework框架

Addressables

Addressables的定义

将资源(如预制体、贴图、场景)标记为“可寻址”(Addressable),通过逻辑标签(Label)或唯一Key动态加载,无需手动管理AB包的依赖和路径。

简化资源加载/卸载流程,自动化处理依赖关系,支持本地和远程资源分发。

Addressable 的核心组件

Lua语法

安装Lua脚本,并测试成功运行

下载地址:https://kkgithub.com/rjpcomputing/luaforwindows

下载后,无脑点点点

基本语法

第一个Hello world

print("hello world")

注释

--这是注释内容

数据类型

type

通过type可以得到对于的类型

print(type(nil))
print(type("hello world"))
print(type(123))
print(type(true))

输出
PS G:\系统默认\桌面\lua> lua .\hello.lua
nil
string
number
boolean

type的返回值时string类型的

print(type(type(nil)))

nil相对于一个null

-- nil 空
a = nil

使用没有赋值的变量不会报错,返回nil

print(b)

输出:nil

nil做比较时,应该加双引号

type(X) == nil 

输出:false

type(X) == "nil"

输出:true

boolean类型

0为false,1为true

a = true

number类型

number可以理解为int、float、double的整合

print(type(2))
print(type(2.2))
print(type(0.2))
print(type(2e+1))
print(type(0.2e-1))
print(type(7.8263692594256e-06))

特殊的数字类型的值

print(type(2e+1)) 
-- 其实就是 2 × 10^1 = 20

print(type(0.2e-1))
-- 其实就是 0.2 × 10^(-1) = 0.02

print(type(7.8263692594256e-06))
-- 其实就是7.8263692594256 × 10 ^ (-6)

table表

table可以是一个空表,也可以往里面添加数据

-- 创建一个空的 table
a = {}
 
-- 直接初始表
b = {"apple", "pear", "orange", "grape"}

table是一个关联数组,数组的索引可以是数字,也可以是字符串

a = {}
a["key"] = "value"

-- 输出:
table: 0xe8ec650
value


b = {}
b[10] = 22
b[10] = b[10] + 10
print(b[10])

-- 输出:32

table的默认索引是从1开始的

a =  {"apple", "pear", "orange", "grape"}
print(a[1])
print(a[2])
print(a[3])
print(a[4])

-- 输出
apple
pear
orange
grape

变量

全局变量

-- 全局变量a
a = 10

-- 全局变量b
function joke()
  b = 5
end

局部变量

变量前加local都是局部变量

-- 局部变量
local a = 5

赋值语句

lua可以对多个变量赋值

a,b = 10, 2*x

会先计算右边的值,再进行赋值操作

-- 交换x和y
x,y = y,x

当变量个数与值的个数不一致时

-- 变量个数 > 值的个数,按变量个数补nil
a,b,c = 1,2
输出:
1,2,nil


-- 变量个数 < 值的个数,多余的值会被忽略
a,b = 1,2,3
输出:
1,2

循环

while循环

-- 先去判断条件,如果条件到了执行statements语句,如果条件没有达到,则直接退出
while(condition)
do
   statements
end

for循环

-- var是循环变量,表示当前的值
=- exp1表示循环变量的初始值
-- exp2表示循环的终止条件
-- var的增量,默认为1
for var=exp1,exp2,exp3 do  
    <执行体>  
end  

简单递增循环

for i = 1,5,1 do
  print(i)
end

输出:
1
2
3
4
5

步长可以省略

for i = 1,5 do
  print(i)
end

输出:
1
2
3
4
5

非整数步长

for i = 0, 2, 0.5 do
  print(i)
end

输出
0
0.5
1
1.5
2

流程控制

if语句

if(布尔表达式)
then
   --[ 在布尔表达式为 true 时执行的语句 --]
end

if-else语句

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

if-elseif-else语句

if( 布尔表达式 1)
then
   --[ 在布尔表达式 1 为 true 时执行该语句块 --]

elseif( 布尔表达式 2)
then
   --[ 在布尔表达式 2 为 true 时执行该语句块 --]

elseif( 布尔表达式 3)
then
   --[ 在布尔表达式 3 为 true 时执行该语句块 --]
else 
   --[ 如果以上布尔表达式都不为 true 则执行该语句块 --]
end

函数

基本语法

function 函数名(参数1, 参数2, ...)
    -- 函数体
    return 返回值  -- 可选
end

function可以理解为第一类值,函数可以存储在变量中

-- 无参
function greet()
    print("Hello, Lua!")
end

greet()  -- 调用函数,输出: Hello, Lua!


-- 有参
function add(a, b)
    return a + b
end

print(add(3, 5))  -- 输出: 8

函数可以返回多个值

function getCoordinates()
    return 10, 20
end

x, y = getCoordinates()
print(x, y)  
-- 输出: 10 20

函数可以使用匿名函数

myFunction = function(name)
    print("Hello, " .. name)
end

myFunction("Alice")  
-- 输出: Hello, Alice

函数参数

函数可以作为参数传递

function apply(func, value)
    return func(value)
end

function double(x)
    return x * 2
end

print(apply(double, 5))  -- 输出: 10

函数可以接收可变的参数

function prints(...)
    print(...)
end

prints(1,2,3,4,5,6)

输出:
1       2       3       4       5       6


-- 获得传入参数的个数
function prints(...)
    local n = select('#', ...)
    print(n)
end

prints(1,2,3,4,5,6,9,91)

输出;8


-- 返回从起点 n 开始到结束位置的所有参数列表
function prints(...)
    print(select(2, ...))
end

prints(1,2,3,4,5,6,9,91)

输出:
2       3       4       5       6       9       91

特殊的变量名 _

-- _ 可以理解为一个占位符,不需要使用的变量
function sum(a,b,c)
  return a + b,c
end

local sum,_ = sum(1,2,5)
print(sum)
print(_)


输出
3
5

运算符

一元运算符

逻辑运算符

or

从左到右依次计算操作数

如果某个操作数的值为真值(只要不是false和nil都是真值),则立刻返回该操作值,并停止后续计算

如果所有操作数都为假值,则返回最后一个操作数的值

print(a or b)
输出:
nil

print(a or 1)
输出:
1

print(3 or 1)
输出:
3

其他运算符

连接两个字符串

print("hello" .. "world")

输出:
helloworld

返回字符串或表的长度

print(#"hello" .. "world")

输出:
5world

字符串

单引号和双引号都是字符串

a = "hello"
a = 'wolrd'

使用[[]]来表示一块字符串

html = [[
<html>
<head></head>
<body>
hello world
</body>
</html>
]]
print(html)

输出
<html>
<head></head>
<body>
hello world
</body>
</html>

在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字

print("2" + 6)
-- 输出:8

print("2" + "6")
-- 输出:8

print("-2e2" * "6")
-- 输出:-1200

print("error" + 1)
-- 直接报错

字符串连接使用的是 ..

print("a" .. 'b')
-- 输出:ab

使用#计算字符串的长度

len = "hello world"
print(#len)
-- 输出:11

数组

迭代器

泛型for迭代器

基本语法

-- k,v是变量列表,t表达式列表
for k,v paris(t) do
  print(k,v)
end

-- 例子
array = {"Google", "Runoob"}
for k, v in pairs(array) do
  print(k, v)
end

输出:
1	Google
2	Runoob

泛型for的执行过程

  • 初始化阶段:泛型for需要三个参数(迭代函数、状态值、初始值),再循环开始之前,lua会将这三个参数分别赋值给内部变量(f-迭代函数、s-状态值、var-初始值)

  • 每次迭代的执行,lua都会调用迭代函数f(s,var),迭代函数会返回一个新的值,作为当前循环的变量

  • 如果迭代函数返回nil,则循环终止

  • 以上步骤会不断重复

-- 迭代函数等价于这段
local f, s, var = function(state), state, initial
while true do
    local var = f(s, var)
    if var == nil then break end
    -- 循环体
end

迭代函数ipars

-- 用于遍历数组的部分表
local t = {10, 20, 30}

for key, value in ipairs(t) do
    print(key, value)
end

输出
1	10
2	20
3	30


执行过程分析
ipars返回三个值迭代函数f=next_ipairs、状态值s=t、初始值var=0
第一次迭代:调用f(s,var)即next_ipairs(t,0),返回键1和值10
第二次迭代:调用f(s,var)即next_ipairs(t,1),返回键2和值20
第三次迭代:调用f(s,var)即next_ipairs(t,2),返回键3和值30
第四次迭代:调用f(s,var)即next_ipairs(t,3),返回nil,循环终止

无状态迭代器

每次调用迭代器函数时,它只根据当前输入计算下一个值,而不需要维护额外的状态信息

多状态迭代器

-- 自定义无状态迭代器

function count_iterator(start, step)
    return function(_, current)
        current = current + (step or 1)
        if current > 3 then -- 假设上限为 100
            return nil
        end
        return current, current * 2 -- 返回当前值和它的两倍
    end, nil, start - (step or 1) -- 状态值和初始值
end

for i, v in count_iterator(1, 2) do
    print(i, v)
end

-- 分析
循环开始之前,将迭代函数、状态值、初始值赋值给内部变量
即 迭代函数为匿名函数、状态值为nil、初始值为 -1
第一次循环,调用匿名函数,返回1,2

模块与包

元表

协同程序

-- lua中一个汉字占3个字符长度
a = "helloworld"
print(#a)
a = "中文"
print(#a)


输出
PS G:\系统默认\桌面\lua> lua .\hello.lua
10
6

a = [[hello
world]]
print(a)

输出
PS G:\系统默认\桌面\lua> lua .\hello.lua
hello
world

a = "hello\nworld"
print(a)

输出
PS G:\系统默认\桌面\lua> lua .\hello.lua
hello
world

xLua

toLua