Programming

Unity Asset Bundle 討論


本文網址:https://wp.me/pBAPd-A2

基礎概念

Asset Bundle 是 Unity 的動態媒體包機制 以下簡稱 (AB)

簡單來講 Unity很難直接使用媒體(如貼圖) (實際要用當然可以但是從原始素材到實際使用的資源有非常多Unity本身的設定要做,很難做的跟先在Unity編好後打包為素材後使用一樣方便)
打比方說材質設定(Material)是一種Unity的特有資源.這是無法在外部自行製作的.
AB 就是素材打包後 Unity 可以認得的各種資源.

如果不使用 AB. 多半是 使用以下兩種方法來存取資源.

1. 將資源拉到場景物件中,直接使用.
2. 使用 Resource.Load 從 Resources/ 資料夾中讀取.

(1.) 的這種方法當讀入那個場景. 資源就會順帶讀進來. 也就是資源其實包在場景內.
(2.) 的這種方法 其實就可以想像 Resources/ 下的所有東西是一個大的資源包, 會隨著遊戲本體打包. 而且當遊戲讀入的時候會一併載入(不管有沒有讀取裡面的東西).

AB的目的.除了能夠不更新本體來更換資源之外,很重要一部分就是減少本體的大小.

也就是說正確使用AB必須達到一種效果. AB之內的資源必須 2. 不在 Resources\ 內 1. 不被任何場景所索引. (否則資源已被包入本體,那麼同時使用AB就會有兩份類似的資源) 也就是本體完全不知道有這份資源的存在. 動態透過 AB 取得其中的資源.

程式差異:使用資源的方法

使用 AB 在程式上最大的差異就是從同步處理到非同步處理.
原本直接使用索引或是呼叫 Resources.Load() 因為資源已經讀入在記憶體,所以可以立即取得資源,而使用 AB 因為原本並非與本體一併讀入的原因,必須先讀入 AB (資源包) ,然後再從資源包中讀入資源使用.也就是需要資源的時間點與能夠使用資源的時間點並不相同(非同步)導致程式碼架構寫作習慣有大大的不同.對已經完成的系統架構改動起來影響非常巨大.

原本得程式架構可能是

void CreateCharacter()
{
GameObject prefab = Resource.Load(…);
GameObject object = GameObject.Instanciate( prefab );
m_Character = object;

// 然後物件在函式完成已經可以正常使用.
m_Anim = m_Character.AddComponent<AnimScript>();
}

void PlayAnim(…)
{
m_Anim.Play(…);
}

改變成

IEnumerator StartCreateCharacter()
{
StartFetchAB(

AsyncFunc( bundle )
{
GameObject prefab = bundle.Load(…);
GameObject object = GameObject.Instanciate( prefab );
m_Character = object;
m_Anim = m_Character.AddComponent<AnimScript>();
// frame B 才產生索引
}
) ;
// frame A 物件在函式完成還沒有出現,無法 使用 m_Character 及 m_Anim
}

void PlayAnim(…)
{
// 如果在 frame B之前呼叫到此函式,則 m_Anim 還未取得腳本
m_Anim.Play(…);
}

又或者因為取得 AB 需要一點時間,導致場景開始時必須等待取得會有數個畫格的空檔,畫面上會出現沒有正確初始化的畫面.甚至如果特效物件是在 AB 之中,那麼撥放特效的時間就會延遲.

靜態管理器,動態讀檔器

5.6 版本以前 Unity 的 AB 其實非常陽春.雖然整個流程直到資源取得是可以串起來了.但是沒有一個官方規範或推薦的機制.也就是說 Unity 官方只提供取得 Bundle 之後的資源取用,卻沒有規範怎麼打包,取得及管理這些 Bundle.
約 5.6 版本前後,官方也只有在 Asset Store 上提供一個插件(而不是內建管理器腳本在引擎中).最近的版本才陸續提供了 AB 的一些預覽機制.也就是因為如此,各家工作室就會自行設計各派別中繼層(同間公司都能有兩三種作法).

中繼層大概會包括打包腳本,一個靜態管理器,負責組織及儲存下載中,已下載的 Bundle.一個動態下載器,負責執行 Coroutine 呼叫網路要求,等待下載(當然也可用遊戲的腳本來負責下載,下載完檔案再交給管理器,就如同上面闡述了各門各派的問題).

打包腳本是一個編輯器腳本(非遊戲腳本,不會打包到遊戲內),簡而言之就是那些檔案要包到哪個包.是的!大概是2015~2016年之前,哪些檔案要包必須使用者自己建表去打包.在某一個版本之後官方終於在各資源 inspector 下方加上了一個 assetBundleName 的選擇框.但這段過渡時期也導致了有些仍使用了自訂清單打包的團隊,覺得用官方這時候提供的選擇框的手動方案十分"不自動化"/"不客製化".

在選擇框決定了各項資源要打包的包體檔案名稱之後,打包腳本最終就會產出 Bundle 的檔案.

也就是說原本資源

1.) Assets/Texture/ABC.png 原本直接使用索引取得該資源,變成打包到一個檔案 <bundle1> 中的 ABC.png.取得包體讀取時要這樣 <bundle1>.Load(“ABC").

2.) Assets/Resources/Weapon/bullet.prefab,原本使用 Resources.Load(“Weapon/bullet"),變成打包到一個檔案 <bundle2> 中的 bullet.prefab 讀取時 <bundle2>.Load(“bullet").

官方提供的插件除了管理已下載,下載中的物件之外,還有負責組合路徑的功能.因應不同平台的包體.包體應該會放置在網路上如下:

<url>/<platform>/<bundle1>

<platform>就代表了不同的平台必須使用不同的包體(因不同平台得使用不同的貼圖壓縮格式,所以不能互通)

如果不使用官方的函式 WWW.LoadFromCacheOrDownload() 來下載,則必須自行下載檔案,存放在平台可放存檔的位置(Application.streamingAssetsPath 或 application.datapath)然後在使用時再從那些已經存檔的本地路徑讀出那些包體檔案後使用.

而這些路徑規則在編輯器執行時也要能正常,注意各平台間路徑斜線,以及前墜的差異 file:// http://

備註:由於編輯器的執行是在 PC 平台上,他使用的包體應當是 PC 平台.如果使用到其他平台的包體,有可能會有貼圖錯誤的問題.這點是開發行動遊戲(常常將平台切換維持在手機平台,或是根本就沒有打 PC 包體來使用)常見會發生的錯誤.

官方提供的插件還有一個值得稱讚的特點,除了在編輯器執行時也能正常下載使用 AB 之外,還提供了模擬模式,也就使用包體不需要下載,而是直接取得資料夾中的資源的功能.省去開發時間變更資源後打包的動作(但是決定各資源到哪一包,以及取得下載包及其中資源的手續還是不能省).

包體的規範

不管是使用檔案建表,資料夾建表,還是編輯器選擇,哪些資源要打包在哪一包中各專案有自己的不同作法.

某些工作室習慣大包體(少數但是有大量資源的包體)的做法優點是因為網路要求少,這樣下載會快點.但是缺點之一就是不能分批更新,一次必須全部更新.缺點之二在於因為資源都在少數包體中,所以一旦必須取得其中一個資源,就必須把整個包體載入,占用相當大的記憶體(除非頻繁卸載).缺點之三就是由於大包體一次需下載的檔案大,所以在網路品質不佳的狀況下有可能會有下載不全失敗變殘檔必須重新下載的問題.

註:這邊特別注意記憶體的使用,包體內(可視為壓縮)算一份,取出來的資源又算一份.

小而大量的包體的優缺點則相反,網路下載時必須考慮一次發多少個 Request 是最佳化的問題.太多或太少的 Request 都會造成下載時間過長.

而小到極致一個資源一個包體的作法就是把每個資源的讀取都下載化(非同步),優點是在取得資源時不用再思考這個資源在哪一包(多半使用資源名稱編碼的方式來決定包體名稱),但缺點是整個遊戲可能會有幾百到幾千個包體,要特別小心前述下載 Request 的問題.

最後有一種工作室是使用壓縮及 AB 的混和型.也就是 AB 打包完成後,再透過壓縮方式把檔案壓成更少數量的包體,然後下載時先下載壓縮包,解壓縮之後放在本地資料夾,然後取用 AB 時一律透過本地路徑來取得包體.

檢查版本

前面說到 AB 的作用之一就是可以小部分更新,不需要更換本體.也因此更新時會碰到版本檢查的問題.官方的函式提供了版號及 hash 兩種做法,也就是在 WWW.LoadFromCacheOrDownload() 中指定版號或 hash,如果本地已取得(cache)的包體的版號比較小,或是 hash 不一致,那麼代表有新檔案,此時 Unity 就會重新下載.否則優先使用曾取得的包體.然而版號及 hash 要怎麼在呼叫下載前讓 client 知道,這點沒有規範.

如果是使用 hash 的做法,那麼在打包完成之後必須使用 AssetBundleManifest.GetAssetBundleHash() 來取得個檔案的 hash 值,在遊戲 呼叫下載之前先取得,然後下載之時就知道應該要抓到的檔案的 hash 值是多少.來做出正確判斷.

使用版本號的方法也是類似,呼叫 WWW.LoadFromCacheOrDownload() 時依照傳入的版號,系統記錄此次下載的檔案的版號,下一次呼叫的時候依照傳入的版號來做比較.也就是說在呼叫該下載函式之前,就必須先知道預期取得的檔案版號為何?

這個版號或 hash 值通常必須透過伺服器在 “取得AB之前" 優先取得算是一個開發上比較需要注意的地方.

備註:如果還沒有建立 AB 版本控制的開發者,當打包更新了 AB 檔案後,請記得曾下載的 AB 會 cache 在本機,有可能會發生一直抓不到更新版的問題,這種情況請使用命令強制清除 cache.

另外,如果是前述下載自訂壓縮包的做法,因為先下載的是壓縮包,所以下載前則自行檢查之前是否已經有下載過?是否需要再下載?已下載的是哪一個版本?是否要透過網路要求(request)的更新機制來檢查新版(網頁的規範中,會帶參數來檢查網頁檔案的更新屬性,讓瀏覽器決定是否會優先用 cache 的頁面)?

而如果對官方的 cache 機制有意見,也可以以類似壓縮包的作法來操作,完全透過自訂的下載方式下載 AB 檔案到本地端,對其檢查增刪,等檔案正確後才用 Unity 的函式自本地取得 AB 包體.

本地包

前段落提到先下載包體存放在本地,然後再使用的作法.而關於本地包還有一種作法是將 AB 包體隨著本體一起打包的作法.這種做法的優點是當玩家從商城下載完成之後,因為本體內部已經有下載包,所以不需要再花費額外的流量(或時間)從第三方空間下載 AB.(附帶好處就是本地包體的流量算商城的,這就是暗黑兵法)

會出現這種作法的原因是因為某些區域市場,玩家習慣在固定網路的地方先用 wifi 下載.等到回家在開始玩的時候不希望再花費數據傳輸費用來下載下載包.本地包的作法同時增進遊戲的首日留存率,也兼顧更新能力.

前面段落描寫使用 AB 程式的寫作方式不同(同步與非同步),從開發的角度來看,同樣是本體內含所有資源,使用本地包,然後再更新 AB 的作法會比使用 Resources.Load() 然後再改用 AB 呼叫上比較一致,避免開發中程式使用兩種不同的資源取用方式.

本地包的打包方式就是將要打入本地包的包體(打包完成)放到 Assets/StreamAssets/ 資料夾中(而非放在專案外),這樣打包時此資料夾內的檔案會被放到遊戲運行中的 Application.streamingAssetsPath 路徑下.取出本地包的路徑也就與網路包的路徑不相同.使用本地包必須額外處理不同路徑的問題.(這種做法就很類似前述先下載檔案到本地再取用的作法)

而由於路徑的不相同,使用 WWW.LoadFromCacheOrDownload() 的版本機制就會失效,因為兩個不同路徑的檔案被視為不相同的檔案.同時因應本地包的取用,本地包打包的時候 client 還必須知道本地包的版本.也就是放在本體內的本地版本表.方便取用時檢查如果本地版本比較高,則優先使用本地包(不下載),如果網路版本比較高,則下載取得網路包體來使用.

甚麼時候會出現本地版本比較高的情況?接著說明實務上會遇到的版本更新流程.

版本更新流程

首先哪個本地版本配上哪些包體版本(甚至是伺服器版本)是每個團隊版本更新時必須自行處理的問題.

理想中 AB 更新的流程是

1. 伺服器進維修
2. 更新 AB 檔案,更新版本表
3. 伺服器開機,玩家重新連上,檢查版本後取得新 AB 包體.

當本體也隨之更新會遇到的問題

1. 本體(帶新版本地包)更新,玩家取得新版本體
2. 玩家使用新版本本體連到舊伺服器(此時會遇到上述本地版本比較高的情形,如果沒有本地包,則要考慮新版本也要能吃舊包體的相容問題)
3. 伺服器進維修
4. 更新 AB 檔案,更新版本表
5. 伺服器開機,玩家重新連上,檢查版本後判斷是否要取得 AB 包體.(新本體則因為網路版本一致,所以不需使用網路包,舊本體則因為網路版號更新,所以下載了新版本)

注意以上這兩種情形的假設是

本體版本會先釋出(因此才要考慮新本體連接到舊伺服器,舊包體的情形)
有維修時間,因此版本更新與 AB 包體檔案的更新對 Client 來說會是同時.
如果是類似 PC 商城(Steam)的更新方式,新版伺服器上線後會讓本體強制更新.

如果沒有維修時間,或是維修時間少到可以忽視,注意 AB 檔案的更新要早於伺服器版本表更新.如果更新的順序相反.會發生一個情形是:

1. 伺服器版本更新
2. 玩家連入伺服器取得網路 AB 版本為2,但此時實際上的包體其實是1.
3. 網路包體更新
4. 已經 cache 版本 1 的玩家本地端 cache 的版號是2,所以也不會再去檢查更新後的網路包體.導致一直無法更新新版包體的問題.

這種情況建議透過

A) 伺服器版本二次更新,在步驟 3 之後再更新一次伺服器版本進到 3 版這樣即便已經吃到 2 版版號(卻使用 1 版檔案)的玩家也會再度下載.
B) 使用 hash 來檢查檔案不正確.

實務上放包體的網路空間,也必須要注意 cache 問題.在某些網路空間上,會有 cache 的設定.即便是檔案已經在空間上更新了,本地下載時還是持續拿到舊的檔案,那是因為下載的過程中被伺服器的 cache 機制提供了舊檔,這時必須強制網路空間清除 cache,再更新網路版號讓本地端下載新版.一直無法下載到新版的客戶端只好建立清除舊版包體的機制,或是建議玩家使用安卓才有的清除資料功能.

最後,假設專案必需處理 AB 的相容版本問題(舊本體要用舊包體遊玩,同一時間新本體使用新包體,新舊包體必須同時存在的情況).那麼伺服器提供的 AB 網址就必須依照不同的 Client 版本號而不同,此時必須小心新舊包體被視為不同檔案,而累積存在本地 cache 的問題.(這種情形就建議自行管理下載,而不要讓 Unity 依照網址來認下載包的唯一性)

variant 功能

Unity 的 AB 還有一個比較少人用的 variant 功能.就是可以打成相同名稱但是不同副檔名的包體.這個 variant 的功能主要是用來製作多國語系,或是在不同情形下的類似資源.作法也很簡單就是編輯相同名稱標籤,同時加上 variant 的設定即可.最後打包的時候就會產出這樣的檔案

<Bundle1>.<variant1>
<Bundle1>.<variant2>
<Bundle1>.<variant3>

而下載讀檔的時候,可以自行設定目前使用的 variant 關鍵字,也可以透過插件設定目前 variant.來達到組合完整 AB 檔案的目的.

檢查下載資源

為了避免使用當下必須等下載的窘境,一般來說在遊戲開始會有資源準備的一段流程,確保各下載包都能正常取得.

為了在這個流程,透過上述所描述的版本表,本地端就知道有那些下載包要下載.將其全部下載一遍,並檢查,就完成了資源檢查的流程.

而如同前述,AB 下載的時候是非同步的,要考慮到 WebRequest 同時要發出多少的網路要求.如果一次請求一個 AB 檔案,則可照表逐個檢查.如果同時發出多個要求,則要追蹤目前下載中的數量,直到全部檔案都取得為止.當然,如果有 AB 檔案無法完成下載.系統該怎麼顯示應對?由於並不是每個下載包都必須同時使用,為了避免記憶體的浪費,下載檢查資源後,是否要針對某些 AB 檔案進行卸載,哪些 AB 檔案在哪些情況不卸載,哪些 AB 檔案使用完在甚麼情況必須卸載,或是哪些 AB 可以是否要透過靜態索引來儲存,方便使用.都是專案必須要考慮的細節.

重複資源

如何能知道 Unity 的打包的狀況.其實每次打包的時候 Unity 都會產生 Log.在其中紀錄打包的資源(甚至本體打包時所引到的素材檔案都會有紀錄.)

而 AB 打包完畢後這些的資訊在哪裡可以取得?答案是在 AB 包體檔案的旁邊會產生相對應的 .manifest 檔案.裡面就會記錄這個包體內的資源.(有經過包體編輯紀錄的)

備註:manifest 不需要釋出給玩家,在 AB 檔案內已經有那些資訊.

因為本體理論上並不應該知道包體的資訊,各包體之間為獨立也不互相溝通,所以否則如果沒有妥善規劃 AB 的包體資源,其實有可能會發生重複打包的現象.

打比方說,prefab1 及 prefab2 被分別打包入 bundle1 及 bundle2.查看這 bundle1 及 bundle2 的 manifest 就會分別看到 prefab1 及 prefab2 等資源.

但 Unity 的 prefab 通常是 GameObject,在其下那些 mesh 及 texture 理當被一併打包進去,方能保證 prefab1 及 prefab2 分別透過不同的包體獨立讀入後都能正常運作.

然而假如 prefab1 及 prefab2 都包含相同的一張 textureA,就會發生一份資源被重複打包的現象.

解決方式就是,將這種共享的資源也打入特定的包體中,以此為例則是標定 textureA 的包體為 bundleTexture.

所以最後打包後就會產生:

bundle1
bundle2
bundleTexture

三個包體.而此時如果我們查看 manifest 檔案的內容,則會看到 bundle1 及 bundle2 內引述了 其他的包體 bundleTexture.

而在 Unity 的官方插件的運作中,則會依照這些包體的依存關係,在取得特定包體時,優先取得其引述依存的包體.進而減少重複資源打包的現象.

新手教學包

上面已說到,AB 的作用之一就是減少本體的大小.但是上述段落也提到,在遊戲開始之前為了確保遊戲運作順暢,必須先把所有包體下載一遍.(否則會面臨當使用的時候必須等下載的窘境)隨著遊戲越大,第一次開始遊戲要等待的時間就越久,不利於首日留存.

為了解決這個問題,某些專案就設計了二次下載的流程.這個部份關係到 Unity AB 內的資源相關性.在此順帶簡單說明.

整個遊戲包含了以下幾個部分:

A) 清除掉所有索引的本體,
B) 新手教學包.使用到部分的素材.如部分的角色.初始要顯示的介面圖片或字型
C) 遊戲資源包.包含基礎素材,如所有的角色.

二次下載遊戲流程如下:

1) 本體開啟,先檢查是否玩家已通過新手教學.
2) 如果已通過新手教學,則直接進入檢查 C) ,檢查完畢就進入正常遊戲流程.
3) 如果未通過新手教學,第一次遊玩,則進入檢查 B) ,檢查完畢開始新手教學.新手教學完畢後回到 2)

這邊有幾個值得注意的地方.

首先是 玩家資訊 如何取得?是否存在本機端?如果要透過網路取得,那麼本體的腳本就必須有連網驗證帳號功能.(否則就要把登入畫面包在本體中)

B) C) 兩個包體中如果有重複的資源(如角色) 要怎麼處理?尤其是假如玩家使用舊帳號在新手機上遊玩,應該要能夠不需要重新執行一次新手教學,也就是可以不需要下載新手教學這部分的包體.減少下載的流量.

以我看過的某遊戲來說,他們的設計是這樣的.首先帳號的 ID 是由本地隨機產生及提供給伺服器,本體包含登入介面提供繼承或社交連動等功能.所以進入遊戲後就可以判斷此帳號是否要進行新手教學.如果是新帳號.那麼這個時候會播放開頭動畫(因此動畫被包在本體中),在開頭動畫約數十秒的播放時間中,背景會開始下載新手教學包.等到開頭動畫播放完畢,正常情況新手教學包已經下載完畢.無縫接軌進入新手教學.新手教學是一個事先設計好能互動但沒有太多選擇看似正常的流程,譬如說只有一種特技.當新手教學進行的時候,背景則開始下載完整的遊戲下載包.新手教學結束的時則可以開始正常遊戲.

理想中,開頭動畫播放如果沒有被中斷,玩家是看不到下載條棒(檢查資源)的畫面.新手教學的長度如果不足於全部下載包下載的時間長度,則新手教學結束的時候還會有一段時間可以看到下載條棒(檢查資源)的畫面.

總結

到此為止,以上談論的架構大致上涵蓋了使用 AB 會遇到的議題.但是我想 Unity 編輯器的版本一直在推進.也有越來越多的函式庫及工具不斷出現.也有更多團隊設計出更優秀的框架及規範.本文拋磚引玉,希望在這個議題上稍加討論.

本文網址:https://wp.me/pBAPd-A2

 

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.