UE4_网络同步原理深入

UE4_网络同步原理深入

本文更多是对Exploring in UE4有关网络同步原理以及官方文档的一些自己理解和总结。

1. 通信的基本流程

1.1 UE4服务器与客户端的通信流程

UE4进程内部服务器Server与客户端Client的通信 主要如下:

UE4_网络同步原理深入_第1张图片

每一个客户端叫做一个Connection,如图,就是一个server连接到两个客户端的效果。对于每一个客户端,都会建立起一个Connection。在服务器上这个Connection叫做ClientConnection,对于客户端这个Connection叫做ServerConnection。每一个Channel都会归属于一个Connection,这样这个Channel才知道他对应的是哪个客户端上的对象。 接下来我们继续细化,图中的Channel只标记了1,2,3,那么实际上都有哪些Channel?这些Channel对应的都是什么对象?其实,在第一部分的概念里我已经列举了常见的3中Channel,分别是ControlChannel,ActorChannel,以及VoiceChannel。一般来说,ControlChannel与VoiceChannel在游戏中只存在一个,而ActorChannel则对应每一个需要同步的Actor。

1.2 Connection和Channel之间的关系和联系。

  • Connection:表示一个网络连接。服务器上,一个客户端到一个服务器的一个连接叫一个ClientConnection。在客户端上,一个服务器到一个客户端的连接叫一个ServerConnection。
  • Channel:数据通道,每一个通道只负责交换某一个特定类型特定实例的数据信息。
    • ControlChannel:客户端服务器之间发送控制信息,主要是发送接收连接与断开的相关消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
    • VoiceChannel:用于发送接收语音消息。在一个Connection中只会在初始化连接的时候创建一个该通道实例。
    • ActorChannel:处理Actor本身相关信息的同步,包括自身的同步以及子组件,属性的同步,RPC调用等。每个Connection连接里的每个同步的Actor都对应着一个ActorChannel实例。 常见的只有这3种:枚举里面还有FileChannel等类型,不过没有使用。

ConnectionChannel之间的关系:


UE4_网络同步原理深入_第2张图片

1.3 接收和发送信息

到这里我们基本上就了解了UE4的基本通信架构了,下面我们进一步分析网络传输数据的流程。首先我们要知道,UE4的数据通信是建立在UDP-Socket的基础上的,与其他的通信程序一样,我们需要对Socket的信息进行封装发送以及接收解析。这里面主要涉及到Bunch,RawBunch,Packet等概念。

如下图所示,这个是主要的接收和发送信息的整体流程:


UE4_网络同步原理深入_第3张图片

通过上图可以看出,主要由TickFlush和TickDispatch来监控接收和发送信息。发送信息过程主要是由Channel根据消息类型来封装信息,Connection再把这些信息处理后发送。而接受信息则与这个恰好相反。

以下是借鉴Exploring in UE4的图,分别是详细的发送消息和接收消息的过程。

1.3.1 发送消息


UE4_网络同步原理深入_第4张图片

1.3.2 接收信息


UE4_网络同步原理深入_第5张图片

2. 连接建立

在第一块我们了解了通信的基本流程,但是在一开始建立通信这个过程是如何连接的呢?

通过这个图我们可以知道网络通信其实是在GameInstance创建之后,然后去创建网络相关的板块。

然而在上面的内容我们都知道了,是由NetDriver去驱动整个网络通信,并且客户端通过Connection连接服务器。

那么这个过程是怎么建立的呢?

2.1 服务器网络模块初始化流程

UE4_网络同步原理深入_第6张图片
UE4_网络同步原理深入_第7张图片

2.2 客户端网络模块初始化流程

UE4_网络同步原理深入_第8张图片

UE4_网络同步原理深入_第9张图片

2.3 服务器与客户端建立连接流程

二者都完成初始化后,客户端就会开始发送一个Hello类型的ControlChannel消息给服务器(上面客户端初始化最后一步)。服务器接收到消息之后开始处理,然后会根据条件再给客户端发送对应的消息,如此来回处理几个回合,完成连接的建立,主要流程如下:

UE4_网络同步原理深入_第10张图片

主要参考这幅图简化而来的。

// message type definitions
DEFINE_CONTROL_CHANNEL_MESSAGE_THREEPARAM(Hello, 0, uint8, uint32, FString); // initial client connection message
DEFINE_CONTROL_CHANNEL_MESSAGE_THREEPARAM(Welcome, 1, FString, FString, FString); // server tells client they're ok'ed to load the server's level
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Upgrade, 2, uint32); // server tells client their version is incompatible
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Challenge, 3, FString); // server sends client challenge string to verify integrity
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Netspeed, 4, int32); // client sends requested transfer rate
DEFINE_CONTROL_CHANNEL_MESSAGE_FOURPARAM(Login, 5, FString, FString, FUniqueNetIdRepl, FString); // client requests to be admitted to the game
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Failure, 6, FString); // indicates connection failure
DEFINE_CONTROL_CHANNEL_MESSAGE_ZEROPARAM(Join, 9); // final join request (spawns PlayerController)
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(JoinSplit, 10, FString, FUniqueNetIdRepl); // child player (splitscreen) join request
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Skip, 12, FGuid); // client request to skip an optional package
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(Abort, 13, FGuid); // client informs server that it aborted a not-yet-verified package due to an UNLOAD request
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(PCSwap, 15, int32); // client tells server it has completed a swap of its Connection->Actor
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(ActorChannelFailure, 16, int32); // client tells server that it failed to open an Actor channel sent by the server (e.g. couldn't serialize Actor archetype)
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(DebugText, 17, FString); // debug text sent to all clients or to server
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(NetGUIDAssign, 18, FNetworkGUID, FString); // Explicit NetworkGUID assignment. This is rare and only happens if a netguid is only serialized client->server (this msg goes server->client to tell client what ID to use in that case)
DEFINE_CONTROL_CHANNEL_MESSAGE_ONEPARAM(SecurityViolation, 19, FString); // server tells client that it has violated security and has been disconnected
DEFINE_CONTROL_CHANNEL_MESSAGE_TWOPARAM(GameSpecific, 20, uint8, FString); // custom game-specific message routed to UGameInstance for processing
DEFINE_CONTROL_CHANNEL_MESSAGE_ZEROPARAM(EncryptionAck, 21);

以上的控制信息具体作用,请阅读源码。

3. Actor同步细节

3.1 Actor同步流程

有了前面的描述,我们已经知道NetDiver负责整个网络的驱动,而ActorChannel就是专门用于Actor同步的通信通道。

服务器在NetDiver的TickFlush里面,每一帧都会去执行ServerReplicateActors来同步Actor的相关内容,大多数 actor 复制操作都发生在 UNetDriver::ServerReplicateActors 内。在这里,服务器将收集所有被认定与各个客户端相关的 actor,并发送那些自上次(已连接的)客户端更新后出现变化的所有属性。

这里还定义了一个专门流程,指定了 actor 的更新方式、要调用的特定框架回调,以及在此过程中使用的特定属性。其中最重要的包括:

  • AActor::NetUpdateFrequency - 用于确定 actor 的复制频度
  • AActor::PreReplication - 在复制发生前调用
  • AActor::bOnlyRelevantToOwner - 如果此 actor 仅复制到所有者,则值为 true
  • AActor::IsRelevancyOwnerFor - 用于确定 bOnlyRelevantToOwner 为 true 时的相关性
  • AActor::IsNetRelevantFor - 用于确定 bOnlyRelevantToOwner 为 false 时的相关性

相应的高级流程如下:

  • 循环每一个主动复制的 actor(AActor::SetReplicates( true )

    • 确定这个 actor 是否在一开始出现休眠(DORM_Initial),如果是这样,则立即跳过。
    • 通过检查 NetUpdateFrequency 的值来确定 actor 是否需要更新,如果不需要就跳过
    • 如果 AActor::bOnlyRelevantToOwner 为 true,则检查此 actor 的所属连接以寻找相关性(对所属连接的观察者调用 AActor::IsRelevancyOwnerFor)。如果相关,则添加到此连接的已有相关列表。
    • 此时,这个 actor 只会发送到单个连接。
    • 对于任何通过这些初始检查的 actor,都将调用 AActor::PreReplication
    • PreReplication 可以让您决定是否针对连接来复制属性。这时要使用 DOREPLIFETIME_ACTIVE_OVERRIDE
    • 如果同过了以上步骤,则添加到所考虑的列表。
  • 对于每个连接(connection):

    • 对于每个所考虑的上述 actor

    • 确定是否休眠

    • 是否还没有通道

      • 确定客户端是否加载了 actor 所处的场景
      • 如未加载则跳过
      • 针对连接调用 AActor::IsNetRelevantFor,以确定 actor 是否相关
      • 如不相关则跳过
    • 在归连接所有的相关列表上添加上述任意 actor

    • 这时,我们拥有了一个针对此连接的相关 actor 列表

    • 按照优先级对 actor 排序

    • 官网文档

    • 优先级排序规则是什么?

      答案:是按照是否有controller,距离以及是否在视野。通过FActorPriority构造代码可以定位到APawn::GetNetPriority,这里面会计算出当前Actor对应的优先级,优先级越高同步越靠前,是否有Controller的权重最大)

    • 对于每个排序的 actor:

    • 如果连接没有加载此 actor 所在的关卡,则关闭通道(channel)(如存在)并继续

    • 每 1 秒钟调用一次 AActor::IsNetRelevantFor,确定 actor 是否与连接相关
    • 如果不相关的时间达到 5 秒钟,则关闭通道(channel)
    • 如果相关且没有通道打开,则立即打开一个通道(channel)
    • 如果此连接出现饱和 (饱和处理)
      • 对于剩下的 actor
      • 如果保持相关的时间不到 1 秒,则强制在下一时钟单位进行更新
      • 如果保持相关的时间超过 1 秒,则调用 AActor::IsNetRelevantFor 以确定是否应当在下一时钟单位更新
    • 对于通过了以上这几点的 actor,将调用 UChannel::ReplicateActor 将其复制到连接。

3.2 将 Actor 复制到连接

UChannel::ReplicateActor 将负责把 actor 及其所有组件复制到连接中。其大致流程如下:

  • 确定这是不是此 actor 通道(channel)打开后的第一次更新
    • 如果是,则将所需的特定信息(初始方位、旋转等)序列化
  • 确定该连接是否拥有这个 actor
    • 如果没有,而且这个 actor 的角色是 ROLE_AutonomousProxy,则降级为 ROLE_SimulatedProxy
  • 复制这个 actor 中已更改的属性
  • 复制每个组件中已更改的属性
  • 对于已经删除的组件,发送专门的删除命令

总之,大体上Actor同步的逻辑就是在TickFlush里面去执行ServerReplicateActors,然后进行前面说的那些处理。最后对每个Actor执行ActorChannel::ReplicateActor将Actor本身的信息,子对象的信息,属性信息封装到Bunch并进一步封装到发送缓存中,最后通过Socket发送出去。

3.3 Actor同步过程

服务器Actor同步堆栈图如下:

UE4_网络同步原理深入_第11张图片

客户端Actor创建同步堆栈图如下:

UE4_网络同步原理深入_第12张图片

客户端Actor初始化后同步图如下:

UE4_网络同步原理深入_第13张图片



UE4_网络同步原理深入_第14张图片

其实Actor同步主要是其子组件和其属性的同步。

4. 属性同步

官方文档

4.1 网络同步的一些数据结构

  • FObjectReplicator
    属性同步的执行器,每个Actorchannel对应一个FObjectReplicator,每一个FObjectReplicator对应一个对象实例。设置ActorChannel通道的时候会创建出来。

  • FRepState
    针对每个连接同步的历史数据,记录同步前用于比较的Object对象信息,存在于FObjectReplicator里面。

  • FRepLayOut
    同步的属性布局表,记录所有当前类需要同步的属性,每个类或者RPC函数有一个。

  • FRepChangedPropertyTracker
    属性变化轨迹记录,一般在同步Actor前创建,Actor销毁的时候删掉。

  • FReplicationChangelistMgr
    存放当前的Object对象,保存属性的变化历史记录


    UE4_网络同步原理深入_第15张图片

    一个Actorchannel类对应一个FObjectReplicator,属于属性同步最重要的类。

    关于FRepLayout中Parents属性与CMD属性:FRepLayout里面,数组parents示当前类所有的需要同步的属性,而数组cmd会将同步的复杂类型属性【包括数组、结构体、结构体数组但不包括类类型的指针】进一步展开放到这里面 。

    下面开始进一步描述属性同步的基本思路:我们给一个Actor类的同步属性A做上标记Replicates(先不考虑其他的宏),然后UClass会将所有需要同步的属性保存到ClassReps列表里面(该过程在反射过程实现),这样我们就可以通过这个Actor的UClass获取这个Actor上所有需要同步的属性,当这个Actor实例化一个可以同步的对象并开始创建对应的同步通道时,我们就需要准备属性同步了。

    首先,我们要有一个同步属性列表来记录当前这个类有哪些属性需要同步(FRepLayout,每个对象有一个,从UClass里面初始化,属于一种参考表,不保存具体属性数据);其次,我们需要针对每个对象保存一个缓存数据,来及时的与发生改变的Actor属性作比较,从而判断与上一次同步前是否发生变化(FRepState,里面有一个Staticbuff来保存具体的属性数据,和FRepLayout对照使用);然后,我们要有一个属性变化跟踪器记录所有发生改变同步属性的序号(可能是因为节省内存开销等原因所以不是保存这个属性),便于发送同步数据时处理(FRepChangedPropertyTracker,对各个Connection可见,被各个Connection的Repstate保存一个共享指针,新版本被FRepChangelistState替换)。最后,我们还需要针对每个连接的每个对象有一个控制前面这些数据的执行者(FObjectReplicator)

    这四个类就是我们属性同步的关键所在,在同步前我们需要对这些数据做好初始化工作,然后在真正同步的时候去判断与处理。

4.2 属性同步过程

在上文,我们知道Actor的同步主要是通过ServerReplcateActors实现的。那么具体的流程又大概是怎样的呢?


UE4_网络同步原理深入_第16张图片

通过该图我们可以看出,首先通过SetChannelActor为上述我们网络同步的四个类初始化,然后就是同步Actor。

4.3 属性同步初始化

当Actor同步时如果发现当前的Actor没有对应的通道,就会给其创建一个通道并执行SetChannelActor。这个SetChannelActor所做的工作就是属性同步的关键所在,这个函数里面会对上面四个关键的类构造并做初始化,详细的内容参考下图:


UE4_网络同步原理深入_第17张图片

通过图片,我们可以看出在SetChannelActor中初始化构建了上述我们所讲的几个类。

4.4 属性同步过程

// 以下代码有删减
bool FObjectReplicator::ReplicateProperties( FOutBunch & Bunch, FReplicationFlags RepFlags )
{
    UObject* Object = GetObject();

    // some games ship checks() in Shipping so we cannot rely on DO_CHECK here, and these checks are in an extremely hot path

    UNetConnection* OwningChannelConnection = OwningChannel->Connection;

    FNetBitWriter Writer( Bunch.PackageMap, 0 );

    // Update change list (this will re-use work done by previous connections)
    ChangelistMgr->Update( Object, Connection->Driver->ReplicationFrame, RepState->LastCompareIndex, RepFlags, OwningChannel->bForceCompareProperties ); //  更新函数,判断属性是否发生变化。

    // Replicate properties in the layout
    const bool bHasRepLayout = RepLayout->ReplicateProperties( RepState.Get(), ChangelistMgr->GetRepChangelistState(), ( uint8* )Object, ObjectClass, OwningChannel, Writer, RepFlags ); //  同步属性过程。
    // Replicate all the custom delta properties (fast arrays, etc)
    ReplicateCustomDeltaProperties( Writer, RepFlags );

    //... 下面删减很大一部分
    return WroteImportantData;
}

再次拿出服务器同步属性的流程,我们可以看到属性同步是通过FObjectReplicator::ReplicateProperties函数执行的,进一步执行RepLayout->ReplicateProperties。这里面比较重要的细节就是服务器是如何判断当前属性发生变化的,我们在前面设置通道Actor的时候给FObjectReplicator设置了一个Object指针,这个指针保存的就是当前同步的对象,而在初始化RepChangelistState的同时我们还创建了一个Staticbuffer,并且把buffer设置和当前Object的大小相同,对bufferOffSet把对应的同步属性值添加到buffer里面。所以,我们真正比较的就是这两个对象,一般来说,staticbuffer在创建通道的同时自己就不会改变了,只有当与Object比较发现不同的时候,才会在发送前把属性值置为改变后的。这对于长期同步的Actor没什么问题,但是对于休眠的Actor就会出现问题了,因为每次删除通道并再次同步强制同步的时候这里面的StaticBuffer都是Object默认的属性值,那比较的时候就可能出现0不同步这样奇怪的现象了。真正比较两个属性是否相同的函数是PropertiesAreIdentical(),他是一个static函数。

static FORCEINLINE bool PropertiesAreIdentical( const FRepLayoutCmd& Cmd, const void* A, const void* B )
{
    const bool bIsIdentical = PropertiesAreIdenticalNative( Cmd, A, B );
    return bIsIdentical;
}

UE4_网络同步原理深入_第18张图片
UE4_网络同步原理深入_第19张图片

Compareproperties函数中把现在UObject的信息和FReplicationChangelistMgr中的StaticBuffer比较更新,然后在属性同步前判断条件复制属性的条件,如果符合则发送属性。

4.5 属性回调函数执行

虽然属性同步是由服务器执行的,但是FObjectReplicatorRepLayOut这些数据可并不是仅仅存在于服务器,客户端也是存在的,客户端也有Channel,也需要执行SetChannelACtor。不过这些数据在客户端上的作用可能就有一些变化,比如StaticBuffer,服务器是用它存储上次同步后的对象,然后与当前的Object比较看是否发生变化。在客户端上,他是用来临时存储当前同步前的对象,然后再把通过过来的属性复制给当前ObjectObject再与StaticBuffer对象比较,看看属性是否发生变化,如果发生变化,就在ReplicatorRepState里面添加一个函数回调通知RepNotifies。 在随后的ProcessBunch处理中,会执行RepLayout->CallRepNotifies( RepState, Object );处理所有的函数回调,所以我们也知道了为什么接收到的属性发生变化才会执行函数回调了。

在随后的ProcessBunch处理中,会执行RepLayout->CallRepNotifies( RepState, Object );处理所有的函数回调,所以我们也知道了为什么接收到的属性发生变化才会执行函数回调了。


UE4_网络同步原理深入_第20张图片

思考:服务器如果发生变化,那么回调函数是否一定会执行?

回答:不是。举个例子;

UPROPERTY(Replicated, ReplicatedUsing = OnRep_Health)
        float health;


void AFPSAIGuard::OnRep_Health()
{
    GEngine->AddOnScreenDebugMessage(1, 10.f, FColor::Red, GetFName().ToString() + FString::SanitizeFloat(health));
    OnHealthChanged(health);
}


void AFPSAIGuard::OnHealthHurt()
{
    health -= 20.0f;
    if (health <= 0.0f) {
        Destroy();
    }
}
// health默认是100

就比上面的例子,当服务器和客户端同时都调用这个OnHealthHurt这个函数的时候,由于服务器的health发生变化改为80,而客户端此时也变为了80,服务器同步到客户端的时候,会去比较两个值,如果发生变化,就在ReplicatorRepState里面添加一个函数回调通知RepNotifies。 在随后的ProcessBunch处理中,会执行RepLayout->CallRepNotifies( RepState, Object );处理所有的函数回调,所以我们也知道了为什么接收到的属性发生变化才会执行函数回调了。

思考:RPC与Actor同步谁先执行?

下面我们讨论一下RPC与同步直接的关系,这里提出一个这样的问题

问题服务器ActorA在创建一个新的ActorB的函数里同时执行自身的一个Client的RPC函数,RPC与ActorB的同步哪个先执行?

答案:是RPC先执行。你可以这样理解,我在创建一个Actor的同时立刻执行了RPC,那么RPC相关的操作会先封装到网络传输的包中,当这个函数执行完毕后,服务器再去调用同步函数并将相关信息封装到网络包中。所以RPC的消息是靠前的。

那么这个问题会造成什么后果呢?

  1. 当你创建一个新的Actor的同时(比如在一个函数内),你将这个Actor作为RPC的参数传到客户端去执行,这时候你会发现客户端的RPC函数的参数为NULL。
  2. 你设置了一个bool类型属性A并用UProperty标记了一个回调函数OnRep_Use。你先在服务器里面修改了A为true,同时你调用了一个RPC函数让客户端把A置为true。结果就导致你的OnRep_Use函数没有执行。但实际上,这会导致你的OnRep_Use函数里面还有其他的操作没有执行。

5. Actor子组件同步

官网文档


UE4_网络同步原理深入_第21张图片

//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;
}

通过上述图片和代码,我们可以看出,同步Actor子组件其实最终也是在同步属性。其实也就是我们在第4部分所做的内容。

6. RPC

其实如果了解了属性回调函数执行过程的话,那么RPC其实也是类似的方法。(前提实现了反射机制)

以下是RPC函数执行封包过程:


UE4_网络同步原理深入_第22张图片

而我们在代码里的函数之所以必须要加上_Implementation,就是因为在调用端里面,实际执行的是.genenrate.cpp文件函数,而不是我们自己写的这个。同时结合下面的RPC执行堆栈,我们可以看到在UObject这个对象系统里,我们可以通过反射系统查找到函数对应的UFuntion结构,同时利用ProcessEvent函数来处理UFuntion。通过识别UFunction里面的标记,可以知道这个函数是不是一个RPC函数,是否需要发送给其他的端。 当我们开始调用CallRemoteFunction的时候,RPC相关的初始化就开始了。NetDiver会进行相关的初始化,并试着获取RPC函数的Replayout,那么问题是函数有属性么?正常来说,函数本身就是一个执行过程,函数名是一个起始的执行地址,他本身是没有内存空间,更不用说存储属性了。不过,在UE4的反射系统里面,函数可以被额外的定义为一个UFunction,从而保存自己相关的数据信息。RPC函数的参数就被保存在UFunction的基类Ustruct的属性链表PropertyLink里面,RepLayOut里面的属性信息就是从这里获取到的。 一旦函数的RepLayOut被创建,也同样会放到NetDiver的RepLayoutMap里面。随后立刻调用FRepLayout::SendPropertiesForRPC将RPC的参数序列化封装与RPC函数一同发送。

以下是RPC函数接收执行过程:


UE4_网络同步原理深入_第23张图片

传递的过程中是以FFieldNetCache的数据形式保存的。

ReceivedRPC()解析函数并且最后执行函数。

UE4版本 4.20

参考:

Exploring in UE4

Actor 复制流程详述

你可能感兴趣的:(UE4,UE4,网络,游戏引擎)