UpdateChangelistMgr详解

关键变量

FRepChangedPropertyTracker

FRepChangedPropertyTracker存储着一帧对应Object Parent Properties是否Dirty. 每个Object只有一份FRepChangedPropertyTracker, 所有Connection共享, 它标志着该Object内Parent层级的属性是否Dirty, 并存储在UNetDriver.RepChangedPropertyTrackerMap中(关键函数UNetDriver::FindOrCreateRepChangedPropertyTracker). 并且FRepChangedPropertyTracker.Parents初始化时候默认为1, 即Dirty. 而且不会自动清理, 即如果使用就要自己维护.

类图

创建流程

初始化设置

在创建ObjectReplicator时, 在DS上同时创建对应的FRepChangedPropertyTracker, 关键堆栈:

初始化

FRepChangedPropertyTracker最为关键的变量就是FRepChangedPropertyTracker.Parents, FRepChangedPropertyTracker.Parents初始化时候默认为1, 即Dirty.

修改

目前只有如下位置使用了该功能. 表示如果某一个标志没有开启, 对整个结构体都不用检测了. AActor.bReplicateMovement控制AActor.ReplicatedMovement是否同步. FRepRootMotionMontage.bIsActive控制ACharacter.RepRootMotion是否同步.

只使用未修改

虽然每个Object只有一份, 所有Connection共享, 但是它却是在FSendingRepState.RepChangedPropertyTracker中被使用, 原因如下:

1.用于检测Parent层级的属性是否Dirty. 如果不为Dirty就不会进行比较, 直接skip. 关键函数``.

2.虽有引用(如下图), 但是并无真正使用.

清理

FRepChangedPropertyTracker.Parents初始化时候默认为1, 即Dirty. 所有不使用该功能的成员都默认为Dirty, 即每次都需要对比子成员. 对于需要使用该功能的成员则需要自己维护. 示例(ACharacter.RepRootMotion):

注意

该功能是有bug的, 一个属性发生变化需要将最终结果同步到远端, 即也需要一次同步. 针对ACharacter.RepRootMotion的修改如下.

思考: 为什么AActor.ReplicatedMovementAActor.AttachmentReplication不用进行如下修改呢?

对比二者OnRep的不同:

可以发现, RepRootMotion.bIsActive是放在RepRootMotion里面的, 即如果不同步, 针对某些Connection远端永远不会接收最后一次的属性修改, 即永远不会为false; 而另一些Connection远端却永远为false, 都不会为true.

某些Connection bIsActive永远为true的原因:最后一次的属性变化没同步. 而false变成true时, shadowData中为true, 所以也不会同步, 造成远端永远为true.

某些Connection bIsActive永远为false的原因: 因为丢包. 发生重传永远是重传最新数据, 而不是Shadow中的, 所以会传到远端为false, 而DS上的bIsActive变为true后, 和ShadowData(也为true)相同, 所以不会传送给远端, 最终远端永远为fasle, 除非发生丢包重传.

FRepChangelistState.StaticBuffer

每个UObject只有一份, 所有Connection共用一份. 它存储着上次比较后的数据.

创建

分配内存, 并拷贝CDO数据. 关键函数FRepLayout.InitRepStateStaticBuffer+FRepLayout.ConstructProperties+FRepLayout.CopyProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void FRepLayout::InitRepStateStaticBuffer(FRepStateStaticBuffer& ShadowData, const FConstRepObjectDataBuffer Source) const
{
check(ShadowData.Buffer.Num() == 0);
ShadowData.Buffer.SetNumZeroed(ShadowDataBufferSize);
ConstructProperties(ShadowData);
CopyProperties(ShadowData, Source);
}

void FRepLayout::ConstructProperties(FRepStateStaticBuffer& InShadowData) const
{
FRepShadowDataBuffer ShadowData = InShadowData.GetData();

// Construct all items
for (const FRepParentCmd& Parent : Parents)
{
// Only construct the 0th element of static arrays (InitializeValue will handle the elements)
if (Parent.ArrayIndex == 0)
{
check((Parent.ShadowOffset + Parent.Property->GetSize()) <= InShadowData.Num());
Parent.Property->InitializeValue(ShadowData + Parent);
}
}
}

void FRepLayout::CopyProperties(FRepStateStaticBuffer& InShadowData, const FConstRepObjectDataBuffer Source) const
{
FRepShadowDataBuffer ShadowData = InShadowData.GetData();

// Init all items
for (const FRepParentCmd& Parent : Parents)
{
// Only copy the 0th element of static arrays (CopyCompleteValue will handle the elements)
if (Parent.ArrayIndex == 0)
{
check((Parent.ShadowOffset + Parent.Property->GetSize()) <= InShadowData.Num());
Parent.Property->CopyCompleteValue(ShadowData + Parent, Source + Parent);
}
}
}

ShadowData最初来源

用CDO的数据进行初始化.

调用堆栈:

ShadowData更新

每个Object独一份, 所有Connection共享. 它存储着上次比较不相等后的最新值.

CompareProperties_r函数中, 通过比较发现如果真的不相等, 就会更新ShadowData.Data数据.

流程

UpdateChangelistMgr流程图

Merge NotSend

合并没有发送的ChangedList.

  1. Actor所有Connection共享的, 每个Actor只有一份FObjectReplicator.ChangelistMgr, 它也是所有Connection共享的.
  2. 每个Connection的Actor Replicate是相互独立的, 即允许Actor在某个Connection上跨帧同步.

基于以上背景, 某些Actor可能因为跨帧, 导致某些帧的Changed还没有加入到同步队列中, 此时需要将其合并到Changed中.

合并没有发送的ChangedList相关代码:

Merge Resend data

Merge Resend Data在FRepLayout.ReplicateProperties中, 这里不赘述. 关键代码:

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

image-20240305123550296

执行堆栈: