Addressable Asset System(可尋址資源系統)
(在這裏強烈推薦大家使用這套系統,內存掌控必備,方便、簡潔、簡單;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都分配了較大的內存空間。如果我們的需求是在玩家遊戲過程中更換多個佔用內存更大的圖集呢?顯然使用這種方式會導致用戶機器的內存被迅速佔滿,導致遊戲卡頓。
使用UAAS編程方式
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;
};
}
- 1.默認不切換天空盒時,內存分配如下圖
可以看出,並沒有關於指定Cubemap的內存分配。
- 2.第一次切換不同的天空盒,內存分配如下。
- 3.第二次切換不同的天空盒材質時,內存分配如下。
通過以上數據對比可以看出,傳統的資源引用方式,就算你沒有使用到在集合中聲明的(Material)材質球對象(天空盒材質球可能持有佔用內存較大的Cubemap),Unity依然會爲你在託管堆內存中劃分一段內存空間,用於存儲你聲明在集合中的Material對象。
而使用UAAS加載資源的方式,你可以將你需要動態加載的資源在編輯器內通過標記的形式表明這個資源時可用通過查找地址的形式來進行索引和加載的,UAAS會爲你處理各個資源之間的相互依賴問題。
使用UAAS加載資源的幾種方式
1.使用地址加載或者實例化資源
創建一個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);
}
2.使用AssetReference類加載或者實例化資源
AssetReference類提供了一種無須知道資源地址即可訪問可尋址資源的方式,在腳本中聲明一個AssetReference類的實例,然後在Inspector面板中選擇一個你需要實例化的資源,指定給這個實例。
public AssetReference assetReference;
private void Start()
{
//異步加載資源
assetReference.LoadAssetAsync<GameObject>()
.Completed += (asset) =>
{
//驗證資源是否有效
if (asset.IsValid())
{
//異步實例化資源
assetReference.InstantiateAsync();
}
};
}
代碼如上所示。
使用異步方式加載資源的幾種方式
一、爲AsyncOperationHandle.Completed事件註冊回調函數
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);
}
};
}
- 使用協程Ienumator迭代
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);
}
- 使用AsyncOperationHandle.Task 支持的 await 方式
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);
}
}
UAAS內存管理
Mirroring 加載與卸載
當時用Addressable Asset時,確保資源的正確加載與卸載是管理內存的主要方式。如何加載資源的依賴,取決於你使用的加載方式。但是,在任何情況下,Release方法可以獲得已經被加載的資源,也可以返回加載資源的操作句柄(operation handle)。例如,在加載Scene時,會爲這個加載過程返回一個AsyncOperationHandle.你可以通過這個Handle來釋放資源,或者獲得handle.Result.
Asset加載
使用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加載
如果要加載一個Scene,s使用Addressable.LoadSceneAsync方法。在Single模式下,你可以使用這個方法加載一個場景(關閉所有已打開的場景),或者以Additive模式加載場景。
如果要卸載場景,使用Addressables.UnloadSceneAsync方法,或者在Single模式下打開一個新的場景,原場景會被自動卸載。你可以使用Addressables的接口打開一個新的場景,也可以使用SceneManager.LoadScene或者SceneManager.LoadSceneAsync方法。打開一個新的場景會關閉當前打開的場景,且引用計數器也會自減。
GameObject Instantiate(遊戲對象的實例化)
要加載和實例化一個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將不再具有可供引用的有效加載紋理,從而產生渲染問題,這種類型的問題很難追蹤,因爲內存的卸載時機取決於你的資源引用方式,並不是立即就會被觸發的。
Data Loading
不需要釋放AsyncOperationHandle.Result的操作句柄,但是需要釋放AsyncOperationHandle本身。如以下:Addressable.LoadResourceLocationsAsync和Addressable.GetDownloadSizeAsync.你可以訪問它們加載的數據在被釋放前。其使用Addressable.Release()釋放。
Background Interactions
有一些AsyncOperation.Result不返回任何內容的操作具有一個可選參數,如果你希望在操作完成之後不再持有這些操作句柄,你可以設置autoReleaseHandle=true,確保操作完成之後被自動清理。如果你需要在Operation Handle 完成之後檢查這個操作的完成狀態,你需要將autoReleaseHandle=false.以下場景中可以使用autoReleaseHandle:Addressables.DownLoadDependenciesAsync和Addressables.UnloadScene.
The Addressable Profiler
使用Addressable Profiler Windows 窗口可以監控所有Addressables System Operation的引用計數。從編輯器中打開這個窗口。Window > Asset Management > Addressable Profiler.
重要:要想在這個Profiler 窗口中可視化查看數據,你必須在AddressableAssetSettings 對象的Inspector窗口中啓用Send Profiler Events選項。
以下信息可能會對你使用Profiler有一些幫助。
- 白色豎線表示發生加載請求時所在幀
- 藍色背景表示當前被加載的資源
- 綠色部分表示資源的引用計數
When is memory cleared ?(什麼時候清理內存)
不再被應用的Asset(Profiler中藍色部分末尾)並不意味着Asset已經被卸載了.一個普通的場景中可能包含同一個asset bundle 中的多個Asset,例如:
- 在一個asset bundle中有三個Assets(treee、tank和cow)
- 當tree資源被加載時,Profiler中的引用計數器分別爲tree和stuff顯示一個引用計數
- 之後,當tank資源被加載時,Profiler中的引用計數器分別爲tree和tank顯示一個引用計數,爲stuff顯示一個引用計數
- 如果你釋放tree資源,它的引用計數將變爲0,並且藍色條將會消失。
在這個示例中,tree資源實際上並沒有被卸載。你可以加載一個assset bundle或者它的一部分內容,但是你不能釋放一部分asset bundle 資源。在捆綁包stuff(asset bundle)本身完全被卸載之前,並不會卸載任何東西。當然也存在一個例外,當調用引擎的Resources.UnloadUnusedAssets API 時。在上述情況下執行這個方法將會導致tree 資源被卸載。因爲Addressable System 不能意識到這些事件。Profiler僅反映Addressables的引用計數(不完全是內存存儲中的內容).注意,如果你選擇使用Resources.UnloadUnusedAssets方式,這將會是一個非常慢的操作, 因此調用這個方法時,最好在場景過渡時(比如,切換場景時的過場場景或者加載場景中)
更多內容,歡迎訪問: