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{ |