技术博客

Iris 复制系统:UE5.3 实验性新协议的架构思路

第 76 / 100 篇 · 2025-08-02 · ue5 · 网络复制Iris源码分析

UE5.3 把 Iris 作为实验性功能放进引擎(Engine/Plugins/Experimental/Iris),目标不是修修补补,而是重写整套复制系统。从 5.3 到 5.5 它一直在实验状态,但架构思路已经很清晰,而且不少设计会回流影响老系统。基于 5.3 源码(Iris/Core、Iris/Replication)梳理它和传统模型的差异。

老系统的结构性痛点

传统复制系统的几个痛是结构性的,不是优化能解决的:

  1. 对象模型绑定 UObject。UActorChannel 直接持 AActor* 裸指针,影子状态按对象内存布局拷贝。这导致:非 actor 对象(比如一个纯数据的子结构)想复制就得包成 UObject;channel 的生命周期和 actor 的 GC 纠缠在一起。
  2. 协议和对象布局耦合。bunch 里的属性 handle 是按 UClass 的 FProperty 顺序编号的——类布局一变(热更、版本差异),协议就断。补丁兼容只能做 Specialize,脆弱。
  3. 相关性计算和状态序列化在一个 tick 里耦合。ReplicationGraph 用缓存缓解了这个痛,但缓存和协议层是两套东西,维护两份一致性。

Iris 对这三个痛各给了一个结构性解法。

解法一:NetRefHandle 解耦对象模型

Iris 引入 NetRefHandle(Iris/Core/NetRefHandle.h)——一个 32 位句柄,替代老系统的 UObject* + channel 组合。对象想参与复制,先向 Iris 注册拿到 handle,之后协议层只认 handle,不再碰 UObject。

这个间接层的意义:

  • 协议层不再依赖 UObject/GC。对象销毁时 Iris 做 detach,handle 失效,客户端收到"handle 失效"通知清理本地镜像——没有 channel close 和 actor 销毁的竞争窗口(老系统里 ConditionalBeginDestroy 和 channel 关闭的时序 bug 不少)。
  • 非 actor 对象可以平等参与复制。一个 USTRUCT 注册成 replicated 对象照样拿 handle,不用伪造 actor。

代价是多一层间接:UObject ↔ handle 的映射表要维护,调试时从 handle 反查对象需要走 NetRefHandleManager。

解法二:Replication Fragment 拆状态

传统模型里"一个对象哪些属性要复制"由 UClass 的反射信息整体描述,粒度是类。Iris 拆成复制片段(Replication Fragment):每个对象可以提供多个 fragment,每个 fragment 描述一组内聚的状态(比如"移动状态"、"战斗属性"、"外观"),fragment 是协议和序列化的基本单元。

好处有三个:

  • 协议按 fragment 组织,fragment 有自己的 schema 和序列化器,类布局变化只要 fragment 的 schema 没变,协议就兼容——补丁场景的脆弱性大幅改善。
  • 相关性可以按 fragment 做。移动 fragment 高频同步、外观 fragment 低频,同一对象的不同状态走不同 cadence,不用拆成多个 actor。
  • 序列化器可插拔。fragment 的序列化不绑定 FProperty 遍历,可以注册自定义 serializer(比如对移动状态做专门的 delta 压缩)。

解法三:Tokenized Protocol + 状态集中管理

Iris 的线协议叫 Tokenized Protocol(Iris/Replication/Protocol):bunch 流被拆成 token 序列(对象创建、fragment 更新、handle 失效……),每个 token 独立解析,不再依赖类布局推导。配套的是服务端状态存储:老系统的发送历史是每 channel 一份影子快照(环形缓冲,属性同步那篇讲过);Iris 把"对象的当前权威状态"集中存放在 ReplicationStorage,delta 计算从存储里直接取,不再每连接存一份影子。

账要这么算:老系统 $N_{\text{conn}}$ 个连接 × 每连接一份影子状态的内存,Iris 收敛为一份集中存储 + 每连接的 ack 游标。100 连接的场景,服务端网络内存的 actor 状态部分降一个量级。

Iris vs 老系统:对象间接层 + fragment 拆状态 + 集中存储 老系统 UObject* → UActorChannel 影子状态:每连接一份 协议:属性 handle 按类布局编号 序列化:FProperty 遍历 相关性:ReplicationGraph 缓存(外挂) 痛点:布局变协议断、内存按连接膨胀 非 actor 对象参与复制要伪装 Iris NetRefHandle 间接层(32 位句柄) 状态存储:集中一份 + ack 游标 协议:token 序列,token 独立解析 序列化:fragment 级可插拔 serializer 相关性:fragment 粒度 cadence 收益:补丁兼容、内存收敛、 非 actor 对象平等参与

现状判断:什么时候碰它

5.3 的 Iris 是实验性的,明确不建议生产使用——GAS 等大量引擎子系统还没适配,开 Iris 跑 GAS 项目会直接缺功能。但两点值得现在做:

  1. 新项目预留接口。如果你的项目要长期维护(三年以上的在线游戏),自定义复制逻辑封装在一层接口后面,将来 Iris 转正时迁移成本可控。
  2. 学它的设计决策。NetRefHandle 的间接层、fragment 的拆状态、集中存储收敛内存——这些思路不依赖 Iris 本身:比如你自己写 replay 系统时,"状态集中存储 + 每连接游标"就是可以直接抄的架构。

判断 Iris 是否可用了,看三件事:GAS/CharacterMovement 是否完成适配、net.Iris 相关的控制台命令是否稳定、Epic 的官方 release note 是否摘掉 experimental 标记。在那之前,传统系统 + ReplicationGraph 仍是生产环境的正解。

Iris 和老系统的关系可以类比 Chaos 取代 PhysX:不是增量优化,是换地基。换地基期间老系统继续维护,新系统的架构决策逐步回流。对引擎使用者的启示是:复制系统的接口抽象(比如你自己封装的网络同步层)值得按"未来会被换掉"来设计。