LiteNetLib
概念
Lite reliable UDP library for .NET Framework 4.7.1, Mono, .NET Core 2.1, .NET Standard 2.0.
RevenantX/LiteNetLib: Lite reliable UDP library for Mono and .NET (github.com)
LiteNetLib也是一个开源的网络框架,相对于之前的Mirror来说,没有那些封装好的上层API(比如[Command]、[SyncVar])。这里会解析官方提供的小例子,了解如何使用这套框架实现状态同步。
示例项目
RevenantX/NetGameExample: Example simple game with LiteNetLib (work in progress) (github.com)
客户端登陆流程
客户端发送JoinPacket
初始场景两个重要物体ClientObject和ServerObject,分别挂载了ClientLogic.cs和ServerLogic.cs脚本

这两个脚本都继承自MonoBehaviour
首先是ClientLogic的Awake方法,订阅消息接收

启动之后点击Host按钮,客户端开始连接

ClientLogic实现了INetEventListener接口,其中包括OnPeerConnected方法。
ClientLogic在Update里调用了

NetManager在PollEvents中遍历事件队列,逐个调用ProcessEvent。在ProcessEvent方法中判断事件类型,如果是连接事件

这个_netEventListener就是ClientLogic在Awake里创建_netManager时传入的

也就是ClientLogic自己,于是在客户端连接时会调用OnPeerConnected方法,发送JoinPacket

服务器接收并发送JoinAcceptPacket
ServerLogic在Awake方法里订阅接收JoinPacket消息

OnJoinReceived首先生成服务器上的对象

然后向新客户端发送JoinAcceptPacket消息

之后是通知原有客户端有新客户端到来,并把原有客户端的状态同步给新客户端
客户端接收
新客户端收到消息后,调用OnJoinAccept方法(ClientLogic在Awake里订阅了),这时才创建玩家预制体

状态同步流程
客户端发送PacketType.Movement
ClientPlayerView继承自MonoBehaviour,在Update方法中使用Input类接收玩家输入

经过一番计算之后调用

然后插值应用到transform上

这里是用了状态同步中的客户端预测,所以立即响应了玩家的输入动作,使用插值是为了防止抖动。可以看下这篇文章
ClientPlayer的SetInput方法设置了_nextCommand

再回到ClientLogic,在Awake方法里实例化了一个LogicTimer,参数是OnLogicUpdate

并且在Update中调用了

而LogicTimer类中固定了更新帧数

在LogicTimer.Update里达到了指定间隔之后才会调用构造函数绑定的_action

这个_action是在它的构造函数赋值的

所以在这里绑定的就是ClientLogic的OnLogicUpdate方法,可以认为OnLogicUpdate是固定每秒30帧进行调用的,其中调用了

_playerManager是ClientPlayerManager类的,它的LogicUpdate如下

这里的_players是

而PlayerHandler类的Update方法仅仅是调用

虽然结构体PlayerHandler中保存的Player类型为BasePlayer,但这里实际上是它的子类ClientPlayer,所以实际调用的是ClientPlayer类覆写的Update
其前半部分是保存原状态,计算新指令并添加到预测状态里

基类BasePlayer的ApplyInput主要是根据命令计算位置和旋转

后面还有射击的处理,但是项目自述文件里说还在TODO状态,所以就先不看
之后遍历_predictionPlayerStates并发送PacketType.Movement类型的数据包

这里_updateCount是控制三次更新才同步一次
ClientLogic的SendPacketSerializable主要是

这个_server字段是在OnPeerConnected里保存的,对应于服务器
服务端接收
ServerLogic实现了INetEventListener接口,跟ClientLogic一样,在接收到消息后会调用OnNetworkReceive方法,如果判断数据类型是PacketType.Movement

OnInputReceived方法主要是反序列化得到数据

然后调用

player字段是ServerPlayer类型的,其ApplyInput方法主要是

ServerPlayer的基类也是BasePlayer,ApplyInput方法前面已经写过了
服务端发送PacketType.ServerState
因为ServerLogic也实例化了一个LogicTimer对象,并且绑定了OnLogicUpdate方法,所以跟之前ClientLogic类一样,可以认为它的OnLogicUpdate是每帧更新30次的。在ServerLogic的onLogicUpdate里调用了

_playerManager字段是ServerPlayerManager类型的,主要是对所有ServerPlayer调用Update并记录NetworkState

ServerPlayer的Update主要是

然后回到ServerLogic的OnLogicUpdate方法,遍历服务端上的所有ServerPlayer

这里可以用foreach遍历,是因为ServerPlayerManager的基类BasePlayerManager实现了IEnumerable<BasePlayer>接口
并发送它们的状态给对应的客户端(在之前创建服务端对象时就绑定了),消息类型为PacketType.ServerState

客户端接收
ClientLogic实现了INetEventListener接口,收到消息后调用OnNetworkReceive方法,如果是PacketType.ServerState消息

OnServerState方法主要是

而ClientPlayerManager的ApplyServerState方法会根据状态id取出对应的PlayerHandler

然后跟当前的_clientPlayer比较,如果是发给自己的消息,则说明是自身的状态更新,否则是通知其它客户端进行状态同步

在ClientPlayer的ReceiveServerState方法里使用了预测,如果服务端发回来的状态跟之前的预测差别不大,那么继续使用预测,否则是因为延迟过大导致预测不对,要清空之前的预测数据

ApplyInput依然是基类BasePlayer的方法,之前已经写过
序列化
文档链接:NetPacketProcessor (revenantx.github.io)
内置类型
内置类型以及相关数组和属性都支持序列化,包括byte,
sbyte, short, ushort,
int, uint, long,
ulong, float, double,
bool, string, char,
IPEndPoint
自定义类型
这里的自定义类型是指传输报文里的使用了自定义类型
比如
1 | class MyPacket{ |
这样仅有内置类型组成的数据包是可以直接序列化的,而下面这个:
1 | class MyPacket{ |
其中包含了类型MyType,属于嵌套nested情况,即使MyType也是仅有内置类型组成的,也需要手动对MyPacket提供序列方法。
有两种做法:
第一种是提供两个静态方法并注册:
1 | struct MyType{ |
另一种是实现INetSerializable接口并注册:
1 | struct MyType : INetSerialiable{ |