The Games DevProgramming and Game Development. Tips, Tricks and Tutorials.

Gameplay Tag Stack Container

· Updated Game FrameworkGameplay Ability System

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 AddStackCount and RemoveStackCount on the server (authority). Clients get the changes, and the TagCountMap lookup, through the PostReplicated* callbacks.
  • TagCountMap is not a UPROPERTY and 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_FASTARRAY is injected by UHT for Iris in 5.8, so no extra macro is needed for this struct.

Hopefully people find this helpful and useful.