# Unreal Iris(四)DataStream

本章主要介绍 UObject 对象的数据在整个 Iris Replication System 的流向。

# UDataStreamChannel

Iris Replication 和 Unreal 原生的 Replication 最大的不同就在于 Channel。原生版本下每个 Connection 中的每个 Actor 都会有单独的 Channel,然后状态信息会被记录在对应的 Channel 中。而 Iris 中一个 Connection 只会有一个 DataStreamChannel 负责同步所有的 Actor,而所有 Actor 的同步信息都会存储在一个叫 FReplicationWriter 的结构中。由于存在对象之间的引用和依赖关系,或者其他业务层面的复杂联系,把多个 Actor 信息打包为一个 Packet 进行发送,肯定是要比分成多个 Channel 进行发送来的要轻松很多,也更可控一些,例如数据的 Ack,写溢出情况下的回滚。

要开启 DataStreamChannel 需要在配置项中添加以下内容:

[/Script/Engine.NetDriver]
; All Iris replication is handled by various DataStream implementations that are ticked via the DataStreamManager instance in this channel.
+ChannelDefinitions=(ChannelName=DataStream, ClassName=/Script/Engine.DataStreamChannel, StaticChannelIndex=2, bTickOnCreate=true, bServerOpen=true, bClientOpen=true, bInitialServer=true, bInitialClient=true)

# DataStream

DataStream 负责数据序列化反序列化的上层抽象。目前 Iris 主要有两大类 DataStream

  • ReplicationDataStream
  • NetTokenDataStream

在配置选项中定义如下:

[/Script/IrisCore.DataStreamDefinitions]
+DataStreamDefinitions=(DataStreamName=NetToken, ClassName=/Script/IrisCore.NetTokenDataStream, DefaultSendStatus=EDataStreamSendStatus::Send, bAutoCreate=true)
+DataStreamDefinitions=(DataStreamName=Replication, ClassName=/Script/IrisCore.ReplicationDataStream, DefaultSendStatus=EDataStreamSendStatus::Send, bAutoCreate=true)

# NetTokenDataStream

这个是用来同步 NetToken 信息,主要是一些无关游戏玩法,单纯客户端服务器之间交互的数据,比如资源加载情况。

# ReplicationDataStream

这个是负责 UObject 对象的属性同步和 RPC(Attachments),主要的实现类分别是:

  • FReplicationWriter:负责将数据序列化到消息包内。
  • FReplicationReader:负责从消息包中反序列化数据到 StateBuffer。

# SendState && RecvState

image-20240310115757920

接收和发送状态整体来看流程比较类似,只不过一个是顺向工程,一个是逆向。

image-20240308165814427

整套流程会涉及到两个核心的结构 ReplicatedObjectDataReplicatedObjectStateBuffers

# FReplicatedObjectData

ReplicatedObjectData 是整个发送流程的核心,主要包含以下几部分内容:

  • FReplicationProtocol:这个在 ReplicationState 部分介绍过了,是记录 UObject 对应 UClass 的 MetaData 信息,可以通过 FReplicationStateDescriptor 来获取每个字段的类型及函数调用的地址偏移,方便按需访问。此外 FReplicationStateDescriptor 中每个字段还会配备相应的 FNetSerializer 定义序列化反序列化。 由于是 UClass 级别的数据,因此同类型的对象可以共用一个 FReplicationProtocol
  • FReplicationInstanceProtocol:这个在 ReplicationState 部分也介绍过了,其内部会存储多个 FReplicationFragment,每个 FReplicationFragment 又包含多个 Property 信息,这些 Property 中会记录 UObject 对象对应的字段内容。如果开启了 PushModle 的情况下,每次同步之前会比对两者的差异,将新的属性从 UObject 写入 FReplicationFragment 并记录下数据的脏标记,为之后 NetObject 的属性同步做好准备。值得一提的是 NetObject 是一个抽象的概念,实际上就是多个 FReplicationFragment 的集合。另外由于 FReplicationInstanceProtocol 需要关联和管理对象属性,因此每个 UObject 都需要有单独的 FReplicationInstanceProtocol
  • ReceiveStateBuffer:接收缓冲区。通过网络送达的对象信息会被 FReplicationReader 读取,然后通过逆向工程解析到 ReceiveStateBuffer,要注意里面的数据依旧还是序列化的。

ExternalOffset && InternalOffset,两者都记录的是内存地址的偏移情况。ExternalOffset 存储的是 FReplicationData 中的偏移,由于 FReplicationData 数据组织比较的结构化,StateBuffer 组装的,因此布局上面有些许差异。 InternalOffset 则是计算整个对象序列化后的各属性偏移,一般参考项是一整块连续的内存空间。常常用在 ReplicatedObjectStateBuffers 或者 BaseLine 的 StateBuffer。

# ReplicatedObjectStateBuffers

该结构和 ReceiveStateBuffer 功能上类似,一个负责缓存发送的序列化数据,另一个负责缓存接收的序列化数据。

ReplicatedObjectStateBuffers 中的数据来自 UObject 本身,因此是最新的。这部分数据会被用来做网络同步亦或是 BaseLine 的创建。

# 发送哪些数据

Iris Replication System 为了保证能够高效的同步对象属性,会尽可能的减少需要同步的内容量,因此知道「需要发送哪些数据」就变得尤为重要,这里主要分了三个步骤来筛选所需的同步内容:

# PollAndRefreshCachedPropertyData—— 获取变更的属性片段

第一步其实就是从 UObject 上把脏数据拷贝过来,触发时机是在每次 Tick 更新的最前面 UObjectReplicationBridge::PreUpdateAndPollImpl ,具体实现可以参考函数 FReplicationInstanceOperations::PollAndRefreshCachedPropertyData ,大致就是把需要同步(通过 GetLifetimeReplicatedProps 注册上的属性)的都过一遍 —— FPropertyReplicationState::SetPropertyValue ,由于 PropertyFragements 会存储前一次的属性值,因此变化了就在 FPropertyReplicationState 打上脏标记,顺带更新一下数据。

# CopyDirtyStateData—— 整理完整的变更属性

接着就是第二步,从 FPropertyReplicationState 把脏数据又给添加到 ReplicatedObjectStateBuffers,不一样的是之前 FPropertyReplicationState 里的数据是一部分属性的,然后通过多个 FPropertyReplicationState 拼凑在一起组成一个完整的 UObject 脏数据。到了 ReplicatedObjectStateBuffers 里,它就变成一块完整的连续空间了。

# WriteObjectInBatch—— 处理属性同步条件和对象依赖

最后就是第三步,执行 FReplicationWriter::WriteObjectInBatchReplicatedObjectStateBuffers 把数据写入到发送包体内,再写入之前还有很多额外的操作。比如需要判断某些属性是否满足同步条件(对象的生命周期,属性是否被设置隐藏标记等),对象同步的时候还需要带上 SubObject 的信息,如果可以的话还需要跟上 Dependent 信息和 Attachment 信息。有关对象的引用,也会在打包的时候尽可能共享一份数据,避免重复打包。数据打包又可以有增量和全量两种方式,打包成功后就会把同步每个对象的同步内容缓存起来,为后续的 Ack 和 BaseLine 创建做准备。

# 参考链接

  • [UFSH2023] Iris Replication System 初探 | 陈宝康 Epic Games
更新于 阅读次数

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

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

微信支付

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

支付宝