最近用Unity做项目时遇到了需要动态加载资源的场景,所谓动态加载就是指用AB包或者Resouces.Load()
之类的方法在运行时动态地加载资源(比如模型,贴图,音频,数据表等)。与之对应的就是静态加载,就是直接把资源拖到Inspector面板里,然后直接使用对应的成员变量就好了。
Resouces.Load()
其实已经不建议使用了,因为打包时无法区分哪些文件需要打包进去,哪些文件不需要,到头来只能一股脑全打包进去,导致最终构建大小变得特别大。官方可能(仅仅是有可能而已)会在以后移除这个API。对于新的项目来说,建议使用纯AssetBundle的加载方案。
先介绍一下AssetBundle是什么吧。AssetBundle也叫AB包,作用很简单,就是把游戏资源(模型,图片,音频等)打包成一个单独的文件,然后在游戏的运行时去读取这个AB包文件,并加载里面的模型等资源。可以从网络加载,也可以从本地文件加载。非常适合用来打包一些游戏里经常会变的东西,比如游戏新闻或者活动内容。内容可以实时更新,而不需要让玩家重新下载一遍新版本的游戏客户端。
AB包可以把它看成是一个zip包,里面有很多被压缩好的游戏资源,当AB包被加载时,程序会自动从里面读取需要的东西。当然这样描述并不准确,仅仅是为了易于理解而已。
加载一个游戏资源实际上分两步。1.加载AssetBundle本身,2.加载AssetBundle里的游戏资源。
也就是说在AssetBundle本身加载完成之后,还需要继续加载AssetBundle里面的资源文件,里面的资源文件才是我们最终需要的东西。
AssetBundle主要的加载源一个是本地文件,一个是HTTP网络加载。既然是IO相关的逻辑,那么一定会涉及到阻塞和非阻塞的概念,我这里选用协程+非阻塞方式从本地文件进行加载。
首先写一个IEnumerator LoadAsset()
协程方法,此方法接收两个形参,一个是AssetBundle的文件路径,一个是AssetBundle里具体要加载的资源的名字,先写一个空方法,然后一步步加载资源。
IEnumerator LoadAsset(string assetBundlePath, string assetName)
{
}
首先需要加载AssetBundle到内存里,可以使用APIAssetBundle.LoadFromFileAsync()
来从文件异步加载。发起加载请求之后,只需要等待协程完成就可以了。
IEnumerator LoadAsset(string assetBundlePath, string assetName)
{
// 从文件加载AssetBundle
var loadRequest = AssetBundle.LoadFromFileAsync(assetBundlePath);
yield return loadRequest;
}
除了傻乎乎的等以外,还可以获取加载进度,可以做成一个进度条告诉玩家当前加载了百分之多少了。这样要比直接显示“加载中”这几个字的用户体验要好得多。当然,记得加载完成后,别忘了做一下错误处理。
IEnumerator LoadAsset(string assetBundlePath, string assetName)
{
var loadRequest = AssetBundle.LoadFromFileAsync(assetBundlePath);
// 加载时顺便更新进度条,告诉玩家当前加载进度
while (!loadRequest.isDone)
{
float progress = loadRequest.progress;
ui.text = $"游戏正在加载中 ({(int) (progress * 100)}%)";
yield return new WaitForSeconds(1);
}
// 检查AssetBundle是否加载成功了
if (loadRequest.assetBundle == null)
throw new AssetBundleFailedToBeLoadedException("AssetBundle加载失败");
}
AssetBundle加载好了以后,接着就可以从AssetBundle里加载需要的那个Asset了。加载Asset和加载AssetBundle大同小异,只需要调用AssetBundle类的LoadAssetAsync()
方法就好了,此方法输入一个字符串,是具体的资源的名字。
// 从AssetBundle里加载需要的Asset(也就是游戏资源)
var assetLoadRequest = assetBundle.LoadAssetAsync(assetName);
yield return assetLoadRequest;
// 检查Asset是否加载成功了
if (assetLoadRequest.asset == null)
throw new AssetFailedToBeLoadedException("Asset加载失败");
// 获取到最终加载好的资源
Object asset = assetLoadRequest.asset;
Asset加载完毕后,就可以获取到最终的Asset对象了。也就是游戏资源对象,这个游戏对象可能是一个预制体GameObject,也可能是一个贴图Texture。只需要将这个asset
对象返回给调用LoadAsset()
方法的对象就好了。这样就实现了AssetBundle和Asset在同一个协程里加载。
当然在实际的项目中需要考虑更多的事情,比如一个AssetBundle加载到内存里以后,就不能再重复加载一遍了。这就需要将加载好的AssetBundle对象缓存起来,等到再次需要读取这个AssetBundle里的资源时,就先查询缓存,缓存没有再去真正地加载AssetBundle,如果缓存里有则直接使用。对于Asset也是同样的道理,不能多次加载,也要最对应的缓存机制。
不仅如此,还需要考虑同时对一个AssetBundle发起两个并发加载请求时,只能有其中一个协程进行实际加载,另一个协程需要进行等待。这个场景在开发中很常见,比如游戏初始化阶段时,可能背包类和菜单类就会同时加载同一个AssetBundle里的同一个Asset(一般是窗口背景贴图之类的共用资源),如果不进行并发处理,就会导致重复加载而报错。
这些稳健性相关的代码每个人都有自己的实现风格,并没有特别统一的规范。这里列举一下我自己写的AssetBundle加载管理类吧,可以供大家参考。(当然直接复制使用也是没有问题的)
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
/// <summary>
/// 负责AB包和包内资源的加载和管理的类
/// </summary>
public class AssetBundleManager : MonoBehaviour
{
// 单例实例
public static AssetBundleManager ins;
// 从哪个目录加载AssetBundle
static readonly string assetBundleFolderPath = Path.Combine(Application.streamingAssetsPath, "AssetBundles/StandaloneWindows");
[SerializeField]
private SerializableMap<string, StatableAssetBundle> assetBundlesLoaded = new(false);
[SerializeField]
private SerializableMap<string, StatableAsset> assetsLoaded = new(false);
void Awake()
{
ins = this;
}
public LoadAssetstruction<TAsset> GetAsset<TAsset>(string assetBundlePath, string assetName) where TAsset : Object
{
return new LoadAssetstruction<TAsset>(this, assetBundlePath, assetName);
}
StatableAssetBundle GetAssetBundleInCache(string assetBundlePath)
{
return assetBundlesLoaded.ContainsKey(assetBundlePath)
? assetBundlesLoaded[assetBundlePath] : null;
}
StatableAsset GetAssetInCache(string assetBundlePath, string assetName)
{
string cacheKey = GetCacheKey(assetBundlePath, assetName);
return GetAssetBundleInCache(assetBundlePath) == null
? null : (assetsLoaded.ContainsKey(cacheKey) ? assetsLoaded[cacheKey] : null);
}
void RegisterAssetBundleLoading(string assetBundlePath)
{
assetBundlesLoaded[assetBundlePath] = new StatableAssetBundle();
}
void RegisterAssetLoading(string assetBundlePath, string assetName)
{
assetsLoaded[GetCacheKey(assetBundlePath, assetName)] = new StatableAsset();
}
void CacheAssetBundleLoaded(string assetBundlePath, AssetBundle assetBundle)
{
StatableAssetBundle sab = GetAssetBundleInCache(assetBundlePath);
sab.FinishLoad(assetBundle);
}
void CacheAssetLoaded(string assetBundlePath, string assetName, Object asset)
{
StatableAsset sa = GetAssetInCache(assetBundlePath, assetName);
sa.FinishLoad(asset);
}
static string GetAssetBundleAbsolutePath(string path)
{
return Path.Combine(assetBundleFolderPath, path);
}
string GetCacheKey(string assetBundlePath, string assetName)
{
return $"{assetBundlePath}|{assetName}";
}
public class LoadAssetstruction<TAsset> : CustomCoroutineInstruction where TAsset : Object
{
public TAsset asset = null;
public string assetBundlePath;
public string assetName;
AssetBundleManager parent;
AssetBundle assetBundle = null;
public LoadAssetstruction(AssetBundleManager parent, string assetBundlePath, string assetName)
{
this.assetBundlePath = assetBundlePath;
this.assetName = assetName;
this.parent = parent;
}
public override IEnumerator Coroutine()
{
yield return MakesureAssetBundleLoaded();
yield return MakesureAssetLoaded();
}
IEnumerator MakesureAssetBundleLoaded()
{
var sab = parent.GetAssetBundleInCache(assetBundlePath);
// AssetBundle还未开始加载
if (sab == null)
{
parent.RegisterAssetBundleLoading(assetBundlePath);
string absolutePath = GetAssetBundleAbsolutePath(assetBundlePath);
var loadRequest = AssetBundle.LoadFromFileAsync(absolutePath);
yield return loadRequest;
if (loadRequest.assetBundle == null)
throw new AssetBundleFailedToBeLoadedException(absolutePath);
parent.CacheAssetBundleLoaded(assetBundlePath, loadRequest.assetBundle);
assetBundle = loadRequest.assetBundle;
} else if (sab.isLoading) { // AssetBundle已经在加载过程中
// 等待加载完成
yield return new WaitWhile(() => sab.isLoading);
assetBundle = sab.assetBundle;
} else { // AssetBundle已经在缓存里
assetBundle = sab.assetBundle;
}
}
IEnumerator MakesureAssetLoaded()
{
var sa = parent.GetAssetInCache(assetBundlePath, assetName);
// Asset还未开始加载
if (sa == null)
{
parent.RegisterAssetLoading(assetBundlePath, assetName);
var loadRequest = assetBundle.LoadAssetAsync<TAsset>(assetName);
yield return loadRequest;
if (loadRequest.asset == null)
throw new AssetFailedToBeLoadedException(assetBundlePath, assetName);
parent.CacheAssetLoaded(assetBundlePath, assetName, loadRequest.asset);
asset = loadRequest.asset as TAsset;
} else if (sa.isLoading) { // Asset已经在加载过程中
// 等待加载完成
yield return new WaitWhile(() => sa.isLoading);
asset = (TAsset) sa.asset;
} else { // Asset已经在缓存里
asset = (TAsset) sa.asset;
}
}
public class AssetBundleFailedToBeLoadedException : Exception
{
public AssetBundleFailedToBeLoadedException(string path)
: base($"failed to load the AssetBundle ({path})") { }
}
public class AssetFailedToBeLoadedException : Exception
{
public AssetFailedToBeLoadedException(string assetBundle, string asset)
: base($"failed to load a Asset ({asset}) in AssetBundle ({assetBundle})") { }
}
}
[Serializable]
public class StatableAssetBundle : ISerializationCallbackReceiver
{
public bool isLoading { get; private set; } = true;
public AssetBundle assetBundle { get; private set; } = null;
public void FinishLoad(AssetBundle assetBundleLoaded)
{
isLoading = false;
assetBundle = assetBundleLoaded;
}
[SerializeField]
string serialization;
public void OnBeforeSerialize()
{
serialization = assetBundle.name + (isLoading ? " (Loading)" : "");
}
public void OnAfterDeserialize() { }
}
[Serializable]
public class StatableAsset : ISerializationCallbackReceiver
{
public bool isLoading { get; private set; } = true;
public Object asset { get; private set; }
public void FinishLoad(Object assetLoaded)
{
isLoading = false;
asset = assetLoaded;
}
[SerializeField]
Object serialization;
public void OnBeforeSerialize() { serialization = asset; }
public void OnAfterDeserialize() { }
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(AssetBundleManager.StatableAssetBundle))]
public class StatableAssetBundleDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var assetBundleName = property.FindPropertyRelative("serialization");
EditorGUI.LabelField(position, new GUIContent(assetBundleName.stringValue));
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 18;
}
}
[CustomPropertyDrawer(typeof(AssetBundleManager.StatableAsset))]
public class StatableAssetDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var asset = property.FindPropertyRelative("serialization");
if (asset.objectReferenceValue != null)
{
EditorGUI.ObjectField(position, asset, GUIContent.none);
} else {
EditorGUI.LabelField(position, new GUIContent("Loading"));
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 18;
}
}
#endif
如果需要直接复制使用的话,还需要顺带复制一个我自己写的使用类,SerializeMap
,这个类是一个可序列化版本的Dictionary
字典类。主要用于在Inspector面板调试使用。能写注释的地方,我会尽量写上详细的注释。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#pragma warning disable 414
/// <summary>
/// SerializableSDictionary,可序列化的Dictionary类
/// </summary>
[Serializable]
public class SerializableMap<TKey, TValue> : Dictionary<TKey, TValue>, ISerializationCallbackReceiver
{
/// <summary>
/// 是否允许在Inspector里编辑Map的内容<br/>
/// (此变量可以从Inspector里修改)
/// </summary>
[SerializeField]
bool editable = false;
/// <summary>
/// 是否禁止在Inspector里修改'editable'字段(俗称锁定可编辑状态)<br/>
/// (此变量无法从Inspector里修改,只能由代码修改)
/// </summary>
[SerializeField]
bool lockEditableState = true;
/// <summary>
/// 用于存储Dictionary数据的列表
/// </summary>
[SerializeField]
List<Pair> pairs = new List<Pair>();
public SerializableMap(bool? forceEditableState)
{
if (forceEditableState != null)
{
lockEditableState = true;
editable = forceEditableState.Value;
}
}
public void OnAfterDeserialize()
{
if (!editable)
return;
Clear();
foreach (var el in pairs)
Add(el.key, el.value);
}
public void OnBeforeSerialize()
{
pairs.Clear();
foreach (var kv in this)
pairs.Add(new Pair { key = kv.Key, value = kv.Value });
}
[Serializable]
public struct Pair
{
public TKey key;
public TValue value;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SerializableMap<,>.Pair))]
public class PairDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var key = property.FindPropertyRelative("key");
var value = property.FindPropertyRelative("value");
var (keyRect, valueRect) = position.Indent(0, 0, 0, 2).Split(position.width * 0.45f, 4, false);
EditorGUI.PropertyField(keyRect, key, GUIContent.none);
EditorGUI.PropertyField(valueRect, value, GUIContent.none);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var key = EditorGUI.GetPropertyHeight(property.FindPropertyRelative("key"), true);
var value = EditorGUI.GetPropertyHeight(property.FindPropertyRelative("value"), true);
return Mathf.Max(key, value);
}
}
#endif
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SerializableMap<,>))]
public class SerializableMapDrawer : PropertyDrawer
{
protected const float IndentPixel = 15;
const float HeaderHeight = 18;
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
{
var stateLock = property.FindPropertyRelative("lockEditableState");
var editable = property.FindPropertyRelative("editable");
var pairs = property.FindPropertyRelative("pairs");
Rect mainRect;
Rect sideRect;
if (!stateLock.boolValue)
{
(mainRect, sideRect) = rect.Split(rect.width - 18, 2, false);
} else {
mainRect = rect;
sideRect = Rect.zero;
}
// 绘制Dictionary内容
var fieldLabel = new GUIContent(
label.text,
editable.boolValue || !pairs.isExpanded ? null : Icons.NotEditable.Value,
label.tooltip
);
EditorGUI.PropertyField(mainRect, pairs, fieldLabel, true);
// 绘制editable按钮
if (!stateLock.boolValue)
{
Vector2 pivot = sideRect.position;
float angle = 90;
GUIUtility.RotateAroundPivot(angle, pivot);
{
var rotated = sideRect.Transpose();
rotated.y -= 18;
editable.boolValue = GUI.Toggle(rotated, editable.boolValue, new GUIContent("Editable", "Editable In Inspector"));
}
GUIUtility.RotateAroundPivot(-angle, pivot);
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var pairs = property.FindPropertyRelative("pairs");
return EditorGUI.GetPropertyHeight(pairs);
}
}
#endif