TArray-backed Map

I am wondering how to implement a TArray-backed Map, or if someone has already made one, please share.
The purpose is entirely for replication purposes. I don’t mind my Map being less performant but it needs to replicate. I am tired of creating a TMap, TArray and writing the functions every time I need this functionality. I just want a class that I can reuse whenever.

I have spent a few hours trying to come up with a solution but I seem to always be limited by something.

The current implementation I have right now is a class inherited from TArray, but has a Map as a member (which is <KeyType, int32>, where int32 is the index within the array). Certain functions have been overridden to ensure the Map is always updated when the array is modified.

The issue with this is that I am unable to make it a UPROPERTY. I would have thought inheriting from TArray would have helped with that but apparently not.

I tried creating a USTRUCT that would hold a TArray and TMap - basically doing the same thing as the other implementation but with all the auto-generated functionality that USTRUCTS provide, but I can’t seem to use template/generic types on USTRUCTs.

Does anyone have any info on this? All I really want is a replicated array that can be accessed almost O(1) through a key.

I think I made some progress on the matter. I have come to the realisation that it’s just not possible to make a TArray-backed Map without some form of copy and paste.

My new implementation is a templated class (TInternalKeyedArray<KeyType, ValueType, PairType>), which a created USTRUCT will then use.
The templated class has all the functionality required as well as a TArray<PairType>* and TMap<KeyType, int32>*, but for serialization/UPROPERTY reasons, USTRUCTS are required to ‘wrap’ around it.
USTRUCTS cannot use templates, hence why a separate class/struct is needed.
USTRUCTS can also not inherit from non-USTRUCTS so that’s why composition is used.

Two USTRUCTS are required to use this system.

One is simply a KeyValue tuple. Example:


USTRUCT(BlueprintType)
struct FFNameUObjectPair
{
	GENERATED_BODY()
		
	UPROPERTY()
	FName Key;

	UPROPERTY()
	UObject* Value;

	FFNameUObjectPair()
	{
		Key = FName();
		Value = nullptr;
	}

	FFNameUObjectPair(FName NewKey, UObject* NewValue)
	{
		Key = NewKey;
		Value = NewValue;
	}
};

The other USTRUCT is the one responsible for ‘wrapping’ around the TInternalKeyedArray. Example:

USTRUCT(BlueprintType)
struct FNameUObjectMapArray
{
	GENERATED_BODY()

public:
	typedef FName KeyType;
	typedef UObject* ValueType;
	typedef FFNameUObjectPair PairType;

protected:
	TInternalKeyedArray<KeyType, ValueType, PairType> Internal;
	
	UPROPERTY()
	TArray<FFNameUObjectPair> BackingPairs;
	
	TMap<KeyType, int32> Translator;
	
public:
	FNameUObjectMapArray()
	{
		Internal = TInternalKeyedArray<KeyType, ValueType, PairType>(&BackingPairs, &Translator);
	}

public:
	/** Call this whenever the array is modified on clients (i.e. OnRep_KeyedArray).
	 * It ensures the Map responsible for allowing key-based access is always up-to-date. */
	void Clean()
	{
		Internal.Clean();
	}

	
public:
	FORCEINLINE int32 Add(const KeyType Key, ValueType&& Item)
	{
		return Internal.Add(Key, Item);
	}
    .../// etc

To make TArray-backed Map using a different key/value, you have to essentially copy and paste the FNameUObjectMapArray and change the typedefs to suit your needs.

It sucks that copy and paste is required but I think this is the best solution I can do. The typedefs definitely help.

If TTuple was serializable (or at least any form of template struct that can be used for key/value pairs), it would make life a bit easier since you wouldn’t need to create your own key/value pair for every type of combination you want to use.

Fortunately, my use-case right now is mainly <FName, float> so it’s not too bad.

1 Like