Variables
Summary
A variable is a named storage location that holds a value a program can read and change while it runs. Every piece of game state a player ever notices — health, position, score, whether a door is locked — lives in a variable somewhere. Variables are the smallest unit of memory a programmer manipulates directly, and nearly everything else in software (functions, objects, components) exists to organize and protect them.
Mental Model
Think of a variable as a labeled box on a shelf.
- The label is the variable's name — how code refers to the box without caring where it physically sits.
- The box's size and shape is the variable's type — an
intbox only holds whole numbers; aboolbox only holdstrueorfalse. The shape determines what can be put in and what operations make sense. - The contents are the current value. Reading the variable looks inside the box; assigning to it swaps out the contents without changing the label or the box itself.
Declaring a variable is putting an empty (or default-filled) box on the shelf and writing a label on it. Assignment is putting something inside. Re-assignment replaces the contents — it does not create a new box, and every piece of code holding that label is now looking at the new contents.
This model is also why scope matters: a box on your desk (a local
variable) is not the same box as one in the shared warehouse (a global or
member variable), even if both happen to have the label health.
Core Explanation
A variable declaration binds three things together: a name, a type, and a region of memory. Which of these are fixed and which can change is what distinguishes the concepts programmers group under "variables":
- Type describes what kind of value the box can hold and how large it
is — a
boolneeds far less memory than aVector3. Statically typed languages (C#, C++) fix the type at compile time; the compiler rejects code that tries to put the wrong shape of value in the box. Dynamically typed languages check this at runtime instead. - Scope is where a variable's label is visible. A variable declared inside a function only exists for that function's call — the box is created when the function starts and destroyed when it returns (this is what "local variable" means). A variable declared on a class is a member variable (a "field") — it lives as long as the object does. A variable declared outside any function or class is global — it lives for the program's whole lifetime and is visible everywhere, which is exactly why it should be used sparingly (see Common Mistakes).
- Mutability controls whether the box's contents can change after the
first assignment.
const(C++) andreadonly/non-varbindings enforce at compile time that a value is written once and never reassigned — a correctness guarantee, not just a style choice, because it makes a whole category of bugs (accidental overwrites) impossible rather than merely unlikely. - Lifetime is how long the memory backing the variable is valid. Locals typically live on the stack and are reclaimed automatically when their scope ends. Objects on the heap live until nothing references them (garbage collected, in C#) or until explicitly freed (manual/RAII, in C++). Confusing lifetime with scope is a common source of bugs — a reference (or pointer) can outlive the scope it was declared in and end up pointing at memory that no longer holds what it once did.
Assignment (=) is not the same as comparison (==) — a distinction that
matters enough to appear again under Common Mistakes, because the two look
almost identical and a typo silently turns one into the other.
Syntax / Examples
C# (Unity)
public class Player : MonoBehaviour
{
// Member variable (field): lives as long as the Player object does.
public int maxHealth = 100;
private int currentHealth;
void Start()
{
// Local variable: lives only for this method call.
int startingBonus = 10;
currentHealth = maxHealth + startingBonus;
}
void TakeDamage(int amount)
{
currentHealth -= amount; // re-assignment: same box, new contents
}
}
C++ (Unreal)
// Header: a member variable (UPROPERTY exposes it to the editor/reflection).
UPROPERTY(EditAnywhere)
int32 MaxHealth = 100;
int32 CurrentHealth;
void APlayerCharacter::BeginPlay()
{
Super::BeginPlay();
// Local variable, block-scoped to BeginPlay.
const int32 StartingBonus = 10;
CurrentHealth = MaxHealth + StartingBonus;
}
void APlayerCharacter::TakeDamage(int32 Amount)
{
CurrentHealth -= Amount;
}
Unreal Blueprint
A Blueprint variable is declared in the My Blueprint panel (name + type,
e.g. CurrentHealth : Integer), with an Editable checkbox controlling
whether level designers can override its default per-instance. Reading it is
a Get node; writing it is a Set node wired into the event graph —
the same name/type/value triad as the code examples, expressed visually.
Practical Example
A health system needs to: store the current and maximum health, reduce current health on damage, and never let it go negative or above the maximum.
public class Health : MonoBehaviour
{
public int Max = 100;
private int current;
void Awake() => current = Max;
public void Damage(int amount)
{
current = Mathf.Clamp(current - amount, 0, Max);
if (current == 0)
Die();
}
void Die() { /* ... */ }
}
current is a private member variable: its scope is the class, so only
Health's own methods can change it directly — everything else must go
through Damage(). That single entry point is what makes Clamp reliable;
if current were public, any other script could set it to a value outside
0..Max and the invariant this method exists to protect would be gone.
Common Mistakes
- Confusing
=and==.if (isDead = true)compiles in some languages and always evaluates true, silently turning a comparison into an assignment. C# rejects this forboolconditions at compile time; C++ does not always catch it, which is why many style guides recommend writing the constant first (if (true == isDead)) so a dropped=fails to compile instead of failing silently. - Assuming reassignment updates every copy. For value types (
int,struct), each variable holds its own independent copy — changing one does not change another that was assigned from it. For reference types (classinstances in C#, pointers/references in C++), multiple variables can name the same underlying object, so changing it through one name is visible through the others. Not knowing which kind of type you're holding is one of the most common sources of "I changed it and nothing happened" or the opposite, "I changed it and something I didn't expect changed too." - Reaching for a global variable to avoid passing a parameter. It solves the immediate problem but makes the variable's value able to change from anywhere in the codebase, which makes bugs involving it much harder to trace — there is no longer a small set of places to look.
- Not initializing a variable before reading it. Some languages guarantee a default value (C# zero-initializes fields); others do not (an uninitialized local in C++ holds whatever garbage was already in that memory). Relying on "it happened to be zero last time I ran it" is not a guarantee.
- Shadowing. Declaring a local variable with the same name as a member variable (or an outer scope's variable) silently hides the outer one for that block — code that looks like it's reading/writing the field is actually reading/writing an unrelated local.
Best Practices
- Give variables names that describe what they hold, not what type they
are (
remainingLives, notintValue) — the type is already visible at the declaration and in the editor/IDE. - Keep scope as narrow as possible: prefer a local variable over a member variable, and a member variable over a global, unless wider visibility is actually needed. Narrower scope means fewer places a bug involving that variable could originate.
- Mark a variable
const/readonlythe moment you know it should never be reassigned. It documents intent and lets the compiler enforce it, rather than relying on every future reader noticing and respecting a convention. - Initialize variables at declaration wherever the language allows it, rather than declaring now and assigning later — it removes an entire window in which the variable exists but holds a meaningless value.
- Be deliberate about value vs. reference semantics: know which one a type uses before assigning it to another variable, especially across a function boundary.
Exercises
- Write a
Counterclass with a private member variablecountand public methodsIncrement()andReset(). Explain, in a comment, whycountis private rather than public. - Given
int a = 5; int b = a; b = 10;— what is the value ofa? Now repeat the exercise with a reference/class type instead ofintand explain why the answer differs. - Find a genuine
=vs.==bug you can reproduce (write one on purpose), then fix it. Note what compiler warning, if any, would have caught it. - Take a global variable in a small sample project and refactor it into a parameter passed explicitly between the functions that need it. Describe what got easier to reason about, and what got more verbose.
Related Concepts
- Object Pooling — reuses variables (fields) on existing objects instead of allocating new ones, so understanding member-variable lifetime matters before pooling makes sense.
- State Machine — a state machine's "current state" is itself a variable whose allowed values and transitions are deliberately constrained.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity Scripting Reference — Variables and Fields: https://docs.unity3d.com/Manual/VariablesAndLiterals.html
- Unreal Engine C++ documentation — Properties (
UPROPERTY): https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-uproperties