# Unreal Iris(一)概述
Iris Replication System 中对于对象关系的组织形式比较晦涩,为了尽可能的节省内存开销,大量使用了 BitArray 以及各种 Bit 运算提效,因此如果对于整个 Iris Replication System 中各个数据之间的关联关系没有大致认知的情况下,很容易迷失在错综复杂的关系网中。本节主要针对 Iris 中比较常见的数据源做个整体性的宏观的梳理。
# 基本概念
# FNetRefHandle && FNetHandle
FNetRefHandle 是一个对象在网络同步中的唯一标识,客户端和服务器保持一致且全局唯一。
FNetHandle 用于表示一个对象是全局唯一的,可以视为 UUID
# ObjectInternalIndex && ConnectionID
ObjectInternalIndex 是用来描述一个 UObject 在数组中的下标,通常一个 UObject 创建之后就会加入到 FUObjectArray 中,并分配其一个唯一的 ObjectInternalIndex。Iris Replication System 为了更好的枝剪这部分内容,会在内部维护多个新的 UObject 数值映射:
不论是在多 World 还是多 NetDriver 的情况下,都会出现多个 FNetRefHandleManager,每个 FNetRefHandleManager 有着自己的映射表,每个 UObject 会在不同的 FNetRefHandleManager 得到一个新的 ObjectInternalIndex。虽然两者都叫 ObjectInternalIndex,但从这里往后,我们所提到的 ObjectInternalIndex 都是 FNetRefHandleManager 内部的下标。
ObjectInternalIndex 在 FNetRefHandleManager 内是唯一的。
ConnectionID 也是用来标识一个 Connection 的下标,从 FReplicationConnections 中可以直接通过 ConnectionID 访问对应的连接。
ConnectionID 在 FReplicationConnections 内是唯一的。
# FNetRefHandleManager
FReplicationSystemInternal 中有非常多的管理器,其中和 UObject 数据打交道最多,也是最常见的应当是 FNetRefHandleManager。而且 FNetRefHandleManager 对于 BitArray 的使用也非常多,整个存储的核心是在对象数量已知的情况下,通过 BitArray 尽可能压缩数据,虽然每次都需要分配最大数量上限的内存,但是对于 BitArray 来说无伤大雅(例如原本是一个 Int32 的索引,放在 BitArray 其实只需要 1 bit,压缩比 32: 1),而且 BitArray 还支持批处理,多个 BitArray 的与或操作也更高效。
FNetRefHandleManager 本质工作是管理 NetObject 的内部数据,这些数据大致可分为以下几类:
- 从外部系统到 ReplicationSystem 内部引用的关系映射:
RefHandleToInternalIndex
&&NetHandleToInternalIndex
- NetObject 对象分配状态:AssignedInternalIndices
- NetObject 对象当前帧和上一帧的有效性:
PrevFrameScopableInternalIndices
&&ScopableInternalIndices
通过对比很容易获取新增和删除的对象。 - NetObject 对象之间的依赖关系,目前已经支持的依赖种类有两大类:
- SubObject 父子关系:子对象通常是组件或其他附加到主对象的实例。子对象在场景中并不是独立的实体,而是依附于主对象的一部分。通过将子对象添加到主对象的子对象列表中,可以确保它们在网络复制过程中一起被处理 ——
SubObjectInternalIndices
。 - Dependent 依赖关系:依赖者可以单独复制,也可以在父对象复制时复制。依赖的角色的过滤条件跟随被依赖方。不能保证两者的数据最终会在同一个数据包中,所以它是一种非常松散的依赖形式 ——
DependentObjectInternalIndices
&&ObjectsWithDependentObjectsInternalIndices
。
- SubObject 父子关系:子对象通常是组件或其他附加到主对象的实例。子对象在场景中并不是独立的实体,而是依附于主对象的一部分。通过将子对象添加到主对象的子对象列表中,可以确保它们在网络复制过程中一起被处理 ——
- NetObject 休眠和销毁状态:
DestroyedStartupObjectInternalIndices
&&WantToBeDormantInternalIndices
。 - NetObject 的 FReplicatedObjectData。这里面的内容就很多了,包括对象的 Protocol,ProtocolInstance,ReceiveStateBuffer 等等。
- NetObject 当前帧的脏数据缓存区
ReplicatedObjectStateBuffers
。
当一个 UObject 需要被复制的时候,就需要先在 FNetRefHandleManager 为其分配对应的 NetRefHandle。然后初始化该对象的 Protocol,ProtocolInstance,ReceiveStateBuffer,SendStateBuffer 等等内容。
# 依赖
复制系统内的两大依赖关系都被存储在 FNetDependencyData 内,每个对象又通过 FNetDependencyData::FDependencyInfo 标识自己的关系网:
- FNetDependencyData::FDependencyInfo:里面有三个元素:
- ArrayIndices:存储了三个下标,分别对应 SubObjectArray、ChildSubObjectArray、DependentObjectArray。
- SubObjectArray 存储了该对象的所有子对象,包括子对象的子对象。
- ChildSubObjectArray 存储了该对象的子对象,只记录一层关系。
- DependentObjectArray 存储了该对象被谁依赖。
- SubObjectConditionalArrayIndex 对应于 SubObjectConditionalsStorage,用于存储所有 ChildSubObject 的同步条件。下标和 DependentObjectsStorage 中的 ChildSubObjectArray 一一对应。
- DependentObjectsInfoArrayIndex 对应于 DependentObjectInfosStorage,存储了该对象依赖了谁。
- ArrayIndices:存储了三个下标,分别对应 SubObjectArray、ChildSubObjectArray、DependentObjectArray。
这些关系都需要在代码中显式的注册:
if (ensure(ReplicatedOwner) && ReplicatedOwner->IsUsingRegisteredSubObjectList()) | |
{ | |
ReplicatedOwner->AddReplicatedSubObject(Child); | |
// ReplicatedOwner->RemoveReplicatedSubObject(Widget); | |
} | |
// 对于非 Actor 类型的对象需要通过 RegisterReplicationFragments 建议依赖 | |
void ULyraEquipmentInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags) | |
{ | |
using namespace UE::Net; | |
Super::RegisterReplicationFragments(Context, RegistrationFlags); | |
// Build descriptors and allocate PropertyReplicationFragments for this object | |
FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags); | |
} |
并且需要开启相关的配置选项:
[SystemSettings] | |
net.SubObjects.DefaultUseSubObjectReplicationList=1 |
# 引用
实际上一个 NetObject 的数据里常常会引用到其他的 NetObject,这些引用指针在同步的时候也需要正确的理顺关系。例如谁引用的谁,哪些引用已经同步其他对象同步过了之类的信息,就不需要再重复打包,并且还有些引用关系可以是延后初始化的,例如对象 A 中引用了 B,但是 A 初始化好之后可以完全不依赖 B 是否可用,这些复杂的关系结构最终会体现在收发数据阶段。
# 附件
Attachment 是网络同步中的一种独特关系,可以简单理解为和发送对象有关的数据,例如 RPC 调用的数据包。这部分数据会存储在一个发送队列中,在 Tick 流程内分两部分行发送:
其中 Out 步骤发送的是当帧失效对象的 Attachment,即会关闭该对象的通信,可能是对象销毁亦或是休眠、擦除等情况。之所以放前面是因为 UpdateFiltering 步骤会执行 FReplicationWriter::UpdateScope,直接修改 Writer 中的数据,后续就没办法得到 OutScope 的内容了。
而 In 步骤则是发送新增或者还在作用域内的对象的 Attachment 信息。注意这里发送实际上都是写入到 FReplicationWriter,并不会立刻发送出去。
每个 FNetObjectAttachment 都有对应的 FReplicationStateDescriptor 用来描述数据的结构和类型,FNetBlobCreationInfo 可以用于确定消息的类型方便接收方识别需要执行不同的函数,因此再调用 Rpc 的时候会创建对应的 FNetBlobCreationInfo,不过这些内容引擎底层都封装好了,开发者是不需要关注的。
TRefCountPtr<UE::Net::Private::FNetRPC> UNetRPCHandler::CreateRPC(const UE::Net::FNetObjectReference& ObjectReference, const UFunction* Function, const void* Parameters) const | |
{ | |
FNetBlobCreationInfo CreationInfo; | |
CreationInfo.Type = GetNetBlobType(); | |
CreationInfo.Flags = ((Function->FunctionFlags & FUNC_NetReliable) != 0) ? UE::Net::ENetBlobFlags::Reliable : UE::Net::ENetBlobFlags::None; | |
FNetRPC* RPC = FNetRPC::Create(ReplicationSystem, CreationInfo, ObjectReference, Function, Parameters); | |
return RPC; | |
} | |
RPC->SetNetObjectReference(OwnerReference, OwnerOrSubObjectReference); | |
AttachmentSendQueue.Enqueue(OwnerIndex, SubObjectIndex, reinterpret_cast<const TRefCountPtr<FNetObjectAttachment>&>(RPC)); |
# FReplicationConnections
如果说 FNetRefHandleManager 是负责管所有对象的,而 FReplicationConnections 则是管所有连接的。里面存储了当前 ReplicationSystem 中全部连接的状态,虽然 FReplicationConnections 本身并不复杂,但是 FReplicationConnection 却有很多值得探讨的地方:
struct FReplicationConnection | |
{ | |
FWeakObjectPtr ReplicationDataStream; | |
FReplicationWriter* ReplicationWriter = nullptr; | |
FReplicationReader* ReplicationReader = nullptr; | |
FObjectPtr UserData = nullptr; | |
}; |
ReplicationDataStream
:用来自定义数据复制的规范,具体实现可以参考UReplicationDataStream
FReplicationWriter
:负责加工同步数据并传递给网络层进行数据包发送的中间层,这个类的功能特别强大,举几个比较 NP 的功能:- 存储每个同步对象的生命周期状态。
- Attachment 附件的处理,包括对象的 RPC 之类的。
- 处理对象打包,序列化,依赖关系,BaseLine 生成。
- 存储对象的同步优先级。
FReplicationReader
:功能也不少:- 自动编排对象间的引用依赖和前后置关系,在收到某些依赖数据后完善后置对象的引用信息。
- 对象网络数据的暂存器。
每个 Connection 通过 ReplicationWriter 进行数据的写入和发送,里面会基于 Record 创建和更新 BaseLine,并基于 BaseLine 和当前状态的差值做增量同步,处在同步中(未 ACK)的信息会通过 Record 记录缓存起来。发送对象数据的时候还会考虑对象的父子关系,对象携带的 Attachment 信息,Dependent 依赖。然后通过 FReplicationReader 把收到的网络包读取并暂存,然后在合适的时机回写到对象里,并处理相关依赖关系。在 DataStream 章节会对这部分内容在做详细的展开,这里有个基本的认知即可。