零、前言
技术博客和公众号也是停更了快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++优化、程序员内功等感兴趣或者想参与讨论的话,欢迎关注笔者公众号.
非特殊说明,本博所有文章均为博主原创,未经许可不得转载。
如经许可后转载,请注明出处:http://43.154.125.150/article/ue4-fastarray-pushmodel/
ProLightsfx博主
补充:
1.经前辈提醒,才明白FastArray与PushModel完全是两套同步机制,实测PushModel对于FastArray不生效。
2.对于“Push Model是否真能减少网络流量”,我的理解是可以的,有些场景下,数据发生了变化但并不想立刻同步给客户端,然后积攒一定的时长,比如一分钟再标脏同步。这样还是减少了同步量的。
3.“普通的TArray数组也不是每次都同步整个数组的”,但到FastArray的同步控制会比普通TArray更精准一些,针对大而复杂的数据同步性能更好些。