skip to main content

Strings and Other Things

Girl playing classic string game, creating shapes

Intro

Unreal has three different string classes which all have their own particular specialisations and caveats. Using the wrong one in the wrong place can cause you problems including hurting your performance, or causing your game submission to fail due to missed text localisation! In this post we’ll outline the types and highlight some key dos and don’ts.  

FString

The FString class is the one most C++ users who’ve not dealt with Unreal will find familiar. FString is essentially an analogue of std::string and comes with a variety of methods that mirror the functionality that the standard library version offers. Under the hood it is a TArray. The oddity in Unreal is that when constructing a dymanic string from a string literal you’re expected to use the Unreal supplied TEXT() macro. 

FString ExampleString = TEXT(“Hello World!”); 

All this macro does is prepend ‘L’ to your string literal to designate your string as being made up of wide characters rather than narrow (ANSI) characters. Historically Unreal typedefed TCHAR to be wide or narrow depending on the platform, and the TEXT() macro was also defined on a per platform basis in order to create string literals using the correct character set. Nowadays all platforms for which the source code is publicly accessible use wide characters. The macro persists however, and its systematic use means the Unreal default character set can relatively easily be switched (this could, for example, be used to support UTF8 characters for example if Epic so chose). 

Instantiating an FString requires a dynamic allocation, so creation, manipulation and copying of them will have a memory and performance cost. Passing by reference is your friend here. In addition, modern Unreal ships with FStringView (an analogue of std::string_view) which can help us as programmers legislate against copy operations when writing functions, by preventing the possibility of implicit FString construction.  

FText

The FText class is for localisable text. In practice that means if your string is ever intended to be seen by an end user, be it a player or an artist using the Unreal editor, then it should be created as an FText. These strings are usually created using LOCTEXT() or NSLOCTEXT() macros and require a namespace, a key and a string literal as parameters.

FText TextWithInlineNamespace(NSLOCTEXT(“Namespace”, “KeyOne”, “Hello”)); 

FText TextWithGlobalNamespace(LOCTEXT(“KeyTwo”, “World”)); 

The LOCTEXT() macro relies on a namespace being defined within your source file. This is done as follows:

#define LOCTEXT_NAMESPACE “MyNamespace” 

// File contents 

#undef LOCTEXT_NAMESPACE // “MyNamespace” 

The combination of namespace and key forms the unique identifier for your string literal. This is what allows your translated text to be inserted into the correct place in the program. As such a namespace and key pairing should not be duplicated with different string literals.  

Note that in blueprint (Format Text node) the namespace and key fields on a text node are hidden by default. If they are not filled in by the user the key will be automatically set, and the namespace left blank. Depending on your project it may well be sensible to fill in the namespace and key (this is handy if you ever need to move the text to a different file, or into code, as it means it’s easier to avoid destroying existing localisation!) 

Image of a Unreal Format Text blueprint node showing the automatically generated namespace and key parameters.
'Format Text' Blueprint Node With Automatic Namespace / Key
Image of a Unreal Format Text blueprint node showing manually entered namespace and key parameters.
'Format Text' Blueprint Node With Custom Namespace / Key

A powerful feature of FText is the ability to construct a dynamic display text. Say we want to report the how a player has been damaged we could do something like the following:

TArray<TPair<FText, FText>> PlayerDamageReasons 
{ 
    { LOCTEXT(“GelWoundMethod”, “Absorbed”),
        LOCTEXT(“GelName”, “a Gelatinous Octahedron”) }, 
    { LOCTEXT(“ChickenWoundMethod”, “Pecked”), 
        LOCTEXT(“ChickenName”, “a Large Angry Chicken”) }, 
    { LOCTEXT(“OrcWoundMethod”, “Stomped”), 
        LOCTEXT(“OrcName”, “3,872 Orcs”) } 
}; 

const int32 DamageTextIndex = GetDamageTextIndex(UMonster::StaticClass()); 

Const TPair<FText, FText>& DamageText = PlayerDamageReasons[DamageTextIndex]; 

FText DamageText = FText::Format(LOCTEXT(“PlayerDamage”, 
    “You have been {0} by {1}!”), DamageText.Key, DamageText.Value); 
 

ShowPlayerDamagePopup(DamageText); 

Great, so this’ll show the appropriate message to the player depending on what attacked them. However, we’ve got to think about what the localisation team will see. They’ll get an entry to translate that reads “You have been {0} by {1}!”, without any other context, which will make it kinda hard to understand! Even worse are FTexts consisting entirely of format codes:

FText::Format(LOCTEXT("ActionFormat", "{0} {1} {2}"), 
    PlayerName, PlayerAction, PlayerTarget);

This sort of thing is impossible to translate as there’s no context to what the final sentence should be in the original language; let alone how it should be reordered for languages with different sentence structures than the original. 

The way to deal with this properly is using named arguments. These take an FString identifier and an FText which will be displayed. Going back to our monster example:

// You can of course use an array of FFormatNamedArguments here, 
// or preferably load them from a data asset! 

FFormatNamedArguments PlayerDamageReason 
{ 
    // { TEXT(“ArgIdentifier”), LOCTEXT(“Key”, “TextToDisplay”) }, 
    { TEXT(“Wounded”), LOCTEXT(“GelWoundMethod”, “Absorbed”) }, 
    { TEXT(“Monster”), LOCTEXT(“GelName”, “a Gelatinous Octahedron”) } 
}; 

FText DamageText = FText::Format(LOCTEXT(“PlayerDamage”, 
    “You have been {Wounded} by {Monster}!”), PlayerDamageReason); 

The format arguments you provide will be matched to the appropriate text variable names. Now your localisation team will see the text “You have been {Wounded} by {Monster}!”. This gives them the context they’ll need to sensibly translate it. 

The equivalent functionality is also available in blueprint via the Format Text node. You simply type your format string in the box and add named arguments in braces. A correctly named node attachment point corresponding to each argument will then be automatically generated for you. 

Image of a Unreal Format Text blueprint node showing the use of wildcard format arguments..
'Format Text' Blueprint Node With Wildcard Format Arguments

FName 

This one is the sneaky one. FNames are designed to avoid making allocations, and to be cheap to compare. They’re good at this! Each unique string gets an entry in the global name table and any comparisons simply compare the name table index, which is considerably cheaper than a string comparison. Similarly, constructing an FName only has to make a string allocation for the very first instance of a name using the given string. All sounds pretty good right? Well, there is a caveat. The one big weakness of this type is that inline construction has a not insignificant performance cost. 

Take the example given in the Unreal documentation:

FRotator PelvisRotator = Mesh->MeshGetInstance(this)
    ->GetBoneRotation(FName(TEXT("pelvis")));

The function being called here takes an FName parameter, and we’re constructing that FName inline from a string literal. This FName constructer is decidedly non trivialAmong other operations it ends up having to do a large number of string comparison operations, searches the name table, has to acquire a thread lock, before potentially searching the name table again. Imagine we’re calling the GetBoneRotation() function regularly, and you’ll realise that this inline construction might not be the best idea. This is an issue we often see when performance profiling.  

Fortunately this perf bottleneck is easily avoided. FNames use the auto generated copy constructor and have only two (or three if case sensitivity is enabled) trivial members: 

private:

	/** Index into the Names array (used to find String portion of the string/number pair used for comparison) */
	FNameEntryId	ComparisonIndex;
#if WITH_CASE_PRESERVING_NAME
	/** Index into the Names array (used to find String portion of the string/number pair used for display) */
	FNameEntryId	DisplayIndex;
#endif // WITH_CASE_PRESERVING_NAME
	/** Number portion of the string/number pair (stored internally as 1 more than actual, so zero'd memory will be the default, no-instance case) */
	uint32			Number;

This means pre-defining your FNames in a similar fashion to the following snippet is an easy perf win. In this case on Tick() being called we copy construct an FName from our global instance at the cost of a few integer copies, and do not have to search the FName name table.

namespace SkeletonNames
{ 
    const FName PelvisName(TEXT("pelvis")); 
} 

void Tick(float DeltaSeconds)
{ 
    FRotator PelvisRotator = Mesh->MeshGetInstance(this)
        ->GetBoneRotation(SkeletonNames::PelvisName); 
} 

Conversion Between String Types 

Don’t do this! Well, you’ll probably have to at some point. But avoid as much as possible, especially in performance critical code. Some important things to note: Converting to an FText from another string type bypasses the localisation system. There are a couple of special cases where you’ll want this behaviour, but it should be avoided in general. Converting to an FName from another string type can result in loss of data as FNames are case insensitive outside of the editor. This conversion will also hit the more computationally expensive FName constructor overloads, as outlined above. 

Conclusion 

There we are then; the Unreal strings are untangled! If you’re using engine code, the choice of which string to use will already be made for you, so just make sure to correctly construct the type used in the Unreal API you’re working with. When writing your own code, hopefully this article will help you figure out which is most appropriate for your use case! 

Credit(s): Josef Gluyas(Coconut Lizard), Gareth Martin (Coconut Lizard)

Facebook Messenger Twitter Pinterest Whatsapp Email
Go to Top