2.4 FastArray:背包系统的正确打开方式
为什么需要 FastArray
上一节说到 TArray<FMyStruct> 是「整表重发」。UE 内置了一个增量复制数组的方案:FFastArrayReplicator(FastArray)。它给数组里每个元素分配稳定的 ReplicationID,只同步「新增/修改/删除」的元素,并为每个元素提供独立的客户端回调。这是 UE 官方背包、装备系统(如 Fortnite)的底层方案。
基本结构
USTRUCT(BlueprintType)
struct FMyItem : public FFastArrayItem // ① 继承 FFastArrayItem
{
GENERATED_BODY()
UPROPERTY() int32 ItemId = 0;
UPROPERTY() int32 Count = 0;
// FFastArrayItem 提供 ReplicationID / ReplicationCounter 等内部字段
};
USTRUCT(BlueprintType)
struct FMyInventory : public FFastArrayReplicator // ② 容器结构体
{
GENERATED_BODY()
TArray<FMyItem> Items; // 必须叫这名字结构内部数组
};
Actor 里:
UPROPERTY(Replicated)
FMyInventory Inventory; // 复制的是容器结构体
void AMyCharacter::GetLifetimeReplicatedProps(...) const
{
DOREPLIFETIME(AMyCharacter, Inventory); // FastArray 内部处理增量
}
服务器端改动数组后,必须通知复制系统「结构脏了」
Inventory.MarkItemDirty(Items[i]); // ③ 单个元素变化
Inventory.MarkArrayDirty(); // ④ 整个数组结构变化(增删)
两套回调机制
FastArray 在客户端提供两级回调:
- 元素级:struct 里加
void PreReplicatedRemove(const TArrayView<int32>& DeletedIndices)/void PostReplicatedAdd(const TArrayView<int32>& AddedIndices)/void PostReplicatedChange(...)。客户端可以精确知道「这次同步删了 2 个元素、加了 1 个」,驱动单个 UI 槽位的增删动画。 - 容器级:Actor 上的 OnRep(通过
ReplicatedUsing或DOREPLIFETIME_CONDITION_NOTIFY)在容器整体变化时触发一次。
FastArray 不是银弹
使用前要清楚它的代价:
- 每个元素多出约 12 字节(ID + 计数器),小元素列表不一定划算。
- 只支持结构体数组(
TArray<FStruct>),不支持基础类型数组(TArray<int32>之类不适用)。 - 客户端到服务器的增量是单向的:FastArray 只做「服务器→客户端」的增量推流;客户端改库存还是要走 RPC(服务器改完再 MarkItemDirty)。
- 序列化版本兼容:元素结构体改动要注意版本兼容(上线后改 FMyItem 字段,老客户端可能解析错位)。
什么时候不用 FastArray
- 列表很小(< 10 个元素)且变化少 → 普通 TArray 足够,FastArray 只是复杂度。
- 客户端不需要细粒度槽位反馈 → 整表 + OnRep 也够。
- 列表纯客户端本地维护(如最近聊天记录的本地缓存)→ 根本不用复制。
与 GAS Attribute 的关系
GameplayAbilitySystem 的 Attribute(血量、攻击力那些 FGameplayAttributeData)底层正是 FastArray——这也是为什么 GAS 属性变化可以精确地、低带宽地同步到客户端,并提供 OnAttributeChanged 回调。理解了这一节,GAS 的属性同步机制也就懂了一大半(见本系列 GAS 实战一书)。
心得:FastArray 的引入时机——当你的「数组复制」开始出现在带宽热点 top 5,或客户端开始抱怨「捡东西时 UI 整体闪一下重排」,就是换 FastArray 的时候。提前引入则徒增复杂度。
