1. 前言
ET算是我刚接触客户端时最早知道的框架,ET我最初眼馋的还是他的双端功能。包揽前后端的功能,这个很有吸引力。但是那时候对我来说这框架太复杂了,没法看。
这两天又来看看,曾经很多不懂的地方现在都能看懂了,看了之后发现这框架还是不适合我,原因总结里再说,在这里做一下记录。
首先说一下很多新手看这个框架的疑惑点,再说一下ET最有特色的几个功能:ETTask、事件系统、双端开发。
2. 新手常见问题
这些问题是我以一个新手的视角来看的。也是我最初接触这个框架以来有疑惑的地方。
2.1 目录结构问题
这里以7.2的目录为例。ET6就不用看了,就是闹着玩儿。
代码位置
主要关心以下三个目录,其中Share文件夹下主要是一些工具和分析器,分析器作用是规范C#的代码规范,编码时做出提示,不需过多关注。
代码都在Unity下,DotNet目录下有csproj,引用的Unity/Assets/Scripts/Codes里的文件,这样服务端开发就只关心Dotnet目录下的代码就行,再次强调,代码文件在Unity目录下,DotNet里相当于是软链接。
这样代码统一放到Unity工程里,双端开发也方便(ps: 实际我还是觉得客户端和服务端分开更好)
Unity目录
这个就是Unity工程目录,代码在Assets\Scripts 下,这下面又有很多目录,
其中Empty是占位用的,占着几个程序集(ps: 具体啥作用没细看,估计编译需要)
Loader是程序入口,加载程序集
Core、Editor、ThirdParty见名知意
Codes就是我们写程序的目录,DotNet也是链接到这个目录的。
其中目录如下,四个目录,Model ModelView Hotfix HotfixView,这样划分是为了逻辑和数据分离。 Model只有数据。hotfix只有逻辑。其中 Model view和HotfixView,只有客户端用,用于表现层。
这四个目录的每个目录下分别有Client Server Share文件夹,Client写客户端代码,Server写服务端代码,Share写两端共用的代码,这样服务端也有客户端代码,可以做AI机器人。
2.2 程序集问题
看了目录划分后对于纯新手来说又有疑惑了,程序集是什么?作用是什么?
这还真是我一开始的疑惑,程序集可以理解为包,一个程序集可以编译成.dll给别的程序集使用。
那为什么这么分程序集呢?
这里你得明白dll或者说so文件的作用,可以动态链接。这样就能实现热重载的功能。
同时能提高编引速度。比如有ABC三个程序集。 A依赖B,B依赖C,这时候如果B修改了,只有AB会重新编译。如果只有A修改了,则只有A会重新编译。
这里你顺便说一个程序域的概念,看源码的时候能看到AppDomain,一个进程可以有多个域,一个域可以加载多个程序集。区域相当于提供一个独立的运行环境。
3. ETTask
为什么不用原生Task呢,因为Task是多线程的,起一个Task会在子线程运行。
ETTask实现了单线程的Task,其他类似的库还有UniTask
使用多线程实际没什么问题,但是要保证另外的线程逻辑是线程安全的,然而有时候你觉得是线程安全的,可能会有各种bug,这种bug就不好找。还要处理数据线程数据同步问题。
而unity主程序本身是单线程的,加上多线程强行增加复杂度,使用单线程异步可以避免很多问题。
Unity对跨线程的处理都是把委托投递到一个队列,主线程Update()不停从队列中取出委托执行
那为什么不用Corotine呢?
Corotine必须在mono脚本下执行,而且效率貌似也不如Task异步。
4. 事件系统与标签
这个可以说是ET的核心了,不过不能被名字欺骗了,这个事件系统不只是其他游戏框架中用到的那种事件,这里面还管理所有的标签类型。
ET的一个特色就是对标签的大量使用,这也是对新手很不友好的地方,也大大提高了代码的阅读难度。
代码就是Core/Module/EventSystem/EventSystem.cs,目前看其中主要实现了对不同标签类型的管理,这里只说两个,常见的事件系统和SystemComponent机制
4.1 常见的事件系统
使用
就是一般游戏框架中用到的那种事件系统,ET的用法比较特殊,如下图所示,对事件的注册不是显式调用register,而是写一个类,打上Event标签,这个类要继承AEvent,AppStartInitFinish就是事件参数,也是通过这个区分事件的,下图中这个事件对应两个处理方法,触发事件的时候会调用这个类的Run()函数
原理
简单来说,就是EventSystem 启动时找到所有打上Event标签的类统一管理,调用Publish发布事件时,根据事件参数找到对应的事件类。依次调用每个事件类的Run方法。
下面是截代码的图分析:
-
启动之前会注册事件,通过Attribute的方式
-
在Add方法中,会找到所有BaseAttribute标签的类型,所有自定义的标签都是继承自这个标签
-
再在其中找到所有event标签的类型,加入allEvents统一管理
-
同理处理Invoke标签的类型。
-
事件触发,就是从allEvents中找到T类型对应的事件。一个事件可能有多个EventInfo,也就是多个处理响应,依次进行调用。比如下面第2张图中的例子。其中加了一个scene的概念,相当于就是对事件又进行分层,只发到对应的层。
-
上图中对时间的使用。添加Event标签才能注册到事件系统里。继承AEvent是因为事件系统会调用它的Handle方法,Handle实际调用的Run,Run()就是自己要实现的对事件的处理。
4.2 SystemComponent机制
这个也是用上面的EventSystem实现的。
-
还是如上面的例子在Add()中找到所有ObjectSystem标签并用typeSystems统一管理。
-
比如Awake的系统。调用EventSystem的Awake后,所有实现该接口的系统都会调用
-
以MoveComponent为例,它的MoveComponentSystem里实现了AwakeSystem,在这里打断点调试一下,可以看到调用栈如图2,总之就是在添加MoveComponent这个组件时,调用了EventSystem.Instance.Awake(component),从而调用了所有继承AwakeSystem<MoveComponent>的类型的Run。
4.3 单独声明一下
因为没用ET写过项目,只是看了下代码,可能对这EventSystem理解有偏差,不过应该没啥大问题
5. 热重载
下面两个图就能知道热重载的大概逻辑。
按R调用CodeLoder的LoadHotfix()方法。
这个方法里重新加载Hotfix的dll。
可见热重载之前要先手动编译一下Hotfix dll,再按R。
5. 双端开发
实际就是客户端代码写在Client 的目录下,服务端写在Server目录下,实际上客户端和服务端的交互还是通过RPC。
这么一看,双端开发就是能在一个编辑器下开发,我反而觉得很不方便。因为每个目录下都划分client 和server,看着太乱了,不如开两个项目。
关于共用代码,这种需求感觉并不是那么需要,使用的一样的代码直接复制就行了。
总结
最后再声明一下,这个框架我还没有弄得特别透彻。因为现在大致看懂了整体架构之后,觉得不太适合我。也就没有了继续深入的想法。以上只是看源码,demo的一些收获,有错误的地方欢迎指正。
再说几句,我的个人想法,仅代表个人意见。我觉得这样整的过于繁琐了,无论是目录的划分,还是所谓的独特的ECS机制。
也没有体会到这种System-Component的好处,只觉得代码过于散乱,可能是没有实际使用ET开发过,只是看了看例子吧。
双端开发也没有当初一无所知时所认为的那么神秘,还是客户端代码和服务端代码分开了,反而代码混合在一起显得更乱了,不如分开。而且服务端定义的zone、scene这一套,我现在还没完全理解这些概念,估计应该是区、服和进程的区分,相当于得完全在这一套逻辑下使用,没用过所以也不知道方不方便拓展。
事件系统和标签的使用也感觉不适应,当然熟悉了这种开发方式后也能用,但是就和JEngine中使用的订阅模式的事件系统一样,可以但没有必要,还是传统观察者模式的事件系统更好用一些,其他也没有什么效率上的优化。
热重载是有用,但对于快速开发又不是很必须。对于服务端来说热重载就应该更不需要了吧,就我有限的服务端开发经历,之前写的寻路服务,需要构建全地图的障碍和navmesh信息,重启虽然不是立刻就启动了,但用的时间也不是不能忍受。
当然,这框架的实现还是挺独特的,可能是我还体会不到这框架的好处,虽然现在暂时pass了,以后还会继续关注,可能以后就用到了呢。