NetDormancy

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
/** Describes if an actor can enter a low network bandwidth dormant mode */
UENUM(BlueprintType)
enum ENetDormancy
{
/** This actor can never go network dormant. */
DORM_Never UMETA(DisplayName = "Never"),
/** This actor can go dormant, but is not currently dormant. Game code will tell it when it go dormant. */
DORM_Awake UMETA(DisplayName = "Awake"),
/** This actor wants to go fully dormant for all connections. */
DORM_DormantAll UMETA(DisplayName = "Dormant All"),
/** This actor may want to go dormant for some connections, GetNetDormancy() will be called to find out which. */
DORM_DormantPartial UMETA(DisplayName = "Dormant Partial"),
/** This actor is initially dormant for all connection if it was placed in map. */
DORM_Initial UMETA(DisplayName = "Initial"),

DORM_MAX UMETA(Hidden),
};

// Actor中的变量
/** Dormancy setting for actor to take itself off of the replication list without being destroyed on clients. */
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
// 其中关键代码:
//TestNetDormancyActor.h
#pragma once
#include "GameFramework/Actor.h"
#include "TestNetDormancyActor.generated.h"


UCLASS(BlueprintType, Blueprintable)
class ATestNetDormancyActor : public AActor
{
GENERATED_BODY()
public:
/** Default constructor for AActor */
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;
};

// TestNetDormancyActor.cpp
#include "TestNetDormancyActor.h"
#include "Net/UnrealNetwork.h"

ATestNetDormancyActor::ATestNetDormancyActor()
: AActor()
{
bReplicates = true;
//NetDormancy = ENetDormancy::DORM_DormantAll;

PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.TickGroup = TG_PrePhysics;
}

void ATestNetDormancyActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);

if (GetLocalRole() == ENetRole::ROLE_Authority)
{
++FrameNumber;
/*
if (FrameNumber%20==19)
{
++TestNumber;
FlushNetDormancy();
}
else */if (FrameNumber % 20 == 10)
{
S2C_Unreliable_ChangeCharacterNumber();
//S2C_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;
}

// 并在ABasePlayerController.tick中添加:
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
 // ActorSpawn时候, 将Actor添加到ReplicationNode中
--UWorld::SpawnActor()
|--UWorld::AddNetworkActor()
| |--// 遍历所有NetDriver, 并将Actor添加到其中
| |--Driver->AddNetworkActor(Actor);
| | |--UNetDriver::AddNetworkActor()
| | | |--GetNetworkObjectList().FindOrAdd(Actor, this);
| | | |--// 向ReplicationGraph中添加NetworkActor
| | | |--ReplicationDriver->AddNetworkActor(Actor);
| | | | |--// 添加Actor对应的描述信息FGlobalActorReplicationInfo
| | | | |--FGlobalActorReplicationInfo& GlobalInfo = GlobalActorReplicationInfoMap.Get(Actor);
| | | | |--// 设置Dormant属性
| | | | |--GlobalInfo.bWantsToBeDormant = Actor->NetDormancy > DORM_Awake;
| | | | |--UReplicationGraph::RouteAddNetworkActorToNodes()
| | | | | |--// 针对ActorInfo.Actor->bAlwaysRelevant, 添加到AlwaysRelevantNode中
| | | | | |--AlwaysRelevantNode->NotifyAddNetworkActor(ActorInfo);
| | | | | |--// 针对ActorInfo.Actor->bOnlyRelevantToOwner, 添加到ActorsWithoutNetConnection中
| | | | | |--ActorsWithoutNetConnection.Add(ActorInfo.Actor);
| | | | | |--// 其他情况添加到GridNode中
| | | | | |--GridNode->AddActor_Dormancy(ActorInfo, GlobalInfo);
| | | | | | |--UReplicationGraphNode_GridSpatialization2D::AddActor_Dormancy
| | | | | | |--// 如果要想要休眠, 调用AddActorInternal_Static函数
| | | | | | |--AddActorInternal_Static(ActorInfo, ActorRepInfo, true);
| | | | | | |--// 否则, 调用AddActorInternal_Dynamic
| | | | | | |--AddActorInternal_Dynamic(ActorInfo);
| | | | | | | |--// 放入了UReplicationGraphNode_GridSpatialization2D.DynamicSpatializedActors中
| | | | | | | |--DynamicSpatializedActors.Emplace(ActorInfo.Actor, ActorInfo);

可以看出, 最终将Actor放入了放入了UReplicationGraphNode_GridSpatialization2D.DynamicSpatializedActors中. 在每帧tick时候, 会将同步到其他端.

Awake到Pending

SetNetDormancy(ENetDormancy::DORM_DormantAll);会将Actor由Awake状态转换到pending状态.

StartBecomingDormant

在SetNetDormancy流程中, 有几个关键点:

  1. 将ActorRepInfo.bWantsToBeDormant设置为true.

  2. 将Actor放入Cell的UReplicationGraphNode_DormancyNode中. 表明不再进行Gather了. 但是会在UReplicationGraphNode_ConnectionDormancyNode中Gather

1
2
3
4
5
6
7
8
9
10
11
12
13
// AActor.SetNetDormancy堆栈
--AActor.SetNetDormancy()
|--NetDriver->NotifyActorDormancyChange(this, OldDormancy);
| |--ReplicationDriver->NotifyActorDormancyChange()
| | |--ActorRepInfo->bWantsToBeDormant = bNewWantsToBeDormant;
| | |--ActorRepInfo->Events.DormancyChange.Broadcast(Actor, *ActorRepInfo, CurrentDormancy, OldDormancyState);
| | | |--// 在UReplicationGraphNode_GridSpatialization2D::AddActor_Dormancy中注册的回调函数
| | | |--UReplicationGraphNode_GridSpatialization2D::OnNetDormancyChange()
| | | | |--UReplicationGraphNode_GridSpatialization2D.AddActorInternal_Static()
| | | | | |--UReplicationGraphNode_GridSpatialization2D.AddActorInternal_Static_Implementation()
| | | | | | |--StaticSpatializedActors.Emplace(Actor, FCachedStaticActorInfo(ActorInfo, bDormancyDriven));
| | | | | | |--UReplicationGraphNode_GridSpatialization2D.PutStaticActorIntoCell()
| | | | | | | |--// 将Actor放入对应的Cell中

在同一帧的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
// AActor.SetNetDormancy同一帧, TickFlush GatherActor的流程
--UNetDriver.TickFlush()
|--UNetDriver.ServerReplicateActors()
| |--UReplicationGraph.ServerReplicateActors()
| | |--UReplicationGraphNode_GridSpatialization2D.GatherActorListsForConnection()
| | | |--UReplicationGraphNode_ActorList.GatherActorListsForConnection()
| | | | |--UReplicationGraphNode_DormancyNode.GatherActorListsForConnection()
| | | | | |--// 如果ConnectionNode为空还要进行创建
| | | | | |--ConnectionNode = CreateConnectionNode(Params);
| | | | | | |--这里会将UReplicationGraphNode_DormancyNode中UReplicationGraphNode_ActorList.ReplicateActorList完全拷贝一份
| | | | | | |--ConnectionNode->DeepCopyActorListsFrom(this);
| | | | | | |--ConnectionNode->InitConnectionNode(RepGraphConnection, Params.ReplicationFrameNum);
| | | | | |--//UReplicationGraphNode_ConnectionDormancyNode收集Actor
| | | | | |--ConnectionNode->GatherActorListsForConnection(Params);
| | | | | | |--UReplicationGraphNode_ConnectionDormancyNode.ConditionalGatherDormantActorsForConnection()
| | | | | | | |--// 将node添加到OutGatheredReplicationLists中
| | | | | | | |--Params.OutGatheredReplicationLists.AddReplicateActorList(ConnectionList);
| | |--UReplicationGraph.ReplicateActorListsForConnections_Default()
| | | |--UReplicationGraph.ReplicateSingleActor()
| | | | |--// 针对GlobalActorInfo.bWantsToBeDormant要进行StartBecomingDormant
| | | | |--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
// AActor.SetNetDormancy之后的BecomeDormant流程
--UNetDriver.TickFlush()
|--// 针对所有ClientConnection, 都会进行tick
|--UIpConnection.Tick()
| |--UNetConnection.Tick()
| | |--// 遍历所有需要Tick的Channel, 进行tick
| | |--UActorChannel.Tick()
| | | |--UChannel.Tick()
| | | | |--// 当满足条件bPendingDormancy && ReadyForDormancy() 时候, 进行BecomeDormant
| | | | |--UChannel.BecomeDormant()
| | | | | |--bPendingDormancy = false;
| | | | | |--bIsInDormancyHysteresis = false
| | | | | |--Dormant = true
| | | | | |--UActorChannel.Close(EChannelCloseReason::Dormancy);
| | | | | | |--Connection->Driver->NotifyActorFullyDormantForConnection(Actor, Connection);
| | | | | | | |--UReplicationDriver.NotifyActorFullyDormantForConnection()
| | | | | | | | |--// 遍历所有UReplicationGraph.Connections, 将FConnectionReplicateActorInfo.bDormantOnConnection设置为true
| | | | | | | | |--FConnectionReplicateActorInfo.bDormantOnConnection
| | | | | | | | |-- // 至此, 该Actor进入Dormancy.

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; // Technically, we don't want to hold up dormancy, but the owner needs to clean us up, so we warn
}

// Can't go dormant until last update produced no new property updates
// 上一次Update必须是空的, 即上一次没有任何更新
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)
{ // 所有数据都ack了
// We have change lists that haven't been acked
return false;
}

if (SendingRepState->NumNaks > 0)
{ // 没有丢包
return false;
}

if (!SendingRepState->bOpenAckedCalled)
{ // 没有调用过OpenAckedCalled
return false;
}

if (SendingRepState->PreOpenAckHistory.Num() > 0)
{// 没有调用过OpenAckedCalled
return false;
}

// Can't go dormant if there are unAckd property updates
for (FPropertyRetirement& Retirement : SendingRepState->Retirement)
{ // 针对Delta Properties类型, 所有消息也要被Ack才可以
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;
}
  1. bLastUpdateEmpty为true, 下图是bLastUpdateEmpty刷新的地方

  1. SendingRepState->HistoryStart必须和SendingRepState->HistoryEnd相同

即已经发送的数据全部被ack了, 并且没有丢包情况. 因为History代表已经发送的历史, 每次UpdateChangelistHistory时候, 会根据AckPacketId清理History数据. 当所有history数据清理干净之后, 就表示所有数据都已经发送完成了.

  1. SendingRepState必须全部都Ack了. 这里处理丢包情况, 即所有丢包数据都已经重发了.

重发丢包数据:

  1. bOpenAckedCalled和PreOpenAckHistory: 所有SpawnActor的ack都收到了.

  2. 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
// ActorChannel进入DormancyClose流程, 向客户端发送DormancyClose Bunch
--UNetDriver.TickFlush()
|--// 针对所有ClientConnection, 都会进行tick
|--UIpConnection.Tick()
| |--UNetConnection.Tick()
| | |--// 遍历所有需要Tick的Channel, 进行tick
| | |--UActorChannel.Tick()
| | | |--UChannel.Tick()
| | | | |--// 当满足条件bPendingDormancy && ReadyForDormancy() 时候, 进行BecomeDormant
| | | | |--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()
| | | | | | | | |--// 遍历所有UReplicationGraph.Connections, 将FConnectionReplicateActorInfo.bDormantOnConnection设置为true
| | | | | | | | |--FConnectionReplicateActorInfo.bDormantOnConnection = true
| | | | | | | | |-- // 至此, 该Actor进入Dormancy.

该堆栈还需要注意一点: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.

测试关键代码:

总结

  1. Dormancy的单位是Actor.
  2. Dormancy会销毁Channel, 所以当其休眠的时候, 不会发生属性同步.
  3. 如果发送RPC, 只有DS向客户端发送的RPC可以执行, C2S会直接丢弃. 并且所有S2C的RPC都会调用, ReplicateActor 进行一次属性同步.
  4. 对于属性不常发生变化的Actor, 在构造函数的时候将其设置成Dormancy.
  5. 对于一段时间内属性不发生变化的Actor, 手动将其设置成Dormancy(TestNetDormancyActor->SetNetDormancy(ENetDormancy::DORM_DormantAll);).