Ue4 NetworkGUID 分析
Ue4 NetworkGUID 分析
1. NetworkGUID
NetworkGUID有何作用?
在网络同步的过程中,在传递一个UObject
类型的指针时,这个UObject
是怎么传递的?
这个处理就需要通过FNetworkGUID
了。服务器在同步一个对象引用(指针)的时候,会给其分配专门的FNetworkGUID
并通过网络进行发送。客户端上通过识别这个ID,就可以找到对应的UObject
。
服务器在同步一个UObject
的时候,需要对一个UObject
序列化(UPackageMapClient::SerializeObject
),
而在序列化对象前,需要检查GUID
缓存表(TMap<FNetworkGUID, FNetGuidCacheObject>ObjectLookup
),如果GUID
缓存表里面有,证明已经分配过,反之则需要分配一个GUID
,并写到数据流里面。不过一般来说,GUID
分配并不是在发送数据的时候才进行,而是在创建FObjectReplicator
(SetChannelActor
)的时候(通过NetDriver
的GuidCache
分配)。
那么这个GUID是在什么时候分配的?服务器如何分配,客户端如何分配的呢?
2. 服务器分配GUID
服务器中是如何分配GUID的呢?
一开始是在SetChannelActor中创建FObjectReplicator的时候通过GetOrAssignNetGUID创建并分配到GuidCache中去。
通过上图,我们可以看到,如果我们要注册一个UObject的GUID,不仅仅是要注册自己的GUID,还得注册自己Outer的GUID(如果Outer的GUID已经注册,则不需要注册)。
而且,GUID还分动态和非动态(动态GUID为偶数,静态GUID为奇数)
FNetworkGUID FNetGUIDCache::AssignNewNetGUID_Server( UObject* Object )
{
check( IsNetGUIDAuthority() );
#define COMPOSE_NET_GUID( Index, IsStatic ) ( ( ( Index ) << 1 ) | ( IsStatic ) )
#define ALLOC_NEW_NET_GUID( IsStatic ) ( COMPOSE_NET_GUID( ++UniqueNetIDs[ IsStatic ], IsStatic ) )
// Generate new NetGUID and assign it
const int32 IsStatic = IsDynamicObject( Object ) ? 0 : 1;
const FNetworkGUID NewNetGuid( ALLOC_NEW_NET_GUID( IsStatic ) );
RegisterNetGUID_Server( NewNetGuid, Object );
return NewNetGuid;
}
最终注册到GuidCache的注册表中
3. 客户端分配GUID
客户端分配GUID,是通关接收服务器发送过来的Bunch反序列化出来的GUID,是通过两种方法来注册到注册表中。
第一种方法
是通过接收服务器发送过来的GUID和PathName来分配GUID。
在UPackageMapClient::InternalLoadObject
中,通过反序列化出GUID和PathName,然后通过PathName去寻找到UObject,然后再注册进注册表中。
FNetworkGUID UPackageMapClient::InternalLoadObject( FArchive & Ar, UObject *& Object, const int InternalLoadObjectRecursionCount )
{
// ----------------
// Read the NetGUID
// ----------------
FNetworkGUID NetGUID;
Ar << NetGUID;
NET_CHECKSUM_OR_END( Ar );
if ( Ar.IsError() )
{
Object = NULL;
return NetGUID;
}
if ( !NetGUID.IsValid() )
{
Object = NULL;
return NetGUID;
}
// ----------------
// Try to resolve NetGUID
// ----------------
if ( NetGUID.IsValid() && !NetGUID.IsDefault() )
{ Object = GetObjectFromNetGUID( NetGUID, GuidCache->IsExportingNetGUIDBunch );
UE_CLOG( !bSuppressLogs, LogNetPackageMap, Log, TEXT( "InternalLoadObject loaded %s from NetGUID <%s>" ), Object ? *Object->GetFullName() : TEXT( "NULL" ), *NetGUID.ToString() );
}
// ----------------
// Read the full if its there
// ----------------
FExportFlags ExportFlags;
if ( NetGUID.IsDefault() || GuidCache->IsExportingNetGUIDBunch )
{
Ar << ExportFlags.Value;
if ( Ar.IsError() )
{
Object = NULL;
return NetGUID;
}
}
if ( GuidCache->IsExportingNetGUIDBunch )
{
GuidCache->ImportedNetGuids.Add( NetGUID );
}
if ( ExportFlags.bHasPath )
{
UObject* ObjOuter = NULL;
FNetworkGUID OuterGUID = InternalLoadObject( Ar, ObjOuter, InternalLoadObjectRecursionCount + 1 );
FString PathName;
uint32 NetworkChecksum = 0;
Ar << PathName;
if ( ExportFlags.bHasNetworkChecksum )
{
Ar << NetworkChecksum;
}
const bool bIsPackage = NetGUID.IsStatic() && !OuterGUID.IsValid();
if ( Ar.IsError() )
{
UE_LOG( LogNetPackageMap, Error, TEXT( "InternalLoadObject: Failed to load path name" ) );
Object = NULL;
return NetGUID;
}
// Remap name for PIE
GEngine->NetworkRemapPath( Connection->Driver, PathName, true );
//
// At this point, only the client gets this far
//
const bool bIgnoreWhenMissing = ExportFlags.bNoLoad;
// Register this path and outer guid combo with the net guid
GuidCache->RegisterNetGUIDFromPath_Client( NetGUID, PathName, OuterGUID, NetworkChecksum, ExportFlags.bNoLoad, bIgnoreWhenMissing );
// Try again now that we've registered the path
Object = GuidCache->GetObjectFromNetGUID( NetGUID, GuidCache->IsExportingNetGUIDBunch );
}
return NetGUID;
}
通过上述代码,我们可以看出,在InternalLoadObject中先反序列化获取其GUID,然后递归注册Outer的GUID,接着反序列出PathName,通过Outer和PathName来找到自身Object。
第二种方法
前面的其实都是静态Object所采取的方法,因为静态Object不会因为变化而变化,并且可以通过文件路径(也就是UPackage)而找到该对象。但是如果是动态Object的话则不能采取第一种方法通过传GUID和PathName来注册GUID和UObject,所以就得采取第二种方法。第二种方法是怎么做的呢?
通过传递该UObject的GUID和其ArchetypeNetGUID,然后通过ArchetypeNetGUID(默认对象是静态的,第一种方法可以注册)构建一个ArchetypeActor,然后赋予其Location,Rotation,Scale,Velocity,然后通过SpawnActorAbsolute生成一个Actor放在该位置中,并且赋予同步而来的GUID。
bool UPackageMapClient::SerializeNewActor(FArchive& Ar, class UActorChannel *Channel, class AActor*& Actor) {
// 反序列出Actor的GUID
FNetworkGUID NetGUID;
UObject *NewObj = Actor;
SerializeObject(Ar, AActor::StaticClass(), NewObj, &NetGUID);
// ...
if ( NetGUID.IsDynamic() )
{
UObject* Archetype = nullptr;
UObject* ActorLevel = nullptr;
FVector_NetQuantize10 Location;
FVector_NetQuantize10 LocalLocation;
FVector_NetQuantize10 Scale;
FVector_NetQuantize10 Velocity;
FRotator Rotation;
bool SerSuccess;
// ...
//反序列化出ArchetypeNetGUID和ArchetypeActor
FNetworkGUID ArchetypeNetGUID;
SerializeObject(Ar, UObject::StaticClass(), Archetype, &ArchetypeNetGUID);
// ...
FActorSpawnParameters SpawnInfo;
SpawnInfo.Template = Cast<AActor>(Archetype);
SpawnInfo.OverrideLevel = SpawnLevel;
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnInfo.bRemoteOwned = true;
SpawnInfo.bNoFail = true;
UWorld* World = Connection->Driver->GetWorld();
FVector SpawnLocation = FRepMovement::RebaseOntoLocalOrigin(Location, World->OriginLocation);
Actor = World->SpawnActorAbsolute(Archetype->GetClass(), FTransform(Rotation, SpawnLocation), SpawnInfo);
if (Actor)
{
// Velocity was serialized by the server
if (bSerializeVelocity)
{
Actor->PostNetReceiveVelocity(Velocity);
}
// Scale was serialized by the server
if (bSerializeScale)
{
Actor->SetActorScale3D(Scale);
}
GuidCache->RegisterNetGUID_Client(NetGUID, Actor);
bActorWasSpawned = true;
}
}
4. 服务器发送GUID
通过上面我们可以发现客户端分配GUID的两种方法,分别是通过接收GUID和PathName来注册GUID表,和接收GUID和ArchetypeGUID来注册GUID表。
那么服务器具体是如何发送GUID的。
第一种方法(下面以PlayerController同步为例)
发送GUID和PathName。
与上面第一种方法对应的InternalLoadObject
,服务器发送的方法是InternalWriteObject
/** Writes an object NetGUID given the NetGUID and either the object itself, or FString full name of the object. Appends full name/path if necessary */
void UPackageMapClient::InternalWriteObject( FArchive & Ar, FNetworkGUID NetGUID, UObject* Object, FString ObjectPathName, UObject* ObjectOuter ) {
// 由于代码量较长,只贴出关键部分
Ar << NetGUID;
if ( Object != NULL )
{
ExportFlags.bHasPath = ShouldSendFullPath( Object, NetGUID ) ? 1 : 0;
}
else
{
ExportFlags.bHasPath = ObjectPathName.IsEmpty() ? 0 : 1;
}
ExportFlags.bNoLoad = bNoLoad ? 1 : 0;
Ar << ExportFlags.Value;
} //反序列出Flag,判断是否要加载路径
FNetworkGUID OuterNetGUID = GuidCache->GetOrAssignNetGUID( ObjectOuter );
// 递归序列化Outer的GUID
InternalWriteObject( Ar, OuterNetGUID, ObjectOuter, TEXT( "" ), NULL );
// 注册PathName
Ar << ObjectPathName;
// 暂时先存放在UPackageMap中,在SendBunch中封装到头部。
if ( GuidCache->IsExportingNetGUIDBunch )
{
CurrentExportNetGUIDs.Add( NetGUID );
int32& Count = NetGUIDExportCountMap.FindOrAdd( NetGUID );
Count++;
}
}
通过上面的代码,我们可以发现服务器在InternalWriteObject
中把一些静态对象通过递归的方式序列化到Bunch中。但是并没有立刻序列化到Bunch中,而是先存放在UPackageMap中去,到SendBunch的时候再封装到头部。
void UPackageMapClient::ExportNetGUIDHeader()
{
// Rewrite how many NetGUIDs were exported.
PatchHeaderCount( *CurrentExportBunch, false, ExportNetGUIDCount );
// CurrentExportBunch *should* always have NetGUIDs to export. If it doesn't warn. This is a bug.
if ( CurrentExportBunch->ExportNetGUIDs.Num() != 0 )
{
ExportBunches.Add( CurrentExportBunch );
}
CurrentExportBunch = NULL;
ExportNetGUIDCount = 0;
}
// 在这个函数中,我们把CurrentExportBunch中的内容取出来,封装到头部bunch中去。
第二种方法
在Actor初始化的时候序列化。
在SerializeNewActor
中序列化
先序列化NetGUID,接着序列化ArchetypeGUID,并且在InternalWriteObject把ArchetypeGUID和PathName的信息封装起来到头部。
如果是动态Actor的话,再判断是否要把该Actor的Location,Rotation,Scale,Velocity序列化。
UE4版本 4.20