NetworkGUID浅析

思考

  1. NetworkGUID是什么

  2. 动态UObject, 静态UObject是什么, DS和客户端的Obj是怎么关联起来的?

  3. Actor是怎么创建和同步的?

  4. 针对DefaultSubObj, 客户端和DS怎么关联起来?

  5. 纯UObject怎么进行网络同步, 怎么在客户端创建, 其所占用的Bunch是否变为Reliable?

  6. 怎么将变量的值和其对应的UObject关联起来? 例如BasePlayerController有个UMyTestObject类型变量pTestObj, 客户端的它是怎么创建, 并和BasePlayerController.pTestObj关联起来?

NetworkGUID规则设定

UE在设计网络系统时做了多个基础规则设定, 并在此基础上搭建了整个网络系统. 这篇文章仅仅阐述关于NetworkGUID的设定.

NetworkGUID

在UE中, 假设有两个Actor A和B, 如果A和B存在于同一个World里, 那么A和B的变动可以相互感知到, 如果它们存在于不同World里, A和B的变动就无法相互感知. 因为DS和客户端都处在各自World中, 但是可以换个角度思考, 可以将它们操控的对象理解为同一个对象不同世界的投射, 这样, 某一个world中物体的变动就会投射到其他World对应的Actor上. 那么现在需要处理的问题是怎么将同一个Actor不同World的投射关联起来呢? NetworkGUID应运而生.

FNetworkGUID是一个一个结构体, 里面只有uint32类型的变量. UE用它唯一标识一个对象(注意, 并不是所有对象都有NetworkGUID, 只有网络对象才有).

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Implements a globally unique identifier for network related use.
*
* For now, this is just a uint32 with some bits meaning special things.
* This may be expanded (beyond a uint32) eventually while keeping the API the same.
*/
class FNetworkGUID
{
public:
uint32 Value;
...
};

动/静态UObject

UE中的UObject大致分成两种: 一种是常驻内存UObject, 一般情况下在整个World(或者Instance)生命周期内都不发生改变. 例如UClass, UPackage, CDO(Class Default Object)等.另一种是动态创建的UObject, 生命周期一般由程序员维护. 例如程序员调用NewObject/SpawnActor创建出来对象.

静态UObject

常驻内存的UObject, 例如UClass, UPackage, ULevel等. 在全局范围某一种类型的常驻对象是唯一的, 例如针对PlayerController的UClass就是全局唯一的, 可以通过PathName唯一确定.

判断UObject类型:

从上述函数中可以看出, 判断UObject是否为静态的关键函数为:UObject::IsFullNameStableForNetworking

RF_WasLoaded类型

该类型表示该Obj是通过加载得到的, 加载类型全部为静态UObject:

Default__NewPlayerController_C(虽然CDO带有RF_WasLoaded flag, 但是将CDO判定为静态并不是通过RF_WasLoaded.)

顺带说一下, CDO都会附带属性:RF_WasLoaded.

RF_DefaultSubObject类型

使用UObject.CreateDefaultSubobject创建的DefaultSubObject.

RF_DefaultSubObject的类型定义为:

DefaultSubObject的创建方式:

示例:

注意, 传入的SubObjectName必须是唯一的, 对于某一个Actor的不同SubObject名字必须唯一, 否则就会报错. 报错内容为:

注意:动态创建的Actor里面所挂载的SubObject都是动态的, 只有当Actor本身为静态时, 其SubObject才是静态.

Native类型

如果仅仅是UClass类型, 那么一定是静态Object(因为重写了IsNameStableForNetworking). (一般不会走到这里, 大多数情况在这之前就已经确定是否为是静态UObject)

示例:

可以明显看出, UClass类型就是Native.

由于UClass已经重写了IsNameStableForNetworking函数, 所以不会走到这里. 上面的表述只是明确UClass确实为Native类型.

DefaultSubobject类型

可以从代码中看出, 如果Outer类型为CDO或者Outer的Archetype与其DefaultObject不同则为DefaultSubObject.一般情况都不会走到这里.

重写IsNameStableForNetworking函数

有些类型(UClass,UPackage等)重写了虚函数IsNameStableForNetworking, 自定义其是否为静态类型.

UPackage:

UPackage类型全部为静态UObject

示例:/Game/GamePlay/NewPlayerController.

UClass

UClass类型全部为静态UObject:

示例:

ULeve

ULevel类型全部为静态类型:

示例:PersistentLevel

CDO类型

以PlayerController的CDO类型为例:Default__NewPlayerController_C, 其根据RF_ClassDefaultObject类型判断其为静态类型.

放置到场景中的Actor

如果一个Actor放置在场景中, 那么它是静态的还是动态的呢?

可以找到, Actor也重写了函数:IsNameStableForNetworking

这里需要关注函数AActor::IsNetStartupActor, 里面有一个变量AActor.bNetStartup .

可以得出结论, 放置在地图上的Actor都是静态的.

总结

概述常用UObject类型中哪些是静态的.

  1. UPackage,ULevel,UClass等都重写了IsNameStableForNetworking函数, 确定为静态UObject.
  2. CDO都为静态的.
  3. 放置在地图中的Actor都是静态的, 都是可以通过PathName直接绑定的.
  4. 如果一个Actor本身是静态的, 那么它所有SubObject都是静态的.

动态UObject

动态Object的判断比较简单, 非静态Object全部是动态Object.

  1. SpawnActor/NewObject出来的UObject都是动态的.
  2. 如果Actor本身是动态的, 那么它所有的SubObject都是动态的.(如果一个Obj的Outer为动态的, 则其本身也为动态)
  3. 动态Actor是在DS创建, 将Actor(GUID+Archetype)/UObject(GUID+UClass)的GUID同步到客户端后,再进行创建. 即动态Actor/UObject的创建一定是DS优先于客户端(下面会讲).

NetworkGUID和UObject关联

在序列化网络数据时, 针对UObject, 是将其转换成NetworkGUID, 传输到远端.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
调用堆栈:
// 序列化UObject
--FObjectPropertyBase::NetSerializeItem()
|--FNetworkGUID NetGUID = GuidCache->GetOrAssignNetGUID( Object );
|--UPackageMapClient.SerializeObject()
| |--UPackageMapClient::InternalWriteObject()
| | |--Ar << NetGUID;

// 序列化Actor
--UActorChannel.ReplicateActor()
|-- // 针对RepFlags.bNetInitial时, 需要序列化Actor本身, 即Actor的GUID
|--UPackageMapClient.SerializeNewActor()
| |--UPackageMapClient.SerializeObject()
| | |--UPackageMapClient::InternalWriteObject()
| | | |--Ar << NetGUID;

从上面堆栈可以看出, 序列化UObject的本质其实是将UObject转换成GUID, 然后将其序列化到网络流中, 发送给远端. 即每个GUID都唯一标识一个UObject.

注意: 是在序列化时候进行才将UObject生成其对应的GUID. 即不提前创建, 只有在使用的时候再进行生成和处理. 这样可以把运行消耗分配到每一帧. 并且不会产生额外消耗. 它和另一种预处理(内存池, 对象池等)的思想截然不同. 引擎中根据情况不同, 需求不同, 选择不同的处理方式.

静态物体和动态物体的GUID类型是不同的, 静态物体是奇数, 动态物体是偶数. 并且从下文可以看出, 初始静态GUID为3, 动态GUID为2. 并且只有在初始化的时候才可能为0, 并且永远不可能为1. 并且每次递增2.

只有大于0的GUID才是有效GUID.

ExportBunch

每个Bunch中都可能会用到GUID, 如何将这些GUID的信息提前汇总起来, 发送给客户端呢? UE的解决方案是如果一个Bunch使用了GUID, 并且这个GUID客户端可能还不知道, 那么就在这个Bunch前面额外添加一个Bunch, 特殊命名为ExportBunch. 专门用于传输GUID相关信息.

1
2
3
4
5
6
7
--UActorChannel.ReplicateActor()
|--AActor.ReplicateSubobjects()
|--FObjectReplicator.ReplicateProperties()
|--UChannel.SendBunch()
| |--UChannel.AppendExportBunches()
| | |--UPackageMapClient.AppendExportBunches()
| | |--OutgoingBunches.Add(Bunch); // add normal bunch

可以看出, 总是先填充ExportBunch, 然后填充普通bunch.

填充ExportBunch

递归序列化GUID+OuterGUID等信息, 填充流程图如下:

当DS序列化一个UObject(将其GUID序列化到网络数据流中), 如果发现该UObject的Outer GUID客户端可能还没收到(第一次发或者发送后DS没收到ACK), 则会将该UObject的Outer GUID等信息填充到Export中.

通过UPackageMapClient.ShouldSendFullPath判断是否应该填充到ExportGUIDBunch. 并通过UPackageMapClient.ExportNetGUID将GUID真正填充到ExportBunch中.

并且在UPackageMapClient::ExportNetGUID首先处理的就是将FOutBunch.bHasPackageMapExports 设置为true, 表明其为ExportBunch.

写入ExportBunch

填入GUID:

填入ExportFlages

递归填入Outer信息:

最后以PathName收尾:

最后填充结果如图所示:

那么, ExportBunch里面只有奇数和0吗? 并不是.

注意看如下代码, UPackageMapClient::ShouldSendFullPath里面判断Object是否为静态并不是根据其GUID判断, 而是根据Object本身, 并且不检测其Outer(是否为静态Object首先判断的就是其Outer, 如果Outer为动态, 则其一定是动态). 所以Export里面是会填充偶数GUID的.

比如序列化TestReplicationComp1时, 判断其需要Export, 然后递归将其本身以及其Outer等信息都填入了GUID.

究其根本原因是因为如果只观察TestReplicationComp1本身, 其是通过CreateDefaultSubobject创建而来, 所以其本身是静态Object, 但是由于其Outer为动态, 才将其划为动态Object, 才会分配偶数GUID.

最终序列化后的ExportBunch

解析ExportBunch

收到ExportGUIDBunch, 进入UPackageMapClient.ReceiveNetGUIDBunch处理.

解析GUID:

解析ExportFlags

如果有Path, 需要递归解析Outer信息, 并以解析pathName收尾.

image-20230621172328831

通过分析UPackageMapClient.InternalLoadObject中的代码, 发现, 其所有逻辑均为查询, 加载对应的UObject.

尝试加载:

各种情况下的尝试加载等等.

所以, 针对ExportBunch而言, 其作用是传输GUID信息, 客户端收到信息后, 尝试在内存中查找对应的UObject, 如果查找不到则根据类型尝试进行加载. 但是永远不会创建新的Obj.

在客户端, 如果一个GUID确实没有查找到, 会将其暂时放入FNetGUIDCache.ObjectLookup中, 但是FNetGuidCacheObject.Object不会进行赋值, 表示当前已经收到DS发来的GUID, 但是这个GUID对应的UObject还没有创建. 在稍后解析UObject的Property时候, 如果用到了该GUID, 会重新查找, 如果找到了就会进行绑定.

总结

在DS端网络序列化时, 发现如果客户端没有相关GUID,则序列化到ExportBunch中, 并放置在该Bunch之前. 在客户端解析的时候, 只进行查询和加载, 对GUID和UObject进行绑定, 但是并不会创建, 创建UObject是在UActorChannel::ReadContentBlockHeader, 而创建Actor是在UPackageMapClient.SerializeNewActor函数中.

DefaultGUID

1非常特殊, 作为defaultGUID使用.

它有特殊的意义. 当主控端向DS发送RPC时, 如果RPC函数相关UObject的GUID DS还没有发送过来, 则使用DefaultGUID.

构造复现场景

在Character中CreateDefaultSubobject UTestActorComponent, 并在UTestActorComponent::BeginPlayer的时候向DS发送RPC, 这种情况下, 由于DS没有需求向主控端发送UTestActorComponent的GUID(只有用到才会发送), 所以主控端的UTestActorComponent不会有GUID, 只能使用DefaultGUID向DS发送信息.

创建UTestActorComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once
#include "Components/ActorComponent.h"
#include "TestActorComp.generated.h"

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UTestActorComponent : public UActorComponent
{
GENERATED_BODY()

public:
virtual void BeginPlay() override;

// RPC 同步给服务端拾取
UFUNCTION(Reliable, Server)
void C2S_Test1Func();

int32 TestIndex = 0;
};

void UTestActorComponent::BeginPlay()
{
Super::BeginPlay();
C2S_Test1Func();
}

void UTestActorComponent::C2S_Test1Func_Implementation()
{
++TestIndex;
}

将其挂载到Character上, 并在构造函数中创建.

1
2
3
4
5
6
// ALSVCharacter.h 
UPROPERTY();
UTestActorComponent *TestActorComponent;

// ALSVCharacter.cpp
TestActorComponent = CreateDefaultSubobject<UTestActorComponent>(TEXT("TestActorComponent"));

添加断点.

主控端发送DefaultGUID

主动端向DS发送RPC时, 由于UTestActorComponent没有DS同步来的GUID, 默认使用DefaultGUID, 并在ContentBlockHeader中填入PathName和Outer.

主控端没有找到Object对应的GUID, 则使用DefaultGUID:

当为Default时, 需要填入PathName和Outer

序列化NetGUID, FExportFlags, OuterGUID以及PathName. 注意这些消息是序列化在ContentBlockHeader里面的.

客户端向DS发送RPC, 某个UObject的GUID为1的时, ContentBlockHeader和一般情况下有所不同:

堆栈:

DS解析DefaultGUID

DS在解析主控端发来的RPC时, 发现GUID为DefaultGUID. 就要继续往下处理. 解析ExportFlags等信息.

序列化FExportFlags

ExportFlags.bHasPath为true, 需要加载

对DefaultGUID对应的Obj进行查找, 注意这里一定是查找, 因为它不会进行创建.

最后还要将该UTestActorComponent的GUID序列化到OutGUIDBunch中, 发送给主控端.

堆栈:

注意: 如果该Obj没有找到, 则直接将Bunch设置为Error, 然后丢弃.

发现bunch Error了, 直接close掉了.

在这里加上返回代码, 会直接close connection

总结

在主控端, 如果一个Obj在没有获得DS分配的GUID时就要向DS发送RPC, 那么就会将DefaultGUID作为Obj的GUID, 并附带Flag+OuterGUID+PathName. 一同发往DS. DS解析之后, 还要将Obj的最终GUID同步到主控端. 如果解析错误, 包括但不显示DS没有找到对应的Obj, 则直接将bunch close掉.

并且得出一个结论: 即使没有在属性上添加Replicated标志, 也可以通过Outer和PathName关联, 然后进行RPC.

通过上文可以知道, 如果一个Obj在发送RPC的时候, 其自身还没有GUID,则需要将OuterGUID+PathName发送给DS, 根据该信息DS可以找到对应的Obj. 但是如果客户端此时Outer也没有呢?

递归处理, 即在客户端没有RegisterGUID的Obj都当做DefaultGUID, 然后附加OuterGUID+PathName发送给DS.

静态UObject与NetworkGUID绑定

静态物体在World中是唯一的, 可以通过PathName唯一确定. 可以为静态物体用NetworkGUID+PathName唯一确定.

什么情况下需要同步

客户端和DS的两个UObject, 在什么情况下需要同步呢?

情况1: 作为动态Object的Outer

当一个UObject创建时,需要传入Outer, 查询一个UObject也可以根据Outer去查询. 当DS上需要创建UObject时候, 需要根据Outer进行创建. 当同步到客户端时, 也需要将Outer信息附带传过去. 所以, 当一个静态UObject作为动态UObject的Outer时候, 需要将Outer信息传输过去. 因为静态UObject是全局唯一的, 每次传输费时费力, 有没有好的方法传输一次永久使用的?

答案就是在客户端首次需要该静态UObject时, 为其分配GUID, 并传输到客户端, 后续可以通过GUID直接使用.

情况2:放置类Actor需要Replicated

如果一个Actor需要属性同步, 那么一定需要通过GUID将其在不同端的UObject绑定.

数据传输

填充ExportBunch流程一致. 将需要传输的GUID序列化到ExportBunch中.

绑定UObject

解析Export流程一致. 当解析ExportBunch时, 都通过查询或者加载找到对应的UObject, 然后将其注册到FNetGUIDCache.ObjectLookup中.

然后将UObject与GUID绑定:

CDO类型关联

当序列化一个新的Actor时, 会将Actor+Archetype序列化到网络字节流中. 其中Actor的Archetype为CDO.

对于PlayerController而言, 其序列化的对象为PlayerController+Default__NewPlayerController_C, 在序列化Default__NewPlayerController_C时候, 依据RF_ClassDefaultObject将其判定位静态类型.

在客户端进行反序列化时, 将Archetype序列化出来.

然后根据archetype的UClass SpawnActor.

这里没有直接传输UClass而用的Archetype, 原因是创建的时候, template使用了Archetype.

并且从一下代码可以看出就是CDO:

如果DS创建一个UObject, 传递到远端. 会序列化CDO吗?

针对ActorComp类型, 构建测试用例, DS创建, 查看其怎么序列化到网络字节流中, 并在远端的创建方式.

1
2
3
4
5
6
7
8
9
10
void ABasePlayerController::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (GetLocalRole() == ENetRole::ROLE_Authority)
{
if (!TestReplicationComp2)
{
TestReplicationComp2 = NewObject<UTestReplicationComp>(this, UTestReplicationComp::StaticClass());
}
}

DS上, 在UActorChannel::WriteContentBlockHeader中, 序列化到网络流的内容为UObjectGUID+UClassGUID.

客户端上, 反序列化在UActorChannel::ReadContentBlockHeader中, 反序列化出UObjectGUID+UClassGUID, 然后进行创建:

自定义UObject类型在DS上创建以及同步到远端创建的流程.

DS将UObject的UClass序列化到网络字节流中. UObjectGUID+UObjectClassGUID.

远端通过反序列化UObjectUClass进行创建.

综上, Actor会传递Archetype(CDO), 其他类型都会传递UClass.

动态UObject与NetworkGUID绑定

动态物体一般由DS先创建, 然后在DS上分配NetworkGUID, 通过ContentBlockHeader同步给其他端, 进行创建. 但是对于DefaultSubObject, 它在客户端先进行创建, 然后通过Outer+Name与DS进行关联.

Actor与GUID绑定

DS先进行创建, 当创建消息同步到客户端之后, 客户端才进行创建, 并同时创建ActorChannel.

DS序列化

在DS端, 如果Actor是第一次同步, 即初始化(RepFlags.bNetInitial为true), 则序列化Actor. 将Actor的GUID序列化到网络字节流中.

序列化Actor及其GUID.

然后再序列化Actor的Archetype和ActorLevel

这样就将Actor的GUID, Actor Archetype的GUID, ActorLevel的GUID信息都序列化到Bunch中了, 而且该Bunch必为Reliable.

客户端反序列化

在客户端检测如果Channel的Actor为nullptr, 则直接开始反序列Actor的化逻辑:

然后根据Archetype+ActorLevel进行创建.

堆栈:

综上, 根据DS传来的信息archetype+ActorLevel创建Actor. 这里需要注意的是, ActorLevel就作为Actor Outer.

CreateDefaultSubobject与GUID绑定

DS和客户端各自进行创建, 通过Outer和Name查找到指定UObj, 然后进行绑定.

在DS序列化UObjectGUID和ClassGUID到ContentBlockHeader中时, 有一个字段IsNameStable会标明当前UObject是否为NameStable. 对于DefaultSubObject, IsNameStable为1, 该UObject不会在序列化时候进行创建, 而是通过查询获得.

由于DefaultSubObject是在构造函数中创建的, 如果一个对象在处理它属性的赋值, 那么该对象一定已经创建过了, 即其一定执行了构造函数, 那么它的DefaultSubObject一定存在, 所以, 直接查询绑定即可.

填充ExportBunch中已经详细讲述了DefaultSubObject在ExportBunch中序列化/反序列化的流程, 这里不再赘述. 如果一个Bunch中使用了DefualtSubObject的GUID, 那么该GUID一定是已经在之前传输过了, 或者该Bunch之前一定有ExportBunch, 该GUID已经在该ExportBunch中解析过了. 结论就是, 不用创建, 一定能查到.

动态创建的SubObject绑定GUID

客户端解析ExportBunch的内容, 根据OuterGUID+PathName注册到FNetGUIDCache.ObjectLookup中

注意, 在处理ExportBunch时, 很有可能并没有找到对应的UObject, 没关系, 此时只要把解析到的GUID以及其相关信息(OuterGUID+PathName)填入FNetGUIDCache.ObjectLookup中, 等到后续使用中再次尝试与UObj绑定.

例如, 上面在尝试绑定GUID为2的UObj时失败. 从上帝视角知道, 2为NewPlayerController_C_0, 即PlayerController, 没有找到它的原因是客户端此时还没创建该对象, 所以在处理ExportBunch的下一个bunch时, 最开始的处理逻辑就是创建PlayerController, 并绑定对应的GUID.

反序列化Item处理TestReplicationComp1时, 根据PathName+Outer查找对应的UObj.

这样客户端就将TestReplicationComp1与GUID(6)绑定了.

DS和客户端各自进行创建, 通过Outer和Name查找到指定UObj, 然后进行绑定.

自定义UObject与GUID绑定

自定义UObject进行属性同步的方式:自定义UObject进行Replicate'

DS序列化UObj

DS在序列化UMyTestObject时, 会将ObjGUID+UClassGUID序列化到ContentBlockHeader中(通过函数:UActorChannel::WriteContentBlockHeader).

客户端解析UObj

根据SubObj的GUID尝试寻找对应的UObj, 如果是第一次, 则这里应该是空, 因为需要客户端创建.

然后根据ClassGUID查询对应的UClass

最后进行创建并注册GUID

DS删除UObj

每次进行Replicate时候, 都会遍历当前Channel的所有UActorChannel.ReplicationMap, 如果发现里面Obj不可用了, 就会进行销毁.

客户端删除UObj

客户端删除逻辑的处理是在UActorChannel::ReadContentBlockHeader函数中进行的, 即与普通ContentBlockHeader结构一样.

在序列化ClassNetGUID时, 如果为0, 则将该UObject看做待删除UObject, 执行清理逻辑.

属性成员与UObj绑定

直接绑定

当能找到GUID对应Obj时候, 直接设置对应的值.

延迟绑定

详情参考:GUID延迟绑定

总结

  1. 注意观察ContentBlockHeader中的逻辑, 每次DS都会序列化SubObj+Class, 而客户端只是解析, 如果没有就会创建, 有就会拿出来直接使用.
  2. UObject的删除逻辑掺杂在ContentBlockHeader中, 当ClassNetGUID为0时候, 即为删除. 充分复用ContentBlockHeader结构.
  3. 在序列化属性值后, 如果找到对应的UObj, 则直接绑定, 否则将其放入UnmappedGUID中, 每帧tick检测, 然后进行绑定, 而且延迟绑定都发生在一帧内.

总结

  • GUID的创建永远在DS, 客户端永远是被动绑定.
  • 静态UObject全部通过查找/加载找到, 然后与GUID绑定.
  • 动态UObject主要通过创建并辅以查找(SubDefaultObj)方式与GUID进行绑定. 而且永远都是客户端反序列化某个GUID时候, 才会去创建或者查找. 如果是延迟创建(即没有找到对应UObj)的还要通过轮询UnmapGUID进行绑定

这里顺带说一下, ExportBunch有可能仅仅只注册了GUID+OuterGUID+PathName等信息, 等到真正反序列化数据时才会将GUID和UObject绑定.

思考

放置在场景中的Actor不能进行RPC, 只能属性同步.

默认不可以, 因为其Owner没有Connection.

例如:对于Pawn类型, 如果其有对应的Controller, 会通过Controller查找NetConnection, 那么其就是主角对应的Pawn(每个客户端只有一个PlayerController, 就是主角自己), 所以其可以进行属性同步.

如果将一个Actor进行RPC, 需要重写AActor::GetNetConnection(), 或者其Owner具有Connection就可以.

当一个ActorChannel还没有创建, 属性同步已经到来, 会怎么处理?

如果Actor第一次属性同步, 则会在Bunch头添加SerializeNewActor.

如果非创建Bunch, 则直接为后边的属性同步.

客户端检测, 由于不是OpenBunch, 所以直接返回了. 整个bunch直接丢弃.

日志输出如下内容, 刚好和代码中error一致.

1
[2023.06.30-17.43.39:385][571]LogNetTraffic: Error: UActorChannel::ProcessBunch: New actor channel received non-open packet. bOpen: 0, bClose: 0, bReliable: 0, bPartial: 0, bPartialInitial: 0, bPartialFinal: 0, ChName: Actor, ChIndex: 3, Closing: 0, OpenedLocally: 0, OpenAcked: 1, NetGUID: 8

回答课前提问

  1. NetworkGUID是什么?

    NetworkGUID是uint32, 在网络中传输, 用来关联不同端的Obj.

  2. 动态UObject, 静态UObject是什么, DS和客户端的Obj是怎么关联起来的?

    是UE用于网络传输而特意划分的Obj类型, 是通过GUID关联起来的, 关联方式详看文中解析.

  3. Actor是怎么创建, 以及其属性是怎么同步的?'

    客户端通过SerializeActor创建, 属性同步是通过ContentBlockHeader+ContentBlockPayload进行同步.

  4. 针对DefaultSubObj, 客户端和DS怎么关联起来?

    通过OuterGUID+PathName关联起来的.

  5. 自定义UObject怎么进行网络同步, 怎么在客户端创建, 其所占用的Bunch是否变为Reliable?

    需要重写ReplicatedSubObj函数, 创建方式依旧通过PalyloadHeader解析, 如果没有找到Obj就会创建. 只有删除Obj的bunch才会被标记为Reliable.

  6. 怎么将变量的值和其对应的UObject关联起来? 例如BasePlayerController有个UMyTestObject类型变量pTestObj, 客户端的它是怎么创建, 并和BasePlayerController.pTestObj关联起来?

    静态GUID是查询, 动态GUID是创建. 而指针是在对已有Object绑定之后的查询赋值. 即通过GUID查找后, 进行赋值. 这里值得注意的是, UE都是通过偏移直接取地址, 根据FProperty进行操作赋值的. 特别的抽象.