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

初始场景两个重要物体ClientObjectServerObject,分别挂载了ClientLogic.csServerLogic.cs脚本

这两个脚本都继承自MonoBehaviour

首先是ClientLogicAwake方法,订阅消息接收

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

ClientLogic实现了INetEventListener接口,其中包括OnPeerConnected方法。

ClientLogicUpdate里调用了

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

这个_netEventListener就是ClientLogicAwake里创建_netManager时传入的

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

服务器接收并发送JoinAcceptPacket

ServerLogicAwake方法里订阅接收JoinPacket消息

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

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

之后是通知原有客户端有新客户端到来,并把原有客户端的状态同步给新客户端

客户端接收

新客户端收到消息后,调用OnJoinAccept方法(ClientLogicAwake里订阅了),这时才创建玩家预制体

状态同步流程

客户端发送PacketType.Movement

ClientPlayerView继承自MonoBehaviour,在Update方法中使用Input类接收玩家输入

经过一番计算之后调用

然后插值应用到transform

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

ClientPlayerSetInput方法设置了_nextCommand

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

并且在Update中调用了

LogicTimer类中固定了更新帧数

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

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

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

_playerManagerClientPlayerManager类的,它的LogicUpdate如下

这里的_players

PlayerHandler类的Update方法仅仅是调用

虽然结构体PlayerHandler中保存的Player类型为BasePlayer,但这里实际上是它的子类ClientPlayer,所以实际调用的是ClientPlayer类覆写的Update

其前半部分是保存原状态,计算新指令并添加到预测状态里

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

后面还有射击的处理,但是项目自述文件里说还在TODO状态,所以就先不看

之后遍历_predictionPlayerStates并发送PacketType.Movement类型的数据包

这里_updateCount是控制三次更新才同步一次

ClientLogicSendPacketSerializable主要是

这个_server字段是在OnPeerConnected里保存的,对应于服务器

服务端接收

ServerLogic实现了INetEventListener接口,跟ClientLogic一样,在接收到消息后会调用OnNetworkReceive方法,如果判断数据类型是PacketType.Movement

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

然后调用

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

ServerPlayer的基类也是BasePlayerApplyInput方法前面已经写过了

服务端发送PacketType.ServerState

因为ServerLogic也实例化了一个LogicTimer对象,并且绑定了OnLogicUpdate方法,所以跟之前ClientLogic类一样,可以认为它的OnLogicUpdate是每帧更新30次的。在ServerLogiconLogicUpdate里调用了

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

ServerPlayerUpdate主要是

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

这里可以用foreach遍历,是因为ServerPlayerManager的基类BasePlayerManager实现了IEnumerable<BasePlayer>接口

并发送它们的状态给对应的客户端(在之前创建服务端对象时就绑定了),消息类型为PacketType.ServerState

客户端接收

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

OnServerState方法主要是

ClientPlayerManagerApplyServerState方法会根据状态id取出对应的PlayerHandler

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

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

ApplyInput依然是基类BasePlayer的方法,之前已经写过

序列化

文档链接:NetPacketProcessor (revenantx.github.io)

内置类型

内置类型以及相关数组和属性都支持序列化,包括byte, sbyte, short, ushort, int, uint, long, ulong, float, double, bool, string, char, IPEndPoint

自定义类型

这里的自定义类型是指传输报文里的使用了自定义类型

比如

1
2
3
class MyPacket{
int[] some { get; set; }
}

这样仅有内置类型组成的数据包是可以直接序列化的,而下面这个:

1
2
3
class MyPacket{
MyType some { get; set; }
}

其中包含了类型MyType,属于嵌套nested情况,即使MyType也是仅有内置类型组成的,也需要手动对MyPacket提供序列方法。

有两种做法:

第一种是提供两个静态方法并注册:

1
2
3
4
5
6
7
8
9
10
struct MyType{
public static void Serialize(NetDataWriter writer, MyType mytype){
// 调用writer.Put写入字段
}
public static MyType Deserialize(NetDataReader reader){
// 从reader.GetXXX读取字段,构造并返回一个对象
}
}
netPacketProcessor = new NetPacketProcessor();
netPacketProcessor.RegisterNestedType( MyType.Serialize, MyType.Deserialize );

另一种是实现INetSerializable接口并注册:

1
2
3
4
5
6
7
8
9
10
struct MyType : INetSerialiable{
public void Serialize(NetDataWriter writer){
// 调用writer.Put写入字段
}
public void Deserialize(NetDataReader reader){
// 从reader.GetXXX读取字段
}
}
netPacketProcessor = new NetPacketProcessor();
netPacketProcessor.RegisterNestedType<MyType>();