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)的时候(通过NetDriverGuidCache分配)。

那么这个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

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值