最近實(shí)現(xiàn)了 Unity 的熱更新葵袭,這里記錄一下實(shí)現(xiàn)的過程尸饺。
引入 Xlua 框架
熱更新大致可以分為代碼的更新與資源的更新。在代碼更新這部分沈堡,我引入了 XLua 框架静陈,使用 Lua 編寫游戲邏輯。需要更新游戲邏輯時(shí),下載新的 Lua 文件鲸拥,并在運(yùn)行中加載即可拐格。
//在游戲主邏輯啟動(dòng)時(shí),新建LuaEnv刑赶,并添加自定義Loader禁荒,讀取指定lua文件
luaEnv = new LuaEnv();
luaEnv.AddLoader((ref string filePath) =>
{
//luaNameToFilePath是一個(gè)Dictionary,鍵值分別為lua文件名與lua文件所在的路徑角撞,在新建LuaEnv前初始化
if(luaNameToFilePath.ContainsKey(filePath))
{
string realPath = luaNameToFilePath[filePath];
filePath = realPath;
//讀取并返回特定lua文件的所有字節(jié)
return File.ReadAllBytes(realPath);
}
return null;
});
注意以上代碼僅能在編輯器模式下運(yùn)行呛伴,在游戲編譯后,Lua 文件會(huì)打包進(jìn)入 AssetBundle谒所,上文中的自定義 Loader 需要修改為從 AssetBundle 加載 Lua 文件热康。
XLua 的詳細(xì)使用方法,Lua 與 C# 間的通信可以參考官方文檔:XLua: 騰訊開源劣领,基于Unity姐军、Lua的熱更新技術(shù),本文不做過多介紹尖淘。
Unity 中的游戲邏輯通常寫在 Start
奕锌,FixedUpdate
,Update
村生,LateUpdate
這幾個(gè)方法中惊暴,因此我在 Lua 的主邏輯代碼中,聲明了同名 Function趁桃,并在 C# 端訪問并調(diào)用辽话,這樣就可以只在 Lua 端編寫大部分游戲邏輯。
startFunction = luaEnv.Global.Get<LuaStart>("Start");
fixedUpdateFunction = luaEnv.Global.Get<LuaUpdate>("FixedUpdate");
updateFunction = luaEnv.Global.Get<LuaUpdate>("Update");
lateUpdate = luaEnv.Global.Get<LuaUpdate>("LateUpdate");
private void FixedUpdate()
{
if(fixedUpdateFunction != null)
{
fixedUpdateFunction(Time.fixedDeltaTime);
}
}
//....其他方法類似
動(dòng)態(tài)創(chuàng)建場(chǎng)景中物體
場(chǎng)景中的物體應(yīng)當(dāng)在運(yùn)行過程中創(chuàng)建卫病,這樣需要更新場(chǎng)景中的部分資源時(shí)油啤,只需要更新對(duì)應(yīng)的資源即可。如果所有物體直接保存在場(chǎng)景中蟀苛,那么對(duì)場(chǎng)景的任何更改都需要更新整個(gè)場(chǎng)景文件益咬。
因?yàn)樵擁?xiàng)目主要是為了了解熱更新的原理,為了節(jié)省工作量帜平,我僅將場(chǎng)景中的部分可互動(dòng)物體(可收集的金幣幽告,鉆石)信息序列化為 Json 文件,并在運(yùn)行過程中動(dòng)態(tài)創(chuàng)建罕模。
打包 AssetBundle
構(gòu)建 AssetBundle 需要使用 BuildPipeline.BuildAssetBundles
方法评腺,需要注意該方法在 UnityEditor
命名空間中帘瞭,該命名空間中的類在編譯后是無法使用的淑掌,因此最好拓展編輯器,并在編輯器拓展腳本中調(diào)用該方法蝶念。
打包 Lua 文件時(shí)的注意事項(xiàng)
另外還有一點(diǎn)需要注意抛腕,Unity 無法識(shí)別后綴為 .Lua
的文件芋绸,需要將 Lua 文件的后綴該為 .txt
或 .bytes
,讓 Unity 將代碼識(shí)別為 TextAsset担敌,才能正確打包進(jìn) AssetBundle 并在運(yùn)行中讀取摔敛。如果在編寫 Lua 代碼時(shí)就使用 .txt
或 .bytes
后綴,很多 Lua 編輯器的功能就無法使用了全封,為了開發(fā)方便马昙,我在編輯器腳本中添加了額外的方法,在構(gòu)建 AssetBundle 時(shí)刹悴,將所有 .Lua
文件復(fù)制到 Unity 中的 Assets
目錄并更改后綴為 .bytes
行楞。這樣就可以正常使用 Lua 編輯器進(jìn)行開發(fā),同時(shí)又不影響 AssetBundle 打包土匀。
總 Manifest 文件
在構(gòu)建 AssetBundle 后子房,除了會(huì)生成用戶指定的 AssetBundle,Unity 還會(huì)自動(dòng)生成一個(gè)額外的 AssetBundle就轧,默認(rèn)情況下該 AssetBundle 與輸出路徑最內(nèi)層文件夾名相同证杭,例如我設(shè)定的輸出路徑為:E:\guyu\projects\unity\UnityJumpJump\jump-jump\Assets\StreamingAssets
,在該路徑下就會(huì)出現(xiàn) StreamingAssets
以及 StreamingAssets.manifest
文件妒御。
在該 AssetBundle 中包含名為"assetbundlemanifest"的總 Manifest 文件解愤,總 Manifest 文件記錄了所有 AssetBundle 間的依賴關(guān)系,在運(yùn)行中加載 AssetBundle 時(shí)需要先從該 AssetBundle 讀取 assetbundlemanifest 文件乎莉,確定所有需要加載的 AssetBundle琢歇。在與服務(wù)器簡(jiǎn)歷鏈接并判斷哪些資源需要熱更新時(shí),也需要從本地與服務(wù)器讀取該 AssetBundle梦鉴。
為了保證在運(yùn)行中可以快速找到該 AssetBundle李茫,我在拓展編輯器中添加了代碼,將該 AssetBundle 重命名為“ResourceMap”肥橙,服務(wù)器中也使用相同的名稱魄宏。
運(yùn)行中加載 AssetBundle
AssetBundle 的存放路徑
存放 AssetBundle 的路徑主要有兩個(gè):Application.streamingAssetsPath
和 Application.persistentDataPath
。
Application-streamingAssetsPath是流媒體文件路徑存筏,在編輯器下對(duì)應(yīng)的路徑為 Assets/StreamingAssets
宠互,該路徑下的文件會(huì)在游戲打包的過程中會(huì)一同打進(jìn)包里,但是在運(yùn)行過程中,該路徑下的文件只能讀取灼伤,不能寫入动遭,通常將游戲初始需要的 AssetBundle 放在該路徑下,在運(yùn)行過程中拷貝到 persistentDataPath券册。
需要注意的是,在安卓平臺(tái)無法直接使用訪問文件的方式從該路徑下讀取文件,需要使用 Networking.UnityWebRequest 訪問烁焙。
Application-persistentDataPath 為可持續(xù)化數(shù)據(jù)路徑航邢,該路徑下允許讀取與寫入文件,通常將游戲存檔文件骄蝇,更新的 AssetBundle 文件存放在該路徑下膳殷,需要注意的是,該路徑只有在游戲運(yùn)行時(shí)才會(huì)存在九火,在編輯器下是不存在的赚窃。這個(gè)路徑的操作就簡(jiǎn)單很多了,直接使用文件操作的方法就去可以在該路徑下讀寫文件岔激。
資源加載模塊
加載資源時(shí)需要避免內(nèi)存中存在重復(fù)的資源考榨,因此使用單例模式實(shí)現(xiàn)資源加載模塊。
資源加載的邏輯是先在 persistentDataPath 下查找 AssetBundle 文件鹦倚,如果文件不存在河质,則在 streamingAssetsPath 下查找,并將文件復(fù)制到 persistentDataPath震叙。
加載資源成功后掀鹅,不論是 AssetBundle 還是 Asset,都應(yīng)當(dāng)緩存在內(nèi)存中媒楼,再次請(qǐng)求時(shí)乐尊,直接從緩存中返回,這樣也可以避免 AssetBundle 重復(fù)加載的問題划址。
從 AssetBundle 中加載場(chǎng)景
從 AssetBundle 中加載場(chǎng)景有些特殊扔嵌,只需要將場(chǎng)景文件所在的 AssetBundle 加載到內(nèi)存中,就可以直接調(diào)用切換場(chǎng)景的方法夺颤,并不需要通過 AssetBundle.LoadAsset
方法從 AssetBundle 中加載場(chǎng)景文件痢缎。
UnityWebRequest assetBundle = UnityWebRequestAssetBundle.GetAssetBundle(path);
yield return assetBundle.SendWebRequest();
AssetBundle scenes = DownloadHandlerAssetBundle.GetContent(assetBundle);
//場(chǎng)景所在AssetBundle的緩存
loadedScenesBundle = scenes;
//封裝的切換場(chǎng)景方法,會(huì)在切換場(chǎng)景前加載場(chǎng)景依賴的資源
StageAssetsLoader.StageSwitcher(sceneName);
資源更新
服務(wù)器配置
該項(xiàng)目中我使用 node.Js 搭建了一個(gè)本地服務(wù)器世澜,啟動(dòng)后監(jiān)聽 8888
端口独旷,服務(wù)器只包含兩個(gè)功能:
- 根據(jù)請(qǐng)求 url 中的平臺(tái)和資源名,返回服務(wù)器中的指定文件寥裂;
- 如果請(qǐng)求 url 以“/ServerMD5_”開頭嵌洼,則返回對(duì)應(yīng)平臺(tái) ResourceMap(上文提到的包含總 manifest 文件的 AssetBundle) 的 MD5 碼;
客戶端
客戶端在啟動(dòng)時(shí)連接服務(wù)器封恰,獲取服務(wù)器上 ResourceMap 文件的 MD5 碼麻养,同時(shí)與本地 ResourceMap 文件的 MD5 碼進(jìn)行比較,如果兩者一致诺舔,則表示客戶端與服務(wù)器的資源版本一致鳖昌,不需要更新备畦,可以直接進(jìn)入游戲。
如果兩者不一致遗遵,則從服務(wù)器請(qǐng)求 ResourceMap 文件并讀取其中的 assetbundlemanifest萍恕,與本地 assetbundlemanifest 進(jìn)行對(duì)比逸嘀,篩選需要更新的 AssetBundle车要,從服務(wù)器下載指定的 AssetBundle,完成資源更新崭倘。