Dormancy定义
Dormancy表示休眠, 即在Dormancy期间, Actor不进行属性同步, 直接略过.
Dormancy的对象是Actor, 和Replication(ActorChannel)的单位一致.
一般用于场景中不常发生变化的物体上, 如果预期其一定时间内不会发生变化,
即可设置为Dormancy.
Dormancy状态转换图:
Dormancy转换大致堆栈流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 UENUM (BlueprintType)enum ENetDormancy { DORM_Never UMETA (DisplayName = "Never" ) , DORM_Awake UMETA (DisplayName = "Awake" ) , DORM_DormantAll UMETA (DisplayName = "Dormant All" ) , DORM_DormantPartial UMETA (DisplayName = "Dormant Partial" ) , DORM_Initial UMETA (DisplayName = "Initial" ) , DORM_MAX UMETA (Hidden) , } ;UPROPERTY (BlueprintReadOnly, EditDefaultsOnly, Category=Replication)TEnumAsByte<enum ENetDormancy > NetDormancy;
可以看出, Actor使用NetDormancy变量实现Dormancy逻辑.
其中DORM_Never和DORM_Awake都表示未休眠, 会进行属性同步,
其他的变量表示休眠, 不进行属性同步.
特殊的DORM_Initial表示放在地图中进行休眠的Actor.
Dormancy使用
初始化时, 默认将NetDormancy设置成DORM_Awake,
表示可以使用Dormancy.
如果想要启用Dormancy需要调用AActor.SetNetDormancy将其设置为Dormacy.
后续会根据设置的状态, 进行处理.
具体参考测试用例:https://github.com/fdcumt/ALSV.git
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 #pragma once #include "GameFramework/Actor.h" #include "TestNetDormancyActor.generated.h" UCLASS (BlueprintType, Blueprintable)class ATestNetDormancyActor : public AActor{ GENERATED_BODY () public : ATestNetDormancyActor (); public : virtual void Tick (float DeltaSeconds) override ; virtual void GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const override ; UFUNCTION (Client, Unreliable) void S2C_Unreliable_ChangeCharacterNumber () ; UFUNCTION (Client, Reliable) void S2C_ChangeCharacterNumber () ; UFUNCTION (Server, Reliable) void C2S_ChangeCharacterNumber () ; public : UPROPERTY (Replicated) int32 TestNumber = 0 ; int32 LocalNumber = 0 ; uint32 FrameNumber = 0 ; }; #include "TestNetDormancyActor.h" #include "Net/UnrealNetwork.h" ATestNetDormancyActor::ATestNetDormancyActor () : AActor () { bReplicates = true ; PrimaryActorTick.bCanEverTick = true ; PrimaryActorTick.TickGroup = TG_PrePhysics; } void ATestNetDormancyActor::Tick (float DeltaSeconds) { Super::Tick (DeltaSeconds); if (GetLocalRole () == ENetRole::ROLE_Authority) { ++FrameNumber; if (FrameNumber % 20 == 10 ) { S2C_Unreliable_ChangeCharacterNumber (); } } else if (GetLocalRole () == ENetRole::ROLE_AutonomousProxy) { C2S_ChangeCharacterNumber (); } } void ATestNetDormancyActor::GetLifetimeReplicatedProps (TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps (OutLifetimeProps); DOREPLIFETIME (ATestNetDormancyActor, TestNumber); } void ATestNetDormancyActor::S2C_Unreliable_ChangeCharacterNumber_Implementation () { ++LocalNumber; } void ATestNetDormancyActor::S2C_ChangeCharacterNumber_Implementation () { ++LocalNumber; } void ATestNetDormancyActor::C2S_ChangeCharacterNumber_Implementation () { ++LocalNumber; } if (GetLocalRole () == ENetRole::ROLE_Authority) { ++LocalFrameIndex; if (TestNetDormancyActor == nullptr && LocalFrameIndex==3 ) { FVector Loc = FVector::ZeroVector; FRotator Rot = FRotator::ZeroRotator; FActorSpawnParameters Par; Par.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; TestNetDormancyActor = GetWorld ()->SpawnActor <ATestNetDormancyActor>(ATestNetDormancyActor::StaticClass (), Loc, Rot, Par); TestNetDormancyActor->SetNetDormancy (ENetDormancy::DORM_DormantAll); TestNetDormancyActor->SetAutonomousProxy (true ); TestNetDormancyActor->SetOwner (this ); } }
在SpawnActor时会将其添加到指定Node节点中.
在调用UReplicationGraph::AddNetworkActor时候,
会设置Actor对应的GlobalInfo.bWantsToBeDormant,
检测其是否需要进行一次属性同步.
例如如果在构造函数中将其设置为ENetDormancy::DORM_DormantAll, 即是这样,
也应该进行一次属性同步, 将初始化的信息同步到远端.
创建之后, 然后将其设置为ENetDormancy::DORM_DormantAll.
当属性发生变化的时候调用FlushActorDormancy, 触发一次属性同步.
Dormancy详解
那么在这期间, Dormant究竟是怎么执行的, 它有什么逻辑呢?
下面开始寻根溯源.
AddNetwork详细流程
根据上面的示例, 在ActorSpawn时候,
会添加到DynamicSpatializedActors中.
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 --UWorld::SpawnActor () |--UWorld::AddNetworkActor () | |-- | |--Driver->AddNetworkActor (Actor); | | |--UNetDriver::AddNetworkActor () | | | |--GetNetworkObjectList ().FindOrAdd (Actor, this ); | | | |-- | | | |--ReplicationDriver->AddNetworkActor (Actor); | | | | |-- | | | | |--FGlobalActorReplicationInfo& GlobalInfo = GlobalActorReplicationInfoMap.Get (Actor); | | | | |-- | | | | |--GlobalInfo.bWantsToBeDormant = Actor->NetDormancy > DORM_Awake; | | | | |--UReplicationGraph::RouteAddNetworkActorToNodes () | | | | | |-- | | | | | |--AlwaysRelevantNode->NotifyAddNetworkActor (ActorInfo); | | | | | |-- | | | | | |--ActorsWithoutNetConnection.Add (ActorInfo.Actor); | | | | | |-- | | | | | |--GridNode->AddActor_Dormancy (ActorInfo, GlobalInfo); | | | | | | |--UReplicationGraphNode_GridSpatialization2D::AddActor_Dormancy | | | | | | |-- | | | | | | |--AddActorInternal_Static (ActorInfo, ActorRepInfo, true ); | | | | | | |-- | | | | | | |--AddActorInternal_Dynamic (ActorInfo); | | | | | | | |-- | | | | | | | |--DynamicSpatializedActors.Emplace (ActorInfo.Actor, ActorInfo);
可以看出,
最终将Actor放入了放入了UReplicationGraphNode_GridSpatialization2D.DynamicSpatializedActors中.
在每帧tick时候, 会将同步到其他端.
Awake到Pending
SetNetDormancy(ENetDormancy::DORM_DormantAll);
会将Actor由Awake状态转换到pending状态.
StartBecomingDormant
在SetNetDormancy流程中, 有几个关键点:
将ActorRepInfo.bWantsToBeDormant设置为true.
将Actor放入Cell的UReplicationGraphNode_DormancyNode中.
表明不再进行Gather了.
但是会在UReplicationGraphNode_ConnectionDormancyNode中Gather
1 2 3 4 5 6 7 8 9 10 11 12 13 --AActor.SetNetDormancy () |--NetDriver->NotifyActorDormancyChange (this , OldDormancy); | |--ReplicationDriver->NotifyActorDormancyChange () | | |--ActorRepInfo->bWantsToBeDormant = bNewWantsToBeDormant; | | |--ActorRepInfo->Events.DormancyChange.Broadcast (Actor, *ActorRepInfo, CurrentDormancy, OldDormancyState); | | | |-- | | | |--UReplicationGraphNode_GridSpatialization2D::OnNetDormancyChange () | | | | |--UReplicationGraphNode_GridSpatialization2D.AddActorInternal_Static () | | | | | |--UReplicationGraphNode_GridSpatialization2D.AddActorInternal_Static_Implementation () | | | | | | |--StaticSpatializedActors.Emplace (Actor, FCachedStaticActorInfo (ActorInfo, bDormancyDriven)); | | | | | | |--UReplicationGraphNode_GridSpatialization2D.PutStaticActorIntoCell () | | | | | | | |--
在同一帧的TickFlush GatherActor时候,
会在UReplicationGraphNode_ConnectionDormancyNode中将Actor Gather出来.
针对DormancyActor会有如下流程(如是DS第一帧, 会有截断, 但是一般情况不会.
这里只说一般情况). 可以看出, Gather之后,
Actor会执行StartBecomingDormant, 准备进行休眠状态.
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 --UNetDriver.TickFlush () |--UNetDriver.ServerReplicateActors () | |--UReplicationGraph.ServerReplicateActors () | | |--UReplicationGraphNode_GridSpatialization2D.GatherActorListsForConnection () | | | |--UReplicationGraphNode_ActorList.GatherActorListsForConnection () | | | | |--UReplicationGraphNode_DormancyNode.GatherActorListsForConnection () | | | | | |-- | | | | | |--ConnectionNode = CreateConnectionNode (Params); | | | | | | |--这里会将UReplicationGraphNode_DormancyNode中UReplicationGraphNode_ActorList.ReplicateActorList完全拷贝一份 | | | | | | |--ConnectionNode->DeepCopyActorListsFrom (this ); | | | | | | |--ConnectionNode->InitConnectionNode (RepGraphConnection, Params.ReplicationFrameNum); | | | | | |-- | | | | | |--ConnectionNode->GatherActorListsForConnection (Params); | | | | | | |--UReplicationGraphNode_ConnectionDormancyNode.ConditionalGatherDormantActorsForConnection () | | | | | | | |-- | | | | | | | |--Params.OutGatheredReplicationLists.AddReplicateActorList (ConnectionList); | | |--UReplicationGraph.ReplicateActorListsForConnections_Default () | | | |--UReplicationGraph.ReplicateSingleActor () | | | | |-- | | | | |--UActorChannel.StartBecomingDormant () | | | | | |--FObjectReplicator::StartBecomingDormant () | | | | | | |--bLastUpdateEmpty = false ; | | | | | |--bPendingDormancy = true ; | | | | | |--bIsInDormancyHysteresis = false ; | | | | | |--Connection->StartTickingChannel (this );
DS第一帧截断原因:
问:
已经StartBecomingDormant的Actor还会进行属性同步吗?
每帧还会将BecomeDormant的Actor Gather出来. 但是, 不会进行属性同步了.
因为, 他没有发生变化:
如果发生丢包, 还会进行同步, 并且这里BitsWritten会大于0,
有数据进行网络传输.
BecomeDormant
在ActorChannelTick的时候, 每帧检测是否满足Dormancy的条件, 如果满足,
直接进入Dormancy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 --UNetDriver.TickFlush () |-- |--UIpConnection.Tick () | |--UNetConnection.Tick () | | |-- | | |--UActorChannel.Tick () | | | |--UChannel.Tick () | | | | |-- | | | | |--UChannel.BecomeDormant () | | | | | |--bPendingDormancy = false ; | | | | | |--bIsInDormancyHysteresis = false | | | | | |--Dormant = true | | | | | |--UActorChannel.Close (EChannelCloseReason::Dormancy); | | | | | | |--Connection->Driver->NotifyActorFullyDormantForConnection (Actor, Connection); | | | | | | | |--UReplicationDriver.NotifyActorFullyDormantForConnection () | | | | | | | | |-- | | | | | | | | |--FConnectionReplicateActorInfo.bDormantOnConnection | | | | | | | | |--
ReadyForDormancy:
该函数可以判断Actor在某个Connection上,
是否将数据传输完成(远端已经Ack)
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 bool FObjectReplicator::ReadyForDormancy (bool bSuppressLogs) { if (GetObject () == nullptr ) { UE_LOG (LogRep, Verbose, TEXT ("ReadyForDormancy: Object == nullptr" )); return true ; } if (!bLastUpdateEmpty) { if (!bSuppressLogs) { UE_LOG (LogRepTraffic, Verbose, TEXT (" [%d] Not ready for dormancy. bLastUpdateEmpty = false" ), OwningChannel->ChIndex); } return false ; } if (FSendingRepState* SendingRepState = RepState.IsValid () ? RepState->GetSendingRepState () : nullptr ) { if (SendingRepState->HistoryStart != SendingRepState->HistoryEnd) { return false ; } if (SendingRepState->NumNaks > 0 ) { return false ; } if (!SendingRepState->bOpenAckedCalled) { return false ; } if (SendingRepState->PreOpenAckHistory.Num () > 0 ) { return false ; } for (FPropertyRetirement& Retirement : SendingRepState->Retirement) { if (Retirement.Next != nullptr ) { if (!bSuppressLogs) { UE_LOG (LogRepTraffic, Verbose, TEXT (" [%d] OutAckPacketId: %d First: %d Last: %d " ), OwningChannel->ChIndex, OwningChannel->Connection->OutAckPacketId, Retirement.OutPacketIdRange.First, Retirement.OutPacketIdRange.Last); } return false ; } } } return true ; }
bLastUpdateEmpty为true, 下图是bLastUpdateEmpty刷新的地方
SendingRepState->HistoryStart必须和SendingRepState->HistoryEnd相同
即已经发送的数据全部被ack了, 并且没有丢包情况.
因为History代表已经发送的历史, 每次UpdateChangelistHistory时候,
会根据AckPacketId清理History数据. 当所有history数据清理干净之后,
就表示所有数据都已经发送完成了.
SendingRepState必须全部都Ack了. 这里处理丢包情况,
即所有丢包数据都已经重发了.
重发丢包数据:
bOpenAckedCalled和PreOpenAckHistory:
所有SpawnActor的ack都收到了.
SendingRepState->Retirement用于记录DeltaProperty的历史.
这个也要全部ack后才能Dormancy.
并且根据上述堆栈,
可以看到BecomeDormant最后设置FConnectionReplicateActorInfo.bDormantOnConnection为true.
即GatherActor也不会收集到DormancyActor了.
FullDormancy关闭Channel
通过如下堆栈可以看出Dormancy CloseBunch并不会将Actor等对象销毁,
仅仅是将Channel回收了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 --UNetDriver.TickFlush () |-- |--UIpConnection.Tick () | |--UNetConnection.Tick () | | |-- | | |--UActorChannel.Tick () | | | |--UChannel.Tick () | | | | |-- | | | | |--UChannel.BecomeDormant () | | | | | |--bPendingDormancy = false ; | | | | | |--bIsInDormancyHysteresis = false | | | | | |--Dormant = true | | | | | |--UActorChannel.Close (EChannelCloseReason::Dormancy); | | | | | | |--UChannel::Close () | | | | | | | |--UChannel::Close () | | | | | | | | |--CloseBunch.bReliable = 1 ; | | | | | | | | |--CloseBunch.CloseReason = Reason; | | | | | | | | |--SendBunch ( &CloseBunch, 0 ); | | | | | | |--Connection->Driver->NotifyActorFullyDormantForConnection (Actor, Connection); | | | | | | | |--UReplicationDriver.NotifyActorFullyDormantForConnection () | | | | | | | | |-- | | | | | | | | |--FConnectionReplicateActorInfo.bDormantOnConnection = true | | | | | | | | |--
该堆栈还需要注意一点:FConnectionReplicateActorInfo.bDormantOnConnection = true
,
该Actor会在某个UReplicationGraphNode_ConnectionDormancyNode
下次gather的过程中,
将其剔除(参考下图).
回收Channel:
客户端调用CloseUp, 进行清理
在清理的过程中, 需要清理Channel的所有资源, 然后将Channel回收.
这些资源包括但不限于ReplicationMap, ActorReplication数据等.
DS收到Ack, CloseChannel
DS收到Ack后, 根据ChannelIndex对Channel进行Close.
清理并回收Channel.
触发UNetReplicationGraphConnection::NotifyActorChannelCleanedUp进行清理:
清理ReplicatorMap相关信息.
FullDormancy到PendingDormancy
FlushActorDormancy主动唤醒 .
FlushActorDormancy会主动唤醒处在FullDormancy的Channel,
然后进去PendingDormancy状态, 等待数据全部传输完成之后,
再次进行FullDormancy状态.
注意在此过程中, 如果Channel销毁了, 会进行重建.
1 2 3 4 5 6 7 8 9 10 11 12 // Actor调用AActor.FlushNetDormancy详细堆栈 --AActor.FlushNetDormancy() |--UNetDriver.FlushActorDormancy() | |--UReplicationDriver.FlushNetDormancy() | | |--GlobalInfo.LastFlushNetDormancyFrame = ReplicationGraphFrame; | | |--GlobalInfo.Events.DormancyFlush.Broadcast(Actor, GlobalInfo); | | | |--UReplicationGraphNode_DormancyNode.OnActorDormancyFlush() | | | | |-- // 针对所有DormancyNode中存储的UReplicationGraphNode_ConnectionDormancyNode, 进行处理 | | | | | |--UReplicationGraphNode_ConnectionDormancyNode::NotifyActorDormancyFlush() | | | | | | |--ReplicateActorList.Add(ActorInfo.Actor); | | |--// 遍历所有Connection, 设置bDormantOnConnection为false | | |--Info->bDormantOnConnection = false
触发下图这些DormancyNode的回调
从上图可以看出,
FlushActorDormancy会将Actor放入UReplicationGraphNode_ConnectionDormancyNode的ReplicateActorList中,
然后和StartBecomingDormant类似, 进行同步后,
等待该Actor所有数据同步完成之后, 进入休眠.
Dormancy->Awake
将Actor设置为DORM_Awake
会重新打开Channel
客户端收到Bunch,查找对应的channel,
没有就创建
根据ChannelIndex索引Channel, 由于Channel时新建的,
所以其对应的Actor为空, 然后根据GUID去查找对应的Actor.
最后将Actor和Channel绑定.
如果在构造函数中将NetDormancy设置为DORM_DormantAll呢?
和SetNetDormancy流程一样, 这里不再赘述.
判断Actor是否进行Dormancy
ShouldActorGoDormant
问
Dormancy期间,能否RPC?
能否S2C
能进行RPC, 并且会创建新的Channel,
而且在RPC之前还会进行ReplicateActor.
构建测试用例:
并且, S2C_ChangeCharacterNumber为Reliable.
在RPC过程中, 会首先创建ActorChannel, 然后ReplicateActor,
最后才会RPC.
image-20230729163913372
只要是Channel刚创建的, 就会首先进行ReplicateActor.
注意在这这个过程中, 是不会触发Dormancy流程的,
只进行了ReliableReplicateActor+Reliable/UnReliable的RPC.
这里还要特意说一下, 针对Dormancy期间的S2C,
都会发送Reliable的ReplicateActor包, 由于是ReliableBunch,
其可靠性由Driver自己保证. 针对UnreliableRPC, 其Bunch为非Reliable,
所以丢就丢了, 不会重发.
为什么属性没有变化还会ReplicateActor呢?
由于为新打开的Channel, 所以设置为Init, 并设定为ReliableBunch.
序列化Actor的GUID, 并强制设置为Dirty.
填充Bunch中的SerializeNewActor消息, 然后进行发送.
能否C2S
不能, 因为Dormancy, 会销毁Channel, 无法进行RPC.
测试关键代码:
总结
Dormancy的单位是Actor.
Dormancy会销毁Channel, 所以当其休眠的时候, 不会发生属性同步.
如果发送RPC, 只有DS向客户端发送的RPC可以执行, C2S会直接丢弃.
并且所有S2C的RPC都会调用, ReplicateActor 进行一次属性同步.
对于属性不常发生变化的Actor,
在构造函数的时候将其设置成Dormancy.
对于一段时间内属性不发生变化的Actor,
手动将其设置成Dormancy(TestNetDormancyActor->SetNetDormancy(ENetDormancy::DORM_DormantAll);
).