本文介紹了 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%\AppData\Local\Packages\<productname>\LocalState
-
iOS:
/var/mobile/Containers/Data/Application/<guid>/Documents
-
Android:
/storage/emulated/0/Android/data/<packagename>/files
該路徑由 android.content.Context.getExternalFilesDir 獲得,部分機型該路徑會指向 SD 卡 -
Mac:
~/Library/Application Support/<company name>/<product name>
,舊版本還可能為~/Library/Caches
或~/Library/Application Support/unity.company name.product name
,Unity 會查詢并使用以上路徑中最早的路徑
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:
"jar:file://" + Application.dataPath + "!/assets"
(壓縮后的 APK/JAR 文件)
Application.temporaryCachePath
可讀寫,臨時數據和緩存路徑,應用更新或覆蓋安裝時不會被清除,手機空間不足時才可能會被系統清除
路徑示例
路徑 | Editor | Windows | Mac OS | iOS | Android |
---|---|---|---|---|---|
Application.dataPath | 項目路徑/Assets | 安裝路徑/ProductName_Data | /Applications/AppName.app/Contents | /var/mobile/Containers/Data/Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/AppName.app/Data | /data/app/package.name.apk |
Application.persistentDataPath | C:/Users/username/AppData/LocalLow/CompanyName/ProductName <br />或<br />/Users/username/Library/Application Support/CompanyName/ProductName | C:\Users\username\AppData\LocalLow\CompanyName\ProductName | /Users/username/Library/Application Support/CompanyName/AppName | /var/mobile/Containers/Data/Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Documents | /data/data/package.name/files |
Application.streamingAssetsPath | 項目路徑/Assets/StreamingAssets | 安裝路徑/ProductName_Data/StreamingAssets | /Applications/AppName.app/Contents/Resources/Data/StreamingAssets | /var/containers/Bundle/Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/AppName.app/Data/Raw | jar:file:///data/app/package.name.apk/!/assets |
Application.temporaryCachePath | C:/Users/username/AppData/Local/Temp/CompanyName/ProductName<br />或<br />/var/folders/xx/xxxxxxxxxxxxxx/X/CompanyName/ProductName | C:\Users\username\AppData\Local\Temp\CompanyName\ProductName | /var/folders/xx/xxxxxxxxxxxxxx/X/CompanyName/ProductName | /var/mobile/Containers/Data/Application/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Library/Caches | /data/data/package.name/cache |
讀寫權限說明
https://blog.csdn.net/BillCYJ/article/details/99712313
資源加載
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 文件就會導致該過程長達數秒,哪怕很多對象在第一個場景沒用到也會被加載
接口 | 說明 |
---|---|
FindObjectsOfTypeAll | 獲取所有指定類型的對象 |
Load | 加載 Resources 指定目錄下的 Asset |
LoadAll | 加載 Resources 指定目錄下的所有 Asset |
LoadAsync | 異步加載 Resources 指定目錄下的 Asset |
UnloadAsset | 將 asset 從內存釋放,重新加載 Asset 不會使之前的引用重新鏈接 |
UnloadUnusedAssets | 釋放未使用的 Asset(包括僅在腳本堆棧使用,未在GameObject 使用) |
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
等接口將資源分配到 AssetBundleimage 然后即可構建 AssetBundle 了,使用
BuildPipeline.BuildAssetBundles()
即可構建 AssetBundle,其中可配置參數輸出路徑、構建選項、目標平臺或者可以使用 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,點擊安裝即可
打開Pacakage Manager安裝 Addressable -
安裝完成后 Addressable 的主要功能都可在頂部工具欄 Window -> Asset Management -> Addressables 中找到
- Groups:Addressable 分組工具
- Settings:Addressable 總體設置
- Profiles:預設構建配置管理
- EventViewer:監測引用計數工具
- Analyze:用于分析打包情況,檢測重復等可自定義規則的分析工具
- Hosting:模擬服務器
打開 Addressables Group -
打開Group,創建新的配置,項目 Assets 目錄下會自動創建一個 AddressableAssetData 目錄,無需直接改動該目錄
image
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 中了
image
(可選) 更改資源名,為資源添加 Label
-
加載 Addressable Asset
-
Addressables.LoadAssetAsync<T>(string)
異步加載資源對象 -
Addressables.InstantiateAsync(string)
場景中創建對象 - 或添加
AssetReference
成員變量,在 Inspector 可選擇 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; } }
-