Static Members
Summary
A static member belongs to the type itself rather than to any particular instance — there is exactly one copy of a static field, shared by every object of that class, and a static method can be called without creating an object at all. Static members exist for state and behavior that genuinely have nothing to do with "which instance," such as a shared counter, a utility function, or a single global manager.
Mental Model
If a class is a blueprint for a house, an instance field is something each individual house has its own copy of — its own furniture, its own mailbox contents. A static field is more like a shared amenity for the whole neighborhood: there's only one community pool, not one per house, and every house references the same pool rather than each getting an independent copy. Changing the pool's water level from one house's back yard changes it for every house, because there was only ever one pool to begin with.
This is exactly why a static field is dangerous when misused: instance
fields are naturally isolated (each Player has its own health), but a
static field is inherently shared, global state — every part of the
codebase that touches it is touching the same value, whether that was
intended or not.
Core Explanation
- One copy per type, not per instance.
static int enemyCount;exists exactly once regardless of how manyEnemyobjects are created — every instance reads and writes the same shared value. - Callable without an instance.
MathUtils.Clamp(x, 0, 1)needs noMathUtilsobject to exist first — the method belongs to the type, and is invoked through the type name directly (Type.Method(), notinstance.Method()). - Initialized once, lives for the program's duration. A static field's memory is allocated when the type is first used and persists for the entire lifetime of the program (or, in Unity, typically for the entire play session unless explicitly reset) — it does not get created and destroyed alongside individual instances the way instance fields do.
- A static class cannot be instantiated at all. Marking an entire
class
static(C#) — commonly used for pure utility/helper classes — makes every member implicitly static and preventsnew MathUtils()entirely, signaling "this type has no meaningful instance state."
Syntax / Examples
C# (Unity)
public class Enemy : MonoBehaviour
{
private static int activeCount = 0; // Shared by every Enemy instance.
void OnEnable() => activeCount++;
void OnDisable() => activeCount--;
public static int ActiveCount => activeCount; // Read without an instance.
}
public static class MathUtils
{
public static float Clamp01(float value) => Mathf.Clamp(value, 0f, 1f);
}
// Called via the type name — no MathUtils instance exists or is needed.
float t = MathUtils.Clamp01(rawValue);
Debug.Log($"Enemies alive: {Enemy.ActiveCount}");
C++ (Unreal)
class AEnemy : public ACharacter
{
public:
AEnemy() { ++ActiveCount; }
virtual ~AEnemy() { --ActiveCount; }
static int32 GetActiveCount() { return ActiveCount; }
private:
static int32 ActiveCount; // Declared here, defined once in the .cpp file.
};
// AEnemy.cpp
int32 AEnemy::ActiveCount = 0;
C++ requires a static member's storage to be defined exactly once outside the class body (as shown), in addition to being declared inside it — the in-class line is only a declaration.
Unreal Blueprint
Blueprint has no direct static-variable equivalent for gameplay classes;
the closest analog is a Function Library (a UBlueprintFunctionLibrary
subclass) exposing static C++ functions as callable nodes with no target
object pin required — matching MathUtils.Clamp01 above.
Practical Example
A settings system needs exactly one shared configuration accessible from anywhere, without every system needing a reference passed to it.
public static class GameSettings
{
public static float MasterVolume { get; set; } = 1f;
public static bool ShowSubtitles { get; set; } = false;
}
public class AudioSource2D : MonoBehaviour
{
void PlaySound(AudioClip clip)
{
audioSource.volume = baseVolume * GameSettings.MasterVolume;
audioSource.PlayOneShot(clip);
}
}
Every AudioSource2D in the game reads the same GameSettings.MasterVolume
without needing a reference to a settings object passed down through every
constructor or method call — the trade-off, made deliberately here rather
than by accident, is that GameSettings is now global, shared, mutable
state (see Common Mistakes).
Common Mistakes
- Using static fields for per-instance state.
static float health;on anEnemyclass means every enemy shares one health value — damage one enemy and they all lose health. This is rarely intended; instance fields exist precisely to avoid this. - Reaching for static state as a shortcut instead of passing
references/dependencies explicitly. It's tempting to make a manager
staticpurely to avoid wiring up a reference — but this makes the dependency invisible (nothing about a class's public API reveals it secretly reads global state) and makes the code far harder to test in isolation. - Not resetting static state between play sessions in the Unity
Editor. Static fields survive across "Play" sessions in some Editor
configurations (domain reload disabled) — a static counter that isn't
explicitly reset in
OnEnable/Awakecan carry a stale value from the previous session into the next. - Mutating shared static state from multiple systems without any clear owner, making it unclear which system is responsible for keeping it correct — the same problem a public mutable field on an instance has (see [[CON-0020]]), amplified because every instance and every system shares the one copy.
Best Practices
- Use static members for things that are genuinely type-level — pure utility functions, constants, or a truly singular piece of shared state (not merely "state I don't want to pass around").
- Prefer passing a reference/dependency explicitly over reaching for static/global access, especially for anything that needs to be substituted or isolated in tests.
- If static state must exist, keep exactly one system responsible for writing to it, and treat every other system as a read-only consumer.
- In Unity specifically, explicitly reset static state that represents
"session" data (counters, caches) in
Awake/OnEnablerather than assuming it starts fresh every play session.
Exercises
- Take the
Enemy.activeCountexample and deliberately introduce the "shared health" bug by making an enemy'shealthfieldstaticinstead of instance-level. Damage one enemy and observe what happens to the others; explain why in terms of "one copy per type." - Convert
GameSettingsinto an instance-based settings object passed explicitly to each system that needs it, and describe one concrete advantage this has for testingAudioSource2Din isolation. - Explain why C++ requires a static member to be defined once outside the class body in addition to being declared inside it, and what would happen (conceptually) if that rule didn't exist and every including file got its own copy.
- Identify a piece of global/static state in a hypothetical project (a score, a difficulty setting, a debug flag) and argue whether it's a legitimate use of static state or should be refactored to an explicitly passed dependency.
Related Concepts
- Access Modifiers — static members are just as subject to
public/private/protectedvisibility rules as instance members. - Enums — a static lookup table or method keyed by an enum value (e.g.
mapping a
Rarityto a display color) is one of the most common uses of static members. - Objects — understanding what makes an instance distinct is the necessary contrast for understanding what makes a static member different from one.
References
KNOWLEDGE_SCHEMA.md— GDKB's concept maturity ladder and cross-linking rules, which this document follows.- Unity/.NET — Static Classes and Static Class Members: https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/static-classes-and-static-class-members
- Unreal Engine C++ documentation — Static Class Members: https://dev.epicgames.com/documentation/en-us/unreal-engine/epic-cplusplus-coding-standard-for-unreal-engine