ToLua熱更新之LuaFramework框架之資源熱更新(2)
1、建立物體
為了除錯的方便,筆者先將框架配置為本地模式,待測試熱更新時再改成更新模式。
圖:配置為本地模式
先測試個簡單的建立物體,新建一個名為go的物體,然後設定它的座標為(1,1,1)。這段程式碼雖然不涉及資源載入,但能展示“把物體新增到場景中”的過程。Main.lua的程式碼如下:
function Main()
local go = UnityEngine.GameObject ('go')
go.transform.position = Vector3.one
end
圖:動態建立一個名為go的空物體
要熱更新資源,便需要製作資源。這裡製作一個名為tankPrefab的坦克模型預設,然後存到Assets/Tank目錄下。接下來對它做打包,然後動態載入。
圖:坦克預設
2、資源打包
LuaFramework在打包方面並沒有做太多的工作,我們需要手動打包。開啟Assets/LuaFramework/Editor/Packager.cs,按照示例的寫法,加上下面這一行:將Assets/Tank目錄下的所有預設(.prefab)打包成名為tank的包。
圖:修改打包程式碼
點選“Build Windows Resource”,即可在StreamingAssets中看到打包好的檔案。
圖:坦克資源的包檔案
如下圖所示,Unity3D資源包裡面包含多個資源,就像一個壓縮檔案一樣。在動態載入的時候,便需要有載入包檔案、或取包中的資源兩步操作(框架已經幫我們做好了這部分工作,直接呼叫API即可)。
圖:Unity3D的資源包
3、動態載入模型
編寫如下lua程式碼(main.lua),使用框架提供的資源管理器(resMgr)載入tank包的TankPrefab檔案,載入完成後回撥OnLoadFinish方法。在OnLoadFinish中使用Instantiate例項化物件。
--主入口函式。從這裡開始lua邏輯 function Main() LuaHelper = LuaFramework.LuaHelper; resMgr = LuaHelper.GetResManager(); resMgr:LoadPrefab('tank', { 'TankPrefab' }, OnLoadFinish); end --載入完成後的回撥-- function OnLoadFinish(objs) local go = UnityEngine.GameObject.Instantiate(objs[0]); LuaFramework.Util.Log("Finish"); end
完成後運行遊戲,即可看到動態加載出來的模型。
圖:動態加載出來的模型
4、載入資源的過程
只有理解了動態載入,即LoadPrefab的過程,才能算是真正的理解了熱更新。LoadPrefab為ResourceManager中定義的方法,在Assets\LuaFramework\Scripts\Manager\ResourceManager.cs中實現,建議配合程式碼看下面的解釋。
LoadPrefab的流程如下所示,先是判定當前是否正在載入該資源包,如果沒有則呼叫OnLoadAsset載入資源包、然後解包獲取資源、呼叫回撥函式。
圖:LoadPrefab的流程
ResourceManager類定義了m_AssetBundleManifest、m_Dependencies、m_LoadedAssetBundles、m_LoadRequests這4個變數,只要理解了這幾個變數的用途,也就能夠理解了資源載入的全過程了。這4個變數的型別如下:
圖:ResourceManager定義的幾個變數
m_AssetBundleManifest
理解m_AssetBundleManifest之前,需要先理解Unity3D的依賴打包。前面的tank.unity3D中,坦克的預設、坦克的貼圖等資源都被打包到一起,沒有依賴關係(只要打包時不給貼圖單獨打包,Unity3D會自動將預設相關的資源都打包進來)。如下圖所示。
圖:前面載入坦克製作的資源包
假如有兩個坦克預設共用一套貼圖,如果像上面那樣打包,每個坦克預設各自包含一份貼圖,資源會比較大。更好的辦法是將公共貼圖放到一個包裡,每個坦克預設不再包含貼圖(如下圖)。這種打包方式下,載入TankPrefab前,需要先載入依賴包common.unity3D,坦克預設才能找到貼圖。
圖:依賴打包
打包後,Unity3D會產生一個名為AssetBundle.manifest的檔案(框架會將該檔案放在StreamingAssets中),該檔案包含所有包的依賴資訊。所以在載入資源前需要先載入這個檔案,m_AssetBundleManifest便是指向這個包的變數。相關程式碼如下:
圖:m_AssetBundleManifest便是指向AssetBundle.manifest的變數
載入這個包後,便可以使用下面的語句獲取某個包所依賴的所有包名,然後載入它們。
string[] dependencies = m_AssetBundleManifest.GetAllDependencies(包名);
注:更多Unity3D的依賴打包的解釋,可以參見這篇文章:
m_LoadedAssetBundles
字典型別的m_LoadedAssetBundles儲存了所有已經載入資源包。如果某個包已經被載入過,那下次需要用到它時,直接從字典中取出即可,減少重複載入。簡化後的程式碼如下:
IEnumerator OnLoadAsset(XXX)
{
AssetBundleInfo bundle = GetLoadedAssetBundle(XXX);
if(!bundle)
bundle = OnLoadAssetBundle(名字);
載入資源
回撥函式處理
}
其中GetLoadedAssetBundle方法會判斷資源包是否存在於m_LoadedAssetBundles中,並返回資源包。OnLoadAssetBundle為重新載入資源包的方法。
載入資源包後,只需通過 bundle.LoadAssetAsync(資源名,型別)便可載入所需的資源。
m_Dependencies
m_Dependencies記錄了所有已載入資源的依賴包,以便在GetLoadedAssetBundle方法中判斷資源包是否被完全載入(主體包和所有依賴包都被載入才算完成載入)。簡化後的程式碼如下:
IEnumerator OnLoadAssetBundle(包名)
{
//獲取依賴包
string[] dependencies = m_AssetBundleManifest.GetAllDependencies(abName);
m_Dependencies.Add(abName, dependencies); //更新依賴表
//載入依賴包
for (int i = 0; i < dependencies.Length; i++)
OnLoadAssetBundle(XXX)
//然後載入主體資源
download = http://WWW.LoadFromCacheOrDownload(包名)
//更新載入表
m_LoadedAssetBundles.Add(XXX)
}
AssetBundleInfo GetLoadedAssetBundle(包名)
{
//判斷載入表
AssetBundleInfo bundle = m_LoadedAssetBundles[包名];
if (bundle == null) return null;
//判斷依賴包
foreach (string 依賴包名 in m_Dependencies[包名])
{
if (m_LoadedAssetBundles[依賴包名]== null)
return null;
}
return bundle;
}
m_LoadRequests
m_LoadRequests是一個<string, List<LoadAssetRequest>>型別的字典,LoadAssetRequest的定義如下,它用於非同步載入後的回撥。
圖:LoadAssetRequest
由於採用非同步載入,載入資源的過程中,程式有可能發起同一個請求,多次載入似乎有些浪費,如下圖所示。
圖:兩次載入同一資源
更好的辦法是,在收到第2次請求時先做判斷,如果該資源正在載入,那先記錄請求2的回撥函式,待資源載入完成,呼叫所有請求該資源的回撥函式,如下圖所示。m_LoadRequests便記錄每種資源的請求,使程式可以判斷該資源是否正在載入,並從m_LoadRequests獲取各個資源請求的回撥函式。
圖:記錄請求2的回撥函式
簡化後的程式碼如下:
void LoadAsset(包名)
{
If(m_LoadRequests[abName] == null)
{
m_LoadRequests[包名].Add(回撥函式等);
OnLoadAsset();
}
else
{
m_LoadRequests[包名].Add(回撥函式等);
}
}
IEnumerator OnLoadAsset(XXX)
{
載入包
載入資源
foreach( request in m_LoadRequests[包名] )
{
Request.回撥函式();
}
}
5、資源熱更新
“資源熱更新”和上一篇的“程式碼熱更新”完全相同,開啟更新模式後,將新的資原始檔複製到伺服器上,框架即可自動下載更新的資源。這裡不再複述。