立即注册
登录
搜索
前端开发
后端开发
虚幻引擎
U3D引擎
体感研发
数据库
论坛
BBS
本版
帖子
用户
麒麟软控
»
论坛
›
麒麟软控
›
虚幻引擎
›
UE4属性同步(三)UObject同步
返回列表
发新帖
UE4属性同步(三)UObject同步
杨弋
杨弋
当前离线
积分
17
5
主题
7
帖子
17
积分
新手上路
新手上路, 积分 17, 距离下一级还需 33 积分
新手上路, 积分 17, 距离下一级还需 33 积分
积分
17
发消息
发表于 2022-12-14 07:44:21
|
显示全部楼层
UObject同步常见用法
大部分情况,我们会用以下4种形式同步UObject,SpawnedActor为动态创建的Actor,ActorPlacedInWorld为预先放置在地图中的Actor,ActorComponent为Actor上创建的Component,Class为某个UClass,可为C++类,也可为蓝图类。
虽然可以把多种UObject作为属性同步,但它们都必须是Actor,或者Actor SubObject的属性,这样才能同步。
NetworkGUID
同步的UObject使用NetworkGUID作为标识,在网络上进行传输,client和server上对应UObject的NetworkGUID相同,两边都记录了Object和NetworkGUID的双向关联。
NetworkGUID实际为一个uint32整数,增长规则为从小到大,这么做是为了节省同步流量,NetworkGUID作为变长int同步的,大部分情况2Byte已足够。
NetworkGUID使用最后一个二进制位来区分静态UObject和动态UObject,静态的为1,动态的为0,分别为奇数和偶数。静态UObject如预先放置在地图中的Actor、Class、CDO,动态UObject就是Character,PlayerController等由服务器运行时创建的。
这就是UE处理UObject同步的基本方式,可以把它当成一种通信协议。但还有很多细节需要处理,比如客户端如何创建Actor,如何获取Actor对应的Guid,服务器又如何知道客户端已创建了Guid对应的Actor?
Actor自身的同步
联网游戏的常见模式为DedicatedServer,有专用服务器,在此模式下,Actor由服务器创建,然后同步到客户端,比如创建一个角色。Actor需要把bReplicates设为true才会同步。
Actor加入同步列表
UNetDriver有NetworkObjects属性,用于管理所有要同步的Actor。
SpawnActor时加入
在SpawnActor调用最后,会执行AddNetworkActor方法,默认把Actor加入到这个列表,无论是否勾选bReplicates属性。bReplicates属性会使Actor创建后的RemoteRole不同,若为true,RemoteRole是ROLE_SimulatedProxy,若为false,RemoteRole是ROLE_None。
运行时移除Actor
在NetDriver每帧执行ServerReplicateActors时,会遍历NetworkObjects列表,移除PendingKill的Actor,以及RemoteRole为ROLE_None的Actor,这样就把上面添加的非bReplicates Actor移除了。
运行时加入
NetworkObjects支持运行时加入Actor,Actor可以动态的把bReplicates开启或关闭,从而控制Actor的同步。具体逻辑在Actor::SetReplicates中。
创建ActorChannel
Actor有对应的ActorChannel用于同步,一开始为空,在ServerReplicateActors_ProcessPrioritizedActors函数中创建,会调用UNetConnection::CreateChannelByName。
回顾网络结构图,ActorChannel是由Connection管理的,每个Connection有自己的Channels数组,同一个Actor如果要被同步到多个客户端,那么在每个ClientConnection中都要创建一个ActorChannel。
创建完Channel后,需要把Actor和Channel进行双向绑定,Channel通过Actor属性指向Actor,而Actor本身无Channel属性,在NetConnectiont中使用ActorChannelsMap记录了Actor和Channel的对应关系。
之后要向客户端发送Actor的初始同步信息,在客户端上也创建该Actor。
PackageMapClient
之前提到UE使用NetworkGUID标识Actor,这个工作由PackageMapClient完成,该对象负责Actor和NetworkGUID的双向映射,以及序列化一个Object。UObject第一次被序列化时,服务器会向客户端传输<NetGUID, Name/Path>,当客户端向服务器发送对这个NetGUID的Ack后,服务器再同步Object时,只会发送NetGUID了。这里先不关心Ack具体实现,知道有这回事即可。
方法
SerializeNewActor:序列化一个之前没序列化过的Actor,客户端收到后会在本地创建Actor
SerializeObject:通用的序列化Object方法,支持普通UObject的同步
FNetGUIDCache
NetGUIDCache是PackageMapClient的一个属性,专门管理NetGUID与Actor的映射关系。为什么名字里有“Cache”?个人理解是NetGUID的同步和客户端实际Actor的创建是分开的,会出现NetGUID收到了,但对应Actor还没创建的情况,因此要先把GUID&#34;Cache&#34;起来,后面再建立联系。
属性
TMap< FNetworkGUID, FNetGuidCacheObject > ObjectLookup:NetGUID到Object的映射
TMap< TWeakObjectPtr< UObject >, FNetworkGUID > NetGUIDLookup:Object到NetGUID的映射
注意到ObjectLookup的值类型为FNetGuidCacheObject,记录了Object,以及PathName,OuterGUID等属性,也是“Cache”含义所在。这个对象可以作为Object的占位符,客户端当Object没创建时先记录下相关信息,等Object真正创建后再更新其中Object属性。
SerializeNewActor
Actor第一次同步时,会执行UPackageMapClient::SerializeNewActor方法进行同步。
首先执行通用的SerializeObject函数,它负责序列化一个普通Object,只负责写入NetworkGUID和路径信息。对于创建的Actor,只需要赋予一个新的NetworkGUID,然后向ObjectLookup和NetGUIDLookup表增加一条映射关系。
此时序列化的数据和PackageMapClient的数据如下:
接着获取到Actor的Archetype,通常为对应Class的CDO,把Archetype通过SerializeObject进行序列化,这是比较关键的一点,因为客户端有了Archetype才能创建Actor。
无论在服务器还是客户端,CDO的路径是固定的,只和资源路径有关,与World无关。比如在Game目录、
下创建了一个蓝图类,那么它的CDO路径为/Game/TestActor.Default__TestActor_C
如果Archetype是C++类,那路径更加毫无疑问是一致的。
想这种路径固定的Object,UE使用IsNameStableForNetworking和IsFullNameStableForNetworking描述,我们可以覆写这个接口,会在下文介绍,其默认实现如下
CDO有RF_WasLoaded标记,因为是从磁盘上加载的,所以是IsNameStableForNetworking。然后其Outer是Package,Package也有RF_WasLoaded标记,因此CDO也为IsFullNameStableForNetworking。
有了这个前提,我们序列化CDO,只要序列化其路径即可,而且UE把所有UObject的网络同步都交给UPackageMapClient统一管理,CDO也有NetworkGUID,只有第一次同步时需要同步路基,后面都同步NetworkGUID,我们先都考虑第一次同步情况。
具体逻辑通过ExportNetGUID函数实现,它会写入<NetworGUID, path>对应关系,客户端收到后能在本地也建立起这个关系。如果Object有Outer,也要对Outer执行同样操作,把Outer关系也发送给客户端。本例的TestActor CDO有Outer,为Package(/Game/TestActor),Package已无Outer。
通过ExportNetGUID生产的序列化数据后续会被写入bunch前部,相对于把Object和GUID的映射先告诉客户端,客户端在处理后续Bunch时碰到对应NetworkGUID,就知道是哪个Object了。
通过SerializeObject序列化后,整个Archive结构如下:
注意其中的Outer递归结构,先是Guid和Flags信息,再是Outer Object,最后是Object自身在Outer下的路径。
然后要序列化Actor所处的Level,这是另一个Actor必要的属性。Level的FullName如下:
Level,World,Package都有RF_WasLoaded标记,因此它们的序列化方式与CDO相同,也都由PackageMapClient管理。同时注意到每个Actor同步都需要Level,因此这里大概率Level已经由NetworkGUID了,客户端已确认过,因此只要序列化一个NetworlGUID即可。
接着获取到Actor的Location,Rotation,Scale,Velocity,这些描述了Actor初始的位置信息,把他们也序列化。
到这里,SerializeNewActor函数执行结束了,接着继续执行UActorChannel::ReplicateActor,使用通用流程执行属性同步和rpc调用。另外Actor的SubObject也可以参与同步,最常见的为ActorComponent,它的数据安排在最后。
最终Archive结构如下,这就是发送给客户端的数据,注意ExportNetworkGUID数据位于Actor同步数据之前。
PackageMapClient内容如下
客户端根据同步信息创建Actor
客户端收到Packet类型数据包后,会进入UNetConnection::ReceivedPacket函数,发现对应Channel还没创建,就本地创建ActorChannel,然后用ActorChannel来接收Bunch信息。接着客户端同样进入SerializeNewActor函数,解析Actor信息。
解析ExportNetworkGUID信息
客户端收到Bunch数据后,检查头部发现有NetGUID信息,就进入UPackageMapClient::ReceiveNetGUIDBunch函数创建GUID与Object的映射关系。
和服务器类似,这是一个递归的过程,首先处理CDO的Outer Package,读取Package NetworkGUID后,客户端会在GuidCache中新增一条记录,然后使用FNetGUIDCache::GetObjectFromNetGUID尝试根据GUID获取到Object。GetObjectFromNetGUID处理流程很复杂,包含了search,LoadObject,AsyncLoad等功能。
对于Package &#34;/Game/TestActor&#34;,会使用LoadPackage方法,加载该资源,加载完后CDO也就存在了。然后处理CDO GUID,只要使用FindObject就能在内存中找到CDO。
过程如下图:
接着以同样方式处理Level。
创建Actor
SpawnActor由UPackageMapClient::SerializeNewActor实现,从Bunch中依次获取Actor NetworkGUID,CDO NetworkGUID,Level NetworkGUID,Location等信息,然后根据GUID获取CDO与Level对象,这样就能SpawnActor了,代码如下:
然后把Actor与GUID注册到PackageMapClient中,创建Actor结束。
Actor销毁关闭Channel
服务器上对Actor执行Destroy后,会发送Close Bunch,把Bunch头部的bClosing属性设为true。
客户端收到Bunch后,发现Bunch头部的bClosing属性为true,会关闭该ActorChannel,销毁对应的Actor以及ActorComponent。之后再把ActorChannel本身也销毁掉。
ActorComponent自身的同步
之前介绍的网络同步都以Actor为单位,需要ActorChannel,但ActorComponent也可以设置Component bReplicates属性,声明同步属性和收发rpc,它本质是一个UObject。因为Actor除了能同步自身属性,还能同步SubObject,SubObject就是Outer为Actor的Object,如ActorComponent,相关数据都会经Actor所属的Channel传输和处理。
SubObject属性同步信息所处位置
把Actor同步信息详细展开,如下图所示。
每个Properties和Rpcs数据之前都有一个BlockHeader,表示这些数据属于Actor还是哪个ActorComponent,甚至可以理解为Actor也是一个特殊的ActorComponent。Actor的BlockHeader信息比较简单,只要一个bit true,表示自己为Actor即可。对于SubObject的BlockHeader,则要包含SubObject的GUID,如果SubObject是由服务器动态创建的,则要包含创建信息, 包括Class等。
ReplicateSubobjects
ActorComponent之所以能同步,离不开Actor的特殊处理,主要为AActor::ReplicateSubobjects方法,这会在每次ActorChannel同步Actor信息时调用。
ReplicatedComponents包含了要同步的Component,可动态增删,关键在于Channel->ReplicateSubobject。
其中首先给ActorComponent赋予一个GUID,当发现ActorComponent还没同步过时,会调用WriteContentBlockHeader方法把初始信息写入。这里分两种情况,ActorComponent是预先在构造函数创建或蓝图挂上的,还是运行时动态创建的。
ActorComponent为预先创建
这种情况比较简单,因为Component属于DefaultSubobject,客户端必定存在一个名称相同的Component挂在Actor上,Component可以被名称索引到,类似Package。查看UActorComponent::IsNameStableForNetworking方法,该情况会返回true。
此时需要写入Component的GUID,并向ExportNetworkGUID中写入注册信息。本例中写入数据如下
ActorComponent为动态创建
此时需要序列化Component的Class和Outer,与NewActor比较相似,直接看例子的序列化结果:
客户端动态创建ActorComponent
如果Component是服务器动态创建的,客户端需要根据同步信息在本地也创建一个。
客户端首先解析ExportNetworkGUID信息,加载TestComponent Class,注册NetworkGUID。然后通过ReadContentBlockPayload读取Component信息,发现本地需要新建Component,于是根据Class GUID找到Class,并创建Component。
UObject作为属性同步的总体框架
以上介绍了Actor和Component自身同步到客户端的流程,而我们也可以创建UObject*或AActor*类型的同步属性,将其同步到客户端,这里将介绍UE的通用UObject属性同步框架。
哪些Object可以同步
静态UObject,如Package和Class资源,CDO,预先放置在场景里的Actor,这些对象的路径是固定的,不依赖于World路径
动态创建Object,如Actor和ActorComponent
仿照ActorComponent实现的SubObject,ActorComponent并无特别之处,我们可以创建单纯的UObject,并使其可以同步
同步Object属性使用的都是UPackageMapClient::SerializeObject方法,这个我们之前已介绍过。
动态Object同步
Object在网络上通过NetworkGUID传输,而GuidCache是由服务器先添加记录,客户端收到信息后再向GuidCache添加记录,这其中存在时间差。
比如这个例子,创建了一个新Actor,赋值给另一个Actor的Prop属性,两个Actor有两个ActorChannel,各自同步数据
客户端先收到Actor3的Spawn数据,生成Actor3,并注册到GuidCache中,这样Actor1收到Prop=Actor3GUID数据时,就能查询到对应Actor为Actor3,把Actor3设置到Prop属性上。
如果Actor3创建晚了,那么客户端收到Prop=Actor3GUID时,查询不到对应的Actor,就无法设置Actor1.Prop属性。
第一步根据Actor3GUID获取Actor3失败,UE会先把
空指针
赋值给Actor1.Prop。
然后需要使用一个数据结构:FGuidReferencesMap,定义如下:
每个同步的Object会拥有一个,key为所有同步的UObject*属性在对象上的内存偏移,value为FGuidReferences,管理了该属性相关的NetworkGUID,主要为UnmappedGUIDs和MappedDynamicGUIDS。
UnmappedGUIDs用于处理上面例子的情况,当收到GUID时对应Actor还未同步到客户端,会先把GUID存储到其中,表示这个GUID还未映射到真正的Actor。MappedDynamicGUID则存储已经映射过的Dynamic Actor GUID,存下来是为了支持AOI功能,在此了解即可。
收到属性同步信息
进入ReceivePropertyHelper函数,接着调用到UPackageMapClient::SerializeObject,读取GUID后,调用GetObjectFromNetGUID函数获取Object,获取失败会把NetGUID加入到临时的TrackedUnmappedNetGuids set。
之后客户端会检查TrackedUnmappedNetGuids中有没有数据,有则说明该GUID对应的Actor在客户端还未创建,需要记下后续处理。具体做法为向GuidReferencesMap添加一条记录,key为该属性偏移,value是根据TrackedUnmappedNetGuids等创建的FGuidReferences对象,连网络同步的二进制数据Bunch也会存下。
Tick检查UnmappedGUID
UE并没有选择在Actor创建时响应式的更新相关Uobject*属性,而是在NetDriver::Tick中执行FObjectReplicator::UpdateUnmappedObjects,不断查找每个属性unmappedGUID对应Actor是否已创建并和GUID建立关联。如果找到了,就仿照ReceivePropertyHelper代码重新执行一遍反序列化流程,RepNotify也会被执行。
注意这里NetSerializeItem的第一个参数Reader,就是之前存下来的Bunch数据,但其中只有一块GUID数据。
当然其中还有一些细节要处理,比如服务器上属性短时间内被设置成多个Actor,客户端需要做很多检查,避免属性被设置成旧值,在此不展开了。
加上NetDriver::Tick保底后的流程如下。
静态Object同步
静态Object与动态Object有一个明显区别,就是静态Object本身位于客户端上,服务器只要把路径同步下去,客户端按照路径加载即可,这直接影响到服务器同步的数据,总体对于动态Object更简单。参照上文同步CDO和Package的流程,服务器会把GUID和路径写入ExportNetworkGUID,客户端收到后会在本地加载资源或FindObject寻找资源,就能在GuidCache中建立关联,后续处理流程与动态Object同步方式相同。
这里有两点需要注意,为NetGUIDAck和网络异步加载(CVarAllowAsyncLoading)选项。
NetGUIDAck
服务器把静态Object的GUID和路径发送到客户端后,客户端本地会加载Object,然后建立映射,并向服务器返回Ack,通知已完成GUID和Object关系建立。服务器会把Ack记录到NetGUIDAckStatus容器,之后处理时就会把该GUID在此Connection上的状态设置为GUID_PACKET_ACKED。这样下次再同步此GUID时,就不会加入到ExportNetworkGUID中,也不会再发送路径了。
CVarAllowAsyncLoading
当客户端收到静态Object的路径时,默认会用LoadPackage执行同步加载,会导致偶然的卡顿,影响体验,因此UE提供了AllowAsyncLoading选项,用于异步加载这些资源,当资源加载好后再处理后续数据。典型的场景为服务器上创建了一个Actor,同步到客户端,客户端先异步加载Actor对应的蓝图类,蓝图类加载好后创建Actor。相比默认的同步加载,Actor的创建实际会稍慢,但避免了卡顿,而且客户端看来相对于网络有些波动而已。
做法为在Bunch头部再添加一个MustBeMappedGUIDs数据区,里面有一些NetworkGUID,表示该Bunch数据包内的业务数据依赖该NetworkGUID对应的UObject,这个区域位于ExportNetworkGUID之后。
值得注意的是,每次同步数据时,都会把用到的GUID加入到MustBeMappedGUIDs中,即使客户端已确认过该GUID,这会带来一点流量开销。
客户端收到后首先处理前面的ExportNetworkGUID,对于GUID Path,会先把GUID加入GuidCache,然后执行asyncload异步加载,而不是之前的LoadObject。之后把该Bunch加入到QueuedBunches数组缓存起来,等到后面异步加载完成后再处理。检查异步加载完成用的是Tick方式,需要等到该Channel上的所有Guid异步加载都完成后,才会处理QueuedBunches。
综上所述,UE同步UObject可以概况为同步一个int类型的GUID,但要让整个框架能正确运行,是需要大量努力的。
上一篇:
Algoryx为虚幻引擎带来高保真物理模拟
下一篇:
虚幻引擎VR游戏开发基础教程
回复
举报
使用道具
分享
返回列表
发新帖
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
|
立即注册
浏览过的版块
前端开发
体感研发
后端开发
快速回复
返回顶部
返回列表