REPNOTIFY_Always的指针类型, 一次同步会触发两次OnRep

前言

当指针类型进行OnRep时候, 可能会触发两次. 触发条件为

  1. 该指针成员指向的对象还没有创建, 但是其GUID已经随Bunch发送而来.
  2. 该成员被设置成REPNOTIFY_Always类型.

触发流程

构造复现环境

创建两个Actor, 然后指针互相指向对方.

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
FVector Loc = FVector::ZeroVector;
FRotator Rot = FRotator::ZeroRotator;
FActorSpawnParameters Par;
Par.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
UClass* pClass = LoadObject<UClass>(this, TEXT("Blueprint'/Game/Test/TestRepFlow/BPTestRepFlowSpawnedActor.BPTestRepFlowSpawnedActor_C'"));
TestRepFlowSpawnedActor = GetWorld()->SpawnActor<AActor>(pClass, Loc, Rot, Par);
TestRepFlowSpawnedActor->SetAutonomousProxy(true);
TestRepFlowSpawnedActor->SetOwner(this);

TestRepFlowSpawnedActor12 = GetWorld()->SpawnActor<AActor>(pClass, Loc, Rot, Par);
TestRepFlowSpawnedActor12->SetAutonomousProxy(true);
TestRepFlowSpawnedActor12->SetOwner(this);

if (ATestRepFlowSpawnedActor *pTestRepFlowSpawnedActor = Cast<ATestRepFlowSpawnedActor>(TestRepFlowSpawnedActor))
{
pTestRepFlowSpawnedActor->TestRepFlowSpawnedActor = TestRepFlowSpawnedActor12;
ATestRepFlowSpawnedActor* pTestRepFlowSpawnedActor12 = Cast<ATestRepFlowSpawnedActor>(TestRepFlowSpawnedActor12);
pTestRepFlowSpawnedActor12->TestRepFlowSpawnedActor = pTestRepFlowSpawnedActor;
}

// 成员设置成REPNOTIFY_Always
void ATestRepFlowSpawnedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(ATestRepFlowSpawnedActor, TestRepFlowSpawnedActor, COND_None, REPNOTIFY_Always);
}

序列化Actor

在收到SerializeNewActor Bunch时候, 创建Actor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Always Replicate的指针类型, 一次同步会触发两次OnRep---创建Actor
--UIpNetDriver.TickDispatch.for.else.if
|--UNetConnection.ReceivedRawPacket.if.if.if
| |--UNetConnection.ReceivedRawPacket.if.if.if
| | |--UChannel.ReceivedRawBunch.else
| | | |--UChannel.ReceivedNextBunch.if
| | | | |--UChannel.ReceivedSequencedBunch
| | | | | |--UActorChannel.ReceivedBunch
| | | | | | |--UActorChannel.ProcessBunch.if
| | | | | | | |--UPackageMapClient.SerializeNewActor.if.if.if.if.if
| | | | | | | | |--UWorld.SpawnActorAbsolute
| | | | | | | | | |--UWorld.SpawnActor
| | | | | | | | | | |--NewObject
| | | | | | | | | | | |--StaticConstructObject_Internal.if
| | | | | | | | | | | | |--ATestRepFlowSpawnedActor.ATestRepFlowSpawnedActor

将Property放入RepNotifies中

在解析Bunch时候, 由于GUID指向的对象还没有创建, 所以序列化属性失败, 但是Property的RepNotifyCondition类型为REPNOTIFY_Always, 所以无论无论序列化成功与否, 都会将其塞入RepNotifies中, 等待OnRep.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Always Replicate的指针类型, 一次同步会触发两次OnRep---虽然没为属性赋值, 但是已经将其放入RepNotifies中
--UIpNetDriver.TickDispatch.for.else.if
|--UNetConnection.ReceivedRawPacket.if.if.if
| |--UNetConnection.ReceivedPacket.while.{
| | |--UChannel.ReceivedRawBunch.else
| | | |--UChannel.ReceivedNextBunch.if
| | | | |--UChannel.ReceivedSequencedBunch
| | | | | |--UActorChannel.ReceivedBunch
| | | | | | |--UActorChannel.ProcessBunch.while
| | | | | | | |--FObjectReplicator.ReceivedBunch.if
| | | | | | | | |--FRepLayout.ReceiveProperties
| | | | | | | | | |--ReceiveProperties_r
| | | | | | | | | | |--ReceivePropertyHelper
| | | | | | | | | | | |--// 序列化属性, 由于GUID对应的Object还没到来, 所以此次赋值失败
| | | | | | | | | | | |--Cmd.Property->NetSerializeItem(Bunch, Bunch.PackageMap, Data + SwappedCmd);
| | | | | | | | | | | |--// 由于Rep类型为REPNOTIFY_Always, 所以将其加入了RepNotifies中
| | | | | | | | | | | |--RepNotifies->AddUnique(Parent.Property);

触发第一次OnRep

触发第一次OnRep时, 此时Actor属性还没有赋值, 即为空.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Always Replicate的指针类型, 一次同步会触发两次OnRep---第一次OnRep
--UIpNetDriver.TickDispatch.for.else.if
|--UNetConnection.ReceivedRawPacket.if.if.if
| |--UNetConnection.ReceivedPacket.while.{
| | |--UChannel.ReceivedRawBunch.else
| | | |--UChannel.ReceivedNextBunch.if
| | | | |--UChannel.ReceivedSequencedBunch
| | | | | |--UActorChannel.ReceivedBunch
| | | | | | |--UActorChannel.ProcessBunch
| | | | | | | |--FObjectReplicator.CallRepNotifies
| | | | | | | | |--FRepLayout.CallRepNotifies.for.switch.{
| | | | | | | | | |--AActor.ProcessEvent.if.if
| | | | | | | | | | |--UObject.ProcessEvent.{
| | | | | | | | | | | |--UFunction.Invoke
| | | | | | | | | | | | |--ATestRepFlowSpawnedActor.OnRep_SpawnedActor

关键调用位置:

触发原因:RepNotifies中存了对应属性, 所以一定会触发. 而且触发完后, 还会清理RepNotifies

设置值

通过每帧Tick, 来查询GUID对应的对象是否存在, 如果存在, 则进行赋值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Always Replicate的指针类型, 一次同步会触发两次OnRep---为指针成员赋值
--UNetDriver.TickFlush.if.if.if.for.if
|--FObjectReplicator.UpdateUnmappedObjects
| |--FRepLayout.UpdateUnmappedObjects.if
| | |--FRepLayout.UpdateUnmappedObjects_r.for.if
| | | |--// 序列化属性
| | | |--FObjectPropertyBase.NetSerializeItem
| | | | |--FObjectProperty.SetObjectPropertyValue
| | | |--// 将其塞入RepNotifies中, 等待OnRep.
| | | |--RepState->RepNotifies.AddUnique(Parent.Property);
| |--// 第二次OnRep
| |--FObjectReplicator.CallRepNotifies
| | |--FRepLayout.CallRepNotifies.for.switch.{
| | | |--AActor.ProcessEvent.if.if
| | | | |--UObject.ProcessEvent.{
| | | | | |--UFunction.Invoke
| | | | | | |--ATestRepFlowSpawnedActor.OnRep_SpawnedActor

在赋值之后, 还要将其塞入RepNotifies中, 等待OnRep.

第二次OnRep

当属性设置完成之后, 会在同一帧调用OnRep.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Always Replicate的指针类型, 一次同步会触发两次OnRep---为指针成员赋值
--UNetDriver.TickFlush.if.if.if.for.if
|--FObjectReplicator.UpdateUnmappedObjects
| |--FRepLayout.UpdateUnmappedObjects.if
| | |--FRepLayout.UpdateUnmappedObjects_r.for.if
| | | |--// 序列化属性
| | | |--FObjectPropertyBase.NetSerializeItem
| | | | |--FObjectProperty.SetObjectPropertyValue
| | | |--// 将其塞入RepNotifies中, 等待OnRep.
| | | |--RepState->RepNotifies.AddUnique(Parent.Property);
| |--// 第二次OnRep
| |--FObjectReplicator.CallRepNotifies
| | |--FRepLayout.CallRepNotifies.for.switch.{
| | | |--AActor.ProcessEvent.if.if
| | | | |--UObject.ProcessEvent.{
| | | | | |--UFunction.Invoke
| | | | | | |--ATestRepFlowSpawnedActor.OnRep_SpawnedActor

思考

带有指针的结构体怎么OnRep呢

从如下代码可以看出, 只要结构体成员中有一个从DS传输过来, 就会直接OnRep, 它不会区分其他成员是否到齐.

优化

通过以上分析, 可以清晰看出, 其为什么调用了两次OnRep, 并且也知道了调用两次OnRep条件什么. 那么我们能不能干掉一次无用的OnRep呢?

针对单个指针类型

当反序列化一个Object指针类型时候, 会进入Map->SerializeObject(UPackageMapClient::SerializeObject).

UPackageMapClient::SerializeObject的返回值即为其是否解析出来了.

所以, 直接用其返回值即可:

其他类型属性也能直接使用返回值判断吗? 可以的. 这里是Cmd, 一定是子属性, 不可能是结构体等复杂属性. 如果是结构体, 那么一定自己重写了NetSerialize等函数, 这个就需要考虑具体情况了.

针对带有指针类型的结构体等

需要添加记录数组结构, 等到都到来的时候才能调用OnRep, 还要考虑多个相关Bunch到来后, 指针类型依旧没有映射的情况.