UE网络-ReplicateActor

概述

ReplicateActor分成三步: SerializeNewActor+序列化属性+序列化删除的Objs.

其中序列化属性中的属性又分成三类: Actor自身属性+ActorComponent属性+SubObject属性. 每类属性的序列化都会调用同一个函数: FObjectReplicator::ReplicateProperties.

FObjectReplicator::ReplicateProperties分成如下几个步骤:

  1. UpdateChangelistMgr
  2. MergeNotSendChangeList
  3. MergeResendChangeHistory
  4. CustomDeltaProperties
  5. Replicate Queued (unreliable functions)
  6. 拼接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分成如下几个步骤:

  1. UpdateChangelistMgr
  2. MergeNotSendChangeList
  3. MergeResendChangeHistory
  4. CustomDeltaProperties
  5. Replicate Queued (unreliable functions)
  6. 拼接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 需要填入的Handle0*5+1+1=2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
USTRUCT(BlueprintType)
struct FMyTestDataItem
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite)
int32 DataItemInt = 0; // PropertyIndex为0
UPROPERTY(BlueprintReadWrite)
bool DataItemBool = false; // PropertyIndex为1
UPROPERTY(BlueprintReadWrite)
FName DataItemName; // PropertyIndex为2
UPROPERTY(BlueprintReadWrite)
float DataItemFloat = 0.1; // PropertyIndex为3
UPROPERTY(BlueprintReadWrite)
EMyTestEnum DataItemEnum = EMyTestEnum::ENone; // PropertyIndex为4
};

// 如下类型需要网络同步
TArray<FMyTestDataItem> ItemList;

实例:

注意: 填入ChangedList中的Handle, 对非数组类型, 其为cmd.RelativeHandle, 对数组类型, 其为:ArrayIndex*PropertyNum+PropertyIndex+1.

MergeNotSendChangeList

每个ConnectionRepstate中会记录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

客户端解析数据

BunchArchetype
解析流程

解析SerializeActor

Channel中的Actornullptr, 并且Bunch.bOpen为true, 则重新反序列化对应的Actor. 这里反序列化是指将Bunch中对应数据取出, 是否重新spawn要看Actor是否真正存在, 如果不存在会Spawn, 否则直接引用. 一般在Dormancy时, Channel会回收, 当FlushDormancy时会重新将Channel和Actor绑定, 这时候客户端就不会创建, 仅仅将消息处理掉, 并将ChannelActor绑定.

无论怎么样, 都会将Bunch中的SerializeNewActor数据读取出来.

解析ContentBlockHeader

解析ContentBlock中的数据, GUID表示当前处理的是哪个UObject, 当UObject不存在时, 使用ClassGUID直接创建, bHasReplayout表示ContentBlockPayload中是否包含属性.

解析NetFieldIndex

解析Property

如果ContentBlockbHasRepLayout为true , 则表明有属性传输, 则进行属性反序列化.

首先解析PropertyHandle, 然后递归解析Property属性.

每次在函数ReceiveProperties_r尾部, 继续读取Handle.

函数ReceivePropertyHelper真正解析属性:

处理数组类型:

数组的Handle从0开始.

每次比较, Handle都会+1.

最终, 如果和DS发来的Handle一致, 则表明是同一个Handle, 解析其后面的属性.

思考

Actor首次同步在进行比较时候和谁进行对比?

该问题换一种说法是ReplicationObj的Shadow初始化对象是什么?

Actor首次同步, 在进行比较时候和谁进行对比?(UObject的CDO). 从代码上看, 是其Archetype, 即一般情况下就是CDO.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在首次ReplicationActor时, 首次并初始化`FRepChangelistState.StaticBuffer`的流程
--UActorChannel.ReplicateActor.if
|--ABasePlayerController.ReplicateSubobjects
| |--AActor.ReplicateSubobjects.for.if
| | |--UActorChannel.ReplicateSubobject
| | | |--UActorChannel.FindOrCreateReplicator
| | | | |--FObjectReplicator.StartReplicating
| | | | | |--UNetDriver.GetReplicationChangeListMgr
| | | | | | |--// 在这里会传入ShadowStateSource, 其值为InObject->GetArchetype();, 可以理解为CDO
| | | | | | |--FRepLayout.CreateReplicationChangelistMgr
| | | | | | | |--FReplicationChangelistMgr::FReplicationChangelistMgr
| | | | | | | | |--FRepChangelistState::FRepChangelistState
| | | | | | | | | |--InRepLayout->CreateShadowBuffer(InSource)
| | | | | | | | | | |--FRepLayout.InitRepStateStaticBuffer()
| | | | | | | | | | | |--ShadowData.Buffer.SetNumZeroed(ShadowDataBufferSize);
| | | | | | | | | | | |--ConstructProperties(ShadowData);
| | | | | | | | | | | | |--// 遍历所有Parent.Property, 进行初始化
| | | | | | | | | | | | |--Parent.Property->InitializeValue(ShadowData + Parent);
| | | | | | | | | | | |--CopyProperties(ShadowData, Source);
| | | | | | | | | | | | |--// 遍历所有Parent.Property, 拷贝Source数据到Shadow中
| | | | | | | | | | | | |--Parent.Property->InitializeValue(ShadowData + Parent);

新的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.

image-20240305162123068

最终发送的数据是ShadowData还是Object本身

:最终发送的数据是Object本身. 所以ShadowData只用于对比, 最终发送给各个端的数据还是Object本身.

在函数FRepLayout.SendProperties_r中序列化Property. 可以看出拷贝的数据源为SourceData

进一步追踪, 查到其来源为在函数FObjectReplicator.ReplicateProperties中, 传入了Channel所对应的Object本身.

image-20240305123550296

执行堆栈:

为什么会MergeNotSendChangeList

因为所有Connection共用一套changeList, 某些Actor在某些情况下针对ConnectionA可能不会进行同步, 而其他Connection同步了该Actor, 那么共享的Changelist中就有ConnectionA没有同步的数据, 所以要将这些数据合并到待发送的Changelsit中.

HistoryListArchitecture

FRepLayoutCmd.RelativeHandleChangeList中的Handle是一个吗?

不完全是. 填入ChangedList中的Handle, 对非数组类型, 其为cmd.RelativeHandle, 对数组类型, 其为:ArrayIndex*PropertyNum+PropertyIndex+1.