Unity 開源雙端框架 ET 中初嘗熱更新技術

貓冬發表於2019-01-06

原文地址:http://frankorz.com/2019/01/06/hotfix-introduction-of-unity-et-framework/

ET 框架簡介

正所謂時勢造英雄,在 Web 開發領域或者傳統軟體開發領域中,人們把經過千錘百煉的程式碼總結出一套開發框架,從而提高開發效率,讓開發者能更專注於業務本身。對於遊戲領域而言,不同遊戲需求的東西也不一樣:有的遊戲對效能有著苛刻要求,有的遊戲需要快速地迭代出來,有的遊戲需要聯網熱更新等等。因此不同的遊戲框架應運而生。

例如:

  • Game Framework 是一個基於 Unity 引擎的遊戲框架,主要對遊戲開發過程中常用模組進行了封裝,很大程度地規範開發過程、加快開發速度並保證產品質量。
  • QFramework 一套漸進式的快速開發框架。框架內部積累了多個專案的在各個技術方向的解決方案。
  • Entitas 一套基於 C# 和 Unity 的實體元件系統。
  • Entities Unity 官方的實體元件系統實現,不過還是 Beta 版本,詳細介紹可以檢視官網
  • StrangeIoC 一套基於 C# 和 Unity 的控制反轉 (Inversion-of-Control) 框架。

今天介紹的是 ET 框架。

ET是一個開源的遊戲客戶端(基於unity3d)服務端雙端框架,服務端是使用C# .net core開發的分散式遊戲服務端,其特點是開發效率高,效能強,雙端共享邏輯程式碼,客戶端服務端熱更機制完善,同時支援可靠udp tcp websocket協議,支援服務端3D recast尋路等等

ET 框架能讓我們只用 C# 就能搞定前後端,熱更新方面也採用了基於 C# 的 IL 執行時——ILRuntime, 貫徹了 "珍愛生命,遠離 Lua" 這句話。目前自己接觸的大多是客戶端部分,因此伺服器方面不做介紹。

框架檔案結構

ET 官網 本身給了很多介紹,我們可以克隆 Git 倉庫到本地。

下面來看看每個資料夾的作用:

客戶端檔案結構

本文主要來介紹客戶端,因此進入到 Unity 資料夾,資料夾結構如下:

當前 Master 分支目前需要 Unity 2018.3 以上版本。使用之前需要參考下官方的 執行指南

在 VS 中重新編譯,或者 Rider Rebuild 一下專案。Scene 選擇 Scenes\Init.unity,點 Play 按鈕應該就能成功執行,看到登陸介面。

元件設計

ET 框架使用了元件的設計,一切都是實體(Entity)和元件(Component),官方文件 元件設計 介紹的很詳細。

看完文件,我們來看看專案程式碼的啟動入口。

這個 Init.cs 檔案,在 Model 資料夾下。可能有同學注意到 Hotfix 資料夾下也有一個 Init.cs 檔案,而且這兩個資料夾的結構大同小異,兩邊都有一些相同的檔案,而它們只是名稱空間不一樣。這是因為我們用到 ILRuntime,而 ILRuntime 最好不要跨域繼承。

Model/Init.cs 檔案中

private async ETVoid StartAsync()
{
    try
    {
        ...
        // 新增了元件,就賦予了各種功能。
        // 例如加了 Timer 元件,就有了計時功能
        Game.Scene.AddComponent<TimerComponent>();
        ...

        // 下載熱更用的 AssetBundle 包
        await BundleHelper.DownloadBundle();
        // 載入熱更用的dll等檔案,呼叫 Hotfix/Init.cs
        Game.Hotfix.LoadHotfixAssembly();

        // 載入配置
        Game.Scene.GetComponent<ResourcesComponent>().LoadBundle("config.unity3d");
        Game.Scene.AddComponent<ConfigComponent>();
        // 載入後解除安裝相應的 AB 包
        Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle("config.unity3d");
        Game.Scene.AddComponent<OpcodeTypeComponent>();
        Game.Scene.AddComponent<MessageDispatcherComponent>();

        Game.Hotfix.GotoHotfix();

        Game.EventSystem.Run(EventIdType.TestHotfixSubscribMonoEvent, "TestHotfixSubscribMonoEvent");
    }
    ...
}

通過元件設計,可以輕易地載入元件和解除安裝元件,例如我可以寫一個心跳包元件來每隔30秒傳送一個心跳包到伺服器,當我需要這個元件的時候,可以直接 AddComponent,不需要的時候可以 RemoveComponent 移除元件。

登陸介面的熱更新啟動過程

接下來看到 Hotfix/Init.cs 檔案中

public static void Start()
{
    try
    {
        // 註冊熱更層回撥
        ETModel.Game.Hotfix.Update = () => { Update(); };
        ETModel.Game.Hotfix.LateUpdate = () => { LateUpdate(); };
        ETModel.Game.Hotfix.OnApplicationQuit = () => { OnApplicationQuit(); };
        ...
        // 載入熱更配置
        ETModel.Game.Scene.GetComponent<ResourcesComponent>().LoadBundle("config.unity3d");
        Game.Scene.AddComponent<ConfigComponent>();
        ETModel.Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle("config.unity3d");

        UnitConfig unitConfig = (UnitConfig)Game.Scene.GetComponent<ConfigComponent>().Get(typeof(UnitConfig), 1001);
        Log.Debug($"config {JsonHelper.ToJson(unitConfig)}");
        // 傳送事件來啟動介面
        Game.EventSystem.Run(EventIdType.InitSceneStart);
    }
    ...
}

來看看傳送的事件,程式碼在 Hotfix\Module\Demo\UI\UILogin\System\InitSceneStart_CreateLoginUI.cs

namespace ETHotfix
{
    // 用 Attribute 來註冊事件
    [Event(EventIdType.InitSceneStart)]
    public class InitSceneStart_CreateLoginUI: AEvent
    {
        public override void Run()
        {
            UI ui = UILoginFactory.Create();
            // 這裡就是啟動登陸介面的地方,介面可以直接 add 或者 remove
            Game.Scene.GetComponent<UIComponent>().Add(ui);
        }
    }
}

再來看看一個介面是怎麼生成的,程式碼在 Hotfix\Module\Demo\UI\UILogin\System\UILoginFactory.cs

public static UI Create()
{
        ...
    ResourcesComponent resourcesComponent = ETModel.Game.Scene.GetComponent<ResourcesComponent>();
        // 讓資源元件讀取登陸介面的 AB 包
    resourcesComponent.LoadBundle(UIType.UILogin.StringToAB());
        // 從 AB 包拿到登陸介面的 GameObject
    GameObject bundleGameObject = (GameObject) resourcesComponent.GetAsset(UIType.UILogin.StringToAB(), UIType.UILogin);
    GameObject gameObject = UnityEngine.Object.Instantiate(bundleGameObject);

    UI ui = ComponentFactory.Create<UI, string, GameObject>(UIType.UILogin, gameObject, false);
        // 新增登陸介面元件
    ui.AddComponent<UILoginComponent>();
    return ui;
        ...
}

來看看登陸介面元件,程式碼在 Hotfix\Module\Demo\UI\UILogin\Component\UILoginComponent.cs

public class UILoginComponent: Component
{
    private GameObject account;
    private GameObject loginBtn;

    public void Awake()
    {
        // 通過引用來獲取 UI 元件,再為其新增點選事件
        ReferenceCollector rc = this.GetParent<UI>().GameObject.GetComponent<ReferenceCollector>();
        loginBtn = rc.Get<GameObject>("LoginBtn");
        loginBtn.GetComponent<Button>().onClick.Add(OnLogin);
        this.account = rc.Get<GameObject>("Account");
    }

    public void OnLogin()
    { // 有興趣可以再進去看看 OnLoginAsync,其中登陸的 Session 連線了伺服器地址 127.0.0.1:10002
        LoginHelper.OnLoginAsync(this.account.GetComponent<InputField>().text).Coroutine();
    }
}

伺服器地址存在了 Tools 選單中的全域性配置,上面的資源路徑則是熱更新伺服器的地址。

遊戲執行後,在 Hierarchy 介面中也可以看到元件的結構,其中 uilogin.unity3d 就是登陸介面的 AB 包引用:

這就是通過熱更新邏輯生成的介面,也就是說,上面的程式碼讓我們可以通過熱更新來給應用載入各種介面和改寫頁面跳轉邏輯,當然還可以通過熱更來增加修改遊戲邏輯和功能。

如果不喜歡這種頁面載入方式,可以考慮不使用 Hotfix/Init.cs 中的 Game.Scene.AddComponent<UIComponent>(); 這個 UIComponent,而使用其他 UI 元件,例如 ET-Modules 中的 FairyGUI 元件,讓 FairyGUI 來單獨負責 UI 介面。這裡也可以看出基於元件的框架的靈活性,我以後也會出文章單獨介紹 FairyGUI。

熱更新切換

首先看看作者的介紹:

7.客戶端熱更新一鍵切換 因為ios的限制,之前unity熱更新一般使用lua,導致unity3d開發人員要寫兩種程式碼,麻煩的要死。之後幸好出了ILRuntime庫,利用ILRuntime庫,unity3d可以利用C#語言載入熱更新dll進行熱更新。ILRuntime一個缺陷就是開發時候不支援VS debug,這有點不爽。ET框架使用了一個預編譯指令ILRuntime,可以無縫切換。平常開發的時候不使用ILRuntime,而是使用Assembly.Load載入熱更新動態庫,這樣可以方便用VS單步除錯。在釋出的時候,定義預編譯指令ILRuntime就可以無縫切換成使用ILRuntime載入熱更新動態庫。這樣開發起來及其方便,再也不用使用狗屎lua了 8.客戶端全熱更新 客戶端可以實現所有邏輯熱更新,包括協議,config,ui等等

預編譯指令指的就是在 Player Setting 中,上圖右下角箭頭指著的地方。當前有兩個預編譯指令,通常在開發中,可以只填寫 NET452,這樣可以得到完整的堆疊資訊來除錯程式。還有一個預編譯指令 ASYNC,加上後,應用就會從前面填寫的熱更新伺服器下載熱更包,該指令在後文會提到。

在國內環境下,手機遊戲熱更新的需求較強烈。市場上手機系統普遍分成 Android 和 iOS 陣營,其中 iOS 不支援 JIT 熱更,因此 ET 框架給了兩種選擇:ILRuntime 熱更新和 Mono 熱更新。

兩者概念可以參考文末的參考連結,在這裡不多說。

體驗熱更新

體驗熱更新之前,先把專案切到 Android 平臺。

按照下圖配置 Mono 熱更新:

確保 Scripting Backend 為 Mono,下面預編譯巨集去掉 ILRuntime,加上 ASYNC,按下Enter鍵執行變更。ASYNC 說明我們現在的熱更新資源從資源伺服器中獲取,這裡的熱更新資源包括 Res 資料夾、Bundles 資料夾、Hotfix 資料夾中的程式碼等。在這個例子中,登陸介面的程式碼就已經寫在熱更新資料夾中了,我們將嘗試通過熱更新來展示登陸介面。

點選 Play 按鈕,會有兩個報錯:

第二個 Log 資訊展示了應用想要獲取資源的熱更資源伺服器地址,這個地址可以在 Tools 選單的全域性配置中找到。報錯資訊提示找不到終端主機。報錯是理所當然的,因為我們還沒有啟動本地伺服器。

首先要生成熱更新檔案,在 Tools 選單中點選打包工具,如下圖所示:

平臺選擇當前的 Android 平臺,目前不需要打包應用,所以無視第一個單選按鈕。

前面的思維導圖提到了 ET 根目錄的 Release 資料夾存的就是熱更新資原始檔。打包工具也會把打包後的資源放在 Release 資料夾下。而第二個按鈕指的是是否把打包的熱更新資源也放在應用中,目前也不需要選擇。開啟熱更新後,應用會比較伺服器和本地應用的 Version 檔案,計算檔案差異後才會下載相關的熱更新資原始檔。

如果勾選了第二個按鈕,打包工具將會把資源也複製到 Assets/StreamingAssets 資料夾下,同時更新 Version 檔案,這樣我們將不能測試下載熱更包的過程。

點選開始打包後,熱更檔案就生成了:

再點選 Tools 選單中的 web 資源伺服器開啟對映了 Release 資料夾的本地檔案伺服器。

點選 Play 按鈕,應用通過下載熱更新資源,生成了登陸介面,也把熱更資源下載到了應用中,也就是 Assets/StreamingAssets 資料夾。

重新啟動 web 資源伺服器清除 log 資訊,再次執行應用,會發現沒有再次下載熱更新資源。因為對比了Version 檔案後,應用本地的檔案已經不需要更新了。

至此,我們完成了一次完整的熱更新。

總結

ET 框架給了我們一種統一的開發體驗,提供了方便的熱更新切換和除錯方案,這足以支撐起一些小遊戲的開發需求,有需要的同學可以瞭解下 ET 框架~

2019 年立了個 Flag:周更技術部落格,歡迎督促和交流,也歡迎常來我部落格 螢火之森 逛!

參考

相關文章