GDKB

Structs

Intermediate25 Minutes readUpdated July 21, 20263 related concepts

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.

On this page

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. Changing b afterward never affects a — the two are fully independent from the moment of assignment, unlike a class instance, where b = a copies 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 Vector3 calculations 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 reference explicitly.

Syntax / Examples

C# (Unity)

text
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)

cpp
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.

csharp
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 — position returns 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 is transform.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 object or 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 (once Equals/== 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, an InventorySystem) — 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/GetHashCode in 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

  1. 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.
  2. Convert the DamageInfo struct into a class, apply the same ApplyDamage method, and describe a scenario where the reference sharing this introduces would cause a subtle bug that the struct version could not have.
  3. Write a struct FixedTimer { float Elapsed; float Duration; } and a method Tick(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.
  4. Given a struct with 20 float fields 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.
  • 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

Related Concepts