前言
ET给我们提供了一个寻路的Demo,本章是基于ET5.0分支讲解的,因为6.0的版本目前客户端还不稳定。但5.0的Demo有一个明显的问题就是断线之后再次同账号登录,场景中原来的角色还在场景中,所以第二次登录就出现了两个角色,算5.0还没解决的小bug,一般断线之后的逻辑处理就是场景中的角色原定不动,等角色再次重连然后还控制这个角色,所以场景中的角色应该跟账号绑定,也有人断线是直接删除场景中的角色这样粗暴的处理方式,不过一般不推荐这种方式,会给其他玩家造成莫名其妙消失的感觉。
Demo运行效果
运行流程分析
先运行Tools->命令行配置,启动逻辑服务器,然后点击Tools->打包工具,编译资源,最后启动Tools->Web资源服务器。
框架启动
看一个Demo从他的入口开始看起,打开客户端,第一个场景中入口就是Init脚本。
Start方法里执行了一个异步方法,关于异步方法不太了解的可以看ET官方文档的教程,我们追踪红框中的代码,估计会有人不太了解后面这个Assembly是个什么样的程序集,或许有人会以为就是反射Init这个类,其实不是的,是反射这个Init脚本所在的程序集,如果反射不太了解的同学,可以看下我之前写的这一篇关于C#的反射,你真的运用自如嘛,那么这个Init脚本所在的程序集是个什么样的程序集呢,断点运行程序我们会知道是Unity.Model.dll,那为什么是这个程序集呢?
看上面两个图你一定猜到答案了吧,我们在文件夹下面创建一个AssemblyDefinition的文件,Unity就会以这个文件夹为单元,创建一个VS的项目工程,那编译一下自然生成一个对应的dll,因为Init脚本在Unity.Model工程中,所以上面红框中的Assembly就是这个Unity.Model程序集。
然后我们再来看下红框里面的方法
上面的代码功能是通过反射获取自定特性的类,然后添加到types容器中,下面是清除ECS中各个System的缓存容器。
上面代码含义是从刚刚缓存的types容易找出ObjectSystem特性的类型,然后通过反射创建类型实例,并且添加到各个System缓存中,下图我们找一个添加了ObjectSystem特性的类来看下长啥样
看到这里我们是不是似曾相识,如果了解UnityECS开发的朋友就熟悉这种开发模式,到这里我抛个问题,为什么我们在Unity中添加了Monobehavior的脚本就能自动执行里面的生命周期函数,例如Awake,Start等等,现在你知道原理了嘛?!
其实上面的操作是框架启动的常见操作,还有一个类似的方式就是通过继承Manager,然后启动的时候反射继承Manager的子类然后创建单例对象,这两种都是比较常用的方式,后面这种方式我在以前一篇文章中有写过 【GFFrameWork】管理器和框架启动,感兴趣的可以点开看一下。
再往下看
上面这段代码同理,就是搜集带有Event标签的类,并且创建实例,至于这些Event类搜集起来有什么作用我们下文再说。
最后还有一个Load方法
这个方法的作用就是在我们添加组件的时候,执行组件的Load方法,loaders是一个组件id的列表,不断的进行轮询,因为我们在添加组件的时候有往这个列表中添加组件id
上面一行添加程序集就分析了这么多,在继续一开始的Init脚本下面就是添加各种组件,添加组件的分析,我在上一篇文章中有详细介绍,这里就不展开,感兴趣的可以看上一篇ET学习笔记
资源加载
AB资源打包和加载
打包
打AB直接用Tools->打包工具,我们来看下具体的打包代码。打开BuildHelper,会看到打包的路径是1
2private const string relativeDirPrefix = "../Release";
public static string BuildFolder = "../Release/{0}/StreamingAssets/";
打包到工程上一级目录的Release中了,
上面代码分别对应下图两个Toggle,如果勾选打包Exe则编译导出不同平台的运营包,如果勾选将资源打进EXE,就会将Release中的AB包拷贝到StreamAssets目录中,这样就不需要进行下载更新。
打包依赖分析
原本准备分析一下打包的依赖分析的,但ET打包这块处理的比较粗糙,就是手动打标签然后打包,我打算进行改性,自动搜集引用关系然后自动打标签,然后打包。主要的思路就是指定要打包的目录,然后以目录作为单位打包,遍历每个资源然后通过AssetDatabase.GetDependence获取到资源的应用关系,提取被引用的资源拎出来单独打包。思路大体就是这样。
加载
资源热更新我之前有写过这方面的文章,可以查看【GFFrameWork】代码热更及资源下载。我们来分析代码,在Init脚本中,一开始就调用了同步AB资源的方法
提醒:如果在Editor下测试资源更新,需要添加上ASYNC的宏定义。然后把本地StreamingAssets目录下的资源列表删除,注意不要把整个Version文件都删除,不然会报错的。
ET是如何判断是该加载AB还是该加载本地Assets资源呢?
如果是Editor模式,首先从AB包里面获取所有的Assets路径,然后遍历添加资源到资源缓存,因为我们要加载一个Asset资源就先需要LoadBundle加载Bundle,然后将资源缓存,然后再是通过ResourcesComponent从缓存中LoadAsset
然后通过组件工厂创建出来并添加到bundles缓存,这里每一个Asset都当成了一个ABInfo
我们可以大致看一下ABInfo的结构,Name就是AB包名(例如:code.unity3d),RefCount就是引用计数。
加载依赖分析
加载资源组件是ResourcesComponent,主要就是加载AB和引用依赖分析,下面对这个组件进行详细分析,从加载一个Asset完整流程分析起,如果是AB加载模式,在Download结束之后会加载一个StreamingAsset的AB包,里面获取Manifest,这个Manifest就是AB依赖分析的重要记录信息
当然Demo里面过于简单,都没有包含依赖的例子,我们假设他有依赖关系,然后走一遍依赖处理流程。
首先是加载AB
进入这个LoadBundle方法
会发现里面有计算要加载的AB它所依赖的AB,如何计算依赖的AB的呢?
上图中知道,我们要查找一个AB的依赖列表,就要深度递归遍历,为什么要深度遍历呢,比如A依赖B,B依赖C,那么要加载A的话,不仅仅要加载B,还要先加载C,这就是为啥要深度递归遍历的原因,C也是A的间接依赖包,下面看一下是如何计算依赖的
到这里就应该知道了是如何计算依赖包的。接来下是一个个的加载所依赖的AB,也就是这个LoadOneBundle方法,这个方法中有一个将AB封装成一个ABInfo的对象,之前也有介绍,还有Editor模式下直接走AssetDatabase加载,这个前面也有介绍,关键是讲一下AB的加载流程
这里采用的是同步加载资源的方法,并且读取AB之后将里面所有的资源缓存,一般适用于AB包里面资源不多的情况,如果资源比较多建议采用异步加载方式,至于查找路径看下图就知道了
当然ET也给我们提供了异步加载资源的方法LoadOneBundleAsync
代码加载
如果是ILRuntime模式则通过IL程序域加载dll,否则通过Assembly反射执行dll代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public void LoadHotfixAssembly()
{
//加载code资源包
Game.Scene.GetComponent<ResourcesComponent>().LoadBundle($"code.unity3d");
//加载code资源
GameObject code = (GameObject)Game.Scene.GetComponent<ResourcesComponent>().GetAsset("code.unity3d", "Code");
//加载dll
byte[] assBytes = code.Get<TextAsset>("Hotfix.dll").bytes;
//加载pdb调试文件
byte[] pdbBytes = code.Get<TextAsset>("Hotfix.pdb").bytes;
#if ILRuntime
Log.Debug($"当前使用的是ILRuntime模式");
//创建ILRuntime程序域
this.appDomain = new ILRuntime.Runtime.Enviorment.AppDomain();
//将dll和pdb加载进内存流
this.dllStream = new MemoryStream(assBytes);
this.pdbStream = new MemoryStream(pdbBytes);
//通过IL程序域加载代码内存流
this.appDomain.LoadAssembly(this.dllStream, this.pdbStream, new Mono.Cecil.Pdb.PdbReaderProvider());
//设置启动方法,从静态方法里查找
this.start = new ILStaticMethod(this.appDomain, "ETHotfix.Init", "Start", 0);
//搜集热更层的所有类型type以便在EventSystem中进行初始化
this.hotfixTypes = this.appDomain.LoadedTypes.Values.Select(x => x.ReflectionType).ToList();
#else
Log.Debug($"当前使用的是Mono模式");
//通过C#的程序集加载dll和pdb
this.assembly = Assembly.Load(assBytes, pdbBytes);
//设置初始化方法
Type hotfixInit = this.assembly.GetType("ETHotfix.Init");
this.start = new MonoStaticMethod(hotfixInit, "Start");
//搜集热更层的类型type
this.hotfixTypes = this.assembly.GetTypes().ToList();
#endif
//卸载Bundle
Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle($"code.unity3d");
}
EventSystem事件系统
事件系统是代码解耦的最常用方式之一,ET里面是EventSystem,EventSystem包含事件消息搜集,事件消息分发和接受,更多的事件类型在ET的Book中也有介绍,这里主要分析一下原理。关于事件系统我之前写过一篇【GFFrameWork】事件系统可以对比学习一下。ET中的事件系统的原理是通过特性和反射进行搜集事件,抛事件到事件接受就是从事件缓存中根据事件类型去查找然后执行。还有一种事件模式是通过事件订阅和事件分发来执行,比如多个地方想关注同一个事件,就在不同的地方(UI界面)进行订阅,当UI关闭或者销毁的时候就取消订阅,这样如果抛出这个事件,就能在所有订阅的地方都接受执行。
举例详解
在DownloadBundle中就有这么一行代码 Game.EventSystem.Run(EventType.LoadingFinish)
意思就是下载AB完成之后抛出一个LoadingFinish的事件,UI层接受到这样的事件之后就开始进行处理,比如关闭LoadingUI。
前面Game.EventSystem.Run方法就是抛事件,而上图中就是接受事件,Run方法里是移除LoadingUI,那么会有人好奇为什么那边抛出这边能接收到呢?下面我们来分析一下原理。
原理分析
前面我们介绍了Init中通过反射来获取自定义特性
这个allEvents就是一个字典,key是我们自定义的事件名字符串,收发事件都是通过这个字符串来标记的,而抛事件呢如下图
这个Handle方法正是调用了我们继承了AEvent时间类的Run抽象方法
也就是调用了上面的LoadingFinishEvent_RemoveLoadingUI中的重写Run方法,到这里应该理解了整个AEvent事件流程了吧,当然ET也给我们提供了三种参数的事件类型,不过带参数的貌似并没有实现,需要我们自己扩展。
事件类型
关于事件类型这里只是详细介绍了好多种类型中的一种普通的Event类型,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int oldhp = 10;
int newhp = 5;
// 抛出hp改变事件
Game.EventSystem.Run("HpChange", oldhp, newhp);
// UI订阅hp改变事件
[ ]
public class HpChange_ShowUI: AEvent<int, int>
{
public override void Run(int a, int b)
{
throw new NotImplementedException();
}
}
// 模型头顶血条模块也订阅hp改变事件
[ ]
public class HpChange_ModelHeadChange: AEvent<int, int>
{
public override void Run(int a, int b)
{
throw new NotImplementedException();
}
}
其他还有一些生命周期系统例如AwakeSystem、StartSystem、UpdateSystem、DestroySystem、ChangeSystem、DeserializeSystem、LoadSystem,还有指定某个类型服务器接收的事件类型1
2
3
4
5
6
7
8
9[ ]
public class C2G_LoginGateHandler : AMRpcHandler<C2G_LoginGate, G2C_LoginGate>
{
protected override void Run(Session session, C2G_LoginGate message, Action<G2C_LoginGate> reply)
{
G2C_LoginGate response = new G2C_LoginGate();
reply(response);
}
}
具体的可以看官方文档或者看我上一篇的介绍。
案例剖析
上面做了那么多基础铺垫,下面真是进入游戏中登录流程的分析
游戏登录
运行游戏,首先进入眼帘的是登录界面,我们就分析一下他是如何被加载出来的,点登录按钮又如何跟服务器交互的,然后如何消失进入Lobby界面的。
加载登录界面
在Hotfix的Init方法中有这么一行代码Game.EventSystem.Run(EventIdType.InitSceneStart)
这个是上面介绍过的发事件,然后下图中就响应了这个事件
还是我们熟悉的通过加载AB然后获取Asset实例化并且通过组件工厂实例化出来然后添加上UILoginComponent,
这里ReferenceCollector组件,是一个UI绑定对应对象的功能组件,这样省的我们代码去GetGameObject
下面我们来看这个OnLogin登录交互逻辑
登录交互
创建一个会话session,然后通过session.Call给服务器发送C2R_Login消息,这个Call方法前面有一个await是异步阻塞方法,直到服务器返回消息才继续往下执行,不然一直阻塞,我们再看下服务器对应的逻辑
服务器根据账号名向网关服务器请求一个key用户客户端的网关登录,或许有人会问,服务器是怎么接受到消息然后执行这个C2R_LoginHandler中的Run方法的,根据上文讲解的知识初步猜测,也是根据特性标签和继承预先缓存这些消息然后通过消息分发的,不过还有待验证。根据这个猜想,然后我们搜索特性表现MessageHandler标签会发现在MessageDispatchComponentSystem有引用,那我们就分析一下这个组件
MessageDispatchComponentSystem
我们知道ET的ECS有AwakeSystem和LoadSystem,在组件加载和初始化的时候会调用对应的方法,
果真证实了我们的猜想,还是通过特性标签+继承的方式搜集所有的消息类,注意一下这个IMHandler接口,我们的消息类C2R——LoginHandler继承的AMRpcHandler<Request,Response>这个类正是继承自这个接口,然后通过GetmessageType方法,这个方法是反射的Request类型作为key去缓存
这个Handle方法被InnerMessageDispatcher和OuterMessageDispatcher引用,而这两个组件中的Dispatch方法在Session中调用,Session中收到消息就调用OnRead方法,然后消息解析,消息分发。
继续上文的登录流程,客户端拿到服务器返回的R2C_Login消息,里面包含网关地址和key,然后客户端继续用这个Key向网关服务器发送登录请求C2G_LoginGame消息
服务器端的处理逻辑
服务器主要是验证Key是否合法,然后创建玩家,客户端拿到返回消息之后根据返回的玩家ID创建玩家,这里有一个reply()方法,这个方法底层是通过session返回response消息给客户端,所以带有返回消息的函数都要调用这个方法,客户端才能接受到返回消息。
发送了登录成功的消息之后,我们看下接受消息是如何处理的,应该就是关闭这个登录界面,跳转到下一个界面
一个事件两个地方响应,一个是删除老的登录UI,一个是创建新的LobbyUI,也证实了我们的想法。
广播创建角色
当我们点击进入地图,场景中就出现一个或者多个角色,那么是如何创建的呢?
客户端创建角色
1 | [MessageHandler] |
主要就是获取服务器的M2C_CreateUnits消息,里面有一个Units的列表,Unit的Proto的结构如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15message UnitInfo
{
int64 UnitId = 1;
float X = 2;
float Y = 3;
float Z = 4;
}
message M2C_CreateUnits // IActorMessage
{
int32 RpcId = 90;
int64 ActorId = 93;
repeated UnitInfo Units = 1;
}
从消息中可以看到UnitInfo记录的是坐标和Id,M2C_CreateUnits里面记录的是Unit列表,那么服务器又是如何给客户端发送这个创建角色的消息的呢?我们来跟踪一下服务器代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16[MessageHandler(AppType.Gate)]
public class C2G_EnterMapHandler : AMRpcHandler<C2G_EnterMap, G2C_EnterMap>
{
protected override async ETTask Run(Session session, C2G_EnterMap request, G2C_EnterMap response, Action reply)
{
Player player = session.GetComponent<SessionPlayerComponent>().Player;
// 获取Map服地址,然后通过这个地址获取对应服务器的Session
IPEndPoint mapAddress = StartConfigComponent.Instance.MapConfigs[0].GetComponent<InnerConfig>().IPEndPoint;
Session mapSession = Game.Scene.GetComponent<NetInnerComponent>().Get(mapAddress);
//网关服
M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session.InstanceId });
player.UnitId = createUnit.UnitId;
response.UnitId = createUnit.UnitId;
reply();
}
}
上面的逻辑是当我们客户端点击进入地图的时候,就会向服务器发送进入地图的消息,服务器对应的处理如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16[MessageHandler(AppType.Gate)]
public class C2G_EnterMapHandler : AMRpcHandler<C2G_EnterMap, G2C_EnterMap>
{
protected override async ETTask Run(Session session, C2G_EnterMap request, G2C_EnterMap response, Action reply)
{
Player player = session.GetComponent<SessionPlayerComponent>().Player;
// 获取Map服地址,然后通过这个地址获取对应服务器的Session
IPEndPoint mapAddress = StartConfigComponent.Instance.MapConfigs[0].GetComponent<InnerConfig>().IPEndPoint;
Session mapSession = Game.Scene.GetComponent<NetInnerComponent>().Get(mapAddress);
//网关服向Map服发送创建角色的消息
M2G_CreateUnit createUnit = (M2G_CreateUnit)await mapSession.Call(new G2M_CreateUnit() { PlayerId = player.Id, GateSessionId = session.InstanceId });
player.UnitId = createUnit.UnitId;
response.UnitId = createUnit.UnitId;
reply();
}
}
接下来就是Map收到消息之后给客户端广播创建对应的角色1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//map服收到网关服传来创建角色的消息
[MessageHandler(AppType.Map)]
public class G2M_CreateUnitHandler : AMRpcHandler<G2M_CreateUnit, M2G_CreateUnit>
{
protected override async ETTask Run(Session session, G2M_CreateUnit request, M2G_CreateUnit response, Action reply)
{
//创建一个角色
Unit unit = ComponentFactory.CreateWithId<Unit>(IdGenerater.GenerateId());
//添加移动和寻路组件
unit.AddComponent<MoveComponent>();
unit.AddComponent<UnitPathComponent>();
unit.Position = new Vector3(-10, 0, -10);//设置出生点的坐标
await unit.AddComponent<MailBoxComponent>().AddLocation();
unit.AddComponent<UnitGateComponent, long>(request.GateSessionId);//添加网关组件
Game.Scene.GetComponent<UnitComponent>().Add(unit); //角色组件添加该玩家
response.UnitId = unit.Id;
// 广播创建的unit
M2C_CreateUnits createUnits = new M2C_CreateUnits();
Unit[] units = Game.Scene.GetComponent<UnitComponent>().GetAll(); //获取所有Unit
foreach (Unit u in units) //进行广播创建
{
UnitInfo unitInfo = new UnitInfo();
unitInfo.X = u.Position.x;
unitInfo.Y = u.Position.y;
unitInfo.Z = u.Position.z;
unitInfo.UnitId = u.Id;
createUnits.Units.Add(unitInfo);
}
MessageHelper.Broadcast(createUnits);
reply();
}
}
上面有角色的出生点坐标是一个固定写死的坐标,这也是为啥我们进入地图之后会有好多角色重叠在同一个点的原因,出生点坐标一般是可以配置的,然后是一个随机的范围,这里处理的比较简单。下面一段广播创建unit的方法Broadcast我们来关注一下,广播的是M2C_CreateUnit消息,这个消息上面我们介绍过,就是客户端接收这个消息之后来创建角色,他本身属于ActorMessage,只要继承自IActorMessage的消息都是给Entity发送的消息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public static class MessageHelper
{
/// <summary>
/// 广播
/// </summary>
/// <param name="message"></param>
public static void Broadcast(IActorMessage message)
{
Unit[] units = Game.Scene.GetComponent<UnitComponent>().GetAll();
//通过ActorMessageSenderComponent组件发消息
ActorMessageSenderComponent actorLocationSenderComponent = Game.Scene.GetComponent<ActorMessageSenderComponent>();
foreach (Unit unit in units)
{
//获取GateSession组件
UnitGateComponent unitGateComponent = unit.GetComponent<UnitGateComponent>();
if (unitGateComponent.IsDisconnect)
{
continue;
}
//通过ActorMessageSender组件和角色身上的网关组件的GateSessionActorId来获取ActorMessageSender发送对应的消息
ActorMessageSender actorMessageSender = actorLocationSenderComponent.Get(unitGateComponent.GateSessionActorId);
actorMessageSender.Send(message);
}
}
}
这个消息发送之后,就能实现向某个角色发送消息,因为知道这个角色所在的网关的SeesionActorId,下面角色广播寻路也会用到这个广播消息。如果是房间模式,就通过房间获取对应房间的Unit集合进行广播,而不是通过Game.Scene.GetComponent
Map地图寻路剖析
我们点击鼠标,就会发现控制的角色在地图中寻路,同步寻路是如何实现的,我们下面追踪一下代码。在客户端MapHelper中有这两行代码1
2Game.Scene.AddComponent<OperaComponent>();
Game.EventSystem.Run(EventIdType.EnterMapFinish);
我们看一下OperationComponent组件
这不正是我们鼠标操作地图的逻辑么,点击地面然后角色朝目标点走,但这里注意有一个Session.Send方法,之前我们学的是Call方法,Call方法是向服务器发送消息并且等待服务器返回消息这么个交互过程,而Send方法只管向服务器发送消息,至于如何起是如何接受处理的我们来看下服务器的逻辑。
客户端向服务器发送的Frame_ClickMap消息是一个继承自IActorLocationMessage的消息,我们就在服务器搜这个消息对应的消息Handler
找到这个角色Unit然后调用它的UnitPathComponent组件的MoveTo寻路方法
广播路径,返回M2C_PathfindingResult消息,包含当前坐标、id、以及后面三个点
获取所有的角色进行广播,我们继续看下客户端接收到消息是如何处理的
角色真正的移动逻辑是在MoveComponent中,由于需要每帧更新角色的位置,所以我们这个MoveComponent组件就需要启动Update功能1
2
3
4
5
6
7
8[ObjectSystem]
public class MoveComponentUpdateSystem : UpdateSystem<MoveComponent>
{
public override void Update(MoveComponent self)
{
self.Update();
}
}
角色的移动就是这样的。
关于ETVoid和ETTask区别
想必看ET的Demo会发现异步编程中大量的ETVoid和ETTask,那么它们之间有什么区别呢?烟雨的文章中有关于这方面的介绍,但我感觉说的并不完全准确,他文章中归纳小结的是从两个特例小结的,例如说ETVoid不能有参数,他举的例子确实没有,但我上文中就有一个反例
这个MoveTo方法就有参数,并且也有await,那么我们还是从源码中去分析这两种到底是什么区别
ETVoid
我们先看下ETVoid源码实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace ETModel
{
[AsyncMethodBuilder(typeof(AsyncETVoidMethodBuilder))]
public struct ETVoid
{
public void Coroutine()
{
}
[DebuggerHidden]
public Awaiter GetAwaiter()
{
return new Awaiter();
}
public struct Awaiter : ICriticalNotifyCompletion
{
[DebuggerHidden]
public bool IsCompleted => true;
[DebuggerHidden]
public void GetResult()
{
throw new InvalidOperationException("ETAvoid can not await, use Coroutine method instead!");
}
[DebuggerHidden]
public void OnCompleted(Action continuation)
{
}
[DebuggerHidden]
public void UnsafeOnCompleted(Action continuation)
{
}
}
}
}
我相信ET应该是参考过Async和Await的IL代码,参考Await的原理自己又封装了一个Awaiter,想要深入理解Async和Await的可以看看下面提供的参考链接,在对比一下ETTask的源码,稍微复杂一些1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace ETModel
{
/// <summary>
/// Lightweight unity specified task-like object.
/// </summary>
[AsyncMethodBuilder(typeof (AsyncETTaskMethodBuilder))]
public partial struct ETTask: IEquatable<ETTask>
{
private readonly IAwaiter awaiter;
[DebuggerHidden]
public ETTask(IAwaiter awaiter)
{
this.awaiter = awaiter;
}
[DebuggerHidden]
public AwaiterStatus Status => awaiter?.Status ?? AwaiterStatus.Succeeded;
[DebuggerHidden]
public bool IsCompleted => awaiter?.IsCompleted ?? true;
[DebuggerHidden]
public void GetResult()
{
if (awaiter != null)
{
awaiter.GetResult();
}
}
public void Coroutine()
{
}
[DebuggerHidden]
public Awaiter GetAwaiter()
{
return new Awaiter(this);
}
public bool Equals(ETTask other)
{
if (this.awaiter == null && other.awaiter == null)
{
return true;
}
if (this.awaiter != null && other.awaiter != null)
{
return this.awaiter == other.awaiter;
}
return false;
}
public override int GetHashCode()
{
if (this.awaiter == null)
{
return 0;
}
return this.awaiter.GetHashCode();
}
public override string ToString()
{
return this.awaiter == null? "()"
: this.awaiter.Status == AwaiterStatus.Succeeded? "()"
: "(" + this.awaiter.Status + ")";
}
public struct Awaiter: IAwaiter
{
private readonly ETTask task;
[DebuggerHidden]
public Awaiter(ETTask task)
{
this.task = task;
}
[DebuggerHidden]
public bool IsCompleted => task.IsCompleted;
[DebuggerHidden]
public AwaiterStatus Status => task.Status;
[DebuggerHidden]
public void GetResult()
{
task.GetResult();
}
[DebuggerHidden]
public void OnCompleted(Action continuation)
{
if (task.awaiter != null)
{
task.awaiter.OnCompleted(continuation);
}
else
{
continuation();
}
}
[DebuggerHidden]
public void UnsafeOnCompleted(Action continuation)
{
if (task.awaiter != null)
{
task.awaiter.UnsafeOnCompleted(continuation);
}
else
{
continuation();
}
}
}
}
/// <summary>
/// Lightweight unity specified task-like object.
/// </summary>
[AsyncMethodBuilder(typeof (ETAsyncTaskMethodBuilder<>))]
public struct ETTask<T>: IEquatable<ETTask<T>>
{
private readonly T result;
private readonly IAwaiter<T> awaiter;
[DebuggerHidden]
public ETTask(T result)
{
this.result = result;
this.awaiter = null;
}
[DebuggerHidden]
public ETTask(IAwaiter<T> awaiter)
{
this.result = default;
this.awaiter = awaiter;
}
[DebuggerHidden]
public AwaiterStatus Status => awaiter?.Status ?? AwaiterStatus.Succeeded;
[DebuggerHidden]
public bool IsCompleted => awaiter?.IsCompleted ?? true;
[DebuggerHidden]
public T Result
{
get
{
if (awaiter == null)
{
return result;
}
return this.awaiter.GetResult();
}
}
public void Coroutine()
{
}
[DebuggerHidden]
public Awaiter GetAwaiter()
{
return new Awaiter(this);
}
public bool Equals(ETTask<T> other)
{
if (this.awaiter == null && other.awaiter == null)
{
return EqualityComparer<T>.Default.Equals(this.result, other.result);
}
if (this.awaiter != null && other.awaiter != null)
{
return this.awaiter == other.awaiter;
}
return false;
}
public override int GetHashCode()
{
if (this.awaiter == null)
{
if (result == null)
{
return 0;
}
return result.GetHashCode();
}
return this.awaiter.GetHashCode();
}
public override string ToString()
{
return this.awaiter == null? result.ToString()
: this.awaiter.Status == AwaiterStatus.Succeeded? this.awaiter.GetResult().ToString()
: "(" + this.awaiter.Status + ")";
}
public static implicit operator ETTask(ETTask<T> task)
{
if (task.awaiter != null)
{
return new ETTask(task.awaiter);
}
return new ETTask();
}
public struct Awaiter: IAwaiter<T>
{
private readonly ETTask<T> task;
[DebuggerHidden]
public Awaiter(ETTask<T> task)
{
this.task = task;
}
[DebuggerHidden]
public bool IsCompleted => task.IsCompleted;
[DebuggerHidden]
public AwaiterStatus Status => task.Status;
[DebuggerHidden]
void IAwaiter.GetResult()
{
GetResult();
}
[DebuggerHidden]
public T GetResult()
{
return task.Result;
}
[DebuggerHidden]
public void OnCompleted(Action continuation)
{
if (task.awaiter != null)
{
task.awaiter.OnCompleted(continuation);
}
else
{
continuation();
}
}
[DebuggerHidden]
public void UnsafeOnCompleted(Action continuation)
{
if (task.awaiter != null)
{
task.awaiter.UnsafeOnCompleted(continuation);
}
else
{
continuation();
}
}
}
}
}
直观对比能知道这两者的一个差别就是ETTask支持参数,也就是这样的异步类型可以返回参数,我们在ET的Demo里面找一个这样的例子来证明一下。
我们最常用的Call方法就是一个ETTask的泛型异步方法,返回给客户端一个消息
结论
相同点
- 都有async修饰符
不同点
- ETTask支持泛型返回参数
- ETVoid方法Init的执行是Init().Coroutine();碰到第一个await就会返回继续往下执行,不会阻塞,ETTask的话要用await Init()执行,会阻塞。但ETTask内部也是单线程执行的。如果ETTask调用时候不用await修饰符则这个方法将不会等内部任务执行完,也就是说执行第一个await语句里面返回的ETTask就返回了然后立马执行下面的代码了。用await的话,就将异步方法当同步方法一样执行了。
我们自己实现一个自定义异步
1 | using System; |
InnerMessage和OuterMessage的区别
简单来说就是InnerMessage是用于服务器之间的消息,而OuterMessage是客户端和服务器交互的消息。
寻路相关
地图数据导出
5.0 unity2018 里
1.在Map场景中创建一个空物体,将Pathfinder脚本组件挂到这个物体上,组件名称显示为【Astar Path(Script)】
2.选择【Save&Load】中的【Load from file】,加载Config/graph.bytes
3.将【Recast Graph】中的【Layer Mask】选择Map
4.点击最底下的【Scan】,即可查看重新生成的导航网格
5.选择【Save to file】,保存替换回Config/graph.bytes
说明:2019的FindPathing插件需要更新,不然Inspector显示有问题。更新插件地址,或者添加下方qq群里面有上传该插件。或者也可以用这个https://github.com/genechiu/NavMesh 开源的寻路。
ET流程图
参考链接
- https://www.xyting.org/2017/02/28/understand-async-await-in-depth.html 深入理解Async和Await
- https://blog.walterlv.com/post/abstract-awaitable-and-awaiter.html 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便
- https://www.cnblogs.com/blueberryzzz/p/8678700.html yield原理
- https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/
- https://www.lfzxb.top/et_ettask_etvoid/ 烟雨ETTask和ETVoid的区别的文章