[toc]本篇记录个人对ET6.0源代码阅读后的理解

1.安装.Net Core

选项一: 使用Visual Studio Installer自动安装/更新最新版本的.Net Core,在工作负载中勾选".Net Core跨平台开发"即可。 选项二: 在.Net Core的官网下载安装指定版本。 注:两者都差不多,用Visual Studio Installer可以方便卸载/添加新组建,目前默认带有3.1和2.1版本的SDK。

2.OneThreadSynchronizationContext类

同步上下文可用于多个线程之间的交流,线程A将要执行的代码包装在Lambda表达式中,发送给线程B来执行。 Update方法:根据先进先出规则,执行完队列中的全部任务,仅主线程执行。 Post方法:非主线程都可以通过此方法向队列添加任务,主线程直接执行任务。 实现高效的单线程服务端:仅当网络组建的Socket完成了Accept、Connect、Recv、Send、Disconnect等操作时才会调用Post方法,这样一来服务端就可以实现接待海量连接的同时有序的执行任务。

3.Game类

Game类的成员都是static类型的,用于快速引用。 其成员EventSystem、Scene、ObjectPool都包含核心逻辑。

4.ET中使用到的容器类型

4.1.Dictionary类 使用案例:allComponents = new Dictionary<long, Entity>(); 这是最常见的词典,存储同类型的实例,key必须要有对比方法。 4.2.UnOrderMultiMapSet类 使用案例:types = new UnOrderMultiMapSet<Type, Type>(); 底层使用HashSet,用于分类整理不同类型的实例,有相同key的value都存在一个列表里面。 4.3.MultiMap类 使用案例:TimeId = new MultiMap<long, long>(); 底层使用SortedDictionary,使词典内的物品根据key进行排序。 4.4.UnOrderMultiMap类 使用案例:awakeSystems = new UnOrderMultiMap<Type, IAwakeSystem>(); 底层使用List。 4.5.DoubleMap类 使用案例:opcodeTypes = new DoubleMap<ushort, Type>(); 用两个Dictionary实现双面词典,可用value查询key。 4.6.ListComponent类 使用案例:using(var list = EntityFactory.Create<ListComponent<T>>(domain)){ //某逻辑 } 封装List,用于重用。

5.EventSystem类

这是一个单例类,public static,Model层和Hotfix层都能访问。 Add方法: assemblies参数中存储程序集,如“Model.dll,Assembly”。 遍历全部Assembly中的Type→排除掉Abstract(必须被Override)类型的Type→ 遍历Type的继承链中的全部Attribute→找出继承了BaseAttribute的Attribute→ 将Attribute的Type作为key,类的Type作为value添加到词典→对value列表进行再分类 存储在types参数中的key类型有: ET.ConfigAttribute ET.ObjectSystemAttribute ET.MessageAttribute ET.EventAttribute 对应配置、Entity、消息、事件等。 这样做的好处是批量检查代码的规范性,划分代码扩展方向。 5.1.EventSystem.awakeSystems参数 对应有awake需求的Entity,可携带0-3个参数。这些Entity在生成后立刻执行Awake方法。 5.2.EventSystem.updateSystems参数 对应内外网组件、计时器组件,每个逻辑帧都反复执行固定逻辑。 5.3.EventSystem.lateUpdateSystems参数 没有对应内容。 5.4.EventSystem.startSystems参数 start系统类似于awake系统,不同点在于: awake方法会在Entity/Component生成后第一时间执行,start方法将在下一个逻辑帧优先执行。 使用举例:ConsoleComponent。 start方法无法传递额外参数。 5.5.EventSystem.loadSystems参数 对应有加载数据操作的组件。 5.6.EventSystem.changeSystems参数 没有对应内容。 5.7.EventSystem.destroySystems参数 对应有释放需求的Entity。 5.8.EventSystem.deserializeSystems参数 没有对应内容。 5.9.EventSystem.allEvents参数 一个事件表示一段相对独立的代码,使用EventSystem.Run方法执行。 参数:事件名和0-3个额外参数。

6.Options类

这个Entity用于保存程序启动时调试参数的解析结果。 只有一个int类型的参数Process,默认值为1,表示当前主机的编号是1。 在分布式构架中,可以存在多个主机,每个主机都运行同样的服务端程序,拉起分布式网络。 这个Options实例将成为Scene的第一个Component。

7.Entity与Component的概念区别

Entity表示场景中的道具,比如玩家、玩家手上的鸡蛋,玩家之间可以互相交易鸡蛋。 每个Entity/Component都是唯一的,构造时或从池中取出时重置InstanceId参数。 Entity用数据的形式静态保存时需要一个长期的编号,用Id参数表示。 Component则表示道具的功能,如玩家会走是因为有移动组件。 在添加Child时需要提供Id参数作为Key,而Component使用Type作为Key。

8.生成日志

LogManager.Configuration.Variables["appIdFormat"] = $"{Game.Scene.Id:0000}"; Log.Info($"server start........................ {Game.Scene.Id}"); 使用Scene的Id参数作为appId,在Logs文件夹中可以看到日志文件名和日志第一条中包含appId。

9.执行事件

Game.EventSystem.Run(EventIdType.AfterScenesAdd); 在EventSystem的Add方法中已经对所有的(Model、HotFix)事件进行了整理。 要声明一个事件,需要为类添加EventAttribute(标识事件类型、作为key存储/搜索),并继承于IEvent接口(需实现带0-3和参数的Run方法)。 就本例来说AfterScenesAdd事件是一个无参AEvent,Run方法中包含场景组件初始化逻辑。

10.Entity类

Enitity类同时扮演Entity和Component,其生成、进出池、数据库存储读取等属于核心逻辑。 Game.Scene是场景中第一个Entity: scene = EntitySceneFactory.CreateScene(1, SceneType.Process, “Process”); 这个Entity是直接new出来的,同时创建了Scene、Entity、Object实例。 new出来的自定义Entity需要补全信息,如Id、InstanceId、EntityStatus。 Parent参数:表示Entity与Entity之间的父子关系,父子共享domain。 ComponentParent参数:表示Entity与Component之间的父子关系,父子共享domain。 domain参数:只有Game.Scene的domain是自己,其所有父子关系链上的Entity和Component的domian都是Game.Scene。 Id参数:除了Game.Scene,Entity使用EntityFactory时赋值,Component和父级共享Id。 InstanceId参数:设置domain时赋值。用于EventSystem。 IsRegister参数:是否注册,(除了Game.Scene)在设置domain时赋值,且跟parent一致。Game.Scene和其子级的IsRegister是true,都会执行EventSystem.Instance.RegisterSystem方法,添加到loadSystems/updateSystems/startSystems/lateUpdateSystems等。 10.1.为Entity添加Component Game.Scene.AddComponent(); AddComponent方法:where K : Entity, new(),参数继承Entity且有无参自构方法。 可以指定如何生成Component(来自池中的回收队列或者使用无参自构方法new一个) IsFormPool参数:标记Entity的生成方式 IsCreate参数:标记Entity使用Entity.Create方法生成 Id参数:继承父级Entity的Id值 ComponentParent参数:仅Set,设置父级Entity,标记自身为IsComponent。 EventSystem.Instance.Awake(component); Awake操作:添加Component时立刻检查是否需要Awake 不同组件Awake时需要的参数数量不同,所以AddComponent操作也对应0-3个参数的版本。 AddToComponent方法:将生成的Component保存到Entity的components(词典)内。 10.2.EntityFatory类 这个类下包含大量static方法,用于批量生成Entity。 CreateWithParent方法:从池中生成Entity,设置Id、Parent、IsFromPool、IsCreate等属性,并Awake,直接返回新Entity。 通常生成Entity都使用这种方法,除了Scene和AChannel等特殊Entity直接自构。

11.Entity的Id与InstanceId的生成

11.1.IdStruct类 使用案例:long id = IdGenerater.GenerateId(); 过程中调用:new IdStruct(process, (uint)time, (ushort)value); process:主机编号,1-10左右。 time:精确到秒钟,比如十进制的1585110791,占30位以上。 value:秒钟内生成的物品的序号,1-1000左右。 Process占??+16+18位中的低18位。 Value占??+16+18位中的中16位。 time占??+16+18位中的高位(30位+)。 11.2.InstanceIdStruct类 使用案例:this.InstanceId = IdGenerater.GenerateInstanceId(); 过程中调用:new InstanceIdStruct(process, ++MaxConfigSceneId); MaxConfigSceneId:初始1024000,每次调用自加1,占20位以上。 Process占??+18位中的低18位。 Value占??+18位中的高位(20位+)。

12.ConfigComponent类

这个组件用于保存各个分布式服务器的配置。 Awake方法: 设置ConfigComponent.Instance为自身,使配置组件成为一个单例类。 从EventSystem.types中找到全部的Config类,这些类必须继承于ACategory。 如:ET.StartProcessConfigCategory、ET.StartSceneConfigCategory、ET.StartZoneConfigCategory、ET.UnitConfigCategory 遍历全部配置类执行自构(所以这几个类都可以直接访问Instance实例)、BeginInit/EndInit方法、添加到AllConfig词典。 AllConfig词典:没有实际使用过。可以查看所有配置,比如用typeof(StartProcessConfig)做key查询到一个StartProcessConfigCategory实例(也就是Instance实例,需要强制转换)。 ACategory<T>.BeginInit方法: 这个源自Object类的虚方法被ACategory<T>重写。 根据泛型名称在Config文件夹中寻找对应的txt文件并读取全部字符串。 将全部字符串根据换行符切分为多个字符串,将每个字符串转换为一个泛型实例,保存到dict词典。 dict词典:每个ACategory<T>的子类实例都有的参数,保存多行配置。 例如:StartProcessConfigCategory继承于ACategory<T>, 在StartProcessConfig.txt中包含2行配置,转换为2个StartProcessConfig实例。key为该条配置的Id属性。 MongoHelper.FromJson(str):将Json字符串解析(反序列化)为对应泛型类。 下面将演示所有配置的作用。 ISupportInitialize.BeginInit/EndInit方法: 这2个方法均在配置实例被反序列化生成的过程中调用,用于初始化各种配置。 继承ISupportInitialize接口的类有:StartProcessConfig、StartSceneConfig、Object 12.1.StartProcessConfig 使用举例:StartProcessConfigCategory.Instance.Get(options.Process); 取StartProcessConfig.txt中的某一行配置,Process参数为App的启动调试参数,默认为1。 每行配置表示一个可用的主机,需要接收外网信息时,主机需要有独立IP。 BeginInit/EndInit方法:设置InnerAddress参数 InnerAddress参数:字符串,比如“127.0.0.1:20001”。 InnerIP参数和InnerPort参数用于初始进程的绑定监听 OuterIP参数用于其他进程访问初始进程。 12.2.StartSceneConfig 使用举例:StartSceneConfigCategory.Instance.LocationConfig.SceneId StartSceneConfig.txt中包含全部服务器的配置。 StartSceneConfig.EndInit方法:赋值Type参数、SceneId参数。 Process参数:有独立IP的主机单位编号,可运行1个或多个服务器进程。 Id参数:服务器进程Id,服务器必须运行在有独立IP的主机上,例如租赁的阿里云ECS实例。 Zone参数:数据读写区域,该区域内使用同一个数据库进行读写。多Zone用于缓解数据库读写压力。 SceneType参数:服务器功能类型。 Name参数:服务器名称。 OuterPort参数:服务器占用的端口号,txt中可以没有该参数。 Type参数:Realm、Gate、Location、Map等,表示服务类型,可以创建新的服务器类型。 SceneId参数:利用Process参数和Id参数生成的InstanceId。 StartSceneConfigCategory.EndInit方法: 对所有服务器配置进行分类整理。 ProcessScenes:以Process参数为key保存全部配置,可查看各主机下的配置。 ZoneScenesByName:以数据读取区域为key保存全部配置,可通过数据区域ID和服务器名查看配置。 Gates:只存储Gate类型服务器配置 LocationConfig:只存储Location服务器配置,该类型服务器只存在1个。 12.3.StartZoneConfig Id参数:大区编号,大区之间不共享数据,通常只有1个大区。 DBConnection参数:数据连接字符串 DBName参数:数据库名

13.TimerComponent类

Awake方法:单例类,设置Instance参数。 Update方法:每个逻辑帧检查是否有定时任务要执行。 NewRepeatedTimer方法:创建一个RepeatedTimer实例,安排首次任务。 minTime参数:任务列表中最近的一次任务的触发时间,默认为0表示下一个逻辑帧必定有任务。 TimeId参数:MultiMap词典,底层是SortedDictionary,会对key进行排序。以任务到期时间为key(可保证排序),Timer的Entity.Id队列为value。 timers参数:以Timer的Entity.Id为key,存储所有待执行的定时器。 13.1.RepeatedTimer类 以一定时间间隔重复执行的定时器。 Awake方法:记录开始时间、重复间隔、任务(Action)、执行次数。 timers参数:保存所有定时器,key为定时器的Entity.Id,value为定时器实例。 Run方法:安排下次任务(timerComponent.AddToTimeId)、执行本次任务。

14.OpcodeTypeComponent类

Awake方法:单例类,设置Instance参数,重置消息列表。 一个消息类,如R2G_GetLoginKey,表示这个消息将由Real服务器发送给Gate服务器(请求一个登录密钥),有MessageAttribute标识Opcode,继承于IMessage。 这些消息有明确的收发规则和流程,从而实现客户端与多台服务器之间的互动。 opcodeTypes中key为Opcode,value为消息类的Type。 typeMessages中key为Opcode,value为消息类的新建实例。 GetOpcode方法:通过消息的Type查询对应的Opcode。 GetType方法:通过Opcode查询对应的Type。

15.MessageDispatcherComponent类

使服务器具备处理客户端-服务器进程之间的Message(区别于ActorMessage)的能力。 Awake方法:单例类,设置Instance参数,整理消息处理类列表。 一个消息处理(MessageHandler)类,如C2R_LoginHandler,表示当客户端向Real服务器发送登录请求(C2R_Login,包含账号和密码),Real服务器向Gate服务器请求一个key并将key回复给客户端,客户端可以拿着key连接Gate服务器。 每个消息处理类对应着一个请求,且在特定的服务器/客户端上执行,可能收到大量种类的消息。 iMHandler.GetMessageType方法:返回请求(Request)的Type 因为请求和消息处理类是一对多关系,用Opcode作为key,MessageHandler列表作为value存储即可。 Handle方法:将消息分配给对应的消息处理类,不应发生有消息未被处理的情况。 AMHandler:客户端-服务端之间不需要回复的消息。 AMRpcHandler:客户端-服务端之间需要回复的消息。

16.服务器分工

注:同一类型服务器可能有多个实例 Realm服务器:验证登录。 Gate服务器:消息转发者,负责和客户端沟通。 Location服务器:定位Actor对象。 Map服务器:游戏逻辑。

17.CoroutineLockComponent类

协程锁组件,单例。 Awake方法:生成13个CoroutineLockQueueType类型的Entity添加到list参数,且设置组件为Parent(被添加到Children列表)。表示13种不同的协程锁。 Wait方法: 使用案例:using (await CoroutineLockComponent.Instance.Wait(Type, Id)){ //锁内逻辑 } 先从组件的Children列表中找到指定类型的子级CoroutineLockQueueType实例,再用Actor的Entity.Id在子级里面找CoroutineLockQueue实例。 不存在CoroutineLockQueue实例就从池里面new一个,并生成一个CoroutineLock实例(不在CoroutineLockQueue的队列里,而是设置CoroutineLockComponent为父级),在执行锁内逻辑后就Dispose。Dispose时如果发现后面有任务在排队,则激活一个任务;没有排队就把队列删除了。 存在CoroutineLockQueue实例(至少有一个同类任务在执行中)就向队列添加一个未完成的任务,并阻塞,前面的任务都完成了才会停止阻塞。 在这个过程中CoroutineLockQueue实例的存在表示有任务正在执行,即使队列中物品数量为0。 17.1.CoroutineLockQueueType类 CoroutineLockComponent的子级Entity,管理一个CoroutineLockQueue词典,key的根据协程锁的类型意义不同。 17.2.CoroutineLockQueue类 CoroutineLockQueueType的子级Entity,管理一ETTaskCompletionSource队列 ETTaskCompletionSource:用于管理异步任务,对应着一个ETTask。 17.3.CoroutineLock类 CoroutineLockComponent的子级Entity,标记协程锁类型和key值。

18.网络消息发送

消息发送前: 消息在发送前会先添加一个存根到requestCallback: self.requestCallback.Add(RpcId, Callback); 其中,RpcId随每次发信+1;Callback是约定受到回复后的处理方法。 回复处理: requestCallback中的存根有两种删除的办法: 1.任务超时(30秒),删除存根并添加错误消息到日志中。 2.收到回复消息,任务超时前删除,使用response作为参数执行Callback,报错或返回结果。 具体沟通形式-普通通信需求 如果发出的消息不需要回复,则是Send形式; 如果发出消息并等待回复,则是await Call形式; 原则上,使用TCP通信的话,是两台主机的端口之间互相发送消息包; 在ET中,我们使用Session来抽象一个对话生命周期,所以Session可以直接发送消息; 但是Session没有指定收信人,虽然我们可以通过额外的条件判断要收信人是谁: 比如:注册消息,没有特定的收信人,我通常理解为“登录服务器”在收信、处理注册业务。 具体沟通形式-指定目标的通信需求 开放注册业务的单位在全局只有这么一个,所以无需特殊定位,这就是约定。 但是当有100个业务员都提供注册业务时,我们就需要指定一个或随机业务员注册。 这些业务员就好比有100个摊位,摆在你眼前(公开了InstanceId)。 具体沟通形式-指定身份的通信需求 由于分布式服务端的特殊性,业务员可以在不同的主机之间游走,InstanceId发生变化。 比如玩家实例,之前在某个世界地图上,之后可能去了某个5人副本或者竞技场。 可以考虑,让业务员挪窝后,主动通报自己的InstanceId,这可以是通常的做法。 又或者,使用第三方服务,跟踪业务员的位置;发信时,查询目标的InstanceId。 对于从数据库中取出的Entity,其Id是恒定的,适合用第三方寻人业务。

18.1.Session类

一般使用方法: session.Send(Message) await session.Call(Message)

18.2.ActorMessageSenderComponent类

普遍用于内网消息(已知目标所在的主机),需提供目标的Entity.InstanceId作为参数。 Awake方法:单例类,设置Instance参数,创建定时器每10秒检查是否有任务超时。 一般使用方法: ActorMessageSenderComponent.Instance.Send(instanceId, ActorMessage) await ActorMessageSenderComponent.Instance.Call(instanceId, ActorMessage)

18.3.ActorLocationSenderComponent类

普遍用于内网消息(不确定目标所在的主机),需提供目标的Entity.Id作为参数。 Awake方法:设置Instance,每10秒执行一次Check方法。 一般使用方法(先手动注册-Add): await LocationProxyComponent.Instance.Add(Id, InstanceId); ActorLocationSenderComponent.Instance.Send(Id, ActorLocationMessage) await ActorLocationSenderComponent.Instance.Call(Id, ActorLocationMessage) 迁移Map操作: await LocationProxyComponent.Instance.Lock(Id, InstanceId); Game.EventSystem.Remove(InstanceId); //发送转移消息 在另一个Map服务器复制实例 获取新的InstanceId entity.Dispose() //本地的实例需要删除 LocationProxyComponent.Instance.UnLock(Id, newInstanceId); ActorLocationSender实例: Awake方法:获取一次Actor对象的InstanceId,设置ActorLocationSenderComponent组件为父级。 ActorId参数:通过LocationProxyComponent访问Location服务器更新Actor对象的InstanceId。

18.4.ActorLocationMessage的特点:

1.判断目标主机内网地址: 不管是哪种组件,发消息的基础条件都是知道对方的主机地址,而ET中主机编号藏在Entity.InstanceId中; 参考:InstanceIdStruct 在发送ActorMessage时,判断目标主机地址,由目标的内网组件转发; 在发送ActorLocationMessage时,通过Entity.Id,像Location服务器查询Entity.InstanceId。 2.Actor对象注册/查询机制: 只要是有邮箱组件的Entity,都有义务上传自己的InstanceId到Location服务器。 3.Actor对象迁移机制: Actor对象可以切换所在的Map服务器,需要向Location Server更新Entity.InstanceId。 4.ActorLocationMessage重发机制: 如果Actor对象迁移导致返回Actor不存在的错误,则发送者等待1秒后重发,该机制可重复5次,5次过后抛出异常。 5.ActorLocationMessage回复机制: Send方法返回空消息表示对方已收到消息,Call方法返回回信。 一个服务器实例不能对同一个Actor对象同时发多个ActorLocationMessage,必须排队等待回执。 6.缓存机制: ActorLocationSender仅在Awake时查询一次Locantion Server,之后遇到发送失败才再次查询。 7.Actor对象加锁机制: Actor对象迁移过程中Location Server会对该key加锁,对该key发送的请求会进行队列。 完成迁移后,需要解锁,并更新Actor对象的Entity.InstanceId,然后响应请求。

19.ETTask

熊猫:更优秀的异步 相关类型:ETVoid,ETTask,ETTask<T>。 根据熊猫的注释:适用于Unity的轻量级task-like类型。 ETTaskCompletionSource实例:用于构建/维护任务实例)。 Check方法:检查是否有任务超时。 SetResult方法:标记source的状态为Succeeded,设置value参数,执行continuation。 continuation:async/await方法中await后的部分。 文献参考: C#中的异步方法译文 C#中的异步方法原文

ETAsyncTaskMethodBuilder类:

管理状态机的分支切换 Create方法:自构 Start方法:状态机.MoveNext,执行下一个分支,第一次运行时执行分支1。 AwaitUnsafeOnCompleted方法:将状态机.MoveNext作为委托赋值给ETTaskCompletionSource<T>.continuation。 SetResult方法:如果分支1可以立即完成的话调用?标记任务完成。 SetException方法:捕捉异常(取消/报错)

状态机(TStateMachine实例,由编译器生成)

Call方法的最后一排:return tcs.Task; 编译器将await后的逻辑分成了2部分,作为状态机的分支: 分支1.await到return task-like实例(ETTask<T>实例) 分支2.await后的逻辑,相当于(result)=>{//continuation逻辑} 在return task-like实例之前,已经生成ETTaskCompletionSource实例(state默认为0,Pending状态),已经生成存根并发送消息。 状态机先执行分支1: ETAsyncTaskMethodBuilder<T>.Create().Start(状态机) 分支1确认一遍ETTask<T>实例的状态,如果任务已完成,则省去设置continuation的步骤直接GetResult;如果任务未完成,设置continuation(支线程阻塞),等待外部调用解除阻塞(如:存根执行Callback)。

18.4. ETTask_T与ETTask、ETVoid的区别:

这3者都是ET包装过的async方法返回类型,未包装时对应:Task<TResult>、Task 和 void。 ETTask<T>:有返回值,捕获异常,可await。 ETTask:无返回值,捕获异常(SetException)⇒需要手动获取异常,可await。 ETVoid:无返回值,不捕获异常⇒直接debug,不可await⇒只能用Coroutine方法作为异步入口。 一般使用方法: 把ETVoid方法作为异步接口,function.Coroutine(); 把ETTask方法作为异步接口内需要等待的任务:await function(); 在定义ETTask方法时,如果没有异步任务,可以直接完成任务:await ETTask.CompletedTask; ETTask<T>的定位和ETTask差不多,有返回值:var back = await function();

20.LocationProxyComponent类

self.ActorId = await Game.Scene.GetComponent<LocationProxyComponent>().Get(self.Id); 获取Actor对象的Entity.InstanceId,参数为Actor对象的Entity.Id。 Awake方法:设置Instance参数,单例类。 Get方法:向Location服务器请求Actor对象的Entity.Id

21.ActorMessageDispatcherComponent类

类似于MessageDispatcherComponent类,使服务器进程具备处理ActorMessage的能力。 Awake/Load方法:收集ActorMessage处理类,Type为key,保存到ActorMessageHandlers词典。 ActorMessage处理类:有ActorMessageHandlerAttribute特性,且继承IMActorHandler接口。 ActorMessage处理类的继承链: AMActorHandler:服务器-服务器之间互相Send消息。 使用案例:ActorMessageSenderComponent.Instance.Send(Id, message); AMActorRpcHandler:服务器-服务器之间互相Call消息。 使用案例:ActorMessageSenderComponent.Instance.Call(Id, message); AMActorLocationHandler:客户端/服务器⇒位置不明的Actor对象发送Send消息。 使用案例:ActorLocationSenderComponent.Instance.Send(Id, message); AMActorLocationRpcHandler:客户端/服务器⇒位置不明的Actor对象发送Call消息。 使用案例:ActorLocationSenderComponent.Instance.Call(Id, message); 注:Location消息通过多次普通Actor消息来实现,内网组件只会收到普通Actor消息。

22.NumericWatcherComponent类

数值变化监视组件。 Awake方法:收集所有的数值监视类。 数值监视类:有NumericWatcherAttribute,指定一种数值类型,且继承INumericWatcher接口。 使用举例:NumericWatcher_Hp_ShowUI,当HP值发生变化时修改血条长度。 目前没有实现Entity对某个类型事件进行关注,等熊猫更新。

23.ConsoleComponent类

控制台组件,用于对服务端程序添加命令。 命令: reload:重新加载Hotfix.dll repl:进入交互模式 exit:退出交互模式 reset:重置交互模式 一段代码文本:执行该段代码

24.NetInnerComponent类

内网组件,添加该组件后,服务器可接受来自内网中其他服务器的消息。 Awake方法: MessageDispatcher参数:消息处理方式,使用InnerMessageDispatcher类。 MessagePacker参数:消息解析工具。 Service参数:端口监听任务,默认TCP模式,使用TService类。 Sessions:保存对话。 网络相关组件是整个框架的底层核心,涉及到数据传输和安全。 NetInnerComponentOn.OnAccept方法:当内网组件调用OnAccept时,表示有外部程序对内网组件监听的端口(比如127.0.0.1:10001)访问,由主线程执行TService.OnAcceptComplete方法时调用NetworkComponent.OnAccept方法。能知道内网端口的目标,理论上只有服务器群中其他服务器的抽象,也有可能是陌生的IP在扫端口,应判断访问者IP是否在内网主机IP列表中。 24.1.TServer类 生成Tservice实例:设置网络组件为父级 Socket类:用于建立网络通信连接。 SocketAsyncEventArgs类:提供Socket增强功能,描述/维护异步Socket操作。 Socket的自构:3个参数:AddressFamily、SocketType、ProtocolType AddressFamily类:指定一种寻址方案。IPv4类型就是我们常用的192.0.2.235这种格式。 127.0.0.x:回送地址,主要用于网络软件测试以及本地机进程间通信,不进行任何网络传输。 SocketType类:指定Socket实例的工作方式(是否建立连接、支持哪种协议和地址),默认Stream。 ProtocolType类:协议类型,默认Tcp。 Socket.SetSocketOption方法:在Socket所有选项中设置其中某一项。 Socket.Bind方法:如"127.0.0.1:20001"。 Socket.Listen方法:Sokect开始监听,参数backlog指定pending connections queue的最大长度。 Socket.AcceptAsync方法:异步等待一个新连接,类似于Socket.Accept(阻塞等待一个连接)。 如果 I/O 操作挂起(空闲状态),返回true,触发innArgs.Completed事件。 如果 I/O 操作同步完成(报错),返回false,不会触发innArgs.Completed事件。 支线程不停的Accepet,一边保持接收,一边处理消息(报错或数据包)。 报错处理:日志输出错误,继续Accept。 正常处理:创建TChannel维护新连接,继续Accept。 TChannel类:设置Tservice为父级,接收数据包。 TChannel.isConnected参数:是否已建立连接。 Tservice.OnAccept方法:指向NetInnerComponent.OnAccept方法,创建Session(父级为NetInnerComponent)。 TChannel.Start方法:有两种接收数据包的形式:Accept/Connect。 ChannelType.Accept:用于接待新的内网连接(还不确定消息的具体内容时)。 ChannelType.Connect:用于请求新的内网连接,连接成功后与accept连接无异。 Socket.ReceiveAsync方法:异步接收一个数据包,类似于Socket.Receive。 数据包-包头:前4个byte表示packet的长度(每个byte相当于8位2进制数)。 CircularBuffer.Read方法:每次读取指定长度的byte后读取位置位置后移。 数据包-包体(PacketParser.memoryStream参数):起点为Begin、偏移0、长度packetSize。 TChannel.OnRead方法:指向Session.OnRead方法。 Session.Awake方法:设置最近接收/发送时间、TChanel的(报错/正常)消息回调方法等。 Session.OnRead方法:读取数据包-包体。 memoryStream.Seek方法:设置流的Position参数。 memoryStream.SetLength方法:设置流的字节长度。 memoryStream.GetBuffer方法:返回(创建该流的)byte数组。 读取Opcode:opcode = BitConverter.ToUInt16(memoryStream.GetBuffer(), Packet.OpcodeIndex); 读取Message:message = this.Network.MessagePacker.DeserializeFrom(instance, memoryStream); 解析工具:内网使用MongoPacker,外网使用ProtobufPacker。 派发消息:this.Network.MessageDispatcher.Dispatch(this, opcode, message); 派发工具:内网使用InnerMessageDispatcher,外网使用OuterMessageDispatcher。 Socket.ConnectAsync方法:异步向另一台主机请求建立新连接,成功后RecvAsync。 Socket.SendAsync方法:异步发送byte数组,发送完为止。 NetInnerComponent.Get方法:在发消息前,使用InnerAddress(内网IP和端口)为key查询session,没有时则创建connect连接和session。 24.2.Session类 session用于维护一个连接(对话的生命周期),可以存在很长时间,直到对方断开连接。 Awake方法:绑定一个AChannel实例,设置ErrorCallback和ReadCallback。 ErrorCallback:捕获AChannel工作过程中的报错,一旦出错移除Session。 ReadCallback:读包操作,指向Session.Run方法。 RunMessage方法:已完成消息的解析工作后的分配处理,因消息类型和接受者角色差异不同。 IMessage/IRequest/(内网)IResponse:使用Session的父级网络组件派发消息。 (客户端)IResponse:Session自己处理,用RpcId搜索requestCallback中的存根,执行存根。 Send/Reply/Call:将消息序列化后,使用绑定的AChannel的Send方法发送。

25.创建本地Scene

每个主机只需要运行一个App.exe。 在Entity的关系链中,最上层是Game.Scene。 其次是各个服务器的抽象Scene实例,如Realm、Gate、Map、Location。 使用案例:SceneFactory.Create(Game.Scene, SceneId, Zone, Name, Type); 批量创建Scene实例,以Game.Scene为父级,Id和InstanceId都是SceneId,按需添加组件。 NetOuterComponent可能存在多个,对应复数的Realm/Gate服务器,监听不同端口。 startSceneConfig.OuterAddress:使用主机外网IP和服务器外网端口。 processConfig.InnerAddress:使用主机内网IP和主机内网专用端口。

26.Actor消息、ActorLocation消息与普通消息

综合本篇以上内容,我们可以在脑内形成一个网络连接图,有: 1.很多客户端 2.1个以上阿里云ECS实例(或者自建专线机房中的主机) 3.Session维护着设备与设备之间的连接/对话。 4.如果想给谁发消息,就找到直连的或者能帮忙传话/找人的Session。 5.Scene、玩家、怪物、房间,只要这些Enity添加了邮箱组件都能收内网消息。 Actor消息为什么能送达指定InstanceId的目标: 因为从InstanceId可以推导出主机编号、主机内网地址、直连的Session。 ActorLocation消息为什么能送达指定Id的目标: 通过向Location服务器(Scene类型Entity)请求目标的InstanceId,推导出直连的Session。 Actor消息、ActorLocation消息与普通消息最终由直连的Session完成消息的发送。