ue 浅谈网络容器
前言
研究PushModel与FastArray等
对于网络的容器的介绍
PushModel
PushModel 是 Unreal Engine 中用于优化网络复制性能的一种机制。它的核心思想是将网络更新的触发从“轮询检查”改为“事件驱动”,从而减少不必要的复制开销。
传统复制模式的问题
在传统的属性复制(Replication)中,Unreal 默认使用 “脏标记”(Dirty Flag) 机制:
- 每个可复制的属性在值发生变化时会被标记为“脏”。
- 每帧(或每个复制周期)服务器会检查所有对象的脏标记,将标记为脏的属性打包发送给客户端。
- 这种轮询检查的方式在属性多、变化少的场景下会产生不必要的性能开销。
PushModel 的工作原理
PushModel 改变了这一流程:
- 主动标记:当某个可复制的属性值发生变化时,开发者需要显式调用
MarkPropertyDirty()或MARK_PROPERTY_DIRTY()宏来通知网络系统:“这个属性需要复制”。 - 按需打包:网络系统只打包那些被显式标记为“脏”的属性,不再进行全量轮询检查。
- 自动清除:属性被成功复制后,其“脏”标记会被自动清除。
核心优势
- 减少CPU开销:避免了每帧对所有属性进行脏检查的循环,特别适合属性数量多但变化频率低的场景(如大量AI角色、环境物体)。
- 精确控制:开发者可以更精细地控制何时触发复制,避免不必要的网络流量。
- 向后兼容:可以与传统的复制模式混合使用,逐步优化。
使用方法示例
1 | // 1. 在属性声明处启用 PushModel |
适用场景
- 大量相似对象:如MMO中的NPC、射击游戏中的弹丸。
- 低频更新属性:如角色的经验值、任务的进度、建筑物的耐久度。
- 性能敏感项目:需要最大限度减少网络和CPU开销的在线游戏。
注意事项
- 需要手动管理:开发者必须记得在属性变化时调用标记函数,否则会导致复制失败。
- 不适合高频变化属性:对于每帧都变化的属性(如位置、旋转),传统模式可能更合适。
- 调试复杂度:如果忘记标记,网络问题可能更难排查。
PushModel 是 Unreal Engine 4.25+ 引入的重要优化特性,特别适合大规模多人游戏或对象数量众多的场景,能够显著提升服务器的运行效率。
FastArray
FastArray 是 Unreal Engine 中专门为高效网络同步动态数组而设计的一套系统。它基于 FFastArraySerializer和 FFastArraySerializerItem这两个核心类,通过差分复制(Delta Replication) 机制,只同步发生变化的部分,从而显著减少网络带宽消耗和CPU开销。
核心设计原理
FastArray 的“快速”体现在其增量同步策略上:
- 传统 TArray 复制:每次数组变化都可能需要序列化整个数组。
- FastArray 复制:通过
ReplicationID和ReplicationKey精确追踪每个元素的变化,只同步新增、修改或删除的元素。
关键组件
FFastArraySerializerItem(数组元素)
- 每个数组元素必须继承此类。
- 包含两个关键标识符:
ReplicationID:元素的唯一标识(类似GUID)。ReplicationKey:元素被修改的次数,用于检测变化。
FFastArraySerializer(数组容器)
- 管理整个数组的复制逻辑。
- 维护
IDCounter(用于生成 ReplicationID)和ArrayReplicationKey(数组整体修改次数)。 - 提供
MarkItemDirty()和MarkArrayDirty()等关键方法。
基本使用流程
1 | // 1. 定义Item结构体 |
网络复制流程
- 服务器端修改:调用
MarkItemDirty()或MarkArrayDirty()标记变化。 - 差分序列化:网络系统比较当前状态与上次复制状态,只打包变化的部分。
- 客户端反序列化:接收差分数据,应用变化,触发相应的回调函数(
PostReplicatedAdd、PostReplicatedChange、PreReplicatedRemove)。
主要特点与优势
- 高效差分同步:只传输变化的数据,特别适合频繁更新的动态数组。
- 精确变化追踪:通过 ReplicationKey 机制准确识别元素级别的变化。
- 回调机制完善:提供三个关键回调函数,方便客户端响应变化。
- 广泛引擎应用:Unreal 的 Gameplay Ability System(GAS)中大量使用,如
ActiveGameplayEffects、ActivatableAbilities等。
重要注意事项
- 次序不保证:FastArray 不保证 服务端和客户端数组元素的顺序一致。
- 手动标记:必须显式调用
MarkItemDirty()或MarkArrayDirty(),否则变化不会同步。 - 回调仅客户端:
PostReplicatedAdd等回调函数只在连接到服务器的客户端触发,服务器和单机模式不会调用。 - 适合场景:最适合元素数量多、变化频繁但每次变化比例小的数组(如背包物品、技能列表、状态效果等)。
实际应用场景
- 背包/库存系统:物品的添加、删除、数量变化。
- 技能/能力系统:GAS 中的 GameplayAbilitySpec 数组。
- 状态效果系统:ActiveGameplayEffects 的同步。
- 玩家列表/队伍管理:动态变化的玩家信息列表。
FastArray 是 Unreal 网络同步中处理动态数组的标准解决方案,虽然需要更多的手动管理,但能带来显著的性能提升,特别适合大规模多人游戏中的高频数据同步需求。
FastArray和TArray的区别
FastArray(特指 FFastArraySerializer)和 TArray 是 Unreal Engine 中两种用途和设计目标完全不同的数组容器。简单来说,TArray 是通用的内存容器,而 FastArray 是专为高效网络同步设计的网络容器。
下面是它们的核心区别对比:
核心区别总览
| 维度 | TArray | FastArray (FFastArraySerializer) |
|---|---|---|
| 本质 | 通用的、标准的内存动态数组容器。 | 专为网络复制(Replication) 优化的、支持差分同步的数组结构。 |
| 设计目标 | 提供高效的内存操作(添加、删除、访问)。 | 提供高效的网络序列化与同步,最小化带宽和CPU开销。 |
| 复制方式 | 全量复制。任何修改(即使只改一个元素)都可能导致整个数组被序列化并发送。 | 增量(差分)复制。只同步发生变化的部分(新增、修改、删除的元素)。 |
| 脏标记机制 | 无内置脏标记。依赖UE的属性复制系统自动检测整个数组的“脏”状态。 | 有。基于 ReplicationKey的脏标记系统,需手动调用 MarkItemDirty()或 MarkArrayDirty()来触发同步。 |
| 客户端回调 | 仅支持标准的 RepNotify(整个数组变化时触发一次)。 |
提供精细的元素级回调:PostReplicatedAdd、PostReplicatedChange、PreReplicatedRemove。 |
| 顺序保证 | 保证服务端与客户端的元素顺序完全一致。 | 不保证 服务端与客户端的元素顺序一致。顺序可能因网络包延迟或重排而不同。 |
| 使用复杂度 | 低。声明即可用,与普通编程中的数组无异。 | 高。需定义从 FFastArraySerializerItem派生的元素结构,并实现特定回调函数。 |
| 典型应用场景 | 存储纯本地数据、配置表、临时计算结果等不需要网络同步的列表。 | 存储需要高效同步的游戏动态列表,如玩家的技能列表、激活的状态效果、背包物品等。 |
详细对比分析
1. 网络同步效率
- TArray:每次数组有任何修改,网络系统在复制时默认会序列化整个数组并发送。如果一个数组有1000个元素,只修改了1个,也会发送1000个元素的数据。带宽消耗大。
- FastArray:内部跟踪每个元素的
ReplicationKey。网络同步时,只打包ReplicationKey发生变化的元素。修改1个元素就只发送1个元素的数据。带宽消耗小。
2. 数据变化追踪
- TArray:依赖UE底层的属性脏标记系统。开发者无法精细控制“哪个元素变了”,系统只知道“这个数组属性变了”。
- FastArray:要求开发者显式标记变化。修改元素内容后需调用
MarkItemDirty(Item),增删元素后需调用MarkArrayDirty()。这提供了精确的变化控制。
3. 客户端响应
- TArray:只能在数组属性上设置一个
RepNotify函数,当整个数组的任何变化从服务器同步下来时触发一次。你无法直接知道是哪个元素变了、怎么变的。 - FastArray:为每个元素提供了独立的回调函数。当客户端收到同步数据时,会精确调用对应元素的
PostReplicatedAdd(新增)、PostReplicatedChange(修改)或PreReplicatedRemove(删除)。这让客户端能高效、精确地更新UI或游戏状态。
4. 内存与性能开销
- TArray:内存布局紧凑,访问速度极快,是UE中最高效的通用容器之一。
- FastArray:为了支持差分同步和元素追踪,每个元素需要额外的内存存储
ReplicationID和ReplicationKey,容器本身也有额外状态。内存开销更大,访问速度略低于 TArray。
5. 代码示例对比
1 | // --- TArray 用法(简单但低效的网络同步)--- |
使用建议
- 使用 TArray 当:数据不需要网络同步,或数组很小且变化不频繁,或你不想引入额外复杂性。
- 使用 FastArray 当:数据需要网络同步,且数组可能很大,或元素会频繁变化(尤其是部分变化),或你需要客户端对特定元素的变化做出精细响应。
在 Unreal 的 Gameplay Ability System (GAS) 中,所有需要网络同步的核心列表(如 ActivatableAbilities、ActiveGameplayEffects)都使用 FastArray,这正是因为GAS需要高效同步大量可变的能力和效果状态。
简单比喻:
- TArray 像一份纸质名单,每次有人员变动(哪怕只改一个字),都需要重新打印整份名单发给所有人。
- FastArray 像一份电子表格,系统只记录“谁变了、怎么变的”,然后把这些变动记录同步给大家,大家在自己的副本上应用这些改动即可。
PushModel 和FastArray的区别
PushModel 和 FastArray 是 Unreal Engine 中两个不同层面、用于优化网络复制的机制,它们的核心区别在于优化的对象和抽象的层次。
简单来说:
- PushModel 是一种优化策略,作用于单个可复制属性的更新通知机制。
- FastArray 是一个专用容器,作用于整个动态数组的同步和数据管理。
下面是详细的对比分析:
1. 核心定位与解决的问题
| 特性 | PushModel | FastArray |
|---|---|---|
| 本质 | 一种属性复制(Replication)的优化策略或模式。 | 一个为网络同步设计的专用数据结构(TArray的替代品)。 |
| 解决的核心问题 | 避免服务器每帧轮询检查所有属性是否变化(“脏”检查)带来的CPU开销。 | 避免在同步大型动态数组时,因微小改动就序列化和传输整个数组带来的带宽和CPU开销。 |
| 优化目标 | 减少服务器端的CPU计算开销(减少不必要的检查)。 | 减少网络带宽占用和序列化开销(只发送增量变化)。 |
2. 工作原理与使用方式
| 特性 | PushModel | FastArray |
|---|---|---|
| 工作原理 | 从“轮询检查”改为“事件驱动”。开发者需在属性值改变时,手动调用 MarkPropertyDirty()来通知网络系统此属性需要复制。 |
基于“差分复制”。容器内部跟踪每个元素(FFastArraySerializerItem)的ReplicationKey,在同步时只打包新增、修改或删除的元素。 |
| 数据结构 | 不引入新的数据结构,它是对现有 UPROPERTY(Replicated)属性的使用方式约束。 |
引入了新的数据结构:必须从 FFastArraySerializerItem派生元素,从 FFastArraySerializer派生容器。 |
| 使用复杂度 | 中。需要开发者改变习惯,记住在属性修改后手动标记,否则复制会失效。 | 高。需要定义新的结构体,实现特定的回调函数(如 PostReplicatedAdd),并理解其同步语义(如不保证顺序)。 |
3. 应用场景
| 特性 | PushModel | FastArray |
|---|---|---|
| 典型场景 | 适用于任何低频更新的可复制属性。例如:角色的金币数、任务状态、建筑的生命值。 | 专为频繁变化的动态列表设计。例如:玩家的技能列表(ActivatableAbilities)、激活的效果列表(ActiveGameplayEffects)、背包物品栏。 |
| 在GAS中的体现 | 可以用于优化GAS中任何标量属性的同步,但文档未明确说明GAS核心组件是否强制使用。 | GAS的核心组件重度依赖FastArray。例如 AbilitySystemComponent中的 ActivatableAbilities(存储GameplayAbilitySpec)和 ActiveGameplayEffects都是FastArray。 |
4. 网络行为
| 特性 | PushModel | FastArray |
|---|---|---|
| 同步粒度 | 属性级。标记哪个属性,就同步哪个属性。 | 元素级。识别到哪个数组项(Item)变化(增、删、改),就同步哪个项。 |
| 客户端回调 | 无。依赖标准的 RepNotify函数(即 ReplicatedUsing指定的函数)。 |
有。提供 PostReplicatedAdd, PostReplicatedChange, PreReplicatedRemove三个精确的回调,方便客户端响应特定元素的变化。 |
关系与协作
它们不是互斥的,而是可以协同工作:
- 你可以在一个
FFastArraySerializerItem(FastArray的元素)内部,对其多个属性使用 PushModel 策略进行优化。 - 例如,一个背包物品Item有“数量”、“耐久度”等属性。当数量变化时,FastArray负责将这个Item的“变化”同步到客户端;而在这个Item内部,如果使用了PushModel,则能优化服务器判断这些属性是否“变脏”的过程。
总结对比表
| 维度 | PushModel | FastArray |
|---|---|---|
| 抽象层次 | 设计模式/优化策略 | 数据结构/容器 |
| 作用对象 | 单个UProperty | 整个TArray |
| 核心机制 | 手动标记脏属性(事件驱动) | 自动追踪元素变化(差分同步) |
| 主要收益 | 节省服务器CPU(免轮询) | 节省网络带宽和序列化成本(免全量) |
| 使用难度 | 中等(需记得标记) | 高(需理解整套机制) |
| 典型用途 | 优化低频变化的属性 | 同步高频变化的动态列表 |
简单比喻:
- PushModel 就像从“每天检查所有仓库的门锁(轮询)”改为“只有保安收到门被动的报告才去检查(事件驱动)”。
- FastArray 就像同步一份员工花名册,不再每天重新打印全员名单,而是只打印“今天新入职、离职或信息有变动的员工”的条目。
因此,选择使用哪一个取决于你要优化的具体问题:是想优化大量分散属性的检查开销(用PushModel),还是想优化一个庞大列表的同步效率(用FastArray)。在GAS这类列表数据密集的系统中,FastArray是基石;而在整个游戏项目的网络优化中,PushModel则是应被广泛采用的通用最佳实践。
FastArray会有脏标记吗
是的,FastArray 有自己的一套脏标记(Dirty Marking)机制,这是其实现高效差分同步(Delta Replication)的核心。与 PushModel 优化通用属性不同,FastArray 的脏标记是专门为其数组结构设计的内部系统。
其工作原理如下:
1. 脏标记的核心:ReplicationKey
FastArray 的脏标记不叫 “Dirty Flag”,而称为 **ReplicationKey**(复制键)。它有两个层级:
- 元素级 ReplicationKey:每个
FFastArraySerializerItem都有一个ReplicationKey,用于追踪该元素自创建以来的修改次数。每次修改元素后,你需要调用MarkItemDirty(),这个函数内部会增加该元素的ReplicationKey。 - 数组级 ReplicationKey:
FFastArraySerializer容器本身有一个ArrayReplicationKey,用于追踪整个数组结构的变化(主要指元素的增加或删除)。当数组结构改变时,你需要调用MarkArrayDirty()来增加ArrayReplicationKey。
2. 脏标记如何驱动同步
服务器在准备复制数据时,会比较当前状态与上一次成功复制时的状态:
- **比较
ArrayReplicationKey**:如果不一致,说明数组结构(元素数量、顺序)有变,需要同步全量的结构信息。 - **逐个比较元素的
ReplicationKey**:将每个元素的当前ReplicationKey与上次复制时记录的ReplicationKey对比。如果某个元素的键值增加了,说明该元素内容有更新,需要将这个元素的数据打包进网络包。
3. 必须“手动”标记
和 PushModel 类似,FastArray 要求开发者显式调用标记函数来通知系统哪些地方发生了变化。这是与普通 TArray自动复制最根本的区别。
- 修改元素内容后:必须调用
MarkItemDirty(Item)。 - 添加或删除元素后:必须调用
MarkArrayDirty()。
与 PushModel 的“脏标记”对比
| 特性 | FastArray 的脏标记 (ReplicationKey) | PushModel 的脏标记 |
|---|---|---|
| 作用对象 | 数组中的元素(FFastArraySerializerItem) |
普通的属性(UPROPERTY) |
| 标记方式 | 调用容器的 MarkItemDirty()或 MarkArrayDirty()方法。 |
调用 MARK_PROPERTY_DIRTY()宏或 MarkPropertyDirty()函数。 |
| 标记目标 | 标记的是具体的某个数组项或整个数组结构。 | 标记的是某个对象实例的某个属性。 |
| 优化目标 | 实现数组的差分同步,只同步变化的元素,节省带宽。 | 避免对所有属性的轮询检查,节省CPU。 |
简单总结:FastArray 不仅“有”脏标记,而且这套标记机制(基于 ReplicationKey)是其实现高效、精确的差分网络同步的基石。它要求开发者在修改数组或元素后手动触发标记,以此驱动网络系统只打包和发送变化的部分数据。

