Gameplay Tag Stack Container
Updated for UE 5.8: the sample code has been fixed (missing forward declaration, mismatched member names, wrong function names in the usage example).
Here is a very simple container which gives you replicated Gameplay Tags as a stack count. Useful for things like weapon ammo, inventory item count, stat points, etc. It is built on FFastArraySerializer, so only changed entries replicate.
Here is the header file:
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "KaosGameplayTagStack.generated.h"
struct FKaosGameplayTagStackContainer;
/** A single gameplay tag and its stack count, replicated as an entry in the container. */
USTRUCT(BlueprintType)
struct FKaosGameplayTagStack : public FFastArraySerializerItem
{
GENERATED_BODY()
FKaosGameplayTagStack()
{}
FKaosGameplayTagStack(FGameplayTag InTag, int32 InCount)
: Tag(InTag)
, Count(InCount)
{}
private:
friend FKaosGameplayTagStackContainer;
/** The tag this stack is for. */
UPROPERTY()
FGameplayTag Tag;
/** Current stack count of the tag. */
UPROPERTY()
int32 Count = 0;
};
/** Replicated list of tag stacks with a local map for fast queries. Modify on the server only. */
USTRUCT(BlueprintType)
struct FKaosGameplayTagStackContainer : public FFastArraySerializer
{
GENERATED_BODY()
FKaosGameplayTagStackContainer()
{}
/** Adds Count to the tag's stack, creating the stack if needed. Server only. */
void AddStackCount(FGameplayTag Tag, int32 Count);
/** Removes Count from the tag's stack, removing the stack when it reaches zero. Server only. */
void RemoveStackCount(FGameplayTag Tag, int32 Count);
/** Returns true if there is a stack for the tag. */
bool HasTag(FGameplayTag Tag) const
{
return TagCountMap.Contains(Tag);
}
/** Returns the stack count for the tag, or 0 if there is none. */
int32 GetStackCount(FGameplayTag Tag) const
{
return TagCountMap.FindRef(Tag);
}
//~ FFastArraySerializer contract
void PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 FinalSize);
void PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 FinalSize);
void PostReplicatedChange(const TArrayView<int32>& ChangedIndices, int32 FinalSize);
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FastArrayDeltaSerialize<FKaosGameplayTagStack, FKaosGameplayTagStackContainer>(Stacks, DeltaParms, *this);
}
private:
/** Replicated list of gameplay tag stacks. Use TagCountMap for queries. */
UPROPERTY()
TArray<FKaosGameplayTagStack> Stacks;
/** Non-replicated lookup rebuilt from Stacks on both server and clients. */
TMap<FGameplayTag, int32> TagCountMap;
};
template<>
struct TStructOpsTypeTraits<FKaosGameplayTagStackContainer> : public TStructOpsTypeTraitsBase2<FKaosGameplayTagStackContainer>
{
enum
{
WithNetDeltaSerializer = true,
};
};
And the cpp file part:
void FKaosGameplayTagStackContainer::AddStackCount(FGameplayTag Tag, int32 Count)
{
if (!Tag.IsValid() || Count <= 0)
{
return;
}
for (FKaosGameplayTagStack& Stack : Stacks)
{
if (Stack.Tag == Tag)
{
Stack.Count += Count;
TagCountMap[Tag] = Stack.Count;
MarkItemDirty(Stack);
return;
}
}
FKaosGameplayTagStack& NewStack = Stacks.Emplace_GetRef(Tag, Count);
MarkItemDirty(NewStack);
TagCountMap.Add(Tag, Count);
}
void FKaosGameplayTagStackContainer::RemoveStackCount(FGameplayTag Tag, int32 Count)
{
if (!Tag.IsValid() || Count <= 0)
{
return;
}
for (auto It = Stacks.CreateIterator(); It; ++It)
{
FKaosGameplayTagStack& Stack = *It;
if (Stack.Tag == Tag)
{
if (Stack.Count <= Count)
{
It.RemoveCurrent();
TagCountMap.Remove(Tag);
MarkArrayDirty();
}
else
{
Stack.Count -= Count;
TagCountMap[Tag] = Stack.Count;
MarkItemDirty(Stack);
}
return;
}
}
}
void FKaosGameplayTagStackContainer::PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 FinalSize)
{
for (const int32 Index : RemovedIndices)
{
TagCountMap.Remove(Stacks[Index].Tag);
}
}
void FKaosGameplayTagStackContainer::PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 FinalSize)
{
for (const int32 Index : AddedIndices)
{
const FKaosGameplayTagStack& Stack = Stacks[Index];
TagCountMap.Add(Stack.Tag, Stack.Count);
}
}
void FKaosGameplayTagStackContainer::PostReplicatedChange(const TArrayView<int32>& ChangedIndices, int32 FinalSize)
{
for (const int32 Index : ChangedIndices)
{
const FKaosGameplayTagStack& Stack = Stacks[Index];
TagCountMap.Add(Stack.Tag, Stack.Count);
}
}
To use it, add a replicated property somewhere, for example on your player:
UPROPERTY(Replicated)
FKaosGameplayTagStackContainer GameplayStats;
Remember to register it in GetLifetimeReplicatedProps with DOREPLIFETIME(AMyCharacter, GameplayStats);.
And you can use it like so:
void AMyCharacter::AddStatTagCount(FGameplayTag Tag, int32 Count)
{
GameplayStats.AddStackCount(Tag, Count);
}
void AMyCharacter::RemoveStatTagCount(FGameplayTag Tag, int32 Count)
{
GameplayStats.RemoveStackCount(Tag, Count);
}
int32 AMyCharacter::GetStatTagStackCount(FGameplayTag Tag) const
{
return GameplayStats.GetStackCount(Tag);
}
bool AMyCharacter::HasStatTag(FGameplayTag Tag) const
{
return GameplayStats.HasTag(Tag);
}
Notes
- Only call
AddStackCountandRemoveStackCounton the server (authority). Clients get the changes, and theTagCountMaplookup, through thePostReplicated*callbacks. TagCountMapis not aUPROPERTYand is not replicated, which is why the callbacks exist. If you ever need to react to changes on the client, this is the place to broadcast a delegate.UE_NET_DECLARE_FASTARRAYis injected by UHT for Iris in 5.8, so no extra macro is needed for this struct.
Hopefully people find this helpful and useful.