前言

研究PushModel与FastArray等

对于网络的容器的介绍

PushModel

PushModel 是 Unreal Engine 中用于优化网络复制性能的一种机制。它的核心思想是将网络更新的触发从“轮询检查”改为“事件驱动”,从而减少不必要的复制开销。

传统复制模式的问题

在传统的属性复制(Replication)中,Unreal 默认使用 “脏标记”(Dirty Flag) 机制:

  • 每个可复制的属性在值发生变化时会被标记为“脏”。
  • 每帧(或每个复制周期)服务器会检查所有对象的脏标记,将标记为脏的属性打包发送给客户端。
  • 这种轮询检查的方式在属性多、变化少的场景下会产生不必要的性能开销。

PushModel 的工作原理

PushModel 改变了这一流程:

  1. 主动标记:当某个可复制的属性值发生变化时,开发者需要显式调用 MarkPropertyDirty()MARK_PROPERTY_DIRTY()宏来通知网络系统:“这个属性需要复制”。
  2. 按需打包:网络系统只打包那些被显式标记为“脏”的属性,不再进行全量轮询检查。
  3. 自动清除:属性被成功复制后,其“脏”标记会被自动清除。

核心优势

  • 减少CPU开销:避免了每帧对所有属性进行脏检查的循环,特别适合属性数量多但变化频率低的场景(如大量AI角色、环境物体)。
  • 精确控制:开发者可以更精细地控制何时触发复制,避免不必要的网络流量。
  • 向后兼容:可以与传统的复制模式混合使用,逐步优化。

使用方法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 在属性声明处启用 PushModel
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;

// 2. 在修改属性的地方显式标记
void TakeDamage(float Amount)
{
Health -= Amount;
MARK_PROPERTY_DIRTY(UActorComponent, Health); // 通知系统此属性已变化
}

// 3. 确保在 GetLifetimeReplicatedProps 中正确设置
void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_WITH_PARAMS_FAST(UActorComponent, Health); // 使用 FAST 宏支持 PushModel
}

适用场景

  • 大量相似对象:如MMO中的NPC、射击游戏中的弹丸。
  • 低频更新属性:如角色的经验值、任务的进度、建筑物的耐久度。
  • 性能敏感项目:需要最大限度减少网络和CPU开销的在线游戏。

注意事项

  • 需要手动管理:开发者必须记得在属性变化时调用标记函数,否则会导致复制失败。
  • 不适合高频变化属性:对于每帧都变化的属性(如位置、旋转),传统模式可能更合适。
  • 调试复杂度:如果忘记标记,网络问题可能更难排查。

PushModel 是 Unreal Engine 4.25+ 引入的重要优化特性,特别适合大规模多人游戏或对象数量众多的场景,能够显著提升服务器的运行效率。

FastArray

FastArray 是 Unreal Engine 中专门为高效网络同步动态数组而设计的一套系统。它基于 FFastArraySerializerFFastArraySerializerItem这两个核心类,通过差分复制(Delta Replication) 机制,只同步发生变化的部分,从而显著减少网络带宽消耗和CPU开销。

核心设计原理

FastArray 的“快速”体现在其增量同步策略上:

  • 传统 TArray 复制:每次数组变化都可能需要序列化整个数组。
  • FastArray 复制:通过 ReplicationIDReplicationKey精确追踪每个元素的变化,只同步新增、修改或删除的元素。

关键组件

  1. FFastArraySerializerItem(数组元素)

    • 每个数组元素必须继承此类。
    • 包含两个关键标识符:
      • ReplicationID:元素的唯一标识(类似GUID)。
      • ReplicationKey:元素被修改的次数,用于检测变化。
  2. FFastArraySerializer(数组容器)

    • 管理整个数组的复制逻辑。
    • 维护 IDCounter(用于生成 ReplicationID)和 ArrayReplicationKey(数组整体修改次数)。
    • 提供 MarkItemDirty()MarkArrayDirty()等关键方法。

基本使用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 1. 定义Item结构体
USTRUCT()
struct FMyItem : public FFastArraySerializerItem
{
GENERATED_BODY()

UPROPERTY()
FName ItemName;

UPROPERTY()
int32 Count;

// 必须实现的回调函数
void PreReplicatedRemove(const FMyArray& Serializer);
void PostReplicatedAdd(const FMyArray& Serializer);
void PostReplicatedChange(const FMyArray& Serializer);
};

// 2. 定义数组容器
USTRUCT()
struct FMyArray : public FFastArraySerializer
{
GENERATED_BODY()

UPROPERTY()
TArray<FMyItem> Items;

// 添加元素
void AddItem(const FMyItem& NewItem)
{
Items.Add(NewItem);
MarkItemDirty(Items.Last()); // 标记新增元素为“脏”
}

// 修改元素
void ModifyItem(int32 Index)
{
Items[Index].Count++;
MarkItemDirty(Items[Index]); // 标记修改元素为“脏”
}

// 删除元素
void RemoveItem(int32 Index)
{
Items.RemoveAt(Index);
MarkArrayDirty(); // 删除元素需要标记整个数组
}
};

网络复制流程

  1. 服务器端修改:调用 MarkItemDirty()MarkArrayDirty()标记变化。
  2. 差分序列化:网络系统比较当前状态与上次复制状态,只打包变化的部分。
  3. 客户端反序列化:接收差分数据,应用变化,触发相应的回调函数(PostReplicatedAddPostReplicatedChangePreReplicatedRemove)。

主要特点与优势

  • 高效差分同步:只传输变化的数据,特别适合频繁更新的动态数组。
  • 精确变化追踪:通过 ReplicationKey 机制准确识别元素级别的变化。
  • 回调机制完善:提供三个关键回调函数,方便客户端响应变化。
  • 广泛引擎应用:Unreal 的 Gameplay Ability System(GAS)中大量使用,如 ActiveGameplayEffectsActivatableAbilities等。

重要注意事项

  1. 次序不保证:FastArray 不保证 服务端和客户端数组元素的顺序一致。
  2. 手动标记:必须显式调用 MarkItemDirty()MarkArrayDirty(),否则变化不会同步。
  3. 回调仅客户端PostReplicatedAdd等回调函数只在连接到服务器的客户端触发,服务器和单机模式不会调用。
  4. 适合场景:最适合元素数量多、变化频繁但每次变化比例小的数组(如背包物品、技能列表、状态效果等)。

实际应用场景

  • 背包/库存系统:物品的添加、删除、数量变化。
  • 技能/能力系统:GAS 中的 GameplayAbilitySpec 数组。
  • 状态效果系统:ActiveGameplayEffects 的同步。
  • 玩家列表/队伍管理:动态变化的玩家信息列表。

FastArray 是 Unreal 网络同步中处理动态数组的标准解决方案,虽然需要更多的手动管理,但能带来显著的性能提升,特别适合大规模多人游戏中的高频数据同步需求。

FastArray和TArray的区别

FastArray(特指 FFastArraySerializer)和 TArray 是 Unreal Engine 中两种用途和设计目标完全不同的数组容器。简单来说,TArray 是通用的内存容器,而 FastArray 是专为高效网络同步设计的网络容器

下面是它们的核心区别对比:

核心区别总览

维度 TArray FastArray (FFastArraySerializer)
本质 通用的、标准的内存动态数组容器。 专为网络复制(Replication) 优化的、支持差分同步的数组结构。
设计目标 提供高效的内存操作(添加、删除、访问)。 提供高效的网络序列化与同步,最小化带宽和CPU开销。
复制方式 全量复制。任何修改(即使只改一个元素)都可能导致整个数组被序列化并发送。 增量(差分)复制。只同步发生变化的部分(新增、修改、删除的元素)。
脏标记机制 无内置脏标记。依赖UE的属性复制系统自动检测整个数组的“脏”状态。 。基于 ReplicationKey的脏标记系统,需手动调用 MarkItemDirty()MarkArrayDirty()来触发同步。
客户端回调 仅支持标准的 RepNotify(整个数组变化时触发一次)。 提供精细的元素级回调PostReplicatedAddPostReplicatedChangePreReplicatedRemove
顺序保证 保证服务端与客户端的元素顺序完全一致。 不保证 服务端与客户端的元素顺序一致。顺序可能因网络包延迟或重排而不同。
使用复杂度 。声明即可用,与普通编程中的数组无异。 。需定义从 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:为了支持差分同步和元素追踪,每个元素需要额外的内存存储 ReplicationIDReplicationKey,容器本身也有额外状态。内存开销更大,访问速度略低于 TArray

5. 代码示例对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// --- TArray 用法(简单但低效的网络同步)---
UPROPERTY(Replicated)
TArray<FMyData> MyItems; // 网络复制时,任何改动都可能同步整个数组

// --- FastArray 用法(高效但复杂)---
// 1. 定义Item
USTRUCT()
struct FMyFastItem : public FFastArraySerializerItem
{
GENERATED_BODY()
UPROPERTY()
FMyData Data;

void PostReplicatedChange(const FMyFastArray& Serializer) { /* 响应变化 */ }
};

// 2. 定义Array
USTRUCT()
struct FMyFastArray : public FFastArraySerializer
{
GENERATED_BODY()
UPROPERTY()
TArray<FMyFastItem> Items;

void AddItem(const FMyData& NewData)
{
FMyFastItem& NewItem = Items.AddDefaulted_GetRef();
NewItem.Data = NewData;
MarkItemDirty(NewItem); // 必须手动标记!
}
};

使用建议

  • 使用 TArray 当:数据不需要网络同步,或数组很小且变化不频繁,或你不想引入额外复杂性。
  • 使用 FastArray 当:数据需要网络同步,且数组可能很大,或元素会频繁变化(尤其是部分变化),或你需要客户端对特定元素的变化做出精细响应。

在 Unreal 的 Gameplay Ability System (GAS) 中,所有需要网络同步的核心列表(如 ActivatableAbilitiesActiveGameplayEffects)都使用 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指定的函数)。 。提供 PostReplicatedAddPostReplicatedChangePreReplicatedRemove三个精确的回调,方便客户端响应特定元素的变化。

关系与协作

它们不是互斥的,而是可以协同工作

  • 你可以在一个 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
  • 数组级 ReplicationKeyFFastArraySerializer容器本身有一个 ArrayReplicationKey,用于追踪整个数组结构的变化(主要指元素的增加或删除)。当数组结构改变时,你需要调用 MarkArrayDirty()来增加 ArrayReplicationKey

2. 脏标记如何驱动同步

服务器在准备复制数据时,会比较当前状态与上一次成功复制时的状态:

  1. **比较 ArrayReplicationKey**:如果不一致,说明数组结构(元素数量、顺序)有变,需要同步全量的结构信息。
  2. **逐个比较元素的 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)是其实现高效、精确的差分网络同步的基石。它要求开发者在修改数组或元素后手动触发标记,以此驱动网络系统只打包和发送变化的部分数据。

参考

UE5 FastArray同步原理 - 知乎

UE5 GAS - 以Lyra为例 - 知乎

UE网络-PushModel详解 - 知乎