Approaches to replicate Unreal Engine's TMap

  ·  7 min read

Introduction #

If you ever attempt to put a Replicated specifier on your UPROPERTY with TMap, your IDE or compiler might yell at you like this:

TMap Replication Attempt
Replication maps are not supported?

Why can’t TMap be replicated by Unreal? #

Unreal’s built-in replication only supports certain types that the replication system knows how to serialize. This includes basic primitives, some containers like TArray, FVector, or custom structs containing UPROPERTY fields.

TMap is considered a dynamic container because of its non-fixed size, ordering, and hashing behavior. That makes replication much trickier to handle reliably - which is likely why Epic doesn’t provide built-in support for replicating TMap.

So… we can’t replicate TMap at all? #

Not exactly. While you can’t simply slap a Replicated specifier onto a TMap, there are still ways to replicate map-like data manually. One is relatively straightforward, while the other requires a bit more advanced serialization work.

The two approaches we’ll cover:

  1. Replicate a separate array of key-value structs alongside your map.
  2. Wrap your TMap inside a custom USTRUCT and implement NetSerialize() for full control over serialization.

Approach 1 - The separate array approach #

The idea here is simple: create a separate replicated TArray (which Unreal does support) containing your map’s key-value pairs as custom structs. Then rebuild the TMap on the client when the array replicates.

The server holds its own TMap, but updates the replicated array to reflect its contents. Clients receive the array and reconstruct their local copy of the map from that.


Step 1 - Define key-value struct #

Suppose you want to replicate a TMap<uint32, float>. First, define a USTRUCT holding the key-value pair:

 1USTRUCT()
 2struct FUnsignedIntFloatEntry
 3{
 4    GENERATED_BODY()
 5
 6    UPROPERTY()
 7    uint32 Key = 0;
 8
 9    UPROPERTY()
10    float Value = 0.f;
11};

Step 2 - Declare replicated array and local map #

Now define both your replicated array and the actual map you’ll use internally:

1UPROPERTY(ReplicatedUsing = OnRep_UnsignedIntFloatArrays)
2TArray<FUnsignedIntFloatEntry> UnsignedIntFloatArrays;
3
4UPROPERTY(Transient)
5TMap<uint32, float> UniqueIdToFloatMap;

Step 3 - Populate your map on the server #

Here’s an example where we grab random actors in the world and assign random float values to build the map:

 1if (!HasAuthority()) return;
 2
 3TArray<AActor*> ActorsInWorld;
 4UGameplayStatics::GetAllActorsOfClass(this, AActor::StaticClass(), ActorsInWorld);
 5
 6for (int I = 0; I < 10; ++I)
 7{
 8    const int32 RandomIdx = FMath::RandRange(0, ActorsInWorld.Num() - 1);
 9
10    const uint32 RandomChosenActorUniqueId = ActorsInWorld[RandomIdx]->GetUniqueID();
11    const float RandomFloat = FMath::FRand();
12    UniqueIdToFloatMap.Emplace(RandomChosenActorUniqueId, RandomFloat);  
13}
14
15const TArray<TPair<uint32, float>>& PairArray = UniqueIdToFloatMap.Array();
16for (const TPair<uint32, float>& Pair : PairArray)
17{
18    FUnsignedIntFloatEntry Entry;
19    Entry.Key = Pair.Key;
20    Entry.Value = Pair.Value;
21
22    UE_LOG(LogTemp, Warning, TEXT("%s - Key: %d, Value: %f"), *NetModeToString(), Pair.Key, Pair.Value);
23    UnsignedIntFloatArrays.Emplace(MoveTemp(Entry));
24}

The important part: only UnsignedIntFloatArrays is replicated. Once it updates, the client receives the notification.


Step 4 - Rebuild the map on client using RepNotify #

The client receives the updated array, and reconstructs its own local map:

1UniqueIdToFloatMap.Reset();
2
3for (const FUnsignedIntFloatEntry& Entry : UnsignedIntFloatArrays)
4{
5    UE_LOG(LogTemp, Warning, TEXT("%s - Key: %d, Value: %f"), *NetModeToString(), Entry.Key, Entry.Value);
6    UniqueIdToFloatMap.Emplace(Entry.Key, Entry.Value);
7}

And that’s it - you’ve successfully replicated a TMap manually.

Warning LogTemp AddMapEntry
Warning LogTemp ListenServer - Key: 141843, Value: 0.205267
Warning LogTemp ListenServer - Key: 102850, Value: 0.510819
Warning LogTemp ListenServer - Key: 141885, Value: 0.806024
Warning LogTemp ListenServer - Key: 141881, Value: 0.757683
Warning LogTemp ListenServer - Key: 102854, Value: 0.755730
Warning LogTemp ListenServer - Key: 103198, Value: 0.526231
Warning LogTemp ListenServer - Key: 141865, Value: 0.435102
Warning LogTemp ListenServer - Key: 141839, Value: 0.839351
Warning LogTemp OnRep_UnsignedIntFloatArrays
Warning LogTemp Client - Key: 141843, Value: 0.205267
Warning LogTemp Client - Key: 102850, Value: 0.510819
Warning LogTemp Client - Key: 141885, Value: 0.806024
Warning LogTemp Client - Key: 141881, Value: 0.757683
Warning LogTemp Client - Key: 102854, Value: 0.755730
Warning LogTemp Client - Key: 103198, Value: 0.526231
Warning LogTemp Client - Key: 141865, Value: 0.435102
Warning LogTemp Client - Key: 141839, Value: 0.839351

Approach 2 - Custom NetSerialize #

This approach takes a bit more work, but offers far more flexibility and cleaner code.

The idea: wrap your TMap inside a custom USTRUCT, and override Unreal’s NetSerialize() function to manually define how it should serialize across the network.


Step 1 - Declare your wrapper struct #

We’ll wrap our TMap and disable default replication on it:

1USTRUCT()
2struct FUnsignedIntFloatMapWrapper
3{
4    GENERATED_BODY()
5
6    UPROPERTY(NotReplicated)
7    TMap<uint32, float> WrappedMap;
8};

Note: if you forget NotReplicated, Unreal’s header tool will still complain about unsupported types!

Unsupported TMap in Struct


Step 2 - Tell Unreal to expect NetSerialize() #

We now define a type trait to let Unreal know our struct implements custom serialization:

1template <>
2struct TStructOpsTypeTraits<FUnsignedIntFloatMapWrapper> : TStructOpsTypeTraitsBase2<FUnsignedIntFloatMapWrapper>
3{
4    enum { WithNetSerializer = true };
5};

You may see your IDE warn you that NetSerialize() is not implemented yet - that’s expected.

Missing NetSerialize() type trait
Which leads us to…


Step 3 - Implement NetSerialize() #

Let’s quickly understand what FArchive is.

Unreal uses FArchive as a low-level stream abstraction for many systems - including replication, asset cooking, file saves, etc.

Within NetSerialize(), we’ll:

  • Send the map’s length first.
  • Serialize each key-value pair.
  • Handle both writing (IsSaving()) and reading (IsLoading()).

Here’s the full implementation:

 1bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
 2{
 3    int32 MapLength = WrappedMap.Num();
 4    Ar << MapLength;
 5
 6    if (Ar.IsLoading())
 7    {
 8        WrappedMap.Empty(MapLength);
 9        for (int I = 0; I < MapLength; ++I)
10        {
11            uint32 TempKey;
12            float TempValue;
13            Ar << TempKey;
14            Ar << TempValue;
15            WrappedMap.Emplace(TempKey, TempValue);
16        }
17    }
18    else if (Ar.IsSaving())
19    {
20        for (TPair<uint32, float>& Pair : WrappedMap)
21        {
22            Ar << Pair.Key;
23            Ar << Pair.Value;
24        }
25    }
26
27    bOutSuccess = true;
28    return true;
29}

Step 4 - Use your struct in RPCs #

Because it’s now fully serializable, you can easily pass it inside RPCs:

 1FUnsignedIntFloatMapWrapper WrappedUnsignedIntFloatMap;
 2
 3TArray<AActor*> ActorsInWorld;
 4UGameplayStatics::GetAllActorsOfClass(this, AActor::StaticClass(), ActorsInWorld);
 5
 6for (int I = 0; I < 10; ++I)
 7{
 8    const int32 RandomIdx = FMath::RandRange(0, ActorsInWorld.Num() - 1);
 9    const uint32 RandomChosenActorUniqueId = ActorsInWorld[RandomIdx]->GetUniqueID();
10    const float RandomFloat = FMath::FRand();
11    WrappedUnsignedIntFloatMap.WrappedMap.Emplace(RandomChosenActorUniqueId, RandomFloat);
12}
13
14Client_UpdateWrappedUnsignedIntFloatMap(WrappedUnsignedIntFloatMap);

And that’s it!

Warning LogTemp Server_UpdateWrappedUnsignedIntFloat_Implementation
Warning LogTemp ListenServer - Key: 102882, Value: 0.963286
Warning LogTemp ListenServer - Key: 103196, Value: 0.561937
Warning LogTemp ListenServer - Key: 141897, Value: 0.334910
Warning LogTemp ListenServer - Key: 103223, Value: 0.789727
Warning LogTemp ListenServer - Key: 141845, Value: 0.871975
Warning LogTemp ListenServer - Key: 102850, Value: 0.330668
Warning LogTemp ListenServer - Key: 103206, Value: 0.947081
Warning LogTemp ListenServer - Key: 141879, Value: 0.844203
Warning LogTemp ListenServer - Key: 141856, Value: 0.412580
Warning LogTemp Client_UpdateWrappedUnsignedIntFloatMap_Implementation
Warning LogTemp Client - Key: 102882, Value: 0.963286
Warning LogTemp Client - Key: 103196, Value: 0.561937
Warning LogTemp Client - Key: 141897, Value: 0.334910
Warning LogTemp Client - Key: 103223, Value: 0.789727
Warning LogTemp Client - Key: 141845, Value: 0.871975
Warning LogTemp Client - Key: 102850, Value: 0.330668
Warning LogTemp Client - Key: 103206, Value: 0.947081
Warning LogTemp Client - Key: 141879, Value: 0.844203
Warning LogTemp Client - Key: 141856, Value: 0.412580

Conclusion #

While Unreal doesn’t support replicating TMap out-of-the-box, you can still work around it depending on your needs:

  • The array approach is simple, easy to integrate with native replication, and perfectly valid if your map changes infrequently.
  • The NetSerialize() approach gives you much tighter control, cleaner encapsulation, and allows you to pass entire maps directly inside RPC calls.

Both approaches showcase how flexible Unreal’s replication system can be once you dig into custom serialization.

Whichever you choose depends on your use case - how large your map is, how often it changes, and how much control you need.

Thanks for reading! I hope this helps anyone looking to replicate TMaps or just exploring Unreal’s replication internals.

See you next time!