小包化 API
Namespace
using StarkMini;
1. 用 address 异步加载单个资源 Asset
异步加载单个 asset
调用:ResLoader.LoadAssetAsync<T>(key)
泛型:T
:需要指定资源类型,例如 GameObject,Sprite,Texture。
参数:key
: asset 配置的 address
返回: handle 结构体
// 加载一个address配置为 "Avatars/hero_1" 的资源
var handle = ResLoader.LoadAssetAsync < GameObject > "Avatars/hero_1";
注:泛型 T 也可以是父类 UnityEngine.Object,但对部分特定类型资源(最常见的就是 Sprite 和 Texture2D),会有可能无法保证得到你真正要使用的类型。因为对于这种资源,可能 bundle 中打入了 2 个或以上不同类型的资源对象,但是同一个 address。
例如:用<UnityEngine.Object>这个父类类型加载一个图片,可能在 Editor 下返回的实际是 Sprite 对象、而在发布的版本运行时返回的是 Texture 对象。
因此:建议尽量指定你确定要使用的类型:例如对于 Sprite, 或 Texture2D (Texture),分别使用 ResLoader.LoadAssetAsync
2.判断、处理异步加载结果
方法 1:可以直接设置 onLoad 回调,处理加载结果。
注:这些回调都是已经成功加载的。
// example 1.1 Prefab GameObject
ResLoader.LoadAssetAsync <
GameObject >
("Avatars/hero_1",
(res) => {
// 使用加载到的 (GameObject) res 对象
var go = Instantiate(res);
});
// example 1.2 AudioClip
ResLoader.LoadAssetAsync <
AudioClip >
("sandloop.wav",
(res) => {
// 使用加载到的 (AudioClip) res 对象
sandLoopSource.clip = res;
});
方法 2:异步 async await 的写法(需要自己判断是否成功)
注:Unity 中 async await Task 类似协程,会保证在主线程运行。(这一点区别于原生的 C#)
注:原 Unity lifecycle 的 Start 等方法,也可以直接改造为 async。
public async void Foo() {
var handle = ResLoader.LoadAssetAsync<GameObject>(key);
// 等待
await handle.Task;
// 判断是否成功
if (handle.Status == AsyncOperationStatus.Succeeded) {
var res = handle.Result;
// 处理成功结果 ...
} else {
// 处理失败
}
}
// 原 Unity lifecycle 的 Start 等方法,也可以直接改造为 async.
public async void Start() {
var handle = ResLoader.LoadAssetAsync<GameObject>(key);
await handle.Task;
// 处理结果 ...
// 继续原同步游戏逻辑 sync logic ...
}
方法 3:异步 Couroutine 协程的写法
注:与 async 类似。
public IEnumerator Foo() {
var handle = ResLoader.LoadAssetAsync<Texture2D>("mytexture");
yield return handle;
// 处理结果 ...
}
3.加载成功后,用 ResCache 同步读取
一个资源加载成功后,可以从资源缓存中同步读取(ResLoader 会缓存已加载对象)。
调用:ResCache.Get<T>(key)
泛型:T
:需要指定资源类型
参数:key
: asset 配置的 address
返回: T 类型的资源对象
public Avatar GetPrefab()
{
return ResCache.Get<Avatar>("Avatars/" + prefabName);
}
说明:对比原 Resources.Load,是相同接口形式:
return Resources.Load < Avatar > "Avatars/" + prefabName;
如果需要判断一个资源是否已经被 cache 缓存,可调用
bool Has<T>(string key)
说明:ResCache.Get 的读取,是依赖于: ResLoader 的大部分 Load 方法,默认用了参数 bool useResCache = true,会在加载成功时同时把该资源引用保存在 ResCache 缓存。
4.用 label 异步加载一批资源 Assets
异步加载一批 assets
调用:ResLoader.LoadAssetsAsync<T>(key)
参数:key
: 配置的一系列资源的 label (打的标签)
返回:handle 结构体
示例代码:
public async void Load() {
var handle = ResLoader.LoadAssetsAsync<UnityEngine.Object>("preload");
// 等待
await handle.Task;
// 判断是否成功、加载结果处理方法,与 LoadAssetAsync 相同 ...
// 区别:返回的 handle.Result 是 IList<T>
var results = handle.Result;
}
void Update() {
// 检查IsValid(),如果handle已释放,会返回false
if (!this.handle.IsValid())
return;
if (this.handle.isDone) {
// 已完成。
} else {
// 获取加载进度的两种方式:
var progress = this.handle.PercentComplete;
var downloadStatus = this.handle.GetDownloadStatus();
}
}
注:自 StarkMini 版本 v1.2 起,LoadAssetsAsync 也统一处理 ResCache 资源缓存。即,加载成功资源后,同样可以从 ResCache 读取。
如果希望 LoadAssetsAsync 不进行资源缓存,传入 bool useResCache = false 即可。
5.通过 handle 获取加载进度
需要注意区分两种方式:
handle.PercentComplete
float PercentComplete // 加载任务的进度百分比,范围 0 ~ 1 的 float, 仅按涉及的bundle数量计算完成度,不精确计算每个bundle的大小。
举例:涉及 9 个文件每个 1 MB、1 个 10 MB 文件时,这个大文件计算 PercentComplete 时占 10%。
handle.GetDownloadStatus()
DownloadStatus GetDownloadStatus() // 下载的进度,会综合计算依赖的每个bundle、及其文件大小,可以进一步取得 Percent 百分比、DownloadedBytes、TotalBytes 字节数。
// 包括属性:
float Percent; // 下载进度百分比,范围 0 ~ 1 的 float
long DownloadedBytes; // 已下载字节数
long TotalBytes; // 总字节数
bool IsDone; // 是否下载完成
举例:涉及 9 个文件每个 1 MB、1 个 10 MB 文件时,这个大文件计算 GetDownloadStatus().Percent 时占 52.6%。
注:按照 Addressable 和 Unity AssetBundle 实现机制,一个加载任务的 PercentComplete 任务进度值,和 bundle 下载状态 GetDownloadStatus() 的 percent 下载进度值, 在任务失败时为 1.0f 是设计如此。
AA、AB 进度百分比值(过程中、完成、失败后)的机制说明:
- 加载的过程中: 任务进度:按 已加载完成(包括失败)的任务数 / 总任务数 计算, 下载进度百分比:按 已下载数据字节数 / 总需要字节数 计算。 如果总分母是 0,视为无需下载,那么百分比为 1.0f。 已下载字节数:按 实际已下载的字节数。
- 加载完成、包括失败时: 任务进度:总是返回 1.0f; 下载进度百分比:总是返回 1.0f; 已下载字节数:总是返回 总需要字节数。
6.多个加载任务的组合
方法 1:在 async 函数、或 coroutine 函数中,调用并等待多个加载任务
注意:需要自己判断每个 handle 是否成功 success
示例代码:
async Task<bool> LoadManyAssets()
{
var isSuccess = true;
// 同时发起加载(底层关联bundle会并发下载)
var handle1 = ResLoader.LoadAssetAsync<Object>(key);
var handle2 = ResLoader.LoadAssetsAsync<Object>(label);
// 等待
await handle1.Task;
await handle2.Task;
// 判断是否成功
isSuccess = isSuccess && handle1.Status == AsyncOperationStatus.Succeeded;
isSuccess = isSuccess && handle2.Status == AsyncOperationStatus.Succeeded;
return isSuccess;
}
方法 2:使用 LoadTaskList
组合多个加载任务:
创建对象:var loadList = LoadTaskList.Create()
方法:Add<T>(handle)
// 添加管理一个 asset 加载任务的 handle
方法:LoadAll()
// 加载所有任务,异步返回 Task
属性:IsDone
// 是否所有加载完成(包括成功或失败)
属性:IsSucceeded
// 是否所有加载任务成功
属性:PercentComplete
// 加载任务进度, 范围为 0 ~ 1 的 float
属性:DownloadStatus GetDownloadStatus()
// 下载的进
示例代码:
async Task<bool> LoadStageAssets() {
var loadList = LoadTaskList.Create();
// 添加 handle
foreach (var data in stageDataList)
{
loadList.Add(data.LoadPrefabHandle());
}
// 添加更多 handle
loadList.Add(ResLoader.LoadAssetAsync<GameObject>("asset_1"));
loadList.Add(ResLoader.LoadAssetAsync<GameObject>("asset_2"));
// 等待全部,返回是否全部成功
var isSuccess = await loadList.LoadAll();
// 也可以通过属性,判断是否全部成功
isSuccess = loadList.IsSucceeded;
return isSuccess;
}
void Update() {
var isDone = loadList.IsDone;
var progress = loadList.PercentComplete;
var downloadStatus = loadList.GetDownloadStatus();
var downloadPercent = downloadStatus.Percent;
}
7.预加载后续资源、队列
预加载后续即将用到的资源,如果多个同时发起,会同时并发下载:
- 调用:
ResLoader.PreDownload
// 只做下载,不处理具体资源,不触发 ResCache 资源缓存 - 参数:
key:
asset group中配置的 label
- 返回:void
也可以使用常规的加载一批资源的方法,但不做等待:
- 调用:
ResLoader.LoadAssets
// 常规加载,会处理具体资源,会触发 ResCache 资源缓存
也可以加入后台队列下载资源,依次排队下载,而不会多个并发:
- 调用:
BackgroundLoader.AddDownloadOnly
接口 // 只做下载
BackgroundLoader.AddAssets<T>
接口 // 会处理具体资源、和 ResCache
- 参数:
key
: asset group 中配置的 label - 返回:void
示例代码:
// 预加载。多个同时发起,会同时并发下载
ResLoader.PreDownload("p1_fonts");
ResLoader.PreDownload("p1_levels");
ResLoader.PreDownload("p1_scenes");
// 调用常规加载,但不做等待
ResLoader.LoadAssets < AudioClip > "g1_sounds";
// 后台加载队列
BackgroundLoader.Start();
BackgroundLoader.AddDownloadOnly("p2_icons");
BackgroundLoader.AddDownloadOnly("p2_levels");
BackgroundLoader.AddAssets < AudioClip > "p2_sounds";
预加载,如果需要等待其完成、执行相应逻辑:
调用: ResLoader.PreDownloadAsync
// 只做下载,不处理具体资源,不触发 ResCache 资源缓存
参数:key: asset group
中配置的 label
返回:Task<bool>
,携带 (bool) isSuccess 的 Result
调用: LoadAssetsAsync<T>
// 常规加载,会处理具体资源,会触发 ResCache 资源缓存
参数:key:
配置的一系列资源的 label (打的标签)
返回:handle 结构体
示例代码:
var isSuccess = await ResLoader.PreDownloadAsync(key);
var handle = ResLoader.LoadAssetsAsync < TObject > key;
await handle.Task;
8.后台下载所有资源 DownloadAll
注:自版本 1.4.0
起支持
DownloadAll 方法:DownloadAllInTheBackground
// 后台下载全部addressable内容,这个接口是启动空闲下载,所有的key是chain的方式一个接一个下载,且只在空闲时下载
Addressables.DownloadAllInTheBackground();
// 参数 `maxActiveWebRequest` 最大允许的并发连接数,只有小于这个连接数才继续下载</param>
const int maxActiveWebRequest = 3;
Addressables.DownloadAllInTheBackground(maxActiveWebRequest);
(注:此功能实验中、改进中)
9.异步加载场景 Scene,和失败重试
调用 ResLoader.LoadSceneAsync
接口
参数:key
: address
参数:mode
: 场景模式,枚举:single, additive
var handle = ResLoader.LoadSceneAsync("battle_scene", LoadSceneMode.Single);
var handle = ResLoader.LoadSceneAsync("result_dialog", LoadSceneMode.Additive);
示例代码 - async 方式:
async void LoadSceneAddressable(string key, LoadSceneMode mode) {
bool isSuccess = false;
bool loop = true;
while (!isSuccess && loop) {
var handle = ResLoader.LoadSceneAsync(scene.ToString(), mode);
await handle.Task;
isSuccess = handle.Status == AsyncOperationStatus.Succeeded;
if (!isSuccess) {
// load error
Debug.LogWarning($"LoadScene - fail! \"{scene.ToString()}\"");
// 错误处理、弱网兜底重试:
// 可选a. 可以做循环重试的间隔 delay in millisecond between retries
await Task.Delay(retryDelayMs);
// 可选b. 可以做循环自定义错误弹框
m_isErrorConfirmed = false;
InvokeNetErrorPopup(OnNetErrorConfirm);
while (!m_isErrorConfirmed) {
await Task.Yield();
}
// 可选c. 可以做等待弹窗按钮确认:“加载失败,请检查网络” “重试”
await LoadErrorPopupView.PopupAsync(LoadErrorPopupView.ErrorMsg, LoadErrorPopupView.RetryText);
}
loop = !isSuccess && Application.isPlaying;
}
// load success
}
private bool m_isErrorConfirmed = false;
void OnNetErrorConfirm() {
m_isErrorConfirmed = true;
}
注:这个 LoadErrorPopupView 界面所用的资源在Packages/com.bytedance.starkminiunity/Runtime/Examples/UI/StarkMiniUICanvas.prefab
, 需要将这个 prefab 拖动到自己工程的 Scene 中,它会保持单例 DontDestroyOnLoad
示例代码(Coroutine 方式):
IEnumerator LoadSceneAddressable(GameScenes scene, LoadSceneMode mode) {
bool isSuccess = false;
bool loop = true;
while (!isSuccess && loop) {
var handle = ResLoader.LoadSceneAsync(scene.ToString(), mode);
// 注意: yield return handle 必需被执行在 MonoBehaviour 的 StartCoroutine() 中,才能保证正确等待。
if (!handle.IsDone) {
yield return handle;
}
// 注: 也可以用 while 循环判断 !IsDone
while (!handle.IsDone) {
yield return null;
}
isSuccess = handle.Status == AsyncOperationStatus.Succeeded;
if (!isSuccess) {
Debug.LogWarning($"LoadScene - fail! \"{scene.ToString()}\"");
{
// 同上 async方式,错误处理、弱网兜底重试
// ...
}
}
loop = !isSuccess && Application.isPlaying;
}
// load success
}
检查工程的 Scenes in build 设置,应当这些转为 addressable 的 scene 都已经自动去除勾选。
说明:如何卸载 AA 场景
被动卸载、自动卸载的情况:如果加载的场景是 single 模式,那么新的场景加载时,原场景就会自动被卸载。
主动卸载、手动卸载,两种接口都可以:
用 UnityEngine 传统的 SceneManager 来卸载场景,会自动兼容处理 AA 场景
SceneManager.UnloadSceneAsync(...)
用 Addressables.UnloadSceneAsync 接口来卸载场景
var unloadHandle = Addressables.UnloadSceneAsync(currentScene);
// 返回类型为:AsyncOperationHandle<SceneInstance> 卸载场景操作的Handle
// 视情况不等待,或异步等待 unloadHandle
// a. async 方式
await handle.Task;
// b. coroutine 方式
yield return handle;
10.方法:AssetReference 与弱引用
AssetReference
是一种 Addressable 中提供的类,用于方便引用一个 Addressable 的 Asset。
特点:
- 是一个弱引用,不会产生强引用的依赖关系。
- 用于 Editor 下设置引用关系,Build 时弱引用不会依赖打包到一起,Runtime 运行时做动态加载。
- 非常适合解决一些资源引用依赖关系会导致 bundle
基本用法:
声明成员变量代码
public AssetReference assetRef;
编辑器 Inspector,指向引用的 asset 资源
示例代码:运行时加载
// 方法1 用ResLoader的static方法
var handle = ResLoader.LoadAssetAsync<ScriptableObject>(assetRef);
// 方法2 用AssetReference的成员方法
var handle = assetRef.LoadAssetAsync<ScriptableObject>();
常见强引用问题和弱引用解决方法:
参见:
11.动态加载特定资源类型
示例:动态加载界面 UI Sprite 图片
示例代码:
public UnityEngine.UI.Image skinImage;
public string id;
void Show()
{
ResLoader.LoadUISpriteAsync(skinImage, "Skins/" + id);
}
示例:动态加载和播放声音
注:既然做动态加载,一般相应地把 Scene、Prefab 中本来直接引用的 AudioClip 去除置空。
示例代码:
void PlaySe(string se)
{
var sound = ResCache.Get<AudioClip>("Sound/" + se);
if (sound != null) {
// 已加载过,有缓存,设置clip,直接播放
audioSource.clip = res;
audioSource.Play();
} else {
// 没有缓存,只做加载
ResLoader.LoadAssetAsync<AudioClip>("sound_effect_key");
}
}
void InitMusic()
{
ResLoader.LoadAssetAsync<AudioClip>("music_key", (res) => {
// 加载成功,设置clip
musicSource.clip = res;
// 自定义逻辑判断是否要播放
if (!musicSource.isPlaying && Setting.MusicOn) {
musicSource.Play();
}
});
}
示例:动态加载字体
注:既然做动态加载,一般相应地把 Scene、Prefab 中本来直接引用的 Font 去除置空。
示例代码:
private Font m_font;
public Font FontRes => m_font;
async void LoadFont(string path) {
var isSuccess = false;
// 此例做了循环重试
var loop = true;
while (loop) {
var handle = ResLoader.LoadAssetAsync<Font>(path);
await handle.Task;
isSuccess = handle.IsValid() && handle.Status == AsyncOperationStatus.Succeeded;
if (isSuccess) {
Debug.Log($"LoadFont success, path: {path}");
m_font = handle.Result;
} else {
Debug.Log($"LoadFont wait to retry...");
await Task.Delay(3000);
}
loop = !isSuccess && Application.isPlaying;
}
}
12.特殊格式 Asset 的 AA 化
有些特殊类型资源,不是 Unity native 处理的类型,通常需要专门的第三方库去加载使用。
举例:常见的这种特殊格式 Asset:
- *.db - SQLite 数据库文件
- bnk, .wem - WWise 声音资源文件
对这种资源 AA 化、设置为 Addressable、加入到 Group 时,通常需要做一些特殊处理:
参见:
13.资源释放、卸载
资源释放、卸载,一般通过这几个方法:
AA 场景卸载:
1)自动卸载,在其他场景的加载导致 AA 场景被替换时,底层自动卸载相关 Unity 资源、AB 资源。
2)主动卸载,有 Addressables 的 UnloadSceneAsync 方法。(一般只用于 Additive 的 scene)
AA 资源释放:有 Addressables 的 Release 方法 (参数为 asset 的 Object, 或 Handle)。内部有引用计数机制,计数归零时底层会执行 AssetBundle.Unload(true)会释放其 AB 和资源。
(适用于绝大部分 ResLoader 或 Addressables 加载得到的资源、Handle)
ResLoader 预加载、预下载:
1)ResLoader.PreLoadAssets 预加载的,有 ReleaseAllPreLoadDownloadHandles 方法。
2)ResLoader.DownloadAsync 下载的,有 ReleaseAllSavedDownloadHandles 方法。(仅对于参数为 bool autoReleaseHandle = false 常驻保存的)
LoadTaskList (组合多个加载任务)加载的,有 ReleaseHandles() 方法。
ResCache 类(配合 ResLoader 的一些加载方法,做资源缓存的),有 ClearAll() 方法,清空所有缓存。
注:ResCache 类只保存加载后资源对象的引用变量,不做实际的 Unity 资源、AB 资源 的管理,不参与加载或卸载、释放。
强制 Unity 资源释放: 有 Resources.UnloadUnusedAssets 方法。
(一般只适用于场景切换、Loading 等画面中)
强制 AB 资源释放: 有 UnloadAllAssetBundles(bool unloadAllObjects) 方法。
(一般只适用于场景切换、Loading 等画面中)