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.