(在这里强烈推荐大家使用这套系统,内存掌控必备,方便、简洁、简单;Unity 官方也会逐步将原本的资源加载方式迁移到Addressable Asset System 上来,以下会做一些数据对比和简单的分析)
Addressable Asset System是Unity提供的一种易于通过地址来加载资源的一种资源加载方式,使用这套系统可以很轻松的创建和部署你游戏中的资源。使用这套系统你可以使用异步的方式来加载你部署在任意位置的任意有资源依赖集合的受支持的资源。不论你是用直接引用的方式还是传统的Assetbundle或者是Resource文件夹来加载资源。Addressable Assets都提供了一种更加简单的方式来使你的游戏的资源管理和加载方式更方便和高效。 。(以下为了简单简称UAAS)
介绍用法之前,我们首先来对比一下使用传统资源加载和管理的方式和使用Addressable管理资源加载的方式做一个数据对比。
测试Demo:游戏中的切换天空盒的功能,我们有6中HDR的天空盒,可在游戏运行时供用户动态切换。
public class TraditionSkyBoxSwitch : MonoBehaviour
{
public List<Material> materialList;
public void SwitchSkyBoxByIndex(int index)
{
RenderSettings.skybox = materialList[index];
}
}
从图中可以看出,使用这种方式,即使我们没有同时使用六个天空盒材质,但是,Unity还是为这六个材质使用的Cubemap都分配了较大的内存空间。如果我们的需求是在玩家游戏过程中更换多个占用内存更大的图集呢?显然使用这种方式会导致用户机器的内存被迅速占满,导致游戏卡顿。
public class AddressableSkyBoxSwitch : MonoBehaviour
{
public List<AssetReference> materialList;
private AsyncOperationHandle _asyncOperationHandle;
public void SwitchSkyBoxByIndex(int index)
{
if (_asyncOperationHandle.IsValid())
{
Addressables.Release(_asyncOperationHandle);
}
_asyncOperationHandle = materialList[index].LoadAssetAsync<Material>();
_asyncOperationHandle.Completed += (mat) =>
{
RenderSettings.skybox = (Material) mat.Result;
};
}
可以看出,并没有关于指定Cubemap的内存分配。
而使用UAAS加载资源的方式,你可以将你需要动态加载的资源在编辑器内通过标记的形式表明这个资源时可用通过查找地址的形式来进行索引和加载的,UAAS会为你处理各个资源之间的相互依赖问题。
创建一个Cube和Sphere预制体,并在Inspector面板中将其标记为可寻址的,并修改与之对应的默认名称为Cube和Sphere,修改好之后,可在Windows>Asset Management>Addressables面板中查看。
Play Mode Script
Fast Mode 开发时选择此模式,无需打包资源,即可测试
Virtual Mode 模拟模式
Packed Play Mode 打包资源模式,需构建资源,从生产环境中加载资源
void Update(){
if (Input.GetKeyDown(KeyCode.C))
{
//加载资源
Addressables.LoadAssetAsync<GameObject>("Cube").Completed += (cube) =>
{
if (cube.IsValid())
{
//实例化对象
var asynOperation = Addressables.InstantiateAsync("Cube");
asynOperation.Completed += (_) =>
{
//释放对象
StartCoroutine(ReleaseInstance(asynOperation));
};
}
};
}
if (Input.GetKeyDown(KeyCode.S))
{
//加载资源
Addressables.LoadAssetAsync<GameObject>("Sphere").Completed += (sphere) =>
{
if (sphere.IsValid())
{
//实例化对象
var asyncOperation = Addressables.InstantiateAsync("Sphere");
asyncOperation.Completed += (_) =>
{
//释放对象
StartCoroutine(ReleaseInstance(asyncOperation));
};
}
};
}
}
//使用协程模拟释放对象
private IEnumerator ReleaseInstance(AsyncOperationHandle<GameObject> asyncOperationHandle)
{
yield return new WaitForSeconds(5);
//释放刚才创建的实例对象
Addressables.ReleaseInstance(asyncOperationHandle);
}
AssetReference类提供了一种无须知道资源地址即可访问可寻址资源的方式,在脚本中声明一个AssetReference类的实例,然后在Inspector面板中选择一个你需要实例化的资源,指定给这个实例。
public AssetReference assetReference;
private void Start()
{
//异步加载资源
assetReference.LoadAssetAsync<GameObject>()
.Completed += (asset) =>
{
//验证资源是否有效
if (asset.IsValid())
{
//异步实例化资源
assetReference.InstantiateAsync();
}
};
}
代码如上所示。
public AssetReference assetReference;
private void Start()
{
var cubeHandle = Addressables.LoadAssetAsync<GameObject>(assetReference);
cubeHandle.Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded && handle.IsValid())
{
GameObject obj = Addressables.InstantiateAsync(assetReference).Result;
Debug.Log(obj);
}
};
}
或者可以使用:
public AssetReference assetReference;
private void Start()
{
var cubeHandle = assetReference.LoadAssetAsync<GameObject>();
cubeHandle.Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded && handle.IsValid())
{
GameObject obj = assetReference.InstantiateAsync(Vector3.zero,Quaternion.identity,parent:null).Result;
Debug.Log(obj);
}
};
}
public IEnumerator Start()
{
AsyncOperationHandle<GameObject> loadAssetHandle = Addressables.LoadAssetAsync<GameObject>("Cube");
yield return loadAssetHandle;
if (loadAssetHandle.Status == AsyncOperationStatus.Succeeded
&& loadAssetHandle.IsValid())
{
var instantiateHandle = Addressables.InstantiateAsync("Cube");
//实例化这个资源
GameObject obj = instantiateHandle.Result;
//当资源被成功使用或者实例化之后,释放加载资源的操作
//这里并不是Destroy对象,只是释放资源的加载操作loadAssetHandle
Addressables.Release(loadAssetHandle);
//如果要释放实例化后的对象,使用如下方式
//StartCoroutine(ReleaseInstance(instantiateHandle));
}
}
private IEnumerator ReleaseInstance(AsyncOperationHandle<GameObject> asyncOperationHandle )
{
//Debug.Log("开始");
yield return new WaitForSeconds(2);
Addressables.ReleaseInstance(asyncOperationHandle);
}
public class AddressableSkyBoxSwitch : MonoBehaviour
{
private async void Start()
{
AsyncOperationHandle<GameObject> loadAssetHandle = Addressables.LoadAssetAsync<GameObject>("Cube");
//等待资源加载完成
var loadAssetTask=await loadAssetHandle.Task;
//实例化资源
AsyncOperationHandle<GameObject> instantiateHandle = Addressables.InstantiateAsync("Cube");
//等待资源实例化完成
var instantiateTask = await instantiateHandle.Task;
//释放资源的加载操作
Addressables.Release(loadAssetHandle);
//实例化完成之后执行一些其它操作,比如3秒之后销毁之类的
await Task.Delay(3000);
//销毁实例
Addressables.Release(instantiateTask);
}
}
当时用Addressable Asset时,确保资源的正确加载与卸载是管理内存的主要方式。如何加载资源的依赖,取决于你使用的加载方式。但是,在任何情况下,Release方法可以获得已经被加载的资源,也可以返回加载资源的操作句柄(operation handle)。例如,在加载Scene时,会为这个加载过程返回一个AsyncOperationHandle.你可以通过这个Handle来释放资源,或者获得handle.Result.
使用Addresssable.LoadAssetAsync或者Addressable.LoadAssetAsync来加载资源。
这个操作仅仅是将资源加载到内存,并不会将资源实例化到场景中。每次执行加载调用时,每个被加载资源的引用计数器(ref-count)会+1,如果你使用同一个地址调用LoadAssetAsync三次,你讲会获得三个不同的AsyncOperationHandle结构体(源代码中将其定义为struct,参看下方),引用均为同一个本地资源体,同时相应的引用计数器也会为3.如果资源被加载成功,加载的结果被赋予AsyncOperationHandle的.Result属性。你可以使用Unity内建(built-in)的方法将其实例化到场景中(GameObject.Instantiate),但是这样不会使引用计数器有自曾操作。
public struct AsyncOperationHandle<TObject> : IEnumerator, IEquatable<AsyncOperationHandle<TObject>>{}
public struct AsyncOperationHandle : IEnumerator{}
如果要卸载资源,请使用Addressable.Release方法,它会使引用计数器递减。当给定的Asset的引用计数器为0时,这这个Asset就可以被卸载了,同时也会使任何依赖于这个Asset的资源的引用计数器减少。
Note: 资源的卸载是不是立即执行,取决于是否存在其它资源对其的依赖。
如果要加载一个Scene,s使用Addressable.LoadSceneAsync方法。在Single模式下,你可以使用这个方法加载一个场景(关闭所有已打开的场景),或者以Additive模式加载场景。
如果要卸载场景,使用Addressables.UnloadSceneAsync方法,或者在Single模式下打开一个新的场景,原场景会被自动卸载。你可以使用Addressables的接口打开一个新的场景,也可以使用SceneManager.LoadScene或者SceneManager.LoadSceneAsync方法。打开一个新的场景会关闭当前打开的场景,且引用计数器也会自减。
要加载和实例化一个GameObject Asset,使用Addressables.InstantiateAsync方法。这会实例化位于location参数指定的位置的Prefab.Addressable Asset System会加载Prefab和它的依赖,并增加所有相关Asset的引用计数器。
对同一个地址调用三次InstantiateAsync会导致所有相关Asset的引用计数器+3。与LoadAssetAsync所不同的是,每次调用InstantiateAsync都会返回一个指向唯一操作的AsyncOperationHandle。这是因为每次调用InstantiateAsync的返回结果都是一个唯一的实例。InstantiateAsync与其它加载方式的区别在于,有一个可选trackHandle参数,当设置trackHandle参数为false时,你必须要保留使用的AsyncOperationHandle才能释放实例。这样效率更高,但是会增加代码量。
要Destroy实例化的GameObject,使用Addressable.ReleaseInstance方法,或者关闭包含当前实例化对象的Scene.Scene可以以Additive或者Single模式加载。已经被加载的场景也可以使用Addressables或者SceneManagment API加载。如上所述,如果你设置trackHandle为false,你只能使用持有Addressable.ReleaseInstance句柄的方法来释放资源,而不能使用GameObject.Destroy方法。
Note:如果你使用Addressable.ReleaseInstance释放一个不是使用Addressables API创建的实例,或者使用Addressables.InstantiateAsync创建实例时可选参数设置为trackHandle=false(默认为true)创建的实例。系统会将其识别为错误,并表示该方法无法释放指定的实例。在这种情况下,当前实例不能被销毁。
Addressable.InstantiateAsync会产生一些性能开销,因此,如果你需要没一帧实例化数百个相同的对象。你可以考虑使用Addressables API 加载资源,然后使用其它方式实例化。在这种情况下,你可以调用Addressables.LoadAssetAsync方法,将返回的Result保存,使用GameObject.Instantiate()方法将其实例化;这样你就可以灵活的使用同步的方式实例化资源。缺点就是。Addressable Asset System 不知道创建了多少个实例,如果管理不当,很容易出现内存问题。例如,引用纹理的Prefab将不再具有可供引用的有效加载纹理,从而产生渲染问题,这种类型的问题很难追踪,因为内存的卸载时机取决于你的资源引用方式,并不是立即就会被触发的。
不需要释放AsyncOperationHandle.Result的操作句柄,但是需要释放AsyncOperationHandle本身。如以下:Addressable.LoadResourceLocationsAsync和Addressable.GetDownloadSizeAsync.你可以访问它们加载的数据在被释放前。其使用Addressable.Release()释放。
有一些AsyncOperation.Result不返回任何内容的操作具有一个可选参数,如果你希望在操作完成之后不再持有这些操作句柄,你可以设置autoReleaseHandle=true,确保操作完成之后被自动清理。如果你需要在Operation Handle 完成之后检查这个操作的完成状态,你需要将autoReleaseHandle=false.以下场景中可以使用autoReleaseHandle:Addressables.DownLoadDependenciesAsync和Addressables.UnloadScene.
使用Addressable Profiler Windows 窗口可以监控所有Addressables System Operation的引用计数。从编辑器中打开这个窗口。Window > Asset Management > Addressable Profiler.
重要:要想在这个Profiler 窗口中可视化查看数据,你必须在AddressableAssetSettings 对象的Inspector窗口中启用Send Profiler Events选项。
以下信息可能会对你使用Profiler有一些帮助。
不再被应用的Asset(Profiler中蓝色部分末尾)并不意味着Asset已经被卸载了.一个普通的场景中可能包含同一个asset bundle 中的多个Asset,例如:
在这个示例中,tree资源实际上并没有被卸载。你可以加载一个assset bundle或者它的一部分内容,但是你不能释放一部分asset bundle 资源。在捆绑包stuff(asset bundle)本身完全被卸载之前,并不会卸载任何东西。当然也存在一个例外,当调用引擎的Resources.UnloadUnusedAssets API 时。在上述情况下执行这个方法将会导致tree 资源被卸载。因为Addressable System 不能意识到这些事件。Profiler仅反映Addressables的引用计数(不完全是内存存储中的内容).注意,如果你选择使用Resources.UnloadUnusedAssets方式,这将会是一个非常慢的操作, 因此调用这个方法时,最好在场景过渡时(比如,切换场景时的过场场景或者加载场景中)
更多内容,欢迎访问: