本文介紹了 Unity 常用四種預設路徑,以及 AssetDataBase、Resources、AssetBundle 和目前最新的 Addressable 四種資源管理方式
文中所有 API 均以版本 2019.3 為準
本文原位址:Unity學習—資源管理概覽
資源路徑
Application.dataPath
官方文檔
隻讀,Editor 可讀寫
遊戲資料相對路徑,即遊戲安裝路徑,PC 上路徑會使用 '/' 分割檔案夾
- Unity Editor :
<項目根路徑>/Assets
- Mac player :
<App 包路徑>/Contents
- iOS player :
<App 包路徑>/<AppName.app>/Data
- Win/Linux player :
<可執行資料檔案夾路徑>
- WebGL : player data 檔案的 絕對 url 位址 (不包含具體檔案名)
- Android : 一般為 APK 路徑, 若使用 split binary build, 則為 OBB 路徑
- Windows Store Apps : player data 檔案夾的絕對位址
Application.persistentDataPath
官方文檔
可讀寫,用于持久化資料存儲,在 iOS 和 Android 平台該路徑指向裝置的公共路徑,該目錄不會随 App 更新而删除,但可被使用者直接删除
persistentDataPath
的路徑由
Bundle Identifier
生成的 GUID 組成,隻要
Bundle Identifier
不變,路徑不變
iOS 會自動将 persistentDataPath 路徑下的檔案備份到 iCloud
- Windows Store Apps :
%userprofile%AppDataLocalPackages<productname>LocalState
- iOS :
/var/mobile/Containers/Data/Application/<guid>/Documents
- Android :
該路徑由 android.content.Context.getExternalFilesDir 獲得,部分機型該路徑會指向 SD 卡/storage/emulated/0/Android/data/<packagename>/files
- Mac :
,舊版本還可能為~/Library/Application Support/<company name>/<product name>
或~/Library/Caches
,Unity 會查詢并使用以上路徑中最早的路徑~/Library/Application Support/unity.company name.product name
Application.streamingAssetsPath
官方文檔 官方手冊
隻讀,Editor 可讀寫
流資料存儲的相對路徑,該目錄下 Asset 在 Unity 編譯時不會被 Unity 打包,使其在運作時可直接通過路徑擷取,可将資源放入 Assets 目錄下任何名為
StreamingAssets
檔案夾
StreamingAssets
中資源可使用 I/O 讀取,但 WebGL 和 Android 平台下該路徑為 URL,不支援直接擷取,是以需使用
UnityWebRequest
擷取。若其他平台使用
UnityWebRequest
擷取,則需在路徑前加上
"file://"
,如
"file://" + Application.streamingAssetsPath + "/file.mp4"
- Unity Editor, Windows, Linux players, PS4, Xbox One, Switch :
Application.dataPath + "/StreamingAssets"
- Mac :
Application.dataPath + "/Resources/Data/StreamingAssets"
- iOS :
Application.dataPath + "/Raw"
- Android :
(壓縮後的 APK/JAR 檔案)"jar:file://" + Application.dataPath + "!/assets"
Application.temporaryCachePath
可讀寫,臨時資料和緩存路徑,應用更新或覆寫安裝時不會被清除,手機空間不足時才可能會被系統清除
路徑示例
讀寫權限說明
說明
資源加載
推薦官方教程
AssetDataBase
AssetDataBase 可在 Editor 環境下對項目 Asset 進行增删改查等操作(可實作與 Unity 編輯器頂部工具欄 Assets 選項下基本相同的功能),使用方法可參考官方手冊 接口文檔
Resources
接口文檔
可在項目 Assets 目錄下任意位置建立
Resources
檔案夾,打包時 Unity 會整合所有位于
Resources
檔案夾的 Asset 及其依賴,并生成一個隻讀的
resources.assets
資産檔案,對于 Resources 目錄中在遊戲中被直接引用的資産,則會被另外打包到
sharedassets0.assets
中
Resources 最佳實踐
官方的建議是不使用 Resources,有以下幾點原因:
- Resources 檔案夾會導緻記憶體管理困難
- 不适當使用 Resources 檔案夾會加長應用啟動和編譯時間,Resources 檔案夾越多,Asset 管理越困難
- Resources 系統降低項目針對指定平台使用自定義内容的能力,并且無法實作增量更新(AssetBundle 是 Unity 針對不同裝置提供特定内容的主要工具)
适合使用 Resources 的場景:
- 因其簡單快速的特性,适合用于快速原型和實驗開發,但當正式開發時應當減少使用
- 适合以下條件都滿足的狀況
- 該内容不會占用大量存儲資源
- 該内容在整個生命周期都需要
- 該内容幾乎不需要修改
- 該内容在不同平台裝置都一緻
Resources 序列化
項目編譯時會将所有 Resources 目錄下 Asset 和 Object 合并到一個序列化的
resources.assets
檔案,該檔案中還包含了類似于 AssetBundle 的中繼資料(metadata)和索引資訊,該資訊包含了由對象名稱轉化得到的 GUID 和 Local ID 的查找樹和對象位于序列化檔案中的位元組偏移量
對于大部分平台,查找樹為時間複雜度為 O(n log(n)) 的平衡查找樹,随着 Resources 中對象的增加,索引加載時間增長速度将超過線形增長速度
Resources 系統在 Splash 展示時初始化,該過程不可跳過,經觀察在低端裝置上,10000 個 Asset 檔案就會導緻該過程長達數秒,哪怕很多對象在第一個場景沒用到也會被加載
void Start()
{
//Load a text file (Assets/Resources/Text/textFile01.txt)
var textFile = Resources.Load<TextAsset>("Text/textFile01");
//Load text from a JSON file (Assets/Resources/Text/jsonFile01.json)
var jsonTextFile = Resources.Load<TextAsset>("Text/jsonFile01");
//Then use JsonUtility.FromJson<T>() to deserialize jsonTextFile into an object
//Load a Texture (Assets/Resources/Textures/texture01.png)
var texture = Resources.Load<Texture2D>("Textures/texture01");
//Load a Sprite (Assets/Resources/Sprites/sprite01.png)
var sprite = Resources.Load<Sprite>("Sprites/sprite01");
//Load an AudioClip (Assets/Resources/Audio/audioClip01.mp3)
var audioClip = Resources.Load<AudioClip>("Audio/audioClip01");
}
AssetBundle
官方手冊 接口文檔
AssetBundle 是外部資産的集合,可獨立于 Unity 建構過程外,是 Unity 更新非代碼内容的主要工具,經常置于伺服器上供使用者終端動态擷取;AssetBundle 使開發者可以送出更小的應用包,最小化運作時記憶體壓力,使終端可以選擇性加載優化内容
該部分僅簡單介紹 AssetBundle,更多資訊可見 Unity學習—AssetBundle
AssetBundle 建構
- 首先配置設定資産對象所在 AssetBundle,在 Project 視窗選中需要打包的 Asset,在 Inspect 視窗底部可見如下圖内容,底部 AssetBundle 後有兩個輸入選擇框,第一個為該資源所在 AssetBundle 名稱,第二個為 AssetBundle 變體名稱
除此之外,Unity 還提供了
AssetImporter.assetBundleName
和
AssetImporter.assetBundleVariant
等接口将資源配置設定到 AssetBundle
2. 然後即可建構 AssetBundle 了,使用
BuildPipeline.BuildAssetBundles()
即可建構 AssetBundle,其中可配置參數輸出路徑、建構選項、目标平台
3. 或者可以使用 Unity 官方提供的工具管理 AssetBundle AssetBundles-Browser 官方手冊
[MenuItem("Build Asset Bundles/Normal")]
static void BuildABsNone()
{
BuildPipeline.BuildAssetBundles("Assets/MyAssetBuilds", BuildAssetBundleOptions.None, BuildTarget.StandaloneOSX);
}
AssetBundle 建構選項
BuildAssetBundleOptions
- None
- UncompressedAssetBundle:不壓縮
- DisableWriteTypeTree:不包含類型資訊
- DeterministicAssetBundle:使用哈希值作為 Asset Id
- ForceRebuildAssetBundle:強制重建
- IgnoreTypeTreeChanges:增量建構檢查時忽略類型樹改動
- AppendHashToAssetBundleName:AssetBundle 名稱後加哈希值
- ChunkBasedCompression:使用 LZ4 壓縮
- StrictMode:建構過程中任務錯誤即建構失敗
- DryRunBuild:試運作
- DisableLoadAssetByFileName:禁用名稱查找資源,可降低運作時記憶體提高加載效率
- DisableLoadAssetByFileNameWithExtension:禁用帶字尾名的名稱查找資源,可降低運作時記憶體提高加載效率
AssetBundle 派發方式
根據實際情況選擇 AssetBundle 時随項目打包,或後續通過網絡下載下傳,一般移動平台由于初始安裝大小和下載下傳限制,會選擇安裝後下載下傳,而主機和電腦則随項目打包
随項目打包有兩個主要原因:
- 減少項目建構時長,簡化疊代開發,針對無需單獨更新的 AssetBundle 可放在 StreamingAssets 目錄下
- 釋出可更新的初始修正内容,用于節省使用者初始安裝後的時間和為後續修複做準備。但 StreamingAssets 不适用于該情況,若不考慮自定義下載下傳和緩存系統,則可以使用 Unity 的緩存系統,從 StreamingAssets 下載下傳初始緩存
一般推薦使用
UnityWebRequest
下載下傳 AssetBundle,若下載下傳包為 LZMA 壓縮,則緩存的為未壓縮或使用 LZ4 重壓縮的内容,若緩存已滿,則 Unity 會删除最近最少使用的 AssetBundle
Unity 内置的 AssetBundle 緩存系統用于緩存
UnityWebRequestAssetBundle.GetAssetBundle
下載下傳的包,緩存僅以名稱作為唯一辨別。另外可通過重載方法可傳入版本号(開發者自己管理版本号),緩存系統會比對版本号,選擇比對版本或下載下傳新包
緩存系統可通過 Caching.expirationDelay 和 Caching.maximumAvailableDiskSpace 修改最小未使用過期時間和最大緩存空間,當緩存檔案在過期時間内沒被打開過即被删除,或緩存空間不足,則優先删除最近最少打開的緩存
IEnumerator GetText()
{
using (UnityWebRequest uwr = UnityWebRequestAssetBundle.GetAssetBundle("http://www.my-server.com/mybundle"))
{
yield return uwr.SendWebRequest();
if (uwr.isNetworkError || uwr.isHttpError)
{
Debug.Log(uwr.error);
}
else
{
// Get downloaded asset bundle
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(uwr);
}
}
}
AssetBundle 加載
有四種不同的 API 用于加載 AssetBundle,但每個 API 的行為随壓縮算法和平台而不同
- AssetBundle.LoadFromMemoryAsync
IEnumerator LoadFromMemoryAsync(string path)
{
AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
yield return createRequest;
AssetBundle bundle = createRequest.assetBundle;
var prefab = bundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab);
}
- AssetBundle.LoadFromFile
該方法可高效地從硬碟加載未壓縮或 LZ4 壓縮的 Assetbundle,加載 LZMA 壓縮包時會先解壓再加載到記憶體
public class LoadFromFileExample : MonoBehaviour {
function Start() {
var myLoadedAssetBundle
= AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
if (myLoadedAssetBundle == null) {
Debug.Log("Failed to load AssetBundle!");
return;
}
var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
Instantiate(prefab);
}
}
- WWW.LoadfromCacheOrDownload(5.6 及以前版本)
舊方法,已抛棄
- UnityWebRequestAssetBundle (5.3 及以後版本)
先使用
UnityWebRequest.GetAssetBundle
建立請求,再将請求傳入
DownloadHandlerAssetBundle.GetContent(UnityWebRequest)
,下載下傳完成後可像
AssetBundle.LoadFromFile
一樣,直接使用 assetBundle 對象
該方法使開發者更靈活處理下載下傳資料,選擇臨時存儲或長期緩存,避免不必要的記憶體使用。同時,由于是原生代碼,沒有托管堆棧擴充風險,DownloadHandler 也不會保留下載下傳資料,進一步減少了記憶體開銷
LZMA 壓縮包會在下載下傳時解壓,并以 LZ4 重新壓縮緩存,可調用 Caching.CompressionEnabled 修改
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request
= UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
官方推薦 盡量使用
AssetBundle.LoadFromFile
,該 API 在速度、磁盤使用和運作時記憶體使用方面都最高效;需要下載下傳則使用 UnityWebRequest
AssetBundle Asset 加載
同步異步加載 Asset 一共有六種 API 可使用,同步方法一定比對應的異步方法快至少一幀
- LoadAsset (LoadAssetAsync)
- LoadAllAssets (LoadAllAssetsAsync)
- LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)
LoadAllAssets
适合加載包中大部分或所有獨立 Unity 對象時使用,相較于多次重複調用另外兩種 API,
LoadAllAssets
速度要稍快一點。是以當 Asset 數量巨大且一次性需要加載的 Asset 少于 2/3 的時候,建議将 AssetBundle 拆分成多個小包體,再使用
LoadAllAssets
加載
LoadAssetWithSubAssets
适合需要加載的對象内嵌了其他對象的情況,若加載對象均來自于一個 Asset 且包中有許多其他無關對象,則使用該 API
其他情況均用
LoadAsset (LoadAssetAsync)
Unity 對象加載時在主線程執行,對象資料是在工作線程 worker thread,任何線程不敏感的操作都在工作線程執行
異步加載時會根據時間片限制每幀加載多個對象,自 Unity 5.3 後,對象加載就并行化了。多個對象在工作線程被反序列化、處理和內建,當對象加載完成,則觸發 Awake 回調
同步加載方法
AssetBundle.Load
會暫停主線程知道加載完成,它們還将加載過程進行時間切片,以使對象內建所占用的幀時間不超過特定的毫秒數,該值可通過
Application.backgroundLoadingPriority
設定
- ThreadPriority.High: 最大 50 毫秒每幀
- ThreadPriority.Normal: 最大 10 毫秒每幀
- ThreadPriority.BelowNormal: 最大 4 毫秒每幀
- ThreadPriority.Low: 最大 2 毫秒每幀
在其他因素相同的情況下,異步加載方法的調用到加載對象可用之間最小有一幀延遲,導緻異步加載方法比同步方法執行所需時間更長
AssetBundle 依賴
根據運作環境可以使用兩個不同的 API 自動追蹤 AssetBundle 之間的依賴。Editor 環境下,可使用
AssetDatabase
查詢依賴,使用
AssetImporter
通路和修改 AssetBundle 的配置設定和依賴;運作時,可以通過基于 ScriptableObject 的
AssetBundleManifest
API 加載在 AssetBundle 建構期間生成的依賴項資訊
當一個對象所在的 AssetBundle 被加載時,該對象就被配置設定了一個唯一的有效執行個體 ID,是以 AssetBundle 的加載順序并不重要,重要的是在加載該對象本身之前,要優先把所有包含其依賴對象的 AssetBundle 加載好。
Unity 不會自動加載子 AssetBundle,具體可詳見手冊,例:
AssetBundle 1 中的 Material A 依賴于 AssetBundle 2 中的 Texture B,若要正常加載,與 AssetBundle 1 和 2 的加載順序無關,但一定要保證加載 Material A 時,AssetBundle 2 已加載
在建構 AssetBundle 時,Unity 建立一個包含每一個 AssetBundle 依賴資訊的類型為 AssetBundleManifest 的序列化對象,該檔案存在一個與其他 AssetBundle 在同一打包路徑下的單獨的 AssetBundle 中,且與父層檔案夾名相同
有兩種 API 查詢依賴
- AssetBundleManifest.GetAllDependencies 擷取 AssetBundle 的所有依賴層級
- AssetBundleManifest.GetDirectDependencies 擷取 AssetBundle 直接依賴
因該 API 會生成字元串數組,是以應盡量少用,且避免性能高峰時使用
官方建議,大部分場合下,在進入性能需求高的場景前,盡可能多地加載對象,尤其對于移動平台這種,通路本地存儲慢,加載解除安裝對象引起記憶體流失會觸發垃圾回收的平台
Asset 分包政策
- 邏輯實體分包
- 對象類型分包
- 并發内容分包
邏輯實體分包
依據資源在項目功能塊的使用位置,如 UI、角色、環境和其他在生命周期中常出現的内容等分包
- 将所有 UI 的紋理和布局資料分包
- 将角色的模型和動畫資料分包
- 将多場景共用的紋理和模型分包
該分包方式适用于制作 DLC,可以隻下載下傳單個實體而無需下載下傳無變化的資源,其關鍵點在于需要開發者清楚了解每個打包的資源所要用到的時機和位置
對象類型分包
該方式适用于針對多平台分包,例如音頻檔案的壓縮設定在 Windows 和 Mac OS 平台一樣,另外由于紋理壓縮格式和設定等改變頻率遠低于腳本和預設體,使用該配置設定方式可以使 AssetBundle 相容更多的 Unity 版本
并發内容分包
并發内容分包可了解為以關卡為分組依據,将一個關卡内獨有的角色、紋理、音樂等需要在同一時機加載的内容分為一包
Tips
- 将常更新與不常更新内容分開
- 将需要同時加載的對象分為一組,如一個模型,其所需的材質和動畫分為一組
- 若多個 AssetBundle 中的多個對象引用了其他 AssetBundle 中的單個 Asset,則将依賴項分離到單獨的包中以減少重複
- 確定兩組完全不可能同時加載的對象不在用一包中,如低清和高清材質包
- 若一個包中隻有低于一半的對象被頻繁加載,可将其拆分
- 将一些同時加載的小包(資源少于5到10個)合并
- 若一個包中的對象僅是版本不同,則可以使用 AssetBundle 變體
Addressable
Addressable 系統為 Unity 新推出的資源管理系統,整合了 Unity 直接引用,Resources 和 AssetBundle 全部三種資源加載方式。通過可尋址資産的方式,便捷地實作了内容包的建立和部署。Addressable 系統使用異步加載的方式實作從任何位置加載任何依賴項,使得任何引用方式都更加便捷動态化
注意:需Unity 2018.3 及其以後版本
Addressable 優勢
- 縮減疊代周期,無需修改代碼優化内容
- 自動依賴管理,将請求内容的依賴項一同加載
- 自動記憶體管理,對管理資源自動引用計數
- 内容打包,負責建構和解析引用鍊,在将資源移動或重命名的情況下,依然可實作本地和遠端部署
- 配置檔案,可配置多個配置檔案,實作快速切換
Addressable 概念
Addressable 由兩個包組成,Addressable Assets package(主要功能) 和 Scriptable Build Pipeline package(依賴項)
- Address:資源的位址标記,用于運作時查詢
- AddressableAssetData:在項目 Assets 目錄下用于存儲 Addressable 資源的檔案目錄
- Asset group:建構時處理的 Addressable 資産組
- Asset group schema:資料建構時的配置
- AssetReference:可根據需求延遲初始化的直接引用對象
- Asynchronous loading:開發過程中無需更改代碼也可修改資産位置和依賴項
- Build script:打包資産,将 Address 和 Resources 映射
- Label:為運作時加載相似項目提供額外的 Addressable Asset 标志
Addressable 使用
Addressable 安裝
- 打開頂部工具欄 Window -> Package Manager,找到 Addressables,點選安裝即可
2. 安裝完成後 Addressable 的主要功能都可在頂部工具欄 Window -> Asset Management -> Addressables 中找到
- Groups:Addressable 分組工具
- Settings:Addressable 總體設定
- Profiles:預設建構配置管理
- EventViewer:監測引用計數工具
- Analyze:用于分析打包情況,檢測重複等可自定義規則的分析工具
- Hosting:模拟伺服器
3. 打開Group,建立新的配置,項目 Assets 目錄下會自動建立一個 AddressableAssetData 目錄,無需直接改動該目錄
Addressable Group 配置
- 在 Addressables Groups 視窗,右鍵或左上角 create 按鈕即可建立 Group
- 選中 Group,在 Inspector 中修改設定,Group 有三種配置項
- Content Packing & Loading:打包的建構和加載路徑,以及其他打包相關設定
- Content Update Restriction:包更新限制
- Resources and Built In Scenes:是否包含 Resources 或建構的場景
- 添加 Asset 後,建構 Addressable Group
- Addressables Groups 視窗,Build > New Build > Default Build Script
- 或者使用 API
AddressableAssetSettings.BuildPlayerContent()
Addressables Asset 配置和加載
- 添加 Addressable Asset
- 将資源直接拖入 Addressables Groups 視窗下分組内即可
- 或者在 Project 中選擇任何 Asset ,在 Inspector 下都可見名為 Addressable 的勾選框,勾選即可将該 Asset,可更改其 Addressable 名稱,此時該 Asset 就被添加到預設的 Addressables Group 中了
4. (可選) 更改資源名,為資源添加 Label
5. 加載 Addressable Asset
-
異步加載資源對象Addressables.LoadAssetAsync<T>(string)
-
場景中建立對象Addressables.InstantiateAsync(string)
- 或添加
成員變量,在 Inspector 可選擇 AssetReference 引用的資源對象AssetReference
public class AddressablesExample : MonoBehaviour {
GameObject myGameObject;
...
Addressables.LoadAssetAsync<GameObject>("AssetAddress").Completed += OnLoadDone;
}
private void OnLoadDone(UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<GameObject> obj)
{
// In a production environment, you should add exception handling to catch scenarios such as a null result.
myGameObject = obj.Result;
}
}
參考
Unity讀取内部、外部資源詳解
Unity資源管理
The Addressable Asset System 正式版應用