NOTE: The following setup has been superseded by “Native Gameplay Tags” in UE4.27 and beyond. New post coming soon.
Gameplay tags are a powerful UE4 feature that allow for high-level tagging and categorisation of concepts and types. This concept of hierarchical tagging can be very powerful, and Gameplay Tags themselves have fully-featured editor support and a great API for use in native code. Gameplay Tags are used extensively in Epic’s Gameplay Abilities System, but are a standalone feature that can be used outside of this to great effect. For designer-facing systems, they can make a great replacement for hardcoded FNames and Enums.
The backbone of FGameplayTag is the FName type. Tags are essentially FName’s that use “.” as a delimiter to denote heirachy and grouping. Unlike FName however, tags must be registered within a central dictionary in order to be valid and used. This dictionary is generated during the early stages of application runtime from serialized data – which can create some nuances when it comes to the most efficient way to refer to them in C++.
Because the dictionary is created at runtime, some common practices employed when using FName (such as static const definitions of FName’s) cannot be applied to Tags. Since Tags defined in config files can also be manipulated by other developers, you run the risk of these tags being changed without your knowledge, potentially breaking code that relies on them.
Typical Usage
The most common method I’ve seen when referring to Gameplay Tags in code is to use the static ‘RequestGameplayTag’ function. Note that the FName-based constructor for FGameplayTag is intentionally private, since the system requires you to lookup tags from the dictionary. If you don’t already have the tag as a variable, this is the only way you can get valid tags in C++.
const FGameplayTag MyTag = FGameplayTag::RequestGameplayTag(FName(“MyTag.MyTagChild”));
This is not entirely foolproof though, and by digging into this function we can see where problems may arise depending on both where, and how often, we are calling it.
Race Conditions
Since we are looking these tags up from a runtime-generated source, there is an inherent data race-condtion here. For example, if you are assigning a default value in a constructor, or validating a serialized value for instance – you run the risk of the dictionary not yet being valid and therefore not being able to retrieve your tags.
Lookup / Validation Cost
Since internally this function performs a lookup into the dictionary to find a valid tag, there is also an unfixed performance cost here. While TMap lookups are *very* fast, this cost is not negligible. As your project becomes ever more complex and designers go crazy with tagging – this cost can also increase over time. It’s not unreasonable to end up with hundreds or even thousands of tags in a project.
Thankfully the engine provides us with a way to prevent these issues. Defining tags natively allows us to garauntee tags are valid, while also having a fixed lookup cost. These tags are still available to designers and blueprint users too.
Native Gameplay Tags
In order to garauntee that tags required by C++ code will always be valid and exist, and to prevent runtime lookup costs – we can register ‘Native’ tags alongside the tags that other developers may create. We can also group these tags together into singletons, for convenient access and usage all across the codebase.
Tag Singleton Class
The following is an example of a singleton class, with some retrievable tags defined as members. I’ve found that it helps to group tags which are typically used together into their own singletons, to better convey their intended usage and meaning.
Note that while you could theoretically implement all the code in the header, I prefer to split into h/cpp to avoid including the substantial GameplayTagManager header all over the codebase.
.h
#pragma once #include "GameplayTagContainer.h" // Declarations class UGameplayTagsManager; /* Common Tags Container */ struct MYGAME_API FCommonTags : public FNoncopyable { public: static FORCEINLINE const FCommonTags& Get() { checkSlow(mSingleton); return *mSingleton; } // Common Tags FORCEINLINE const FGameplayTag& Success() const { return mGenericSuccess; } FORCEINLINE const FGameplayTag& Failure() const { return mGenericFailure; } private: // Allow module to initialize friend class FMyModule; static void Startup(UGameplayTagsManager& TagManager) { checkSlow(!mSingleton); mSingleton = new FCommonTags(TagManager); } static const FCommonTags* mSingleton; FCommonTags(UGameplayTagsManager& TagManager); // Common FGameplayTag mGenericSuccess; FGameplayTag mGenericFailure; };
.cpp
#include "CommonTags.h" #include "GameplayTagsManager.h" const FCommonTags* FCommonTags::mSingleton = nullptr; FCommonTags::FCommonTags(UGameplayTagsManager& TagManager) { // Common mGenericFailure = TagManager.AddNativeGameplayTag("Failed", TEXT("Generic Failure")); mGenericSuccess = TagManager.AddNativeGameplayTag("Success", TEXT("Generic Success")); };
Tag Registration
We now have a global singleton, with access protection against invalid tags. All we need to do now is register and create those singletons at the approriatte time, preferably as early as feasibly possible.
There are a couple of ways you can do this, but I prefer to do this from the modules’ startup function like so. Note that in order for this to work, your module must initialize before PostEngineInit is called (the ‘Default’ loading phase is fine for this).
FMyModule::StartupModule { // Bind to the register delegate so that we can register native gameplay tags. // Note: This *MUST* be done before PostEngineInit! UGameplayTagsManager::Get().OnLastChanceToAddNativeTags().AddRaw(this, &FMyModule::RegisterNativeGameplayTags); } void FMyModule::RegisterNativeGameplayTags() { UGameplayTagsManager& Manager = UGameplayTagsManager::Get(); // Initialize Tag singletons here. FCommonTags::Startup(Manager); FSomeOtherTags::Startup(Manager); // etc.. Manager.OnLastChanceToAddNativeTags().RemoveAll(this); }
Using these tags around the codebase is now both fixed-cost and safer.
const FGameplayTag SuccessTag = FCommonTags::Get().Success(); const FGameplayTag FailedTag = FCommonTags::Get().Failed();
Happy Coding!
Comments (1)