Strings
Summary
A string is a sequence of characters treated as a single piece of text — a display name, a save file path, dialogue for an NPC. In most languages a game studio uses, strings are immutable: every operation that looks like it "changes" a string actually produces a brand-new one, which is the single fact that explains most string-related performance and correctness surprises.
Mental Model
A string is like a printed sentence on a card — a fixed row of characters,
the same contiguous-memory idea as an array ([[CON-0012]]), because a
string is effectively an array of characters under the hood. "Editing" a
printed card isn't possible; if you want a different sentence, you print a
brand-new card and throw the old one away. name = name + "!"; is exactly
that: it doesn't append ! onto the existing card, it prints an entirely
new card with the combined text and points name at the new one, leaving
the old card to be discarded (garbage collected, in managed languages).
That mental model — every "change" is actually a brand-new card — is why
building a long piece of text one small append at a time in a loop is much
more expensive than it looks: each += prints a whole new card, longer
than the last, and throws the previous one away.
Core Explanation
- Immutability. A
stringvalue, once created, never changes.str.ToUpper(),str.Substring(2),str + otherall return new strings; none of them modifystritself. This makes strings inherently thread-safe to read and eliminates a whole class of aliasing bugs a mutable buffer would have — but it means repeated concatenation allocates repeatedly. - Character array underneath. Indexing a string (
str[0]) reads a single character the same way indexing an array reads an element, because that's essentially what a string is — a length-prefixed (or null-terminated, in raw C) block of characters. - Comparison is by value, not identity.
"abc" == "abc"istruein C# even though they may be two distinct objects in memory — string equality compares character-by-character content by default, unlike the reference-identity default most other reference types use. - Encoding matters. C#'s
stringis UTF-16 internally; Unreal'sFStringis similar. Text containing non-ASCII characters (localization, player-entered names) behaves correctly because of this, but naive byte-length assumptions ("café".Lengthcounting bytes instead of characters) will not.
Syntax / Examples
C# (Unity)
public class PlayerNameTag : MonoBehaviour
{
public string PlayerName = "Hero";
void Start()
{
string display = PlayerName.ToUpper(); // new string
string greeting = $"Welcome, {display}!"; // interpolation
Debug.Log(greeting);
Debug.Log(PlayerName); // unchanged: "Hero"
}
}
C++ (Unreal)
FString PlayerName = TEXT("Hero");
void APlayerNameTag::BeginPlay()
{
Super::BeginPlay();
FString Display = PlayerName.ToUpper();
FString Greeting = FString::Printf(TEXT("Welcome, %s!"), *Display);
UE_LOG(LogTemp, Log, TEXT("%s"), *Greeting);
// PlayerName is unchanged: "Hero"
}
Unreal Blueprint
Format Text and Append nodes build new Text/String values from
existing ones, mirroring the C++/C# examples' immutability — there is no
"mutate in place" node, only nodes that consume inputs and produce a new
output string.
Practical Example
Building a debug HUD line that summarizes several stats should avoid repeated concatenation in a loop that runs every frame.
using System.Text;
public class DebugHud : MonoBehaviour
{
public void BuildSummary(string[] statNames, float[] statValues)
{
var builder = new StringBuilder();
for (int i = 0; i < statNames.Length; i++)
{
builder.Append(statNames[i]);
builder.Append(": ");
builder.Append(statValues[i]);
builder.Append('\n');
}
string summary = builder.ToString();
Debug.Log(summary);
}
}
StringBuilder maintains one mutable internal buffer and only produces a
single final string at the end — exactly one "new card" instead of one
per append, which is the fix for the immutability cost described above when
a string is being assembled in a loop.
Common Mistakes
- Concatenating strings in a loop with
+=. Each iteration allocates a brand-new string containing the whole accumulated text so far — anO(n²)total cost fornappends. UseStringBuilder(C#) orTStringBuilder(Unreal) for anything beyond a handful of concatenations, especially insideUpdate(). - Comparing strings with
==when culture/case sensitivity matters."Hero" == "hero"isfalse— case-sensitive, ordinal comparison by default in C#. Player-entered text (usernames, chat commands) usually needs an explicitStringComparison.OrdinalIgnoreCasecomparison. - Assuming
.Lengthcounts what a player perceives as "characters." Combining characters, emoji, and some non-Latin scripts can be represented by more than one UTF-16 code unit, so.Lengthcan diverge from the visual character count — relevant for input validation on player-entered names. - Allocating new strings every frame for UI text that rarely changes.
Rebuilding a formatted string in
Update()every frame when the underlying value only changes occasionally wastes allocations; only rebuild the string when the source value actually changes.
Best Practices
- Use
StringBuilder/TStringBuilderfor any text built incrementally in a loop, and reserve plain+/+=for one-off, fixed-count concatenations. - Cache formatted strings that don't change every frame instead of
reformatting them in
Update(). - Be explicit about
StringComparison(C#) when comparing player-entered or externally-sourced text, rather than relying on the default ordinal comparison always being what's intended. - Prefer string interpolation (
$"{a} {b}") over manual+concatenation for readability on one-off formatting — it compiles to efficient code and is far easier to review for typos.
Exercises
- Build the same 100-line summary string once with
+=in a loop and once withStringBuilder, and explain — in terms of the mental model above — why one allocates far more than the other even though both produce the same final text. - Write a case-insensitive username comparison function and explain which
StringComparisonvalue you chose and why. - Given
string s = "café";, predict whethers.Lengthequals 4, and verify. Research (or note as a follow-up) what encoding detail explains the result. - Refactor a hypothetical
Update()method that rebuilds a "Score: X" label every frame into one that only rebuilds the string when the score actually changes.
Related Concepts
- Arrays — a string is conceptually a character array; many string behaviors (indexing, length, immutability trade-offs) mirror array behaviors directly.
- Collections — building a string from many pieces is a specialized case
of accumulating many elements, which is why
StringBuilderexists as a purpose-built collection-like tool. - Exceptions — malformed or unexpected string input (parsing a number from player text, reading a corrupted save file) is a common source of exceptions that must be handled explicitly.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity/.NET —
System.StringandStringBuilder: https://learn.microsoft.com/dotnet/api/system.text.stringbuilder - Unreal Engine C++ documentation —
FString: https://dev.epicgames.com/documentation/en-us/unreal-engine/string-handling-in-unreal-engine