文章目录
- 使用示例
- 原理说明
- 修改操作
- 序列化保存部分
- 序列化读取部分
使用示例
// 结构体继承FFastArraySerializerItem
USTRUCT()
struct FXXX: public FFastArraySerializerItem
{GENERATED_USTRUCT_BODY()UPROPERTY()int32 XXX;// 客户端同步回调void PreReplicatedRemove();void PostReplicatedAdd();void PostReplicatedChange();
};// 数组包成一个类,继承FFastArraySerializerItem
USTRUCT()
struct FXXXContainer: public FFastArraySerializer
{GENERATED_USTRUCT_BODY()UPROPERTY()TArray<FXXX> Items;bool NetDeltaSerialize(FNetDeltaSerializeInfo & DeltaParms){return FFastArraySerializer::FastArrayDeltaSerialize<FXXX, FXXXContainer>(Items, DeltaParms, *this);}template< typename Type, typename SerializerType >bool ShouldWriteFastArrayItem(const Type& Item, const bool bIsWritingOnClient){// Type is FXXX, 定制是否需要同步...if (bIsWritingOnClient){return Item.ReplicationID != INDEX_NONE;}return true;}void AddItem(FXX XX){FXX& Item = Items.Add_GetRef(MoveTemp(XX));// 增加或修改元素后,需要对相应的元素手动 MarkDirtyMarkItemDirty(Item);}void RemoveItem(const FXX& XX){for(int32 Idx = 0; Idx < Items.Num(); ++Idx){if(Items[Idx] == XX){Items.RemoveAt(Idx);// 删除后,需要手动 MarkArrayDirtyMarkArrayDirty();return}}}
};template<>
struct TStructOpsTypeTraits<FXXXContainer> : public TStructOpsTypeTraitsBase2<FXXXContainer>
{enum{WithNetDeltaSerializer = true,};
};
原理说明
可以看下FastArraySerializer.h内的说明,大致意思为:
- 类似数组和结构体这种不定内存的结构,需要使用 NetDeltaSerialize 进行差异序列化
- 结构体的默认的差异序列化就是发送不用的属性,也就是Generic Delta Replication
- 开发者可以通过将WithNetDeltaSerializer 改为 true,定制自己的 NetDeltaSerialize
- 对于比较通用的TArray,也提供了 Fast TArray Replication 的方式
对于 FFastArraySerializer,ArrayReplicationKey 用于判定整个数组内是否存在变化(ConditionalCreateNewDeltaState内判断)
对于 FFastArraySerializerItem,ReplicationID 为该Item的标识,ReplicationKey 用于判定是否有修改。
修改操作
void MarkItemDirty(FFastArraySerializerItem & Item)if (Item.ReplicationID == INDEX_NONE)Item.ReplicationID = ++IDCounter;// 标记Item有修改Item.ReplicationKey++;MarkArrayDirty();void MarkArrayDirty()IncrementArrayReplicationKey();// 标记数组有修改ArrayReplicationKey++;
序列化保存部分
template<typename Type, typename SerializerType >
bool FFastArraySerializer::FastArrayDeltaSerialize(TArray<Type> &Items, FNetDeltaSerializeInfo& Parms, SerializerType& ArraySerializer)// if (Parms.Writer)TMap<int32, int32> * OldMap = nullptr;int32 BaseReplicationKey = INDEX_NONE;if (Parms.OldState)OldMap = &((FNetFastTArrayBaseState*)Parms.OldState)->IDToCLMap;// 上次同步的序号BaseReplicationKey = ((FNetFastTArrayBaseState*)Parms.OldState)->ArrayReplicationKey;// 判断是否需要同步if (Parms.OldState)if (!Helper.ConditionalCreateNewDeltaState(*OldMap, BaseReplicationKey))if (ArraySerializer.ArrayReplicationKey == BaseReplicationKey)// 没有变化时使用回旧的Stateif (Parms.OldState)*Parms.NewState = Parms.OldState->AsShared();return false;FNetFastTArrayBaseState * NewState = new FNetFastTArrayBaseState();TMap<int32, int32>& NewMap = NewState->IDToCLMap;NewState->ArrayReplicationKey = ArraySerializer.ArrayReplicationKey;if (Parms.bIsInitializingBaseFromDefault)// CDO的处理Helper.BuildChangedAndDeletedBuffersFromDefault(NewMap, ChangedElements);else:Helper.BuildChangedAndDeletedBuffers(NewMap, OldMap, ChangedElements, Header.DeletedIndices);NewState->ArrayReplicationKey = ArraySerializer.ArrayReplicationKey;Header.NumChanged = ChangedElements.Num();Helper.WriteDeltaHeader(Header);for (auto It = ChangedElements.CreateIterator(); It; ++It)void* ThisElement = &Items[It->Idx];uint32 ID = It->ID;// 写入变化Item的IDWriter << ID;Parms.Struct = InnerStruct;Parms.Data = ThisElement;// 写入变化的数据Parms.NetSerializeCB->NetSerializeStruct(Parms);// 直接序列化目标if (EnumHasAnyFlags(CachedRequestState.Struct->StructFlags, STRUCT_NetSerializeNative))UScriptStruct::ICppStructOps* CppStructOps = CachedRequestState.Struct->GetCppStructOps();if (!CppStructOps->NetSerialize(Ar, Params.Map, bSuccess, Params.Data)elseCachedRequestState.RepLayout->SerializePropertiesForStruct(Params.Struct, Ar, Params.Map, Params.Data, Params.bOutHasMoreUnmapped, Params.Object);
处理差异:
template<typename Type, typename SerializerType>
void FFastArraySerializer::TFastArraySerializeHelper<Type, SerializerType>::BuildChangedAndDeletedBuffers(TMap<int32, int32>& NewIDToKeyMap,const TMap<int32, int32>* OldIDToKeyMap,TArray<FFastArraySerializer_FastArrayDeltaSerialize_FIdxIDPair, TInlineAllocator<8>>& ChangedElements,TArray<int32, TInlineAllocator<8>>& DeletedElements)// 遍历所有Item,得出还需要保持写入的数量(Remove了,或者ShouldWriteFastArrayItem返回false了)const int32 NumConsideredItems = CalcNumItemsForConsideration();// 与原来的数量的差就是需要删除的数量int32 DeleteCount = (OldIDToKeyMap ? OldIDToKeyMap->Num() : 0) - NumConsideredItems;for (int32 i = 0; i < Items.Num(); ++i)Type& Item = Items[i];// 新的需要写入的表NewIDToKeyMap.Add(Item.ReplicationID, Item.ReplicationKey);// 读出之前的ArrayReplicationKeyconst int32* OldValuePtr = OldIDToKeyMap ? OldIDToKeyMap->Find(Item.ReplicationID) : NULL;if (OldValuePtr)// ArrayReplicationKey相同表示当轮并没有修改if (*OldValuePtr == Item.ReplicationKey)continueelseChangedElements.Add(FFastArraySerializer_FastArrayDeltaSerialize_FIdxIDPair(i, Item.ReplicationID));else// 新加的也算到Change内ChangedElements.Add(FFastArraySerializer_FastArrayDeltaSerialize_FIdxIDPair(i, Item.ReplicationID));// 因为新加了,所以原来的删除应该更多一个++DeleteCount;// 再次遍历找出删除的ItemIDif (DeleteCount > 0 && OldIDToKeyMap)for (auto It = OldIDToKeyMap->CreateConstIterator(); It; ++It)if (!NewIDToKeyMap.Contains(It.Key()))DeletedElements.Add(It.Key());if (--DeleteCount <= 0)break;
序列化读取部分
template<typename Type, typename SerializerType >
bool FFastArraySerializer::FastArrayDeltaSerialize(TArray<Type> &Items, FNetDeltaSerializeInfo& Parms, SerializerType& ArraySerializer)// if (Parms.Reader)FFastArraySerializerHeader Header;for(int32 i = 0; i < Header.NumChanged; ++i)Reader << ElementID;// 读取ReplicationID对应的下标int32* ElementIndexPtr = ArraySerializer.ItemMap.Find(ElementID);if (!ElementIndexPtr)// 新建项ThisElement = &Items.AddDefaulted_GetRef();ThisElement->ReplicationID = ElementID;ElementIndex = Items.Num()-1;ArraySerializer.ItemMap.Add(ElementID, ElementIndex);else// 修改项ElementIndex = *ElementIndexPtr;ThisElement = &Items[ElementIndex];ChangedIndices.Add(ElementIndex);// 标记是这次同步的修改ThisElement->MostRecentArrayReplicationKey = Header.ArrayReplicationKey;// 客户端修改标记ThisElement->ReplicationKey++;// 序列化目标Parms.NetSerializeCB->NetSerializeStruct(Parms);// 删除并触发各种Item和Contrainer的回调Helper.template PostReceiveCleanup<>(Header, ChangedIndices, AddedIndices, ArraySerializer.GuidReferencesMap);Helper.CallPostReplicatedReceiveOrNot(OldNumItems);
客户端删除并触发各种Item的回调
template<typename Type, typename SerializerType>
template<typename GuidMapType>
void FFastArraySerializer::TFastArraySerializeHelper<Type, SerializerType>::PostReceiveCleanup(FFastArraySerializerHeader& Header,TArray<int32, TInlineAllocator<8>>& ChangedIndices,TArray<int32, TInlineAllocator<8>>& AddedIndices,GuidMapType& GuidMap)// 在ReadDeltaHeader内已经转化为下标了for (int32 idx : Header.DeletedIndices)if (Items.IsValidIndex(idx))Items[idx].PreReplicatedRemove(ArraySerializer);ArraySerializer.PreReplicatedRemove(Header.DeletedIndices, FinalSize);for (int32 idx : AddedIndices)Items[idx].PostReplicatedAdd(ArraySerializer);ArraySerializer.PostReplicatedAdd(AddedIndices, FinalSize);for (int32 idx : ChangedIndices)Items[idx].PostReplicatedChange(ArraySerializer);ArraySerializer.PostReplicatedChange(ChangedIndices, FinalSize);if (Header.DeletedIndices.Num() > 0)Header.DeletedIndices.Sort();for (int32 i = Header.DeletedIndices.Num() - 1; i >= 0; --i)Items.RemoveAtSwap(DeleteIndex, 1, false);// 下标变了,需要之后重新构建,参考ConditionalRebuildItemMap// Clear the map now that the indices are all shifted around. This kind of sucks, we could use slightly better data structures here I think.// This will force the ItemMap to be rebuilt for the current Items arrayArraySerializer.ItemMap.Empty();