Unity 中的动态资源加载

前言

游戏中资源的状态可以分为三种:

  • 状态1: 仅在磁盘中.
  • 状态2: 被读取到内存中, 但是游戏此时并没有引用此资源.
  • 状态3: 被读取到内存中, 并且游戏此时正在引用此资源.

Unity 有两种模式: 编辑器模式和运行模式. 两种模式下都可以实现动态资源加载.

编辑模式

编辑模式下可以使用 AssetDatabase 类来实现资源的动态加载.

AssetDatabase.LoadAssetAtPath(filePath);

由于 Unity 资源的根目录为 Assets, 因此 filePath 参数必须以 Assets/ 开头, 另外需要加后缀名. 通过这个方法就可以加载指定 Assets 路径下的资源, 资源加载后便处于状态 2 了, 如果紧接着使用了此资源, 资源状态就变成了状态 3.

如果从资源目录中读取了一个 Prefab 资源, 运行模式下可以使用 GameObject.Instantiate() 来实例化 Prefab, 但是编辑模式下怎么实例化 Prefab 呢? 可以使用 PrefabUtility.InstantiatePrefab() 方法在编辑器模式下实例化 Prefab.

另外在编辑模式下使用以下方法来更新或者创建新的 Prefab, 需要传入游戏物体, 保存路径等信息.

  • PrefabUtility.SavePrefabAsset()
  • PrefabUtility.SaveAsPrefabAsset()
  • PrefabUtility.SaveAsPrefabAssetAndConnect()

如果想要销毁一个游戏对象, 运行模式下可以使用 Destroy(); 方法, 但是编辑器模式下怎么销毁一个游戏对象呢? 编辑模式下只能使用 Object.DestroyImmediate(Selection.activeObject, true); 来销毁游戏对象, 第二个参数决定是否卸载游戏物体引用的资源.

  • 执行 Object.DestroyImmediate(go, false); 之后资源就变成状态 2 了.
  • 执行 Object.DestroyImmediate(go, false); 之后资源就变成状态 1 了.

运行模式

运行模式下实现资源的动态加载有两种方式: Resources 和 AssetsBundle.

Resources 实现运行时资源的动态加载

Unity 在发布打包的时候会自动排除掉没有引用的资源, 只有 ResourcesStreamingAssets 文件夹中的资源无论是否被引用都会被打包.

另外场景中如果直接引用了 Resources 中的资源, 打包的时候 Resources 文件夹中的资源会被场景和 Resources 重复打包成两份.

基于上述两个原因约定:

  • Resources 文件夹只能存放运行时动态加载的资源
  • Scene 不能直接引用 Resources 中的资源

Resources.Load(fileName) 可以实现在运行模式下动态加载 Resources 文件夹下的资源. 其中 fileName 参数必须是 Resources 文件夹的相对路径, 且不能带有后缀名.

运行模式下删除对象的方法:

1
2
3
4
5
6
// 立即删除游戏对象
DestroyImmediate(go);
// 在下一帧的时候删除游戏对象
Destroy(go);
// 特定秒数后删除游戏对象
Destroy(go, 5f);

运行模式下 Resources 卸载资源的方式:

  • 使用 Resources.UnloadAsset(go); 卸载内存中指定游戏物体所引用的资源.
  • 使用 Resources.UnloadUnusedAssets(); 卸载内存中所有未被引用的资源. 另外这是一个异步进程, 可以在 Update 中使用 isDone 来判断是否执行完毕.

下面是推荐的 Resources 卸载资源的工具代码:

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
using System;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Unity 资源回收工具
/// </summary>
public class UnityGCUtility : MonoBehaviour
{
private AsyncOperation asyncOperation;
private UnityAction callBack;

/// <summary>
/// 彻底卸载 Resources 中所有未被引用的资源
/// </summary>
public void UnloadUnusedAssets()
{
UnloadUnusedAssetsUnit(() =>
{
UnloadUnusedAssetsUnit();
});
}

/// <summary>
/// 单次卸载资源模块
/// </summary>
/// <param name="callBackAction">回调</param>
private void UnloadUnusedAssetsUnit(UnityAction callBackAction = null)
{
callBack = callBackAction;
GC.Collect();
asyncOperation = Resources.UnloadUnusedAssets();
}

private void Update()
{
if (asyncOperation == null) return;
if (!asyncOperation.isDone) return;

asyncOperation = null;
callBack?.Invoke();
DestroyImmediate(this);
}
}

AssetBundle 实现运行时资源的动态加载

要想使用 AssetBundle 的方式实现动态资源加载, 首先需要将资源打包成一个 AssetBundle 包, 打包之前要对待打包资源进行依赖设置, 设置依赖的方式有两种:

  • 在 Inspector 面板的最下方设置, 不推荐使用.

    • 选中所有需要打入 AssetBundle 包的资源, 在这些资源的 Inspector 面板中设置 AssetBundle 的名称和后缀名.

    • 对于会被重复依赖的贴图材质等资源需要进行单独的 AssetBundle 名称以及后缀名设置, 不设置的话这些被依赖的资源会被重复打包, 浪费资源.

  • 直接在一个脚本中设置好需要打包的资源以及他们的依赖关系.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var builds = new List<AssetBundleBuild> {
    new AssetBundleBuild() {
    assetBundleName = "prefab",
    assetNames = new[] {
    "Assets/Resources/Materials/1.prefab",
    "Assets/Resources/Materials/2.prefab",
    "Assets/Resources/Materials/3.prefab"
    }
    },
    new AssetBundleBuild() {
    assetBundleName = "material",
    assetNames = new[] {
    "Assets/Resources/Materials/ground.mat"
    }
    }
    };

之后在编辑器模式的脚本中调用构建管线来创建 AssetBundle, 需要的参数有: 输出路径, 压缩选项, 目标平台.

BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows);

下面是推荐的输出路径 outPath 构建代码.

1
2
3
4
5
6
7
8
9
#if UNITY_ANDROID
var outPath = Application.dataPath + "!assets";
#else
var outPath = Application.streamingAssetsPath;
#endif
if (Directory.Exists(outPath)) {
Directory.Delete(outPath, true);
}
Directory.CreateDirectory(outPath);

[] 推荐的代码中使用的路径是 Assets 根目录下的 StreamingAssets 文件夹, 因为这个文件夹中的所有资源都会直接打包并发布, 不会进行任何的压缩以及改变, 适合游戏中长期使用且不随版本变化的资源.

AssetBundle 资源包已经打好了, 那 Unity 怎么读取这种资源包呢?

因为 AssetBundle 包体之间是具有依赖关系的, 因此在读取可能具有依赖关系的资源之前, 应该先将这个包体所依赖的所有包体全部读取出来, 然后再对需要使用的资源进行处理.

假设需要加载一个名称为 abPre 的包体, 那么首先应该读取这个包体的依赖关系, 所有的依赖关系均在 StreamingAssets 这个 AssetBundle 中, 依赖关系的文件名为: AssetBundleManifest.

1
2
3
4
5
6
var assetBundlePath = Application.streamingAssetsPath;
var assetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, "StreamingAssets"));
var manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
foreach (var dependence in manifest.GetAllDependencies("abPre")) {
AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependence));
}

abPre 包体所依赖的包已经全部使用 AssetBundle.LoadFromFile() 加载完毕了, 接下来就可以加载 adPre 包体了, 加载完之后便可以读取 abPre 包体中的资源了. 假设需要读取 adPre 包体中的一个名为 myCube 的预制体, 则:

1
2
var assetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, "abPre"));
var go = assetBundle.LoadAsset<GameObject>("myCube");

之后可以使用 Instantiate(go); 来实例化资源.

使用完之后需要卸载资源, 卸载 AssetBundle 中的资源需要使用 AssetBundle.UnloadAllAssetBundles(false); 方法. 其中的参数 unloadAllObjects:

  • false: 只卸载 AssetBundle 对象, 不卸载资源对象.
  • true: 同时卸载 AssetBundle 对象和资源对象.