ET學習筆記之五星麻將0

前言

HexMap無限地圖已經悄無聲息的完成了,之所以悄無聲息的完成,是因爲棄用了ECS框架,棄用的原因在於不夠成熟,而我要開發的遊戲需要成熟的解決方案。既然不成熟,那麼當初爲啥要入坑呢?豈不是浪費了太多時間,在Unite 哥本哈根2019的大會上,官方演示了ECS的多人在線遊戲的案例,這讓ECS或DOTS着實讓人期待,這也是明知不成熟,還是要入坑的原因:一項新技術的吸引力對於一個好奇心強的程序猿來說,幾乎是不可抗拒的,所以說優化是程序猿的致命毒藥,而新技術從某種層面上講,就是優化。
試問:一個武林高手在拿到絕世武功祕籍的時候,如何能夠放手?誰不想煉就絕世神功?
我們程序猿時時刻刻都在面臨優化的誘惑,這種歇斯底里的渴望促使我們不斷學習,不斷進步。
我們在靈魂深處知道:我們離Matrix還有非常非常遙遠的距離,程序猿想要成爲新世界的神!
所以這是一場造神革命,我們可以看到未來,因此我們才知道差距有多大,因此才肝着努力。
這樣寫也許太誇張了,不過內心深處就是有這樣誇張的渴望,實際上已經飢渴難耐到飲鴆止渴了。
不得已,個人技術水平太Low,不得不沿用原作者的OOP架構,貌似回到了原點。於是耐着性子把原作者的教程拜讀了一遍,寫得太好了,所以根本沒有補充的地方。
站在巨人的肩膀上,無限地圖順利完成,我做的無非是導入了一些Polygon風格的資源,使地圖看起來更漂亮一些。這些都不值得大書特書,有興趣的朋友看看原教程都可以輕易完成。
無限地圖完成後,終於又要開新坑了,我的獨立遊戲是末世生存遊戲,我希望末世有着無限的挑戰,因而做了無限地圖。接下來我需要這款遊戲可以像饑荒那樣聯網,可以使用多人模式,也可以單機模式。
因爲我需要一個全面的解決方案,之前研究了太多框架GameFrameworkSkynetYouyouFrameworkxluaFramework等等,研究的這些東西都是非常優秀的,但是好的不一定是適合的。每個框架在設計的時候,或多或少都有取捨,畢竟這是一個百家爭鳴的時代,非常多傑出的作者開源了自己的傑作,他們都有自己的優勢。
我最終選擇了ET,它的優勢作者熊貓國寶已經表述得非常明瞭,這些優勢使我最終下定決心要使用ET,如果DOTS革新成熟了,也許會融入進來,在那之前我會安安心心地夯實ET開發之路。
在開始學習筆記之前,我已經學習了兩天ET了,以下是我的學習路徑:

兩天時間做了以上研究,即便是這樣,我覺得離我的獨立遊戲還有距離。我需要研究一個更加完整的案例,於是我克隆了五星麻將,當然也可以選擇鬥地主案例,選擇五星麻將的原因是我以前開發過一款麻將遊戲,所以對麻將比較熟悉。那個時候一下班就被老闆叫去機麻,爲了熟悉麻將邏輯。後來中途又被調到老虎機項目,委以重任。扯遠了,總之選擇了五星麻將來作爲自己的入門案例。

準備工作

其實上面例舉的學習路徑也算是準備工作了,畢竟五星麻將還是有一定難度的。
如果上面的案例大家都掌握了,那麼就開始着手準備五星麻將案例吧:
0下載Unity編輯器(2018.4.5f1 or 更新的版本),if(已經下載了)continue;
1克隆:git clone https://github.com/wufanjoin/fivestar.git --recurse下載Zip壓縮包
2如果下載的是壓縮包,解壓。將$GitProject\fivestar文件夾下的Unity添加到Unity Hub項目中;
3用Unity Hub打開項目:Unity,等待Unity進行編譯工作;
4打開項目後,啓動場景在Scenes目錄下,打開Init場景。

配置報錯

按照作者要求進行本地配置時報錯了,操作參考運行指南,錯誤如下圖所示:
加載配置失敗
點擊去發現了MongoHelper類衝突了,如下圖所示:
衝突
解決方法很簡單,既然衝突了,改個名字就好了。利用VS的重命名快捷鍵對MongoHelper重命名爲MongoHelpero。於是可以順利加載配置文件了,如下圖所示:
本地服務器配置
配置好了,順利啓動遊戲,如下圖所示:
登錄界面

熱更新模式

如果閣下走過前言列出的學習路徑的話,一些基礎知識我就不詳細講解了,畢竟在下也是一知半解的狀態。不過,接下來的熱更新就是難點了,所以還是稍微解釋一下下吧。
ET的熱更新方案正如作者所說:

因爲ios的限制,之前unity熱更新一般使用lua,導致unity3d開發人員要寫兩種代碼,麻煩的要死。之後幸好出了ILRuntime庫,利用ILRuntime庫,unity3d可以利用C#語言加載熱更新dll進行熱更新。ILRuntime一個缺陷就是開發時候不支持VS debug,這有點不爽。ET框架使用了一個預編譯指令ILRuntime,可以無縫切換。平常開發的時候不使用ILRuntime,而是使用Assembly.Load加載熱更新動態庫,這樣可以方便用VS單步調試。在發佈的時候,定義預編譯指令ILRuntime就可以無縫切換成使用ILRuntime加載熱更新動態庫。

是利用ILRuntime庫來進行熱更的,原理作者解釋了,就是把需要熱更的代碼打成dll(Dynamic Link Library的英文縮寫,意思是動態鏈接庫),dll的方便之處在於動態加載,從而使代碼得到更新,而不必重新安裝整個程序,這就是所謂的熱更新了。那麼具體如何操作呢?
其實之前的教程已經解釋過了,我就再演示一下吧,如下圖所示:
熱更新解決方案
開發的時候把需要熱更新的代碼放到Hotfix解決方案下面,就可以熱更新了,當前添加宏之類的操作就不必我囉嗦了。實際上ET支持整個項目都熱更新,爲了省事,也爲了備不時之需,我覺得完全可以把所有開發的代碼都放在Hotfix下面,全部熱更就好了。當然,如果確定是萬年不變的代碼,其實也沒必要放進來,這樣可以減少熱更新的開銷,熱更新必然是有代價的,具體這裏就不解釋了(其實我也不甚明白Orz)。

熱更新程序集HotfixAssembly

五星麻將作者提到過字段isNetworkBundle,使用該字段就可以控制是否使用網絡資源了,如果你想使用就勾選true,這樣的話需要部署一個文件服務器,這個肉餅老師的視頻中演示了,我就不多說了。
我們這裏不勾選,按照本地流程走,按照ET的流程,初始化需要添加一大堆需要用的組件,以備不時之需。這些屬於基本操作了,因此也不做詳細解釋了,下面是熱更新的入口:

			    //加載熱更項目
			    Game.Hotfix.LoadHotfixAssembly();

通過這一個方法,我們就加載了熱更新的代碼了,流程如下:

		/// <summary>
        /// 加載熱更新程序集
        /// </summary>
		public void LoadHotfixAssembly()
		{
            //0.加載打包的代碼資源包,內含熱更新代碼程序集dll動態鏈接庫,對應的路徑:Assets\Res\Code
            Game.Scene.GetComponent<ResourcesComponent>().LoadBundle($"code.unity3d");
            //1.從加載的AssetBundle資源中獲取代碼資源並轉化成遊戲對象
			GameObject code = (GameObject)Game.Scene.GetComponent<ResourcesComponent>().GetAsset("code.unity3d", "Code");
			
            //2.從遊戲對象上獲取對應的動態鏈接庫和程序數據庫資源轉化成字節
			byte[] assBytes = code.Get<TextAsset>("Hotfix.dll").bytes;
			byte[] pdbBytes = code.Get<TextAsset>("Hotfix.pdb").bytes;

#if ILRuntime
            //因爲設置了ILRuntime的宏,所以會進入到這裏,這意味着熱更新模式運行遊戲
            Log.Debug($"當前使用的是ILRuntime模式");
            //3.獲取熱更庫的環境域,這個屬於ILRuntime的知識了
            this.appDomain = new ILRuntime.Runtime.Enviorment.AppDomain();

            //4.把動態鏈接庫庫和PDB(Program Database File,程序數據庫文件)加入內存
            this.dllStream = new MemoryStream(assBytes);
			this.pdbStream = new MemoryStream(pdbBytes);
            //5.通過內存加載上面的資源
			this.appDomain.LoadAssembly(this.dllStream, this.pdbStream, new Mono.Cecil.Pdb.PdbReaderProvider());
            //6.熱更代碼的啓動方法,直接定位到ETHotfix.Init類下的啓動方法
            this.start = new ILStaticMethod(this.appDomain, "ETHotfix.Init", "Start", 0);
			//7.熱更類型通過反射
			this.hotfixTypes = this.appDomain.LoadedTypes.Values.Select(x => x.ReflectionType).ToList();
#else
			Log.Debug($"當前使用的是Mono模式");

			this.assembly = Assembly.Load(assBytes, pdbBytes);

			Type hotfixInit = this.assembly.GetType("ETHotfix.Init");
			this.start = new MonoStaticMethod(hotfixInit, "Start");
			
			this.hotfixTypes = this.assembly.GetTypes().ToList();
#endif
			//8.秉承過河拆橋的原則,呸,優化內存的原則,卸載AssetBundle資源
			Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle($"code.unity3d");
		}
	

這個流程是定死的,也不用去改動,照着用即可。

熱更新啓動GotoHotfix

上面爲熱更新的使用鋪平了道路,只需一聲令下即可開始運行熱更新裏面的代碼,命令如下:

                //執行熱更項目
                Game.Hotfix.GotoHotfix();

這行代碼最終執行的是ETHotfix.Init.Start方法,只是過程比較委婉曲折而已,下面正式啓動:

                // 註冊熱更層回調
                ETModel.Game.Hotfix.Update = () => { Update(); };
				ETModel.Game.Hotfix.LateUpdate = () => { LateUpdate(); };
				ETModel.Game.Hotfix.OnApplicationQuit = () => { OnApplicationQuit(); };

			    //添加UI組件
                Game.Scene.AddComponent<UIComponent>();
				Game.Scene.AddComponent<OpcodeTypeComponent>();
				Game.Scene.AddComponent<MessageDispatcherComponent>();
							
                // 加載熱更配置
                ETModel.Game.Scene.GetComponent<ResourcesComponent>().LoadBundle("config.unity3d");
				Game.Scene.AddComponent<ConfigComponent>();
				ETModel.Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle("config.unity3d");

                //房間配置
			    AnnouncementConfig cardFiveStarRoom = (AnnouncementConfig)Game.Scene.GetComponent<ConfigComponent>().Get(typeof(AnnouncementConfig), 1);
		
                Log.Debug($"config {JsonHelper.ToJson(cardFiveStarRoom)}");

			    //直接添加Session組件
                Game.Scene.AddComponent<SessionComponent>();

                //GameGather新加的組件
			    Game.Scene.AddComponent<VersionsShowComponent>();//版本號顯示組件
                Game.Scene.AddComponent<KCPUseManage>();//KCP使用組件
			    Game.Scene.AddComponent<UserComponent>();//用戶信息管理組件
			    Game.Scene.AddComponent<ToyGameComponent>();//遊戲場景 管理組件
                Game.Scene.AddComponent<MusicSoundComponent>();//音樂 音效組件
                Game.Scene.AddComponent<FrienCircleComponet>();//親友圈組件
                Game.Scene.GetComponent<ToyGameComponent>().StartGame(ToyGameId.Login);
			    GameObject.Find("Reporter").SetActive(ETModel.Init.IsAdministrator);//打印日誌

這裏打印的配置文件如下圖所示:
配置文件
由此我們就知道了配置文件的使用方式了,類名和配置都是AnnouncementConfig,所以就能在加載的資源中進行刷選。底層的實現大家可以自行閱讀源碼,這裏我只需要知道如何使用即可。
當然,我選擇與底層分離的原因主要是水平有限,不想消耗精力去研究底層,而我要開發的遊戲僅僅關心如何實現高級的功能,而沒有太多富餘的時間去研究底層(富餘的時間都去打牌了Orz)。
所以,原本要研究代碼的時間用來打了三圈牌,輸了120大洋(輸的是從老爹那裏借來的錢,借200,最終還80,想想自己真是坑爹!),於是乾脆跳過一些底層實現。

登錄Login

Anyway,熱更新跑起來了,上面的代碼StartGame(ToyGameId.Login);直接跳轉到登錄界面。
基於ET的事件機制,其實在ComponentFactory.Create<ToyGameComponent>();的時候就觸發了下面的事件,當然組件工廠(ComponentFactory)的Create方法是由AddComponent方法間接觸發的。

    [ObjectSystem]
    public class ToyGameComponentAwakeSystem : ETHotfix.AwakeSystem<ToyGameComponent>
    {
        public override void Awake(ToyGameComponent self)
        {
            self.Awake();
        }
    }

所以AddComponent是觸發上面事件的始作俑者,當然底層做了非常多的工作,詳見肉餅老師的解說視頻。我們這裏只需知道會觸發該事件,還是那句老話,水平有限,停留於使用層面。
該事件的使用方式其實作者寫得非常清楚了,整個ET的運行都是基於這樣的事件機制的,因而事件機制是必修課,基於事件機制我們可以做很多工作,就像這裏的Awake方法:

        /// <summary>
        /// 由事件機制觸發的喚醒方法,在每次添加組件的時候執行一次
        /// </summary>
        public void Awake()
        {
            mGameAisleBaseDic.Clear();
            List<Type> types = Game.EventSystem.GetTypes();

            foreach (Type type in types)
            {
                object[] attrs = type.GetCustomAttributes(typeof(ToyGameAttribute), false);
           
                if (attrs.Length == 0)
                {
                    continue;
                }
                ToyGameAttribute toyGameAttribute= attrs[0] as ToyGameAttribute;
                ToyGameAisleBase toyGameAisleBase = Activator.CreateInstance(type) as ToyGameAisleBase;
                toyGameAisleBase.Awake(toyGameAttribute.Type);
                mGameAisleBaseDic.Add(toyGameAttribute.Type, toyGameAisleBase);
            }
        }

這裏的Awake幹了啥?其實我也是一知半解,只能靠着程序猿的本能猜測一下,大概是維護一個字典,這個字典的用途大概是根據遊戲類型(ToyGameAttribute)來獲取不同的通道(ToyGameAisleBase)。
ToyGameAisleBase負責對應遊戲類型的進出,進進出出的地方就是通道了Orz,它有很多子類,後面會講。
有了Awake所做的工作,我們可以過渡到Start方法了,也就是上面調用的StartGame(ToyGameId.Login);

        public void StartGame(long gameType,params object[] objs)
        {
            if (mGameAisleBaseDic.ContainsKey(gameType))
            {
                if (CurrToyGame != ToyGameId.None)
                {
                    mGameAisleBaseDic[CurrToyGame].EndAndStartOtherGame();
                }
                mGameAisleBaseDic[gameType].StartGame(objs);
            }
            else
            {
                Log.Error("想要進入的遊戲不存在:"+ gameType);
            }
        }

因爲有Awake所做的工作,所以mGameAisleBaseDic字典裏面纔有對應的Key,否則就會報錯。
這些Key實際上就是ToyGameId,這些是屬於自定義的字段,應該是通過發射機制註冊的,如果沒有明白我在說什麼,這裏是入門指南傳送門,走你。當然這純屬我的猜測,總之不必在意這些細節,我們只需知道字典裏有什麼,我們該如何使用,我的格言大概就是:站在巨人的肩膀上致敬巨人!

namespace ETHotfix
{
    public class ToyGameId
    {
        public const long None = 0;
        public const long Login = 1;
        public const long Lobby = 2;
        public const long Common = 1000;
        public const long JoyLandlords = 1001;
        public const long CardFiveStar =1002;
        public const long CardFiveStarVideo = 2002;
    }
}

字典裏面其實就是上面這些Key了,我們可以在這裏自定義自己想做的遊戲類型,上面已經定義了登錄、大廳等字段,我們也完全可以加上public const long CSDN = 3;這樣的字段。
Anyway,我們現在已經推測出字典裏面有什麼了,如果大家懷疑我的推測,完全可以循環遍歷打印出來了。我絲毫不懷疑自己的推理,所以就不打印了。
這裏的public long CurrToyGame = ToyGameId.None;字段意思是當前的遊戲模式,所以最終又調用了mGameAisleBaseDic[gameType].StartGame(objs);正式開始登錄。

登錄通道LoginAisle

負責登錄的是登錄通道(LoginAisle),所有的遊戲通道都是繼承ToyGameAisleBase,這樣才能通過上面的字典統一調用。如果閣下不知道這是什麼設計模式的話,即使在評論區留言我也不會告訴閣下的。

        public override void StartGame(params object[] objs)
        {
            base.StartGame();
            Log.Debug("進入登陸界面");
            Game.Scene.GetComponent<KCPUseManage>().InitiativeDisconnect();
            Game.Scene.GetComponent<UIComponent>().Show(UIType.LoginPanel);
        }

於是,通過登錄通道,我們終於進入了登錄流程。

登錄界面LoginPanel

在展示UI界面前,進行了KCP斷開連接的操作,之所以這麼做其實是爲了幹掉Session,我想這是爲了應對多賬號用戶的註銷操作。這也純屬個人推理,如果閣下不明白,我也不解釋。
KCP是啥?閣下可以把它當作是TCP的兄弟了,都是CP嘛。那麼TCP是啥?這裏是入門指南傳送門,走你!
送走了一波王莽(網盲,源於文盲、法盲、色盲,王莽就是不懂網絡的莽夫!)
在開始展示登錄界面之前,有必要先看看UIComponent都做了些什麼工作:
首先觸發的是事件系統:

    [ObjectSystem]
    public class UiComponentAwakeSystem : AwakeSystem<UIComponent>
    {
        public override void Awake(UIComponent self)
        {
            self.Awake();
        }
    }

    [ObjectSystem]
    public class UiComponentLoadSystem : LoadSystem<UIComponent>
    {
        public override void Load(UIComponent self)
        {
            self.Load();
        }
    }

通過ET的事件機制進而觸發Awake:

        public void Awake()
        {
            this.Root = GameObject.Find("Global/UI/");
            this.Load();
            Ins = this;
        }

在Awake中設置了UI的根節點,然後進行加載以及單例模式(Unity必修課)。

        public void Load()
        {
            uiMvcVessel.Clear();

            List<Type> types = Game.EventSystem.GetTypes();
            foreach (Type type in types)
            {
                object[] attrs = type.GetCustomAttributes(typeof(UIFactoryAttribute), true);
                if (attrs.Length == 0)
                {
                    attrs = type.GetCustomAttributes(typeof(UIComponentAttribute), true);
                    if (attrs.Length == 0)
                    {
                        continue;
                    }
                }

                Type attrType = attrs[0].GetType();
                if (typeof(UIFactoryAttribute) == attrType)
                {
                    UIFactoryAttribute factoryAttribute = attrs[0] as UIFactoryAttribute;
                    uiMvcVessel.AddUIMvcVessel(UIMvcVesselType.Factory, factoryAttribute.Type, type);
                }
                else if (typeof(UIComponentAttribute) == attrType)
                {
                    UIComponentAttribute componentAttribute = attrs[0] as UIComponentAttribute;
                    uiMvcVessel.AddUIMvcVessel(UIMvcVesselType.Componet, componentAttribute.Type, type);
                }
            }
        }

這段代碼大家眼熟嗎?幾乎同樣的設計模式,啥?沒看出來!
設計模式
Ok,只能幫到這裏了,因此我們同樣可以通過UIType來自定義自己需要的UI字段。
原理是一樣的,這裏使用uiMvcVessel來統一管理UI,如果閣下不知道MVC,這裏是入門指南傳送門,走你!
恐怕新手都送走了,所以ET的門檻其實很高,所以才叫外星人嘛。
咱其實也喫不消,不然爲啥要寫學習筆記呢?一起學習,共同進步嘛。
Whatever,已經撐到這裏了,我們還是繼續吧。UIType中定義了所有用到的UI面板,這裏就不復制代碼了。
這裏使用MVC模式來管理UI,把對應的UI類型加入UIMvcVessel中,便於後面的方法調用。

        public void Show(string type)
        {
            UI ui;
            if (uis.TryGetValue(type, out ui))
            {
                UIView uiView = ui.GetComponent<UIView>();
                uiView.Show();
            }
            else
            {
                Create(type);
            }
        }

It’s Showtime!終於輪到展示UI界面了,那麼LoginPanel一開始進入Show方法時,是沒有創建的。如果已經創建了,就直接展示。我們還是從創建開始:

        public UI Create(string type)
        {
            try
            {
                UI ui;
                IUIFactory uiFactory = uiMvcVessel.GetUIMvcVessel(UIMvcVesselType.Factory, type) as IUIFactory;
                if (uiFactory != null)
                {
                    ui = uiFactory.Create(this.GetParent<Scene>(), type, Root);
                }
                else
                {
                    UIView uiCommpoentView = uiMvcVessel.GetUIMvcVessel(UIMvcVesselType.Componet, type) as UIView;
                    ui = DefaultUIFactory.Create(this.GetParent<Scene>(), type, Root, uiCommpoentView);
                }
                UIView uiView = ui.GetComponent<UIView>();
                uiView.pViewState = ViewState.CreateIn;//狀態改爲正在創建中
                Type t = uiView.GetType();
                ui.GameObject.transform.SetParent(this.Root.Get<GameObject>(uiView.pCavasName).transform, false);
                uiView.OnCrete(ui.GameObject);
                uis.Add(type, ui);
                uiViews.Add(uiView);
                return ui;
            }
            catch (Exception e)
            {
                throw new Exception($"{type} UI 錯誤: {e}");
            }
        }

這裏使用的是工廠模式,而不是組件模式,兩者有啥優劣呢?閣下已經知道在哪裏尋找答案。
當然,我們也可以不知道工廠模式和組件模式的區別,這裏走了一個創建流程,下面正式Showtime。
登錄界面
至此,這一篇流水賬總算記完了,下一篇將完成登錄流程,從客戶端登錄,進入遊戲大廳!

作者的話

Alt

如果喜歡可以點贊支持一下,謝謝鼓勵!如果有什麼疑問可以給我留言,有錯漏的地方請批評指證!
技術難題?加入開發者聯盟:566189328(QQ付費羣)提供有限技術探討,以及,心靈雞湯Orz!
當然,不需要技術探討也歡迎加入進來,在這裏劈柴、遛狗、聊天、擼貓!( ̄┰ ̄*)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章