# Unreal Iris(一)概述

Iris Replication System 中对于对象关系的组织形式比较晦涩,为了尽可能的节省内存开销,大量使用了 BitArray 以及各种 Bit 运算提效,因此如果对于整个 Iris Replication System 中各个数据之间的关联关系没有大致认知的情况下,很容易迷失在错综复杂的关系网中。本节主要针对 Iris 中比较常见的数据源做个整体性的宏观的梳理。

# 基本概念

# FNetRefHandle && FNetHandle

FNetRefHandle 是一个对象在网络同步中的唯一标识,客户端和服务器保持一致且全局唯一。

image-20240307101801472

FNetHandle 用于表示一个对象是全局唯一的,可以视为 UUID

image-20240307105322488

# ObjectInternalIndex && ConnectionID

ObjectInternalIndex 是用来描述一个 UObject 在数组中的下标,通常一个 UObject 创建之后就会加入到 FUObjectArray 中,并分配其一个唯一的 ObjectInternalIndex。Iris Replication System 为了更好的枝剪这部分内容,会在内部维护多个新的 UObject 数值映射:

image-20240307113116806

不论是在多 World 还是多 NetDriver 的情况下,都会出现多个 FNetRefHandleManager,每个 FNetRefHandleManager 有着自己的映射表,每个 UObject 会在不同的 FNetRefHandleManager 得到一个新的 ObjectInternalIndex。虽然两者都叫 ObjectInternalIndex,但从这里往后,我们所提到的 ObjectInternalIndex 都是 FNetRefHandleManager 内部的下标。

ObjectInternalIndexFNetRefHandleManager 内是唯一的。

ConnectionID 也是用来标识一个 Connection 的下标,从 FReplicationConnections 中可以直接通过 ConnectionID 访问对应的连接。

ConnectionIDFReplicationConnections 内是唯一的。

# FNetRefHandleManager

FReplicationSystemInternal 中有非常多的管理器,其中和 UObject 数据打交道最多,也是最常见的应当是 FNetRefHandleManager。而且 FNetRefHandleManager 对于 BitArray 的使用也非常多,整个存储的核心是在对象数量已知的情况下,通过 BitArray 尽可能压缩数据,虽然每次都需要分配最大数量上限的内存,但是对于 BitArray 来说无伤大雅(例如原本是一个 Int32 的索引,放在 BitArray 其实只需要 1 bit,压缩比 32: 1),而且 BitArray 还支持批处理,多个 BitArray 的与或操作也更高效。

image-20240307154249294

FNetRefHandleManager 本质工作是管理 NetObject 的内部数据,这些数据大致可分为以下几类:

  • 从外部系统到 ReplicationSystem 内部引用的关系映射: RefHandleToInternalIndex && NetHandleToInternalIndex
  • NetObject 对象分配状态:AssignedInternalIndices
  • NetObject 对象当前帧和上一帧的有效性: PrevFrameScopableInternalIndices && ScopableInternalIndices 通过对比很容易获取新增和删除的对象。
  • NetObject 对象之间的依赖关系,目前已经支持的依赖种类有两大类:
    • SubObject 父子关系:子对象通常是组件或其他附加到主对象的实例。子对象在场景中并不是独立的实体,而是依附于主对象的一部分。通过将子对象添加到主对象的子对象列表中,可以确保它们在网络复制过程中一起被处理 —— SubObjectInternalIndices
    • Dependent 依赖关系:依赖者可以单独复制,也可以在父对象复制时复制。依赖的角色的过滤条件跟随被依赖方。不能保证两者的数据最终会在同一个数据包中,所以它是一种非常松散的依赖形式 —— DependentObjectInternalIndices && ObjectsWithDependentObjectsInternalIndices
  • NetObject 休眠和销毁状态: DestroyedStartupObjectInternalIndices && WantToBeDormantInternalIndices
  • NetObject 的 FReplicatedObjectData。这里面的内容就很多了,包括对象的 Protocol,ProtocolInstance,ReceiveStateBuffer 等等。
  • NetObject 当前帧的脏数据缓存区 ReplicatedObjectStateBuffers

当一个 UObject 需要被复制的时候,就需要先在 FNetRefHandleManager 为其分配对应的 NetRefHandle。然后初始化该对象的 Protocol,ProtocolInstance,ReceiveStateBuffer,SendStateBuffer 等等内容。

# 依赖

复制系统内的两大依赖关系都被存储在 FNetDependencyData 内,每个对象又通过 FNetDependencyData::FDependencyInfo 标识自己的关系网:

image-20240308120114607

  • FNetDependencyData::FDependencyInfo:里面有三个元素:
    • ArrayIndices:存储了三个下标,分别对应 SubObjectArray、ChildSubObjectArray、DependentObjectArray。
      • SubObjectArray 存储了该对象的所有子对象,包括子对象的子对象。
      • ChildSubObjectArray 存储了该对象的子对象,只记录一层关系。
      • DependentObjectArray 存储了该对象被谁依赖。
    • SubObjectConditionalArrayIndex 对应于 SubObjectConditionalsStorage,用于存储所有 ChildSubObject 的同步条件。下标和 DependentObjectsStorage 中的 ChildSubObjectArray 一一对应。
    • DependentObjectsInfoArrayIndex 对应于 DependentObjectInfosStorage,存储了该对象依赖了谁。

这些关系都需要在代码中显式的注册:

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 是否可用,这些复杂的关系结构最终会体现在收发数据阶段。

image-20240308155547062

# 附件

Attachment 是网络同步中的一种独特关系,可以简单理解为和发送对象有关的数据,例如 RPC 调用的数据包。这部分数据会存储在一个发送队列中,在 Tick 流程内分两部分行发送:

image-20240308160730648

其中 Out 步骤发送的是当帧失效对象的 Attachment,即会关闭该对象的通信,可能是对象销毁亦或是休眠、擦除等情况。之所以放前面是因为 UpdateFiltering 步骤会执行 FReplicationWriter::UpdateScope,直接修改 Writer 中的数据,后续就没办法得到 OutScope 的内容了。

而 In 步骤则是发送新增或者还在作用域内的对象的 Attachment 信息。注意这里发送实际上都是写入到 FReplicationWriter,并不会立刻发送出去。

image-20240308161851026

每个 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 章节会对这部分内容在做详细的展开,这里有个基本的认知即可。

更新于 阅读次数

请我[恰饭]~( ̄▽ ̄)~*

鑫酱(●'◡'●) 微信支付

微信支付

鑫酱(●'◡'●) 支付宝

支付宝