Hi!请登陆

UE4 FBitArray怎么无法通过 FastArray和Push Model来单独同步

ProLightsfx 2025-3-3 83 3/3

零、前言

技术博客和公众号也是停更了快3个月了。 这次长时间停更,最早的时候是项目突然开始赶进度,然后加大了加班力度。后来项目又被突然叫停,合并到了其它项目,所以又在新项目组做一些挺费时间和精力的事情。

这种情况其实在互联网行业很常见的,大家都懂的。 (身体、时间、精力)也是最近才缓过来,所以想着整理下最近做的技术和踩的坑,总结和记录一下。

一、导语

如题这次的文章与UE4有关,对于把游戏服务器主体逻辑放到DS上还是GS上的思考,想了下还是打算再研究研究,之后再出专门的文章来写了。说实在的,感触挺大的。

这里主要总结和记录下笔者在使用FastArray、Push Model、FBitArray进行网络同步的时候踩的坑。文章内容比较偏新手,老司机可以自行return。

二、背景

1. FastArray的概念介绍

在 Unreal Engine 4 (UE4) 中,FastArray 是一种高效的数据结构,通常用于需要频繁更新和同步的数组数据。FastArray 是 FFastArraySerializer 的子类,专门为网络同步优化,适用于需要高性能同步的场景,比如库存系统、状态效果列表等。 FastArray 的核心优势在于它能够高效地同步数组的增删改操作,而不是每次都同步整个数组。

2. Push Model的概念介绍

2.1 Push Model

Push Model是一种优化网络同步的机制,它允许开发者显式地标记哪些属性需要同步,而不是依赖引擎自动检测属性的变化。这种机制可以显著减少不必要的网络流量,特别是在属性变化不频繁的情况下。 Push Model 的核心思想是:只有被标记为“脏”(Dirty)的属性才会被同步到客户端。这样可以避免引擎每帧检查所有属性的变化,从而提高性能。

2.2 Push Model 的优势

  • 减少网络流量: 只有被标记为“脏”的属性才会被同步,避免了不必要的属性检查。 特别适合属性变化不频繁的场景。
  • 提高性能: 减少了引擎每帧检查属性变化的开销。
  • 显式控制: 开发者可以精确控制哪些属性需要同步,避免意外的网络流量。

2.3 注意事项

  • Push Model 的适用范围:

Push Model 适用于属性变化不频繁的场景。如果属性每帧都在变化,Push Model 的优势可能不明显。

  • 手动标记“脏”状态:

开发者需要确保在属性变化时调用 MARK_PROPERTY_DIRTY,否则属性不会被同步。

  • 兼容性:

Push Model 是 UE4.23 引入的功能,确保引擎版本支持。

三、实践与踩坑

3.1 定义 FFastArraySerializerItem:

这是数组中的单个元素类型。需要继承 FFastArraySerializerItem 并定义所需数据结构。

USTRUCT()
struct FCoderMeow :public FFastArraySerializerItem
{
    GENERATED_BODY()
    
    // 其它结构
    UPROPERTY()
    int32 OtherData;
    
    // 某种proto里定义的枚举
    UPROPERTY()
    pb::EnSomeType SomeType;
    
    // TBitArray
    TBitArray<> BitArrayData;
}

3.2 定义 FFastArraySerializer:

这是数组容器类型。需要继承 FFastArraySerializer 并定义自己的数组。

template <>
struct TStructOpsTypeTraits<
  FCoderMeow> :
public TStructOpsTypeTraitsBase2<FCoderMeow>
{
    enum
    {
        WithNetSerializer = true,
    };
};

USTRUCT()
struct FCoderMeowArray :public FFastArraySerializer
{
    GENERATED_BODY()

    UPROPERTY()
    TArray<FCoderMeow> CoderMeowArray;

    bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams)
    
{
        return FFastArraySerializer::FastArrayDeltaSerialize<
      FCoderMeow, FCoderMeowArray>(
      CoderMeowArray, DeltaParams, *this);
    }
};

protocol里定义的枚举类型,无法直接进行属性同步,这里需要手动转化成int32再同步。接受同步数据的时候也是一样手动把int32转回protocol枚举。

bool FCoderMeow::NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
    bOutSuccess = true;
    Ar << OtherData;

    // protocol里定义的枚举类型,无法直接进行属性同步,这里需要手动转化成int32再同步。接受同步数据的时候也是一样手动把int32转回protocol枚举
    if (Ar.IsLoading())
    {
        int32 Type;
        Ar << Type;
        SomeType = static_cast<pb::EnSomeType>(Type);
    }
    else
    {
        int32 Type = static_cast<int32>(SomeType);
        Ar << Type;
    }
    Ar << BitArrayData;

    returntrue;
}

3.3 在 Actor 或 Object 中使用 FastArray:

将 FCoderMeowArray 添加到自己的 Actor 或 Object 中,并确保它被标记为 Replicated。 并在GetLifetimeReplicatedProps中开启使用Push Model来进行网络同步。

UCLASS()
class ACoderMeowActor :public AActor
{
    GENERATED_BODY()

public:
    ACoderMeowActor();
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
    
    void MarkItemDirty(FCoderMeow& ACoderMeow);
    void MarkArrayDirty();

private:
    UPROPERTY(Replicated)
    FCoderMeowArray MyCoderMeows;
};

ACoderMeowActor::ACoderMeowActor()
{
    SetIsReplicatedByDefault(true);
}

void ACoderMeowActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    FDoRepLifetimeParams SharedParams1;
    SharedParams1.bIsPushBased = true;
    DOREPLIFETIME_WITH_PARAMS_FAST(ACoderMeowActor, MyCoderMeows, SharedParams1);
}

void ACoderMeowActor::MarkItemDirty(FCoderMeow& ACoderMeow)
{
    MyCoderMeows.MarkItemDirty(ACoderMeow);
    MARK_PROPERTY_DIRTY_FROM_NAME(ACoderMeowActor, MyCoderMeows, this);
}

void ACoderMeowActor::MarkArrayDirty()
{
    UnRecoverArray.MarkArrayDirty();
    MARK_PROPERTY_DIRTY_FROM_NAME(ACoderMeowActor, MyCoderMeows, this);
}

笔者之前使用的时候需要标记哪些Index已经被处理过,看到有个UE内置的TBitArray,想着比TArray 或 TArray<uint8>应当是更节约内存的,就掏出来用了。

实测,当往CoderMewArray数组里添加数据的时候,TBitArray<> BitArrayData是可以正常同步给客户端的。 但是仅修改BitArrayData内的值,然后MarkDirty,此时无法进行同步。 或许当时给TBitArray<> BitArrayData加UPROPERTY()的时候编译会报错就该意识到TBitArray不支持属性同步,只是当时下意识觉得既然是UE自带的那当然都能同步了。

至于其原因,查了下文档, 完全出乎意料,在UE4中的TBitArray 本身就是不直接支持属性同步(Replication)的。TBitArray 是一个用于高效存储布尔值的容器,主要用于本地数据处理。那只能换成TArray<bool> 或 TArray<uint8>来同步了。

四、总结

DS的属性同步机制,感觉对比之前用过的Gamesvr Client模式,从代码编写和维护方面,方便了不少。 Gamesvr Client模式则需要手动进行标脏,然后手动根据proto协议打包数据,走RPC同步给客户端,客户端收到数据之后自行解包,然后缓存到本地内存。 目前感觉主要少了个手动协议定义和打包,以及因为客户端和ds都使用该复制的对象,很多逻辑代码也可以直接复用。 不过UE代码编译起来是真的慢。

对于把游戏服务器主体逻辑放到DS上还是放到GS上,感觉各自有不少优缺点。之后的文章再总结了。

DS相关开发知识,笔者也是刚学没多久,还在学。以上哪里说的不对的话,欢迎评论指正。

关于

本博所有文章均为博主原创,未经许可不得转载。

https://www.prolightsfxjh.com/article/ue4-fastarray-pushmodel/

Thank you!

                                                                                                                                             ------from ProLightsfx

如果你对推荐系统、游戏开发、C++优化、程序员内功等感兴趣或者想参与讨论的话,欢迎关注笔者公众号.

UE4 FBitArray怎么无法通过 FastArray和Push Model来单独同步

 

 

- THE END -

ProLightsfx

3月04日10:00

最后修改:2025年3月4日
0

非特殊说明,本博所有文章均为博主原创,未经许可不得转载。

共有 1 条评论

  1. ProLightsfx博主

    补充:
    1.经前辈提醒,才明白FastArray与PushModel完全是两套同步机制,实测PushModel对于FastArray不生效。
    2.对于“Push Model是否真能减少网络流量”,我的理解是可以的,有些场景下,数据发生了变化但并不想立刻同步给客户端,然后积攒一定的时长,比如一分钟再标脏同步。这样还是减少了同步量的。
    3.“普通的TArray数组也不是每次都同步整个数组的”,但到FastArray的同步控制会比普通TArray更精准一些,针对大而复杂的数据同步性能更好些。