Ue4 Actor同步与序列化
主要讨论的是服务器Actor同步到客户端的过程,和序列化的过程。
1. 基本概念
服务器在NetDiver的TickFlush里面,每一帧都会去执行ServerReplicateActors
来同步Actor的相关内容,大多数 actor 复制操作都发生在 UNetDriver::ServerReplicateActors
内。在这里,服务器将收集所有被认定与各个客户端相关的 actor,并发送那些自上次(已连接的)客户端更新后出现变化的所有属性。
而UChannel::ReplicateActor
将负责把 actor 及其所有组件复制到连接中。其大致流程如下:
- 确定这是不是此 actor 通道(channel)打开后的第一次更新
- 如果是,则将所需的特定信息(初始方位、旋转等)序列化
- 确定该连接是否拥有这个 actor
- 如果没有,而且这个 actor 的角色是
ROLE_AutonomousProxy
,则降级为ROLE_SimulatedProxy
- 如果没有,而且这个 actor 的角色是
- 复制这个 actor 中已更改的属性
- 复制每个组件中已更改的属性
- 对于已经删除的组件,发送专门的删除命令
总之,大体上Actor同步的逻辑就是在TickFlush里面去执行ServerReplicateActors,然后进行前面说的那些处理。最后对每个Actor执行ActorChannel::ReplicateActor将Actor本身的信息,子对象的信息,属性信息封装到Bunch并进一步封装到发送缓存中,最后通过Socket发送出去。
2. Actor同步与序列化的基本数据结构
-
FObjectReplicator
属性同步的执行器,每个Actorchannel对应一个FObjectReplicator,每一个FObjectReplicator对应一个对象实例。设置ActorChannel通道的时候会创建出来。 -
FRepState
针对每个连接同步的历史数据,记录同步前用于比较的Object对象信息,存在于FObjectReplicator里面。 -
FRepLayOut
同步的属性布局表,记录所有当前类需要同步的属性,每个类或者RPC函数有一个。 -
FRepChangedPropertyTracker
属性变化轨迹记录,一般在同步Actor前创建,Actor销毁的时候删掉。 -
FReplicationChangelistMgr
存放当前的Object对象,保存属性的变化历史记录 -
NetworkGUID
在UObject类同步中标记作用。
-
FOutBunch
序列化主要的数据结构。
-
SendBunch
由Connection拥有,最后同步封装到的bunch中。
一个Actorchannel类其实对应着一个FObjectReplicator,属于属性同步最重要的类。
通俗点而言,FObjectReplicator是一个属性同步的执行者,FRepLayout是参照表,FRepState是追踪者,FReplicationChangelistMgr是实际数据存放者,记录属性的变化过程。
3. Actor同步初始化
3.1 初始化前的准备
当Actor同步时如果发现当前的Actor没有对应的通道,就会给其创建一个通道并执行SetChannelActor
。这个SetChannelActor
所做的工作就是属性同步的关键所在。
在SetChannelActor主要负责做了几件事。
- 构建了FObjectReplicator;
- 构建了FRepLayout;
- 构建FRepState;
- 构建FReplicationChangelistMgr;
- 为当前Actor创建NetWorkGUID。
或者也可以参考这个图
3.2 SerializeNewActor
// in UActorChannel::ReplicateActor() DataChannel.cpp 2626
// ----------------------------------------------------------
// If initial, send init data.
// ----------------------------------------------------------
if( RepFlags.bNetInitial && OpenedLocally )
{
Connection->PackageMap->SerializeNewActor(Bunch, this, Actor);
WroteSomethingImportant = true;
Actor->OnSerializeNewActor(Bunch);
}
序列化一个新的Actor的主要过程如下:
序列化出一个新的Actor分两种,一种是静态Actor,一种是动态Actor。
何为静态,何为动态呢?
bool FNetGUIDCache::IsDynamicObject( const UObject* Object )
{
check( Object != NULL );
check( Object->IsSupportedForNetworking() );
// Any non net addressable object is dynamic
return !Object->IsFullNameStableForNetworking();
}
/** IsNameStableForNetworking means an object can be referred to its path name (relative to outer) over the network */
bool UObject::IsNameStableForNetworking() const
{
return HasAnyFlags(RF_WasLoaded | RF_DefaultSubObject) || IsNative() || IsDefaultSubobject();
}
/** IsFullNameStableForNetworking means an object can be referred to its full path name over the network */
bool UObject::IsFullNameStableForNetworking() const
{
if ( GetOuter() != NULL && !GetOuter()->IsNameStableForNetworking() )
{
return false; // If any outer isn't stable, we can't consider the full name stable
}
return IsNameStableForNetworking();
}
通过代码来看,如果该同步的Actor是HasAnyFlags(RF_WasLoaded | RF_DefaultSubObject) || IsNative() || IsDefaultSubobject()
,也就是说如果Actor是已经加载到地图了,或者是CDO对象,或者是原生的对象,则它是静态的对象。
-
如果是静态的对象,序列化的过程,则不需要序列化三维信息,但是需要序列化其
Outer
的GUID
和PathName
。因为客户端需要找到该UObject绑定NetGUID。(因此就通过其Outer
和自身的PathName
来找到该UObject
)。 -
如果是动态的对象的话,则不需要其Outer的GUID和路径。而是通过一个Archetype并通过序列化一些基本的三维信息和速度信息来在客户端Spawn一个Archetype,并赋予它一些三维信息和速度,使它和原来的Actor看起来一模一样。
3.3 Actor属性同步
Actor的同步主要实现在UActorChannel::ReplicateProperties
函数中。
首先在Update函数中更新StaticBuff,并且与UObject进行比较,判断是否发生变化,如果发生变化,添加进Changed列表中。在ReplicateProperties的时候根据Changed列表来对属性进行同步,如果Changed列表为空的话,则不同步,返回false。
4. 封装到SendBuff中
封装FOutBunch
的过程中主要在UActorChannel::SendBunch
中实现。
SendBunch
主要发送的Bunch
有两种。
一种是在AppendExportBunches
中获取到ExportBunches
(这个Bunch
中存储的内容是前面序列化该UObject
的GUID
和PathName
)。
另一种则是在SerializeNewActor
中同步过来的ActorGUID
和三维信息和ReplicatedProperties
中属性同步的信息还有ReplicatedSubobject
子组件的信息。
两种最后存储在OutgoingBunches
中,经过PreBunch
处理后,再SendRawBunch
,存储到SendBuffer
中。
以PlayerController
为例:
AppendExportBunches中所存储的内容的大小为1369bits。存储的内容主要是GUID和PathName。
而这部分存储的内容就是ActorGUID,三维信息,还有属性同步组件同步的信息。
以PlayerController为例,第一部分存储的内容主要是。
思考: 上述第一部分序列化,最主要的带宽消耗来自于PathName,虽然是在Actor初始化的时候才需要同步这部分内容,但是如果需要大量SpawnActor的时候,是否会影响到同步的效果?并且,因为与同步路径的长度相关,尤其是UMapPackageName是每一个Actor初始化同步的时候都需要序列化。如果减少路径的命名长度是否会优化呢?
Ue4.20