UE网络-ReplicateActor
概述
ReplicateActor
分成三步:
SerializeNewActor
+序列化属性
+序列化删除的Objs
.
其中序列化属性中的属性又分成三类:
Actor自身属性
+ActorComponent属性
+SubObject属性
.
每类属性的序列化都会调用同一个函数:
FObjectReplicator::ReplicateProperties
.
FObjectReplicator::ReplicateProperties
分成如下几个步骤:
- UpdateChangelistMgr
- MergeNotSendChangeList
- MergeResendChangeHistory
- CustomDeltaProperties
- Replicate Queued (unreliable functions)
- 拼接ContentBlock
整体架构
SerializeNewActor
当该Channel第一次(OpenPacketId.First
为空)发送数据时,
会序列化SerializeNewActor
,
将Actor的ArchetypeGUID
,
LevelGUID
等信息全部进行序列化.
注意大约什么情况下是Channel第一次发送消息呢? 一般而言,
是Actor第一次创建Channel, 或者休眠恢复之后, 重建Channel,
该Channel第一次发送消息, 而不是该Actor第一次发送信息.
序列化属性
下文详述.
序列化删除Objs的属性
遍历该Actor下所有Replicator
(UActorChannel.ReplicationMap
),
检测FObjectReplicator.WeakObjectPtr
的有效性, 如果失效了,
证明该Obj被销毁了, 需要删除, 然后将其信息记录下来, 序列化到Bunch内.
带有删除信息的Bunch
必定是Reliable
.
消息结构:
FObjectReplicator.ReplicateProperties
注意,
以下流程是拼接ContentBlockPayload
FObjectReplicator::ReplicateProperties
分成如下几个步骤:
- UpdateChangelistMgr
- MergeNotSendChangeList
- MergeResendChangeHistory
- CustomDeltaProperties
- Replicate Queued (unreliable functions)
- 拼接ContentBlock
最终会组建ContentBlockPayload:
UpdateChangelistMgr
遍历所有ParentCmd
,
如果PushModel
为dirty就进行比较, 否则直接跳过.
遍历ParentCmd
对应的Cmd
,
使用PropertiesAreIdentical
进行比较, 如果确实发生变化,
则将Handle
放入ChangedList
中. 还会将当前数据放入Shadow
中.
对于数组类型, 要展开进行递归处理. 首先递归比较其数组中的每个元素:
如果数组发生了变化,
会将数组的Handle
+数组数量
+数组中变化元素的Handle
+结束符0
放入ChangedList
中
如果数组被清空了(数组大小发生了变化, 但是比较结果又是空的,
证明数组被清空了),
会将数组的Handle
+数组数量(空数组,数量为0)
+结束符0
填入ChangedList
中.
其中数组元素属性的对应Handle
的计算方式为ArrayIndex*PropertyNum+PropertyIndex+1
.
以FMyTestDataItem
为例, 结构体内部有五个属性,
并且有一个TArray<FMyTestDataItem> ItemList;
需要网络同步,
当ItemList[0].DataItemBool
发生变化时候,
根据公式:ArrayIndex*PropertyNum+PropertyIndex
需要填入的Handle
为0*5+1+1=2
.
1 | USTRUCT(BlueprintType) |
实例:
注意:
填入ChangedList
中的Handle
, 对非数组类型,
其为cmd.RelativeHandle
, 对数组类型,
其为:ArrayIndex*PropertyNum+PropertyIndex+1
.
MergeNotSendChangeList
每个Connection
的Repstate
中会记录LastChangelistIndex
,
每次Replication时,
会将[LastChangelistIndex
-最新数据的ChangeIndex]合并.
并且这些数据会合并到HistoryItem中.
为什么会有额外未发送(除了当帧的)的Changelist呢?
因为所有Connection
共用一套changeList
,
某些Actor
在某些情况下针对ConnectionA
可能不会进行同步,
而其他Connection
同步了该Actor
,
那么共享的Changelist
中就有ConnectionA
没有同步的数据,
所以要将这些数据合并到待发送的Changelsit
中.
MergeResendChangeHistory
每次收到Nak的时候, 需要将HistoryList中的Item标记为Resend, 在属性同步时将其合并, 进行重发. 并且这些数据会合并到HistoryItem中. 并且将之前的ResendHistoryItem清空, 意味着责任转移, 如果当前包丢失, 只需要重传当前HistoryItem就可以了.
BuildSharedSerialize
根据ChangedData, 将允许SharedSerialize
的属性,
放入共享结构FRepSerializationSharedInfo
中.
CustomDeltaProperties
FastArray中有详细介绍.
Replicate Queued(unreliable functions)
详见:RPC浅析中的QueueBunch
拼接ContentBlock
ContentBlockHeader
客户端解析数据
解析SerializeActor
当Channel
中的Actor
为nullptr
,
并且Bunch.bOpen
为true, 则重新反序列化对应的Actor.
这里反序列化是指将Bunch
中对应数据取出,
是否重新spawn
要看Actor
是否真正存在,
如果不存在会Spawn
, 否则直接引用. 一般在Dormancy时,
Channel会回收, 当FlushDormancy
时会重新将Channel和Actor绑定,
这时候客户端就不会创建, 仅仅将消息处理掉,
并将Channel
和Actor
绑定.
无论怎么样,
都会将Bunch中的SerializeNewActor
数据读取出来.
解析ContentBlockHeader
解析ContentBlock中的数据,
GUID
表示当前处理的是哪个UObject
,
当UObject不存在时, 使用ClassGUID
直接创建,
bHasReplayout
表示ContentBlockPayload
中是否包含属性.
解析NetFieldIndex
解析Property
如果ContentBlock
中bHasRepLayout
为true ,
则表明有属性传输, 则进行属性反序列化.
首先解析PropertyHandle
,
然后递归解析Property属性
.
每次在函数ReceiveProperties_r
尾部,
继续读取Handle
.
函数ReceivePropertyHelper
真正解析属性:
处理数组类型:
数组的Handle
从0开始.
每次比较, Handle
都会+1.
最终, 如果和DS发来的Handle
一致, 则表明是同一个Handle,
解析其后面的属性.
思考
Actor首次同步在进行比较时候和谁进行对比?
该问题换一种说法是ReplicationObj
的Shadow初始化对象是什么?
Actor首次同步, 在进行比较时候和谁进行对比?(UObject的CDO). 从代码上看,
是其Archetype
, 即一般情况下就是CDO.
1 | 在首次ReplicationActor时, 首次并初始化`FRepChangelistState.StaticBuffer`的流程 |
新的Connection
,
在第一次同步场景中某个Actor, 是怎么处理的?
背景: 某个Actor已经经过很多次修改,
这时候新来了一个Connection
, 怎么同步此时的Actor?
答案: 如果在记录History时,
有一种方法能记录某个时间点之前的全部变化,
那么针对新的Connection
到来时,
只要将之前的变化全部发送给对应的Connection
就可以了.
UE的方式也是如此, UE使用一个大小为64的buffer, 此buffer不是环形buffer,
它是累积buffer, 如果超过了目前使用的ChangedList超过了64,
那么就会将第一个合并到第二个上, 然后第一个会空余出来,
第二个变成新序列的第一个,
即FRepChangelistState.HistoryStart++
.
UE的理念第一个元素表示某个时间点之前的所有变化,
而其他的Item依然存储当时Compare的Changed结果.
关键函数FRepLayout.CompareProperties
.
最终发送的数据是ShadowData
还是Object
本身
答:最终发送的数据是Object
本身.
所以ShadowData
只用于对比,
最终发送给各个端的数据还是Object
本身.
在函数FRepLayout.SendProperties_r
中序列化Property
.
可以看出拷贝的数据源为SourceData
进一步追踪,
查到其来源为在函数FObjectReplicator.ReplicateProperties
中,
传入了Channel
所对应的Object
本身.
执行堆栈:
为什么会MergeNotSendChangeList
因为所有Connection
共用一套changeList
,
某些Actor
在某些情况下针对ConnectionA
可能不会进行同步,
而其他Connection
同步了该Actor
,
那么共享的Changelist
中就有ConnectionA
没有同步的数据,
所以要将这些数据合并到待发送的Changelsit
中.
FRepLayoutCmd.RelativeHandle
和ChangeList
中的Handle
是一个吗?
不完全是.
填入ChangedList
中的Handle
, 对非数组类型,
其为cmd.RelativeHandle
, 对数组类型,
其为:ArrayIndex*PropertyNum+PropertyIndex+1
.