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!
当然,不需要技术探讨也欢迎加入进来,在这里劈柴、遛狗、聊天、撸猫!( ̄┰ ̄*)

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CloudHu1989

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值