Skip to main content

Generating Gameplay Tags in C++

This shows how to generate the .h and .cpp files implementing gameplay tags in C++ as part of the build process for an Unreal project.

Gameplay tags are usually implemented in C++ split across a .h and a .cpp file.

The .h file uses the UE_DECLARE_GAMEPLAY_TAG_EXTERN macro to declare the tags, and the .cpp file uses the UE_DEFINE_GAMEPLAY_TAG to define them.

To simplify keeping the files in sync it is easy to generate both files as part of the build process using a small python script.

.Target.cs files

Assuming the files related to code generation are in a directory call Generation under the main project directory.

In a Project.Target.cs file you can add these lines:

string ProjectName = Target.ProjectFile != null 
? ProjectName = Path.GetFileNameWithoutExtension( Target.ProjectFile.FullName )
: "Unknown";

if (HostPlatform == UnrealTargetPlatform.Win64)
{
PreBuildSteps.Add(
@"python $(ProjectDir)\Generation\Generate.py --tags=$(ProjectDir)\Generation\Tags.json --header=$(ProjectDir)\Source\"
+ ProjectName + @"\Public\GameplayTags.h --cpp=$(ProjectDir)\Source\"
+ ProjectName + @"\Private\GameplayTags.cpp" );
}

Generation

When the project is built it will run the python script Generate.py and pass it the names of the input and output files.

The Tags.json file contains a list of tags:

{
"tags" :
[
"Target",
"Target.Stats.Updated",
"Player",
"Player.Stats.Updated"
]
}

The generated .h file will contain this:

#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "NativeGameplayTags.h"
namespace GameplayTags
{
UE_DECLARE_GAMEPLAY_TAG_EXTERN( Target );
UE_DECLARE_GAMEPLAY_TAG_EXTERN( Target_Stats_Updated );
UE_DECLARE_GAMEPLAY_TAG_EXTERN( Player );
UE_DECLARE_GAMEPLAY_TAG_EXTERN( Player_Stats_Updated );
}

and the generated .cpp file will contain this:

#include "GameplayTags.h"
namespace GameplayTags {
UE_DEFINE_GAMEPLAY_TAG( Target, "Target" );
UE_DEFINE_GAMEPLAY_TAG( Target_Stats_Updated, "Target.Stats.Updated" );
UE_DEFINE_GAMEPLAY_TAG( Player, "Player" );
UE_DEFINE_GAMEPLAY_TAG( Player_Stats_Updated, "Player.Stats.Updated" );
}

If using Visual Studio, the first time the files are generated it is necessary to update the .sln file by using the "Generate Visual Studio project files" option from Windows Explorer. You only need to do this once.

Generate.py

This python script:

  • parses the arguments
  • checks if the Tags.json file is newer than the .h and .cpp file, if it is not, it does nothing
  • if Tags.json is newer, or the .h and .cpp file do not exist, it will generate the files
import json
import os
import sys
import argparse
import os.path

json_file_path:str = ""

def write_header_prefix(header_file):
header_file.writelines(
[
"#pragma once\n",
"#include \"CoreMinimal.h\"\n",
"#include \"GameplayTagContainer.h\"\n",
"#include \"NativeGameplayTags.h\"\n",
"namespace GameplayTags\n",
"{\n",
f"\t// GENERATED FROM {json_file_path} DO NOT EDIT\n"
])

def write_header_suffix(header_file):
header_file.write("}\n")

def write_header_body(header_file, data ):
for tag in data:
if tag.startswith("#"):
continue
# handle input with either _ or . separators
tag = tag.replace(".","_")
header_file.write(f"\tUE_DECLARE_GAMEPLAY_TAG_EXTERN( {tag} );\n" )

def write_source_prefix(source_file):
source_file.writelines(
[
"#include \"GameplayTags.h\"\n",
"namespace GameplayTags {\n",
f"\t// GENERATED FROM {json_file_path} DO NOT EDIT\n"
])

def write_source_suffix(source_file):
source_file.write("}\n")

def write_source_body(source_file, data ):
for tag in data:
if tag.startswith("#"):
continue

# UE_DEFINE_GAMEPLAY_TAG(Montage_Attack_LeftHand, "Montage.Attack.LeftHand");
tag = tag.replace(".","_")
expanded = tag.replace("_",".")
source_file.write(f"\tUE_DEFINE_GAMEPLAY_TAG( {tag}, \"{expanded}\" );\n")


parser = argparse.ArgumentParser()
parser.add_argument("--header")
parser.add_argument("--cpp")
parser.add_argument("--tags")
args = parser.parse_args()

header_file_path = args.header
source_file_path = args.cpp
json_file_path = args.tags

bail: bool = True

tag_file_time = os.path.getmtime( json_file_path )

if not os.path.exists( header_file_path ):
bail = False
elif not os.path.exists( source_file_path ):
bail = False
elif os.path.getmtime( header_file_path ) < tag_file_time:
bail = False
elif os.path.getmtime( source_file_path ) < tag_file_time:
bail = False
if bail:
sys.exit()

with open(json_file_path, 'r') as json_file:
# Load JSON data from the file
data = json.load(json_file)

# Accessing the parsed data
with open(header_file_path, 'w') as header_file:
write_header_prefix(header_file)
write_header_body(header_file, data['tags'] )
write_header_suffix(header_file)

with open(source_file_path, 'w') as source_file:
write_source_prefix(source_file)
write_source_body(source_file, data['tags'] )
write_source_suffix(source_file)