Structs
Summary
A struct is a value type that groups related fields together, like a class, but is copied by value instead of shared by reference. It exists for small, data-focused bundles — a 3D vector, a color, a grid coordinate — where the cost and semantics of copying the whole value are actually preferable to sharing one object through a reference.
Mental Model
A class is a box on a shelf that everyone points to by address — handing someone "the box" hands them a note with the shelf location on it, and writing into the box through that note is visible to everyone else holding the same note. A struct is a slip of paper with the values written directly on it — handing someone "the struct" hands them their own physical copy of that paper. Writing on their copy never changes yours, because there is no longer a shared box at all; there are just two identical, independent slips.
This is why Vector3 is a struct in both Unity and Unreal: passing a
position into a function should hand that function a copy of the
coordinates, not a shared reference to "the" position that the function
could then unexpectedly mutate out from under the caller.
Core Explanation
- Value semantics. Assigning a struct (
Vector3 b = a;) or passing one as a parameter copies every field. Changingbafterward never affectsa— the two are fully independent from the moment of assignment, unlike a class instance, whereb = acopies only the reference. - Stack allocation (usually). A local struct variable is typically
allocated on the stack rather than the garbage-collected heap, which
makes creating and discarding many of them (e.g. thousands of
Vector3calculations per frame) cheap and allocation-free — no GC pressure, no heap fragmentation. - No inheritance. Structs in C# cannot inherit from another struct or class (though they can implement interfaces, see [[CON-0011]]) — they are meant to be small, final, data-first types, not the base of a polymorphic hierarchy. That role belongs to classes ([[CON-0010]]).
- Copy cost scales with size. A struct with many large fields copies
all of that data on every assignment and every method call that takes it
by value — which is exactly backwards from what a struct is good at.
Large data bundles should stay classes, or the struct should be passed
in/ref/by referenceexplicitly.
Syntax / Examples
C# (Unity)
public struct GridCoord
{
public int X;
public int Y;
public GridCoord(int x, int y)
{
X = x;
Y = y;
}
public override string ToString() => $"({X}, {Y})";
}
public class TileMap : MonoBehaviour
{
void Start()
{
GridCoord origin = new GridCoord(0, 0);
GridCoord moved = origin; // Copy, not a reference.
moved.X = 5;
Debug.Log(origin); // (0, 0) — unchanged.
Debug.Log(moved); // (5, 0)
}
}
C++ (Unreal)
USTRUCT(BlueprintType)
struct FGridCoord
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
int32 X = 0;
UPROPERTY(EditAnywhere)
int32 Y = 0;
};
void ATileMap::BeginPlay()
{
Super::BeginPlay();
FGridCoord Origin{0, 0};
FGridCoord Moved = Origin; // Copy, not a reference.
Moved.X = 5;
// Origin.X is still 0; Moved.X is 5.
}
Unreal Blueprint
A USTRUCT(BlueprintType) (as above) appears in Blueprint as a
Make FGridCoord node (builds one from its fields) and a Break
FGridCoord node (reads its fields back out) — assigning the result of
Make to two different variables produces two independent copies, the
same value semantics as the C++/C# examples.
Practical Example
A damage system needs to pass around "how much damage, of what type, from what direction" without those values ever being unexpectedly shared or mutated by whichever system last touched them.
public struct DamageInfo
{
public float Amount;
public DamageType Type;
public Vector3 SourceDirection;
}
public class Health : MonoBehaviour
{
private float current = 100f;
public void ApplyDamage(DamageInfo info)
{
// `info` is this method's own copy — mutating it here,
// if it were mutated, could never affect the caller's version.
current -= info.Amount;
}
}
DamageInfo being a struct means every system that receives it — a hit
reaction, a damage number popup, a combat log — gets its own independent
snapshot of "what happened," which cannot be retroactively changed by one
of those systems processing it after another already has.
Common Mistakes
- Mutating a struct returned from a property or method, expecting it to
stick.
transform.position.x = 5;does not compile in C# for exactly this reason —positionreturns a copy of the struct, so mutating that copy would silently do nothing, and the compiler refuses rather than letting that bug through. The fix istransform.position = new Vector3(5, transform.position.y, transform.position.z);. - Making a struct large and mutable. A struct with a dozen fields copied on every function call is a performance and correctness trap — every copy is real memory-copy work, and it defeats the "matches a small value" intuition the type is supposed to signal.
- Boxing a struct into an
objector interface reference repeatedly in a hot path (C#) — this allocates a heap copy each time, silently reintroducing the GC pressure structs exist to avoid. - Expecting reference equality (
==comparing identity) to work like it does for classes. Two separately created structs with identical field values are equal by value (onceEquals/==are implemented or auto-generated appropriately), not by identity — there is no single "the" instance to compare identities against.
Best Practices
- Keep structs small (a handful of fields, ideally primitive types or other small structs) and, where the language supports it, immutable — set every field in the constructor and never mutate after.
- Use a struct when the type represents a value (a position, a color, a
timestamp) and a class when it represents an entity with identity
(a
Player, anInventorySystem) — this is the deciding question, not performance alone. - Pass large structs by
in/ref/const&where the language allows it, to avoid copying their contents on every call when only reading is needed. - Implement value equality explicitly (
Equals/GetHashCodein C#,operator==in C++) when a struct will be compared or used as a dictionary key, rather than relying on default field-by-field comparison behaving as expected in every case.
Exercises
- Reproduce the
transform.position.x = 5;compiler error in a Unity script (or reason through it if Unity isn't available), and explain in your own words why C# refuses to compile it instead of silently doing nothing. - Convert the
DamageInfostruct into a class, apply the sameApplyDamagemethod, and describe a scenario where the reference sharing this introduces would cause a subtle bug that the struct version could not have. - Write a struct
FixedTimer { float Elapsed; float Duration; }and a methodTick(FixedTimer timer, float deltaTime)that returns an updated copy rather than mutating the parameter. Explain why the return-a-copy pattern is the natural fit for a struct. - Given a struct with 20
floatfields passed by value into a function called every frame for 10,000 objects, explain — in terms of what a struct actually does on assignment — why this could become a measurable performance problem, and what change would fix it.
Related Concepts
- Classes — the reference-type counterpart to structs; the deciding factor between them is whether the type represents a value or an entity with identity.
- Enums — frequently stored as a field inside a struct to describe one categorical aspect of that value.
- Arrays — an array of structs stores every element's data contiguously in place, which is often the fastest layout for large sets of small records.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity Scripting Reference —
Vector3(a canonical built-in struct): https://docs.unity3d.com/ScriptReference/Vector3.html - Unreal Engine C++ documentation —
USTRUCT: https://dev.epicgames.com/documentation/en-us/unreal-engine/structs-in-unreal-engine