Actor的同步可以说是UE4网络里面最大的一个模块了,里面包括属性同步,RPC调用等,这里为了方便我将他们拆成了3个部分来分别叙述。
有了前面的描述,我们已经知道NetDiver负责整个网络的驱动,而ActorChannel就是专门用于Actor同步的通信通道。
这里对Actor同步做一个比较细致的描述:服务器在NetDiver的TickFlush里面,每一帧都会去执行ServerReplicateActors来同步Actor的相关内容。在这里我们需要做以下处理:
- 获取到所有连接到服务器的ClientConnections,首先获取引擎每帧可以同步的最大Connection的数量,超过这个限制的忽略。然后对每个Connection几乎都要进行下面所有的操作
- 找到要同步的Actor,只有被放到World.NetworkActors里面的Actor才会被考虑,Actor在被Spawn时候就会添加到这个NetworkActors列表里面(新的版本里面已经把需要同的ACtor放到了NetDriver的NetworkObjects列表里面了)
- 找到客户端玩家控制的角色ViewTarget(ViewTaget与摄像机绑定在一起),这个角色的位置是决定其他Actor是否同步的关键
- 验证Actor,对于要销毁的以及所有权Role为ROLE_NONE的Actor不会同步
- 是否到达Actor同步时间,Actor的同步是有一定频率的,Actor身上有一个NetUpdateTime,每次同步前都会通过下面这个公式来计算下一次Actor的同步时间,如果没有到达这个时间就会放弃本次同步Actor->NetUpdateTime = World->TimeSeconds + FMath::SRand() * ServerTickTime + 1.f/Actor->NetUpdateFrequency;
- 如果这个Actor设置OnlyRelevantToOwner,那么就会放到一个特殊的列表里面OwnedConsiderList然后只同步给属于他的客户端。否则会把Actor放到ConsiderList里面
- 对于休眠状态的Actor不会进行同步,对于要进入休眠状态的Actor也要特殊处理关闭同步通道
- 查看当前的Actor是否有通道Channel,如果没有,还要看看Actor是否已经加在了场景,没有加载就跳过同步
- 接第8个条件——没有Channel的情况下,还会执行Actor::IsNetRelevantFor判断是否网络相关,对于不可见的或者太远的Actor会返回false,不会同步
- Actor的同步数量可能非常大,所以有必要对所有的Actor进行一个优先级的排列
处理完上面的逻辑后会对优先级表里的所有Actor进行排序 - 排序后,如果连接没有加载此 actor 所在的关卡,则关闭通道(如果存在)并继续
每 1 秒钟调用一次 AActor::IsNetRelevantFor,确定 actor 是否与连接相关,如果不相关的时间达到 5 秒钟,则关闭通道
如果要同步的Actor没有ActorChannel就给其创建一个并绑定Actor,执行同步并更新NetUpdateTime = Actor->GetWorld()->TimeSeconds + 0.2f * FMath::FRand();
如果此连接出现饱和剩下的 actor会根据连接相关时间判断是否在下一个时钟更新 - 执行UActorChannel::ReplicateActor执行真正的Actor同步以及内部数据的同步,这里会将Actor(PackageMap->SerializeNewActor),Actor子对象以及其属性序列化(ReplicateProperties)封装到OutBunch并发送给客户端
(备注:我们当前版本下面的逻辑都是写在UNetDriver::ServerReplicateActors里面,4.12以后的UE4已经分别把Connection预处理,获取同步Actor列表,优先级处理等逻辑封装到单独的函数里了,详见ServerReplicateActors_BuildConsiderlist, ServerReplicateActors_PrioritizedActors, ServerReplicateActors_ProsessPrioritizedActors等函数
优先级排序规则是什么?答案是按照是否有controller,距离以及是否在视野。通过FActorPriority构造代码可以定位到APawn::GetNetPriority,这里面会计算出当前Actor对应的优先级,优先级越高同步越靠前,是否有Controller的权重最大)
总之,大体上Actor同步的逻辑就是在TickFlush里面去执行ServerReplicateActors,然后进行前面说的那些处理。最后对每个Actor执行ActorChannel::ReplicateActor将Actor本身的信息,子对象的信息,属性信息封装到Bunch并进一步封装到发送缓存中,最后通过Socket发送出去。
下面是服务器的同步Actor的发送Bunch堆栈:(代码修改过,与UE默认的有些不同)
图4-1 服务器同步Actor堆栈图
下面描述客户端是如何接收到服务器同步过来的Actor的。首先客户端TickDispatch检测服务器的消息,收到消息后通过Connection以及Channel进行解析,最后一步解析出完整数据的操作在UActorChannel::ProcessBunch执行,在这个函数里面:
- 如果发现当前的ActorChannel对应的Actor为NULL,就对当前的Bunch进行反序列化Connection->PackageMap->SerializeNewActor(Bunch, this, NewChannelActor);解析出Actor的内容并执行PostInitializeComponents。如果Actor不为NULL,跳过这一步(参考下面图一堆栈)
- 随后根据Bunch信息找到同步过来的属性值并对当前Actor对应的属性进行赋值
- 最后执行PostNetInit调用Actor的BeginPlay。(参考下面堆栈)
下面截取了客户端接收到同步Actor并初始化的调用堆栈:
图4-2 客户端接收并序列化同步的Actor堆栈图
图4-3 客户端初始化同步过来Actor堆栈图
从上面的描述来看,基本上我们可以很容易的分析出当前的Actor是否被同步,比如在UActorChannel::ReceivedBunch里面打个断点,看看当前通道里有没有你要的Actor就可以了。
1.组件(子对象)同步
组件(还有其他子对象)是挂在Actor上面的,所以组件的同步与Actor同步是紧密相连的,当一个Actor进行同步的时候会判断所有的子对象是否标记了Replicate,如果标记了,就对其以及其属性进行同步。
这些子对象同步方式(RPC等)也与Actor相差无几,实际上他们想要同步的话需要借助ActorChannel创建自己的FObjectReplicator以及属性同步的相关数据结构。简单来说,就是一个Actor身上的组件同步需要借用这个Actor的通道来进行。下面3段代码是服务器序列化子对象准备发送的逻辑:
//UActorChannel::ReplicateActor() DataChannel.cpp
// The Actor
WroteSomethingImportant |= ActorReplicator->ReplicateProperties( Bunch, RepFlags );
// 子对象的同步操作
WroteSomethingImportant |= Actor->ReplicateSubobjects(this, &Bunch, &RepFlags);
//ActorReplication.cpp
boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
{
check(Channel);
check(Bunch);
check(RepFlags);
bool WroteSomething = false;
for (int32 CompIdx =0; CompIdx < ReplicatedComponents.Num(); ++CompIdx )
{
UActorComponent * ActorComp = ReplicatedComponents[CompIdx].Get();
//如果组件标记同步
if (ActorComp && ActorComp->GetIsReplicated())
{
WroteSomething |= ActorComp->ReplicateSubobjects(Channel, Bunch, RepFlags); // Lets the component add subobjects before replicating its own properties.检测组件否还有子组件
WroteSomething |= Channel->ReplicateSubobject(ActorComp, *Bunch, *RepFlags); // (this makes those subobjects 'supported', and from here on those objects may have reference replicated) 同步该组件
}
}
return WroteSomething;
}
//DataChannel.cpp
boolUActorChannel::ReplicateSubobject(UObject *Obj, FOutBunch&Bunch, constFReplicationFlags&RepFlags)
{
if ( !Connection->Driver->GuidCache->SupportsObject( Obj ) )
{
FNetworkGUID NetGUID = Connection->Driver->GuidCache->AssignNewNetGUID_Server(Obj ); //Make sure he gets a NetGUID so that he is now 'supported'
}
bool NewSubobject = false;
if (!ObjectHasReplicator(Obj))
{
Bunch.bReliable = true;
NewSubobject = true;
}
//组件的属性同步需要先在当前的ActorChannel里面创建新的FObjectReplicator
bool WroteSomething = FindOrCreateReplicator(Obj).Get().ReplicateProperties(Bunch, RepFlags);
if (NewSubobject && !WroteSomething)
{
......
}
return WroteSomething;
}
下面一段代码是客户端接收服务器同步过来的子对象逻辑:
// void UActorChannel::ProcessBunch( FInBunch & Bunch )DataChannel.cpp
// 该函数前面的代码主要是是进行反序列化当前Actor的相关操作
while ( !Bunch.AtEnd() && Connection != NULL&& Connection->State != USOCK_Closed )
{
bool bObjectDeleted = false;
//当前通道的Actor以及反序列化成功,这里开始继续从Bunch里面寻找子对象进行反序列化
//如果当前Actor没有子组件,这里返回的就是Actor自身
......
TSharedRef<FObjectReplicator>& Replicator = FindOrCreateReplicator( RepObj );
bool bHasUnmapped = false;
// 找到当前子对象(或当前Actor)的Replicator以后,这里开始进行属性值的读取了
if ( !Replicator->ReceivedBunch( Bunch, RepFlags, bHasUnmapped ) )
{
......
}
......
}
前面Actor同步有提到,当从ActorChannel解析Bunch信息的时候就可以尝试对该数据流进行Actor的反序列化。从这段代码可以进一步看出,Actor反序列化之后会立刻开始判断Bunch里面是否存在其子对象,如果存在还会进一步读取子对象同步过来的属性值。如果没有子对象,就读取自身同步过来的属性。
关于子组件的反序列化还分为两种情况。要想理解这两种情况,还需要清楚两个概念——动态组件与静态组件。
对于静态组件:一旦一个Actor被标记为同步,那么这个Actor身上默认所挂载的组件也会随Actor一起同步到客户端(也需要序列化发送)。什么是默认挂载的组件?就是C++构造函数里面创建的默认组件或者在蓝图里面添加构建的组件。所以,这个过程与该组件是否标记为Replicate是没有关系的。
对于动态组件:就是我们在游戏运行的时候,服务器创建或者删除的组件。比如,当玩家走进一个洞穴时,给洞穴里面的火把生成一个粒子特效组件,然后同步到客户端上,当玩家离开的时候再删除这个组件,玩家的客户端上也随之删除这个组件。
对于动态组件,我们必须要设置他的Replicate属性为true,即通过函数 AActorComponent::SetIsReplicated(true)来操作。而对于静态组件,如果我们不想同步组件上面的属性,我们就没有必要设置Replicate属性。下面截取了函数ReadContentBlockHeader部分代码来区分这两种情况:
//静态组件,不需要客户端Spawn
FNetworkGUID NetGUID;
UObject * SubObj = NULL;
Connection->PackageMap->SerializeObject(Bunch, UObject::StaticClass(), SubObj, &NetGUID );
//动态组件,需要在客户端Spawn出来
FNetworkGUID ClassNetGUID;
UObject * SubObjClassObj = NULL;
Connection->PackageMap->SerializeObject(Bunch, UObject::StaticClass(), SubObjClassObj, &ClassNetGUID );
我们在这两段代码看到了FNetworkGUID的使用,因为这里涉及到UObject的引用(指针)同步。对于不同端的同一个对象,他们的内存地址肯定是不同的,那服务器上指向A的指针同步到客户端上如何也能正确的指向A呢?这就需要通过FNetworkGUID来解析,具体细节在下一节属性同步里面分析。
摘自: 《Exploring in UE4》网络同步原理深入(上)[原理分析] - 知乎
学习交流使用