The solution is reusable and can be used to solve similar problems. In this article we are going to focus on design patterns focused on video game programming.
Design patterns emerge from the world of architecture when in 1979 architect and mathematician, Christopher Alexander publishes the book ‘The Timeless Way of Building’. In the author’s words:
Each pattern describes a problem that occurs an infinite number of times in our environment, as well as the solution to it, so that we can use this solution a million times later without having to rethink it again.
Inspired by the ancient medieval cities, he published with other colleagues his next book ‘A Pattern Language’, where he formalizes the idea of a design pattern as a solution to a problem within a given context.
It describes methods for creating practical, safe and attractive designs regardless of the scale at which one works.
Its principles are still used as building code in many cities.
The leap into the programming world occurred in 1987 when Ward Cunningham and Kent Beck found parallels in Alexander’s work and what a good architecture based on object-oriented should be. They developed five patterns and published a paper at the OOPSLA conference in the same year under the name ‘Using Pattern Languages for OO Programs’.
But it was not until 1994 with the publication of the famous ‘Design Patterns: Elements of Reusable Object-Oriented Software’ by the group called ‘Gang of Four’ (or simply ‘GoF’) that the term and its use became popular. It sold over 500,000 copies in English and was translated into 13 other languages.
In the book the authors lay the foundations of what we understand today as a design pattern and compile the first 23 patterns. Since then their number has continued to grow.
The game development community has both adopted and expanded the concept. Many GoF patterns translate naturally to games, while others like Game Loop, Update Method, Component or Spatial Partition were born from the specific challenges of real-time simulations. In 2014, Robert Nystrom published ‘Game Programming Patterns’, the definitive book that bridges classic design patterns with the realities of game engines, and it is available for free on his website.
If you look at Unity itself, you will see patterns everywhere. The entire engine is built on the Component pattern: GameObjects are containers for MonoBehaviour components. Update, FixedUpdate and LateUpdate implement the Game Loop pattern. ScriptableObjects enable the Type Object pattern. The Input System uses the Observer and Bridge patterns. Once you start recognizing them, you cannot unsee them.
Because they can help us create robust software that is easy to understand and modify. In addition, it can provide you with a common vocabulary for planning or solving problems with other programmers.
Let’s imagine you want to make toys out of plastic parts. You could make a mold for each new toy you need, but these toys could not be easily modified and you would not take advantage of any of the old molds.
Instead of building molds of complete toys, we could make molds of smaller pieces and build the toys with these blocks. In addition to being able to reuse them for other toys, owners could modify them and use blocks from other toys to build their own.
But toys built with only basic blocks would look very crude and unoriginal. The same goes for software, not everything can be made from patterns, but you can use them as a solid and reliable base.
From them you can add the blocks you need to make your game a true work of art of software architecture ;).
In this article we are going to analyze some design patterns specially useful for game development, specifically we will use C# as language and Unity as 3D engine.
The patterns we will see can be classified according to their purpose or level of abstraction in five groups:
graph TD
Client1[Client 1] -->|requests| Instance[Singleton Instance]
Client2[Client 2] -->|requests| Instance
Client3[Client 3] -->|requests| Instance
Instance -.->|creates if null| Instance
The first design pattern we are going to see is possibly the most controversial and most/misused of all. Its simplicity when implementing it and its ease of use make it the design pattern that is usually learned first and, in some cases, the only pattern that many programmers know.
The Singleton pattern can be summarized as:
Only have one instance and provide a single global access to it.
Ensuring that only one instance of a class exists means that only one copy of a class type will exist at a time, it would not allow us to create a second copy of that class. In addition, anyone will be able to access it. This last characteristic is what gives it its bad reputation, since it is not usually recommended to have objects with global access.
In the development of video games they are usually used mainly in the so-called managers, objects with a well-defined and limited purpose that records and/or modifies information or states of the same type.
For example, a class that is in charge of playing audio and modifying the volume can be an audio manager. Unity is literally riddled with these managers, such as Input, Debug, etc.
A simple example of a singleton can be:
public class LazySingleton
{
// Static variable that references the only instance of Singleton.
private static LazySingleton instance = null;
// Requests the single instance.
public static LazySingleton Instance
{
get
{
// If it does not exist, it is created.
if (instance == null)
instance = new LazySingleton();
return instance;
}
}
public void DoSomething() { }
// Private constructor so no one external to this class will be able to create one, and without parameters.
private LazySingleton() { }
}If someone wants to execute the DoSomething function of Singleton just do:
Singleton.Instance.DoSomething();The single instance of Singleton will be created the first time Instance is called. This is called ‘lazy initialization’ and can generate several instances if different threads call Instance at the same time. To avoid this you can use a lock block, sacrificing some performance, like this:
public class ThreadSafeSingleton
{
// Static variable that references the single instance of ThreadSafeSingleton.
private static ThreadSafeSingleton instance = null;
// Object used in the lock.
private static object @lock = new object();
// Requests the single instance.
public static ThreadSafeSingleton Instance
{
get
{
lock (@lock)
{
// This code block is Thread-safe.
// If it does not exist, it is created.
if (instance == null)
instance = new ThreadSafeSingleton();
return instance;
}
}
}
public void DoSomething() { }
// Private constructor so no one external to this class will be able to create one, and without parameters.
private ThreadSafeSingleton() { }
}Another option may be to leave the task of creation to the CLR in this way:
public class Singleton
{
// It is executed once per app-domain, the CLR ensures that it is thread-safe.
private static readonly Singleton instance = new Singleton();
// Requests the single instance.
public static Singleton Instance => instance;
public void DoSomething() { }
// Explicit static constructor to tell the compiler not to mark the type as beforefieldinit.
static Singleton() { }
// Private constructor so no one external to this class will be able to create one, and without parameters.
private Singleton() { }
}The Singleton is useful when a system should logically exist only once during the lifetime of the application. Typical examples in Unity are managers: GameManager (controls the overall game flow), AudioManager (handles sounds and music), SceneLoader (manages scene transitions) or SaveSystem (manages save and load operations).
In Unity this often goes hand in hand with DontDestroyOnLoad, since managers usually need to survive between scene changes to maintain state.
However, Singleton should not be used for everything. Objects that can have multiple copies (players, enemies, bullets, pickups, UI elements) should never be Singletons.
In Unity, the pattern needs to work with the component lifecycle. A common generic implementation that automatically creates a GameObject if none exists in the scene, persists across scenes, and destroys duplicates on Awake:
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance == null)
{
GameObject obj = new GameObject(typeof(T).Name);
instance = obj.AddComponent<T>();
}
}
return instance;
}
}
protected virtual void Awake()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
}
}
}To use it, simply inherit from Singleton<T>:
public class AudioManager : Singleton<AudioManager>
{
public void PlaySound(string soundName) { /* ... */ }
}
// Any other script can then call:
AudioManager.Instance.PlaySound("explosion");Advantages:
Disadvantages:
Awake, because Awake runs before Start. Always check whether Instance is null before doing any setup.DontDestroyOnLoad(gameObject). But always check for duplicates first, otherwise each scene load will create a new persistent instance.Awake can cause problems. If another script tries to access the Singleton inside its own Awake, the Singleton may not be ready yet. Consider using the Script Execution Order settings to ensure the Singleton initializes first.ScriptableObject, or dependency injection. These are often cleaner and more testable solutions.
graph LR
Director[Director] -->|constructs| Builder[Builder]
Builder -->|step 1| Product[Product]
Builder -->|step 2| Product
Builder -->|step 3| Product
The Builder pattern is a design pattern used to create complex objects step by step instead of passing many values into a long constructor. It separates the object creation process from the object itself, making the code easier to read and manage.
The Builder pattern can be summarized as:
Construct complex objects step by step, separating the construction process from the final representation.
Instead of writing a constructor with many parameters, you can build the object with a fluent interface:
new EnemyBuilder()
.SetName("Goblin")
.SetHealth(100)
.SetSpeed(2.5f)
.Build();This makes it clear what each value represents. In most Unity projects, a simple fluent builder is usually enough, without needing the full GoF structure with Director and ConcreteBuilder.
Builder is useful when an object has many parameters or when some of its fields are optional. If a constructor starts getting too long, the code becomes harder to understand and easier to misuse.
It is commonly used when:
In Unity, Builder can be useful for enemy creation, character customization, procedural level generation, quest setup, dialogue systems or dynamic UI creation. However, if an object is simple and only has a few parameters, a normal constructor is enough.
A fluent builder returns this from each setter, allowing chained calls:
public class EnemyBuilder
{
private readonly GameObject prefab;
private readonly Vector3 position;
private string name = "Enemy";
private int health = 100;
private float speed = 2f;
private int damage = 10;
private Color color = Color.white;
public EnemyBuilder(GameObject prefab, Vector3 position)
{
this.prefab = prefab;
this.position = position;
}
public EnemyBuilder SetName(string name)
{
this.name = name;
return this;
}
public EnemyBuilder SetHealth(int health)
{
this.health = health;
return this;
}
public EnemyBuilder SetSpeed(float speed)
{
this.speed = speed;
return this;
}
public EnemyBuilder SetDamage(int damage)
{
this.damage = damage;
return this;
}
public EnemyBuilder SetColor(Color color)
{
this.color = color;
return this;
}
public Enemy Build()
{
GameObject obj = Object.Instantiate(prefab, position, Quaternion.identity);
Enemy enemy = obj.GetComponent<Enemy>();
enemy.Init(name, health, speed, damage, color);
return enemy;
}
}Using the builder to spawn different enemy variations:
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab;
private void Start()
{
var goblin = new EnemyBuilder(enemyPrefab, new Vector3(0, 0, 0))
.SetName("Goblin Scout")
.SetHealth(50)
.SetSpeed(3f)
.SetDamage(5)
.Build();
var boss = new EnemyBuilder(enemyPrefab, new Vector3(5, 0, 0))
.SetName("Orc Warlord")
.SetHealth(300)
.SetSpeed(1.5f)
.SetDamage(30)
.SetColor(Color.red)
.Build();
}
}Advantages:
Disadvantages:
MonoBehaviour objects as naturally as plain C# classes.this is often enough without adding a Director class.EnemyData, LevelConfig or QuestData. After the data object is built, pass it into a MonoBehaviour or another system.ScriptableObjects. Store default values in ScriptableObjects and pass them into the Builder as starting data.Reset() method to clear its state.
graph TD
Client[Client] -->|requests| Factory[Factory]
Factory -->|creates| ProductA[Product A]
Factory -->|creates| ProductB[Product B]
Factory -->|creates| ProductC[Product C]
The Factory pattern is a creational design pattern that moves the responsibility of creating objects away from the main code and into a separate factory class or method. Instead of scattering new Goblin() or Instantiate(zombiePrefab) across the codebase, the code simply asks the factory for the object it needs.
The Factory pattern can be summarized as:
Define an interface for creating objects, letting subclasses decide which concrete class to instantiate.
In Unity, imagine an enemy spawning system. The GameManager does not need to know how a Goblin, Orc or Dragon is created. It only asks the EnemyFactory for an enemy. The factory handles prefab selection, instantiation, initialization and returns the result through a common interface.
Factory is useful when a project needs to create different types of objects from the same family and the correct one must be chosen at runtime. It is commonly used when:
In Unity, common use cases include enemy spawning, weapon creation, bullet or projectile systems, UI window management and visual effect pools. However, if the project only creates one simple object type, a factory may add extra structure without much benefit.
The factory returns objects through a common interface, keeping the client decoupled:
public interface IEnemy
{
void Initialize(Vector3 position);
}Concrete enemy types implement the interface:
public class Goblin : MonoBehaviour, IEnemy
{
public void Initialize(Vector3 position) { /* setup */ }
}
public class Orc : MonoBehaviour, IEnemy
{
public void Initialize(Vector3 position) { /* setup */ }
}The factory centralizes creation logic:
public enum EnemyType { Goblin, Orc, Dragon }
public class EnemyFactory : MonoBehaviour
{
[SerializeField] private GameObject goblinPrefab;
[SerializeField] private GameObject orcPrefab;
[SerializeField] private GameObject dragonPrefab;
public IEnemy CreateEnemy(EnemyType type, Vector3 position)
{
GameObject prefab = type switch
{
EnemyType.Goblin => goblinPrefab,
EnemyType.Orc => orcPrefab,
EnemyType.Dragon => dragonPrefab,
_ => throw new System.ArgumentException($"Unknown type: {type}")
};
var instance = Instantiate(prefab, position, Quaternion.identity);
return instance.GetComponent<IEnemy>();
}
}The client only depends on the interface and the factory:
public class GameManager : MonoBehaviour
{
[SerializeField] private EnemyFactory factory;
private void Start()
{
factory.CreateEnemy(EnemyType.Goblin, new Vector3(-5, 0, 0));
factory.CreateEnemy(EnemyType.Orc, new Vector3(0, 0, 0));
factory.CreateEnemy(EnemyType.Dragon, new Vector3(5, 0, 0));
}
}Advantages:
Disadvantages:
switch structures can become hard to maintain as types grow.[SerializeField] or ScriptableObjects to manage prefab references instead of hardcoding them.switch block.
graph TD
Prototype[Prototype] -->|Clone| Copy1[Copy 1]
Prototype -->|Clone| Copy2[Copy 2]
Prototype -->|Clone| Copy3[Copy 3]
The Prototype pattern lets you create new objects by cloning an existing template instead of building everything from scratch. Prepare one well-configured object, then clone it whenever you need a new version.
The Prototype pattern can be summarized as:
Create new objects by copying an existing prototype instance, avoiding the cost of building from scratch.
The most important detail is how the copy is made. A shallow copy still shares some references with the original, while a deep copy creates independent copies of all internal data. Choose carefully based on your needs.
Prototype is useful when creating an object from zero is expensive or repetitive. If an object needs many values, references or setup steps before it is ready, cloning an already prepared version is usually cleaner.
It also works well when many variations come from the same base. For example, create a basic enemy data object, clone it, then slightly change its health, speed or damage for different waves.
In Unity, it is especially useful with ScriptableObjects. Instead of modifying an editor asset directly during runtime, you clone it and apply changes to the copy.
However, if an object is simple and cheap to create with new, adding a clone system may only add unnecessary complexity.
In Unity, Instantiate(prefab) already works very similarly to the Prototype pattern, the prefab is the prototype, and Unity creates a copy.
For plain C# data objects, a Clone() method provides the same capability:
public class WeaponData
{
public string Name;
public int Damage;
public float Range;
public List<string> Abilities;
public WeaponData ShallowClone()
{
return (WeaponData)MemberwiseClone();
}
public WeaponData DeepClone()
{
var clone = (WeaponData)MemberwiseClone();
clone.Abilities = new List<string>(Abilities);
return clone;
}
}Using the prototype to create enemy variations:
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private WeaponData baseWeapon;
private void Start()
{
var goblinWeapon = baseWeapon.DeepClone();
goblinWeapon.Name = "Rusty Dagger";
goblinWeapon.Damage = 5;
var bossWeapon = baseWeapon.DeepClone();
bossWeapon.Name = "Flaming Greatsword";
bossWeapon.Damage = 40;
bossWeapon.Abilities.Add("Fire Aura");
}
}Advantages:
Disadvantages:
Instantiate creates the copy.ScriptableObjects are excellent for prototype-style data: enemy stats, weapon settings, item data and level configurations can be stored as ScriptableObjects and cloned at runtime.DeepClone() method that creates new instances of internal collections.
graph LR
Client[Client] -->|Target Interface| Adapter[Adapter]
Adapter -->|Adaptee Interface| Adaptee[Adaptee]
The Adapter pattern is a structural design pattern that allows two incompatible interfaces to work together. Like a power adapter that lets a foreign plug fit a local socket, the software adapter sits between your code and an incompatible API, translating calls so neither side needs to change.
The Adapter pattern can be summarized as:
Convert the interface of a class into another interface that clients expect, letting incompatible systems work together.
The Adapter implements the interface your system already knows. Inside, it holds the incompatible object and translates calls into a format that object understands. The rest of your code keeps working with the same clean interface, unaware of what happens behind the scenes.
Adapter is useful when you want to connect a new or incompatible system without rewriting existing code. It is especially helpful when you do not own the source code or when changing the original class would create more problems.
In Unity projects, common use cases include:
IDataPersistence interface.IAdsService.Consider a third-party logger that only exposes WriteLine(string) but your project expects an ILogger interface with a Log(string) method:
public interface ILogger
{
void Log(string message);
}The incompatible third-party class:
public class ThirdPartyLogger
{
public void WriteLine(string message)
{
Debug.Log($"[ThirdParty] {message}");
}
}The Adapter bridges the gap:
public class LoggerAdapter : ILogger
{
private ThirdPartyLogger adaptee;
public LoggerAdapter(ThirdPartyLogger adaptee)
{
this.adaptee = adaptee;
}
public void Log(string message)
{
adaptee.WriteLine(message);
}
}Now the client works against the interface, not the concrete SDK:
public class GameManager : MonoBehaviour
{
private ILogger logger;
private void Awake()
{
logger = new LoggerAdapter(new ThirdPartyLogger());
}
private void Start()
{
logger.Log("Game started!");
}
}Advantages:
Disadvantages:
MonoBehaviour. This keeps them independent from the scene and easier to test.Awake, pass it from the outside when possible.#if UNITY_IOS and #if UNITY_ANDROID checks across your code. Create separate adapters for each platform and use a factory to select the right one.ScriptableObject-based systems. A ScriptableObject should hold data, while the adapter exposes that data through a clean interface.
graph TD
Root[Composite] -->|operation| Leaf1[Leaf]
Root -->|operation| Child1[Composite]
Child1 -->|operation| Leaf2[Leaf]
Child1 -->|operation| Leaf3[Leaf]
The Composite pattern is a structural design pattern that lets you treat single objects and groups of objects in the same way. It hides the difference between one object and many objects from the code that uses them.
The Composite pattern can be summarized as:
Compose objects into tree structures to represent part-whole hierarchies, letting clients treat individual objects and compositions uniformly.
For example, calling TakeDamage() on a single Enemy or on an EnemyGroup containing several enemies works through the same method. If it is a single enemy, it takes the damage directly. If it is a group, the call is passed down to all children.
Composite is useful when individual objects and groups of objects need to behave in a similar way. A common example is an army system: giving an attack command to one soldier and giving an attack command to a squad can be handled through the same method.
In Unity, this pattern can be used for enemy groups, UI panels and menus, buff or skill systems, behavior trees and parent-child object structures. It is a good choice when your system naturally has a hierarchy and the same action should work on both single and grouped objects.
However, if you only need to loop through a few objects, a simple List<T> with a foreach is usually enough.
Unity’s Transform hierarchy already works with parent-child relationships, so build on top of that instead of recreating the hierarchy manually.
The shared interface:
public interface IDamageable
{
void TakeDamage(float amount);
}The leaf node, a single enemy:
public class Enemy : MonoBehaviour, IDamageable
{
[SerializeField] private float health = 100f;
public void TakeDamage(float amount)
{
health -= amount;
Debug.Log($"{name} took {amount} damage, remaining health: {health}");
if (health <= 0)
Destroy(gameObject);
}
}The composite that holds and delegates to children:
public class EnemyGroup : MonoBehaviour, IDamageable
{
private List<IDamageable> children = new List<IDamageable>();
private void Awake()
{
foreach (var child in GetComponentsInChildren<IDamageable>())
{
if (child != (IDamageable)this)
children.Add(child);
}
}
public void Add(IDamageable child)
{
children.Add(child);
}
public void TakeDamage(float amount)
{
foreach (var child in children)
child.TakeDamage(amount);
}
}public class GameController : MonoBehaviour
{
[SerializeField] private EnemyGroup squad;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
squad.TakeDamage(10f);
}
}Advantages:
Disadvantages:
Transform hierarchy. Unity already works with parent-child relationships, so build on top of that instead of recreating the hierarchy manually.GetComponentsInChildren<T>() can be useful to collect child components. Cache the results instead of calling it every frame.Update() is already running and a Composite also manually calls update methods on its children, the same objects may be updated twice.
graph LR
Component[Component] --> ConcreteComponent[ConcreteComponent]
Component --> Decorator[Decorator]
Decorator --> ConcreteDecoratorA[Fire Enchantment]
Decorator --> ConcreteDecoratorB[Poison Enchantment]
The Decorator is one of those patterns I genuinely love. It is elegant, flexible and solves a real problem: how to add behavior without creating an explosion of subclasses. Once you grasp it, you start seeing FirePoisonCriticalSword class names as the nightmare they are.
The Decorator pattern can be summarized as:
Attach additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.
Instead of creating separate classes for every combination, you wrap the base object with decorator objects that all implement the same interface. Each decorator adds its own behavior and delegates the rest to the wrapped object.
Decorator is useful when you need to add or remove features from an object at runtime. A character gaining armor, becoming poisoned, turning invisible or receiving a temporary damage boost are all good examples.
It is also helpful when inheritance starts to become too complicated. Instead of creating a separate subclass for every possible combination (FireSword, PoisonSword, FirePoisonSword, FirePoisonCriticalSword), you stack small decorators together.
In Unity, it is especially suitable for weapon modifiers, buff/debuff systems, status effects, ability chains and layered UI or audio systems.
Decorators work best as plain C# classes sharing a common interface:
public interface IWeapon
{
float GetDamage();
string GetDescription();
}The base weapon that will be wrapped:
public class BasicSword : IWeapon
{
public float GetDamage() => 10f;
public string GetDescription() => "Basic Sword";
}An abstract decorator that delegates to the wrapped weapon:
public abstract class WeaponDecorator : IWeapon
{
protected IWeapon weapon;
public WeaponDecorator(IWeapon weapon)
{
this.weapon = weapon;
}
public virtual float GetDamage() => weapon.GetDamage();
public virtual string GetDescription() => weapon.GetDescription();
}Concrete decorators that add effects:
public class FireEnchantment : WeaponDecorator
{
public FireEnchantment(IWeapon weapon) : base(weapon) { }
public override float GetDamage() => weapon.GetDamage() + 5f;
public override string GetDescription() => weapon.GetDescription() + " + Fire";
}
public class PoisonEnchantment : WeaponDecorator
{
public PoisonEnchantment(IWeapon weapon) : base(weapon) { }
public override float GetDamage() => weapon.GetDamage() + 3f;
public override string GetDescription() => weapon.GetDescription() + " + Poison";
}
public class CriticalEnchantment : WeaponDecorator
{
public CriticalEnchantment(IWeapon weapon) : base(weapon) { }
public override float GetDamage() => weapon.GetDamage() * 2f;
public override string GetDescription() => weapon.GetDescription() + " + Critical";
}Stacking decorators at runtime:
public class Player : MonoBehaviour
{
private void Start()
{
IWeapon weapon = new BasicSword();
Debug.Log($"{weapon.GetDescription()} deals {weapon.GetDamage()} damage");
weapon = new FireEnchantment(weapon);
Debug.Log($"{weapon.GetDescription()} deals {weapon.GetDamage()} damage");
weapon = new CriticalEnchantment(weapon);
Debug.Log($"{weapon.GetDescription()} deals {weapon.GetDamage()} damage");
}
}Output:
Basic Sword deals 10 damage
Basic Sword + Fire deals 15 damage
Basic Sword + Fire + Critical deals 30 damageAdvantages:
Disadvantages:
Fire then Critical gives a different result from Critical then Fire.MonoBehaviour for decorators. This keeps the system cleaner and avoids unnecessary component overhead.GetComponent<T>(). If your decorator structure exists outside Unity’s component system, Unity will not automatically find the wrapped object.Update(), cache the calculated values and update them only when something changes.WeaponBuilder keeps the order consistent and intentional.
graph TD
Client[Client] -->|simple call| Facade[Facade]
Facade -->|coordinates| Subsystem1[Subsystem 1]
Facade -->|coordinates| Subsystem2[Subsystem 2]
Facade -->|coordinates| Subsystem3[Subsystem 3]
The Facade pattern provides a simple, unified interface in front of a complex subsystem. Instead of forcing the rest of the code to deal with many different classes, the Facade offers a clean and controlled access point.
The Facade pattern can be summarized as:
Provide a simplified interface to a complex subsystem, hiding its internal complexity from the client.
The Facade does not remove the subsystem. It only hides its complexity from the outside, keeping the code easier to read, maintain and change later.
Facade is useful when several classes need to work together to complete one larger task. Audio systems, UI systems, save/load systems, networking, achievements and third-party SDK integrations are common examples.
It is especially helpful when the same group of operations is repeated in different parts of the project. For example, a single ChangeMusic() method can handle lowering the current track, starting a new track and fading the volume back in, all behind one simple call.
Consider an audio system with separate classes for music, sound effects and fading. A Facade hides this complexity:
public class MusicPlayer
{
private AudioSource source;
public MusicPlayer(AudioSource source) => this.source = source;
public void Play(AudioClip clip) { source.clip = clip; source.Play(); }
public void SetVolume(float v) => source.volume = v;
}
public class SfxPlayer
{
private AudioSource source;
public SfxPlayer(AudioSource source) => this.source = source;
public void PlayOneShot(AudioClip clip) => source.PlayOneShot(clip);
public void SetVolume(float v) => source.volume = v;
}
public class AudioFader
{
public IEnumerator FadeOut(AudioSource source, float duration)
{
float startVolume = source.volume;
for (float t = 0; t < duration; t += Time.deltaTime)
{
source.volume = Mathf.Lerp(startVolume, 0, t / duration);
yield return null;
}
source.volume = 0;
}
}The Facade ties them together behind simple methods:
public class AudioFacade : MonoBehaviour
{
[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource sfxSource;
private MusicPlayer music;
private SfxPlayer sfx;
private AudioFader fader;
private void Awake()
{
music = new MusicPlayer(musicSource);
sfx = new SfxPlayer(sfxSource);
fader = new AudioFader();
}
public void PlayMusic(AudioClip clip) => music.Play(clip);
public void PlaySfx(AudioClip clip) => sfx.PlayOneShot(clip);
public void ChangeMusic(AudioClip newClip, float fadeDuration = 1f)
{
StartCoroutine(ChangeMusicRoutine(newClip, fadeDuration));
}
private IEnumerator ChangeMusicRoutine(AudioClip newClip, float duration)
{
yield return fader.FadeOut(musicSource, duration);
music.Play(newClip);
musicSource.volume = 1f;
}
public void SetMasterVolume(float value)
{
music.SetVolume(value);
sfx.SetVolume(value);
}
}The client only talks to the Facade:
public class Player : MonoBehaviour
{
[SerializeField] private AudioFacade audio;
[SerializeField] private AudioClip explosionClip;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
audio.PlaySfx(explosionClip);
}
}Advantages:
Disadvantages:
GameFacade for everything, split into smaller facades like AudioFacade, UIFacade or SaveFacade.Initialize() method.
graph LR
Client[Client] -->|uses| Proxy[Proxy]
Proxy -->|delegates| RealSubject[Real Subject]
Proxy is another one of my favorites. It is deceptively simple: an object that stands in for another object, controlling how and when the real one gets used. The elegance is in how much power you get without the client ever knowing the difference.
The Proxy pattern can be summarized as:
Provide a placeholder that controls access to another object, adding a layer of indirection for lazy loading, access control, logging or caching.
The proxy implements the same interface as the real object, so the client does not need to know which one it is talking to. The proxy can allow, block, delay, log, cache or even lazy-create the real object before forwarding the request.
Proxy is useful when access to an object needs to be controlled or managed. Common scenarios include:
The interface shared by both the real object and the proxy:
public interface IEnemy
{
void Attack();
}The real, heavy object:
public class Enemy : IEnemy
{
public void Attack()
{
Debug.Log("Enemy attacks!");
}
}The proxy that controls access and creates the real enemy lazily:
public class EnemyProxy : IEnemy
{
private Enemy realEnemy;
private bool canAttack;
public EnemyProxy(bool canAttack)
{
this.canAttack = canAttack;
}
public void Attack()
{
if (!canAttack)
{
Debug.Log("Enemy cannot attack yet.");
return;
}
if (realEnemy == null)
{
Debug.Log("Creating real enemy lazily.");
realEnemy = new Enemy();
}
realEnemy.Attack();
}
}The client only knows the interface:
public class GameManager : MonoBehaviour
{
private void Start()
{
IEnemy lockedEnemy = new EnemyProxy(false);
lockedEnemy.Attack();
IEnemy activeEnemy = new EnemyProxy(true);
activeEnemy.Attack();
}
}Advantages:
Disadvantages:
MonoBehaviour.Update() or FixedUpdate(). Move proxy logic outside the frame loop when possible.
graph TD
Client1[Client 1] -->|requests| Factory[Flyweight Factory]
Client2[Client 2] -->|requests| Factory
Factory -->|reuses| Shared[Shared Flyweight]
Factory -->|creates| Unique1[Unique State 1]
Factory -->|creates| Unique2[Unique State 2]
The Flyweight pattern shares common, immutable data across many objects to save memory. Instead of each object storing all its data, the shared data is extracted into a flyweight object that many instances reference.
The pattern can be summarized as:
Share common state across many objects to reduce memory usage, separating intrinsic data from extrinsic data.
Intrinsic state is what does not change across instances, like the mesh, material, texture, or shared stats of an enemy type. Extrinsic state is what varies per instance, like position, rotation, current health or color tint. In Unity, the intrinsic data often lives in a prefab or a ScriptableObject, and only the extrinsic data stays on each instance.
Flyweight shines when you have thousands of objects that share most of their data. Think a forest where every tree shares the same mesh and material, with only position and scale being unique. Or a bullet hell shooter where hundreds of bullets share the same prefab and behavior.
The pattern is also used heavily in UI systems, tilemaps and particle systems, where Unity internally shares materials, textures and mesh data.
However, if objects are few or each one is truly unique, Flyweight adds unnecessary complexity.
Unity’s prefab system already embodies Flyweight principles. A ScriptableObject makes the pattern explicit:
[CreateAssetMenu(menuName = "Patterns/Flyweight/EnemyType")]
public class EnemyType : ScriptableObject
{
public string enemyName;
public float maxHealth;
public float speed;
public float attackDamage;
public Mesh mesh;
public Material material;
public Color tint;
}Each instance only stores what varies:
public class Enemy : MonoBehaviour
{
[SerializeField] private EnemyType type;
private float currentHealth;
private Vector3 velocity;
private void Start()
{
currentHealth = type.maxHealth;
GetComponent<MeshFilter>().mesh = type.mesh;
GetComponent<MeshRenderer>().material = type.material;
}
public void TakeDamage(float damage)
{
currentHealth -= damage;
}
}public class EnemySpawner : MonoBehaviour
{
[SerializeField] private Enemy prefab;
[SerializeField] private List<EnemyType> types;
private void SpawnEnemy(EnemyType type, Vector3 position)
{
var enemy = Instantiate(prefab, position, Quaternion.identity);
enemy.GetComponent<Enemy>().type = type;
}
}Advantages:
Disadvantages:
ScriptableObjects as flyweight containers. They hold the shared data and can be assigned via the Inspector.
graph LR
Abstraction[Abstraction] --> Implementation[Implementation]
RefinedAbstraction[RefinedAbstraction] --> Implementation
Implementation --> ConcreteImplA[ConcreteImplA]
Implementation --> ConcreteImplB[ConcreteImplB]
The Bridge pattern separates an abstraction from its implementation so that both can evolve independently. Instead of having one monolithic class that handles everything, you split it into two hierarchies connected by a bridge, usually a reference to an interface.
The pattern can be summarized as:
Decouple an abstraction from its implementation so that the two can vary independently.
In Unity, this is useful when you have a single concept that needs to work with multiple platforms, input devices, rendering backends or data sources. The abstraction defines what to do, the implementation handles how to do it.
Bridge is useful when both the abstraction and the implementation are likely to change over time. For example, a character controller might need to support keyboard, gamepad and touch input. Instead of writing three separate controllers, one abstraction uses different input implementations.
It is also useful for cross-platform support, rendering backends, save systems with different storage backends, or any place where you have a clear “what” vs “how” split.
A character controller abstraction with swappable input implementations:
public interface IInputDevice
{
Vector3 GetMovementDirection();
bool IsJumpPressed();
}Different input implementations:
public class KeyboardInput : IInputDevice
{
public Vector3 GetMovementDirection()
{
var direction = new Vector3(
Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
return direction.normalized;
}
public bool IsJumpPressed() => Input.GetButtonDown("Jump");
}
public class GamepadInput : IInputDevice
{
public Vector3 GetMovementDirection()
{
var direction = new Vector3(
Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
return direction.normalized;
}
public bool IsJumpPressed() => Input.GetButtonDown("Jump");
}The abstraction uses the bridge:
public class PlayerController : MonoBehaviour
{
[SerializeField] private float speed = 5f;
[SerializeField] private float jumpForce = 10f;
private IInputDevice input;
private Rigidbody rb;
public void SetInput(IInputDevice device)
{
input = device;
}
private void Awake()
{
rb = GetComponent<Rigidbody>();
SetInput(new KeyboardInput());
}
private void Update()
{
var movement = input.GetMovementDirection() * speed;
rb.linearVelocity = new Vector3(movement.x, rb.linearVelocity.y, movement.z);
if (input.IsJumpPressed())
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}Advantages:
Disadvantages:
graph TD
Invoker[Invoker] -->|stores/executes| Command[Command]
Command -->|calls| Receiver[Receiver]
Client[Client] -->|creates| Command
The Command pattern is a behavioral design pattern that turns a request or action into a separate object instead of executing it directly. Instead of calling something like player.MoveForward() directly, the “move forward” action becomes a MoveCommand object, triggered by calling Execute() on it.
The Command pattern can be summarized as:
Encapsulate a request as an object, decoupling the invoker from the executor.
The main purpose is to separate the code that triggers an action from the code that performs it. For example, an InputHandler does not move the character directly. Instead, it creates the related command and sends it to a CommandInvoker. The command itself contains what should happen, who it should affect, and, if needed, how the action can be undone.
This makes commands easier to store, queue, delay, replay, log, or undo.
The Command pattern is useful when actions need to be managed, not just executed. If an action needs to be stored, undone, redone, delayed, queued, or triggered later, this pattern becomes very helpful.
One of its most common use cases is undo / redo systems. In tools like level editors, drawing programs, or text editors, each user action can be stored as a command. This allows the last executed command to be taken back by calling its Undo() method.
It is also useful for key rebinding systems. If a player wants to bind the “jump” action to a different key, the input system does not need to be tightly connected to the actual behavior. The input system only chooses the correct command, while the command handles the action itself.
The pattern is also commonly used in turn-based games, replay systems, macro systems and combo systems. Commands can be placed in a queue, executed in order, saved, or replayed later in the same sequence.
A typical Unity structure consists of three parts: an ICommand interface with Execute() and optionally Undo(), concrete commands such as MoveCommand that implement the interface, and a CommandInvoker that executes and stores commands.
public interface ICommand
{
void Execute();
void Undo();
}A concrete command encapsulates all the information needed to perform and undo an action:
public class MoveCommand : ICommand
{
private Transform transform;
private Vector3 direction;
private float distance;
private Vector3 previousPosition;
public MoveCommand(Transform transform, Vector3 direction, float distance)
{
this.transform = transform;
this.direction = direction;
this.distance = distance;
}
public void Execute()
{
previousPosition = transform.position;
transform.position += direction * distance;
}
public void Undo()
{
transform.position = previousPosition;
}
}The CommandInvoker executes commands and keeps a history for undo:
public class CommandInvoker : MonoBehaviour
{
private Stack<ICommand> history = new Stack<ICommand>();
public void ExecuteCommand(ICommand command)
{
command.Execute();
history.Push(command);
}
public void UndoLastCommand()
{
if (history.Count == 0)
return;
var last = history.Pop();
last.Undo();
}
}A simple InputHandler that uses commands instead of direct method calls:
public class InputHandler : MonoBehaviour
{
[SerializeField] private Transform player;
[SerializeField] private CommandInvoker invoker;
private void Update()
{
if (Input.GetKeyDown(KeyCode.W))
invoker.ExecuteCommand(new MoveCommand(player, Vector3.forward, 1f));
if (Input.GetKeyDown(KeyCode.S))
invoker.ExecuteCommand(new MoveCommand(player, Vector3.back, 1f));
if (Input.GetKeyDown(KeyCode.Z))
invoker.UndoLastCommand();
}
}Advantages:
Disadvantages:
Undo() from the beginning, not only Execute(). Undoing an action can sometimes be harder than performing it. Each command should keep the state information it needs to reverse itself.CommandInvoker. Calling Execute() directly from random places bypasses the history stack, making undo impossible.
graph TD
Colleague1[Colleague 1] -->|communicates| Mediator[Mediator]
Colleague2[Colleague 2] -->|communicates| Mediator
Colleague3[Colleague 3] -->|communicates| Mediator
Mediator -->|relays| Colleague1
Mediator -->|relays| Colleague2
Mediator -->|relays| Colleague3
The Mediator pattern is a behavioral design pattern that manages communication between objects through a central intermediary instead of letting them talk to each other directly. Its main purpose is to prevent classes from becoming tightly connected to one another.
The Mediator pattern can be summarized as:
Centralize complex communications and control between related objects, so they don’t need to know about each other.
Normally, one object directly calls another. As the project grows, this can create too many references, complicated dependencies, and code flow that becomes hard to follow. With Mediator, objects do not need to know each other; they only know the mediator. Communication happens through this central structure.
In Unity, this is often seen through systems like UIMediator, CombatMediator, GameFlowMediator, or carefully designed manager classes. For example, when an enemy dies, the Enemy class does not directly call the score system, audio system, UI system and achievement system. Instead, it notifies the mediator, which then decides which systems should be updated and what actions should happen.
Mediator is especially useful when one action requires coordination between multiple systems. For example, when the player takes damage, the mediator can coordinate updating the health bar, playing a sound effect, shaking the camera, and notifying the achievement system. Connecting all of these systems directly to the Player class would make the code messy.
It is also useful in UI systems. If buttons, panels, sliders and popups affect each other, it is cleaner to manage them through a UIMediator instead of making each panel directly reference the others.
Use Mediator when:
However, it should not be used everywhere. If there is a simple, fixed, one-to-one relationship between two classes , like Player and PlayerInventory , Mediator may create an unnecessary extra layer.
A typical structure consists of a mediator interface defining the possible notifications, a concrete mediator that routes messages, and colleague classes that communicate only through the mediator.
The mediator interface defines the events that systems can notify:
public interface IGameMediator
{
void NotifyEnemyKilled(Enemy enemy, int scoreValue);
void NotifyPlayerDamaged(int damage);
void NotifyGameOver();
}The concrete mediator coordinates the systems:
public class GameMediator : MonoBehaviour, IGameMediator
{
[SerializeField] private ScoreManager scoreManager;
[SerializeField] private AudioManager audioManager;
[SerializeField] private UIManager uiManager;
public void NotifyEnemyKilled(Enemy enemy, int scoreValue)
{
scoreManager.AddScore(scoreValue);
audioManager.PlaySound("enemy_death");
}
public void NotifyPlayerDamaged(int damage)
{
uiManager.UpdateHealthBar(damage);
audioManager.PlaySound("player_hurt");
}
public void NotifyGameOver()
{
uiManager.ShowGameOverScreen();
audioManager.StopMusic();
}
}An Enemy class that talks only to the mediator, not to every system:
public class Enemy : MonoBehaviour
{
[SerializeField] private int scoreValue = 100;
private IGameMediator mediator;
private void Start()
{
mediator = FindObjectOfType<GameMediator>();
}
public void Die()
{
mediator.NotifyEnemyKilled(this, scoreValue);
Destroy(gameObject);
}
}A Player class that also communicates through the mediator:
public class Player : MonoBehaviour
{
[SerializeField] private int health = 100;
private IGameMediator mediator;
private void Start()
{
mediator = FindObjectOfType<GameMediator>();
}
public void TakeDamage(int damage)
{
health -= damage;
mediator.NotifyPlayerDamaged(damage);
if (health <= 0)
mediator.NotifyGameOver();
}
}Advantages:
Disadvantages:
UIMediator, combat-related communication by a CombatMediator, and level flow by a GameFlowMediator. This prevents one central class from controlling the entire project.GameManager, be careful not to turn it into a class that does everything. A GameManager can manage the general game flow, but UI, combat, audio and input logic should be separated when the project grows.UIMediator instead of directly connecting panels to each other.Enemy, Player or NPC independent from UI and audio systems.GameManager. If the mediator starts getting too large, split it by responsibility.
graph LR
Originator[Originator] -->|creates| Memento[Memento]
Caretaker[Caretaker] -->|stores| Memento
Originator -->|restores from| Memento
The Memento pattern is a behavioral design pattern that allows an object’s current state to be saved without exposing its internal structure, and restored later when needed.
The Memento pattern can be summarized as:
Save the current state of an object so it can be restored later, without breaking encapsulation.
There are three main roles in this pattern. The Originator is the actual object whose state will be saved; it creates its own snapshot and restores itself from that snapshot. The Memento is the container that holds this saved state; its contents are not modified or directly accessed from the outside. The Caretaker is the structure that stores these snapshots; it does not need to know what is inside them, it only keeps them and gives them back when necessary.
Memento is generally used when a system needs to return to a previous state. The most common example is an undo / redo system: before a change is made, the current state is saved, and when undo is triggered, the system returns to the last saved state.
In games, it is often used for save / load, checkpoint and sometimes replay systems. For example, a player’s position, health, inventory or current level can be saved. If the player makes a mistake or hits a dangerous obstacle, the system can restore them to the last saved point.
It is also suitable for level editors, character customization screens, playtesting tools and experimental game mechanics where the user should be able to try something and return to the previous state if needed.
In Unity, the Memento does not need to be a MonoBehaviour. A simple [Serializable] class or struct is usually enough and more efficient.
The Memento stores the snapshot:
[System.Serializable]
public class PlayerMemento
{
public Vector3 Position { get; }
public int Health { get; }
public List<string> Inventory { get; }
public PlayerMemento(Vector3 position, int health, List<string> inventory)
{
Position = position;
Health = health;
Inventory = new List<string>(inventory);
}
}The Originator (the player) creates and restores its own snapshots:
public class Player : MonoBehaviour
{
[SerializeField] private int health = 100;
[SerializeField] private List<string> inventory = new List<string>();
public PlayerMemento SaveState()
{
return new PlayerMemento(transform.position, health, inventory);
}
public void RestoreState(PlayerMemento memento)
{
transform.position = memento.Position;
health = memento.Health;
inventory = new List<string>(memento.Inventory);
}
public void TakeDamage(int damage) => health -= damage;
public void AddItem(string item) => inventory.Add(item);
}The Caretaker stores and retrieves snapshots without knowing their contents:
public class PlayerHistory : MonoBehaviour
{
private Stack<PlayerMemento> history = new Stack<PlayerMemento>();
private const int MaxHistory = 50;
public void Save(PlayerMemento memento)
{
history.Push(memento);
if (history.Count > MaxHistory)
history = new Stack<PlayerMemento>(history.Take(MaxHistory));
}
public PlayerMemento Undo()
{
if (history.Count == 0)
return null;
return history.Pop();
}
}A GameController that ties the pieces together:
public class GameController : MonoBehaviour
{
[SerializeField] private Player player;
[SerializeField] private PlayerHistory history;
private void Update()
{
if (Input.GetKeyDown(KeyCode.S))
history.Save(player.SaveState());
if (Input.GetKeyDown(KeyCode.Z))
{
var memento = history.Undo();
if (memento != null)
player.RestoreState(memento);
}
}
}Advantages:
Disadvantages:
MonoBehaviour. A simple [Serializable] class is enough. The purpose of Memento is to carry the data of a specific moment, not to exist as an object in the scene.Stack<T> makes sense because the last saved state is the first one to be restored. However, this stack should not be unlimited; limit it to 50 or 100 records.List<Item> or List<string> are assigned directly, a real snapshot is not created. Always create a new list: new List<string>(original).[Serializable] and convert it to JSON with JsonUtility. Save files under Application.persistentDataPath.
graph TD
Subject[Subject] -->|notifies| Observer1[Observer 1]
Subject -->|notifies| Observer2[Observer 2]
Subject -->|notifies| Observer3[Observer 3]
The Observer pattern is, without a doubt, one of my favorite patterns. It is everywhere , C# events, UnityEvents, delegates, Actions , and once you internalize it, you start seeing opportunities to decouple code everywhere. It is the backbone of event-driven architecture in Unity.
The Observer pattern can be summarized as:
Define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically.
There are two sides in this structure: the Subject holds the data or triggers the event, and the Observers listen for that change and react when it happens. The Subject does not need to know exactly who is listening; it just publishes, and whoever cares reacts.
The Observer pattern is useful when one event needs to affect multiple systems, especially when you do not know in advance how many objects will react to a change, or when that number may grow later.
In Unity, common use cases include: player health changes updating the HUD, playing a damage sound, triggering a screen shake and checking for game over; score updates refreshing the UI; quest progression; achievement unlocks; and general game events that ripple across systems.
Instead of writing all of this logic inside the Player class , creating a tangled web of references , each related system listens for the event and handles its own responsibility independently.
C# events are a natural fit for the Observer pattern. The Subject exposes an event, and any number of Observers subscribe to it.
The Subject that holds the data and raises the event:
public class PlayerHealth : MonoBehaviour
{
public event System.Action<int> OnHealthChanged;
public event System.Action OnPlayerDied;
[SerializeField] private int maxHealth = 100;
private int currentHealth;
private void Start()
{
currentHealth = maxHealth;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
OnHealthChanged?.Invoke(currentHealth);
if (currentHealth <= 0)
OnPlayerDied?.Invoke();
}
public void Heal(int amount)
{
currentHealth = Mathf.Min(currentHealth + amount, maxHealth);
OnHealthChanged?.Invoke(currentHealth);
}
}Observers subscribe in OnEnable and unsubscribe in OnDisable to avoid dangling references:
public class HealthUI : MonoBehaviour
{
[SerializeField] private PlayerHealth playerHealth;
private void OnEnable()
{
playerHealth.OnHealthChanged += UpdateHealthBar;
}
private void OnDisable()
{
playerHealth.OnHealthChanged -= UpdateHealthBar;
}
private void UpdateHealthBar(int health)
{
Debug.Log($"Health bar updated: {health}");
}
}Another observer that plays a sound when the player is hurt:
public class AudioManager : MonoBehaviour
{
[SerializeField] private PlayerHealth playerHealth;
private void OnEnable()
{
playerHealth.OnHealthChanged += OnHealthChanged;
playerHealth.OnPlayerDied += OnPlayerDied;
}
private void OnDisable()
{
playerHealth.OnHealthChanged -= OnHealthChanged;
playerHealth.OnPlayerDied -= OnPlayerDied;
}
private void OnHealthChanged(int health)
{
Debug.Log("Playing damage sound");
}
private void OnPlayerDied()
{
Debug.Log("Playing death sound");
}
}Advantages:
Disadvantages:
OnEnable and unsubscribe in OnDisable. This is the most important habit when using Observer in Unity.UnityEvent is useful for Inspector-based wiring between designers and programmers, while C# event or Action delegates are lighter and better for code-only, frequent calls.Debug.Log lines can make the event flow much easier to trace.
graph TD
Context[Context] -->|delegates| State[State]
State --> ConcreteStateA[ConcreteStateA]
State --> ConcreteStateB[ConcreteStateB]
ConcreteStateA -->|transition| ConcreteStateB
ConcreteStateB -->|transition| ConcreteStateA
The State pattern is a behavioral design pattern that allows an object to change its behavior depending on its current internal state. The object stays the same, but it behaves differently in different situations.
The State pattern can be summarized as:
Allow an object to alter its behavior when its internal state changes, as if it were a different class.
In this pattern, each state is a separate class. The main object, called the Context, keeps track of the active state and delegates behavior to that state object. This avoids long if-else or switch blocks and creates a cleaner, more readable structure that respects the Open/Closed principle.
The State pattern is useful when an object’s behavior changes frequently at runtime and these changes start creating complicated conditions in the code. It is especially helpful when the same object needs to act in completely different ways depending on its current mode.
In Unity, it is commonly used for:
However, if there are only two or three simple states, a bool, enum or small conditional structure may be enough.
A typical structure consists of an IState interface with Enter(), Tick() and Exit() methods, a StateMachine that manages transitions, and concrete state classes.
The state interface:
public interface IState
{
void Enter();
void Tick();
void Exit();
}A concrete state that handles the chase behavior:
public class ChaseState : IState
{
private Enemy enemy;
private Transform target;
public ChaseState(Enemy enemy, Transform target)
{
this.enemy = enemy;
this.target = target;
}
public void Enter()
{
Debug.Log("Entering Chase state");
}
public void Tick()
{
enemy.transform.position = Vector3.MoveTowards(
enemy.transform.position,
target.position,
enemy.ChaseSpeed * Time.deltaTime
);
float distance = Vector3.Distance(enemy.transform.position, target.position);
if (distance < enemy.AttackRange)
enemy.ChangeState(new AttackState(enemy, target));
else if (distance > enemy.DetectRange)
enemy.ChangeState(new IdleState(enemy));
}
public void Exit()
{
Debug.Log("Exiting Chase state");
}
}Another state for attacking:
public class AttackState : IState
{
private Enemy enemy;
private Transform target;
public AttackState(Enemy enemy, Transform target)
{
this.enemy = enemy;
this.target = target;
}
public void Enter() => Debug.Log("Entering Attack state");
public void Tick()
{
float distance = Vector3.Distance(enemy.transform.position, target.position);
if (distance > enemy.AttackRange)
enemy.ChangeState(new ChaseState(enemy, target));
}
public void Exit() => Debug.Log("Exiting Attack state");
}An idle state:
public class IdleState : IState
{
private Enemy enemy;
public IdleState(Enemy enemy)
{
this.enemy = enemy;
}
public void Enter() => Debug.Log("Entering Idle state");
public void Tick()
{
var player = GameObject.FindWithTag("Player");
if (player == null) return;
float distance = Vector3.Distance(enemy.transform.position, player.transform.position);
if (distance < enemy.DetectRange)
enemy.ChangeState(new ChaseState(enemy, player.transform));
}
public void Exit() => Debug.Log("Exiting Idle state");
}The Enemy Context that delegates to the current state:
public class Enemy : MonoBehaviour
{
public float DetectRange = 10f;
public float AttackRange = 2f;
public float ChaseSpeed = 3f;
private IState currentState;
private void Start()
{
ChangeState(new IdleState(this));
}
private void Update()
{
currentState?.Tick();
}
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState.Enter();
}
}Advantages:
Disadvantages:
Enter, Tick and Exit clearly for each state. Enter sets things up, Tick runs the logic each frame, and Exit cleans up before transitioning.StateMachine, rather than from inside the states themselves when possible.Animator, use Animator.StringToHash() instead of repeating string parameters for better performance.
graph TD
Context[Context] -->|uses| Strategy[Strategy]
Strategy --> StrategyA[Strategy A]
Strategy --> StrategyB[Strategy B]
Strategy --> StrategyC[Strategy C]
The Strategy pattern is a behavioral design pattern that lets you define a family of algorithms in separate classes and switch between them at runtime. The main class, called the Context, only knows the common interface; it does not need to understand how each strategy works internally.
The Strategy pattern can be summarized as:
Define a family of interchangeable algorithms and let them vary independently from the clients that use them.
For example, a character’s attack type can change: sword attack, bow attack or magic attack. The character itself stays the same; only the attack strategy changes. When you want to add a new behavior, you simply create a new strategy class instead of modifying the existing system.
Strategy is used when the same behavior has different variants and you need to switch between them at runtime. In general, if you see if-else or switch blocks growing inside a class, Strategy might be a good candidate.
In Unity, it is commonly used for:
However, if there are only two simple options and the system is unlikely to grow, using Strategy may add unnecessary complexity.
Most strategies do not need Unity’s MonoBehaviour lifecycle; plain C# classes are often better. The Context holds a reference to the interface and delegates to it.
The strategy interface:
public interface IAttackStrategy
{
void Attack(Transform origin, LayerMask targetMask);
}Concrete strategies implement the interface with their own logic:
public class SwordAttack : IAttackStrategy
{
public void Attack(Transform origin, LayerMask targetMask)
{
Debug.Log("Swinging sword!");
var hits = Physics.OverlapSphere(origin.position, 1.5f, targetMask);
foreach (var hit in hits)
Debug.Log($"Hit {hit.name} with sword");
}
}
public class BowAttack : IAttackStrategy
{
public void Attack(Transform origin, LayerMask targetMask)
{
Debug.Log("Shooting arrow!");
if (Physics.Raycast(origin.position, origin.forward, out var hit, 50f, targetMask))
Debug.Log($"Hit {hit.collider.name} with arrow");
}
}
public class MagicAttack : IAttackStrategy
{
public void Attack(Transform origin, LayerMask targetMask)
{
Debug.Log("Casting fireball!");
var hits = Physics.OverlapSphere(origin.position + origin.forward * 3f, 2f, targetMask);
foreach (var hit in hits)
Debug.Log($"Hit {hit.name} with magic");
}
}The Context uses the strategy without knowing the details:
public class PlayerCombat : MonoBehaviour
{
private IAttackStrategy currentStrategy;
private IAttackStrategy swordAttack;
private IAttackStrategy bowAttack;
private IAttackStrategy magicAttack;
[SerializeField] private LayerMask targetMask;
private void Awake()
{
swordAttack = new SwordAttack();
bowAttack = new BowAttack();
magicAttack = new MagicAttack();
}
public void SetStrategy(IAttackStrategy strategy)
{
currentStrategy = strategy;
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
currentStrategy?.Attack(transform, targetMask);
if (Input.GetKeyDown(KeyCode.Alpha1))
SetStrategy(swordAttack);
if (Input.GetKeyDown(KeyCode.Alpha2))
SetStrategy(bowAttack);
if (Input.GetKeyDown(KeyCode.Alpha3))
SetStrategy(magicAttack);
}
}Advantages:
Disadvantages:
MonoBehaviour and work better without it.ScriptableObjects. This allows you to assign them from the Inspector, share them between prefabs, and let designers test behaviors without touching code.
graph LR
Handler1[Handler 1] -->|can't handle| Handler2[Handler 2]
Handler2 -->|can't handle| Handler3[Handler 3]
Handler3 -->|handles| Request[Request]
Chain of Responsibility passes a request through a chain of handlers until one of them processes it. Each handler decides whether to handle the request or pass it to the next handler in the chain.
The pattern can be summarized as:
Avoid coupling the sender of a request to its receiver by giving multiple objects a chance to handle the request.
In Unity, this is useful for input systems where UI should consume clicks before the game world does, for damage pipelines where armor absorbs damage before health is reduced, or for achievement systems that chain-check conditions.
Chain of Responsibility is useful when multiple objects might handle a request and the handler is not known at compile time. It is common in:
If the set of handlers is fixed and known, a simple list or array with a loop is often simpler.
A damage pipeline where each handler can intercept damage:
public abstract class DamageHandler
{
protected DamageHandler next;
public DamageHandler SetNext(DamageHandler handler)
{
next = handler;
return handler;
}
public virtual void Handle(DamageData data)
{
next?.Handle(data);
}
}
public class DamageData
{
public int amount;
public bool consumed;
}Concrete handlers in the chain:
public class DodgeHandler : DamageHandler
{
public override void Handle(DamageData data)
{
if (Random.value < 0.2f)
{
Debug.Log("Dodged!");
data.consumed = true;
return;
}
next?.Handle(data);
}
}
public class ArmorHandler : DamageHandler
{
public override void Handle(DamageData data)
{
int absorbed = Mathf.Min(data.amount, 5);
data.amount -= absorbed;
Debug.Log($"Armor absorbed {absorbed} damage");
next?.Handle(data);
}
}
public class HealthHandler : DamageHandler
{
[SerializeField] private int health = 100;
public override void Handle(DamageData data)
{
health -= data.amount;
Debug.Log($"Health reduced to {health}");
}
}Building and using the chain:
public class CombatSystem : MonoBehaviour
{
private DamageHandler pipeline;
private void Awake()
{
var dodge = new DodgeHandler();
var armor = new ArmorHandler();
var health = new HealthHandler();
dodge.SetNext(armor).SetNext(health);
pipeline = dodge;
}
public void ApplyDamage(int amount)
{
pipeline.Handle(new DamageData { amount = amount });
}
}Advantages:
Disadvantages:
SetNext method that returns the next handler, enabling fluent chain construction.
graph TD
Visitor[Visitor] -->|visits| ElementA[Element A]
Visitor -->|visits| ElementB[Element B]
Client[Client] -->|accepts| Visitor
The Visitor pattern separates an algorithm from the object structure it operates on. Instead of adding a method to every class in a hierarchy each time you need a new operation, you write a single Visitor class that handles all types.
I love this one. Its power is subtle, it takes a moment to click, but once it does you see the elegance. Adding a new operation across a whole hierarchy without touching a single existing class feels like cheating.
The pattern can be summarized as:
Separate an algorithm from the object structure it operates on, allowing you to add new operations without modifying the classes.
Visitor is useful when you have a stable object hierarchy but frequently need to add new operations across all types. Common examples include applying effects to different enemy types, serializing a complex object graph, calculating damage based on weapon-vs-armor type combinations, or generating reports from a data model.
It becomes especially powerful when the alternative would be a cascade of if (enemy is Goblin) type checks scattered across the codebase, or adding a method to every type every time a new feature is needed.
An enemy hierarchy that accepts visitors:
public interface IEnemyVisitor
{
void Visit(Goblin goblin);
void Visit(Orc orc);
void Visit(Dragon dragon);
}
public abstract class Enemy : MonoBehaviour
{
public abstract void Accept(IEnemyVisitor visitor);
}
public class Goblin : Enemy
{
public override void Accept(IEnemyVisitor visitor) => visitor.Visit(this);
public int StolenGold = 5;
}
public class Orc : Enemy
{
public override void Accept(IEnemyVisitor visitor) => visitor.Visit(this);
public int Rage = 100;
}
public class Dragon : Enemy
{
public override void Accept(IEnemyVisitor visitor) => visitor.Visit(this);
public bool CanBreatheFire = true;
}Visitors that add operations without touching the enemy classes:
public class DamageCalculator : IEnemyVisitor
{
public int Result { get; private set; }
public void Visit(Goblin goblin) => Result = 10 + goblin.StolenGold;
public void Visit(Orc orc) => Result = 20 + orc.Rage / 10;
public void Visit(Dragon dragon) => Result = 50 + (dragon.CanBreatheFire ? 30 : 0);
}
public class LootGenerator : IEnemyVisitor
{
public List<string> Result { get; private set; }
public void Visit(Goblin goblin)
{
Result = new List<string> { "Gold Coin", "Rusty Dagger" };
}
public void Visit(Orc orc)
{
Result = new List<string> { "Orcish Axe", "Leather Armor" };
}
public void Visit(Dragon dragon)
{
Result = new List<string> { "Dragon Scale", "Fire Essence", "Legendary Sword" };
}
}Using the visitor:
public class CombatManager : MonoBehaviour
{
public void CalculateAndLoot(Enemy enemy)
{
var damageCalc = new DamageCalculator();
enemy.Accept(damageCalc);
Debug.Log($"Damage dealt: {damageCalc.Result}");
var lootGen = new LootGenerator();
enemy.Accept(lootGen);
Debug.Log($"Loot: {string.Join(", ", lootGen.Result)}");
}
}Advantages:
Disadvantages:
Accept method is just double dispatch. It calls visitor.Visit(this), which invokes the correct overload based on the concrete type at compile time.
graph TD
DIContainer[DI Container] -->|provides| ServiceA[Service A]
DIContainer -->|provides| ServiceB[Service B]
Client[Client] -->|requests dependencies| DIContainer
Dependency Injection is the dragon slayer of coupled code. It does one simple thing, a class receives its dependencies from the outside instead of creating them internally, and in doing so it dismantles the tangled web of references that makes projects rigid and untestable.
The Dependency Injection principle can be summarized as:
Depend on abstractions, not on concrete implementations. Let someone else provide the objects you need.
A class should not need to know how the systems it uses are created. It should only use the dependency given to it. This keeps systems like PlayerController, AudioManager, SaveSystem and InputService loosely connected. The responsibility of creating and wiring objects moves outside the class, a concept known as Inversion of Control.
Dependency Injection is useful when a system may need to change later. For example, you may use LocalSaveSystem today and replace it with CloudSaveSystem later. With DI, you do not need to modify the PlayerController; you only change the dependency being provided.
It is also very useful when writing tests. Instead of using a real AudioManager or HealthService, you can provide a fake version such as MockAudioManager or FakeHealthService. This makes it easier to test classes without setting up a full Unity scene.
DI becomes more valuable as a project grows. When systems start depending on each other, creating them directly inside each class can quickly become messy. Managing dependencies from a central place keeps the project cleaner.
For very small prototypes, simple data classes or one-file experiments, DI may be unnecessary.
Classic constructor injection does not work directly with MonoBehaviour because Unity creates those objects. The most common approaches in Unity are:
The interface that defines the contract:
public interface IHealthService
{
void Reduce(int amount);
int GetCurrentHealth();
}A concrete implementation:
public class HealthService : IHealthService
{
private int health = 100;
public void Reduce(int amount)
{
health -= amount;
Debug.Log($"Health reduced by {amount}, current: {health}");
}
public int GetCurrentHealth() => health;
}The class receives its dependency through the constructor:
public class Player
{
private readonly IHealthService healthService;
public Player(IHealthService healthService)
{
this.healthService = healthService;
}
public void TakeDamage(int amount)
{
healthService.Reduce(amount);
}
}A bootstrap that wires everything together:
public class GameInstaller : MonoBehaviour
{
private void Awake()
{
IHealthService healthService = new HealthService();
var player = new Player(healthService);
player.TakeDamage(10);
}
}Advantages:
Disadvantages:
Bootstrap or GameInstaller object can create the services once and pass them to the scripts that need them.MonoBehaviour, use [SerializeField] references from the Inspector or pass dependencies manually through an Init(...) method.FindObjectOfType, GameObject.Find or repeated GetComponent calls. These make your classes depend on scene structure.ISaveService instead of SaveService so you can swap implementations later.ScriptableObjects can work as a lightweight alternative to DI. Shared data or service references can be placed inside a ScriptableObject and assigned through the Inspector.GameManager.Instance may look convenient, but over time it creates a codebase where everything depends on everything else. DI keeps dependencies clear, visible and manageable.
graph TD
View[View] -->|user input| Presenter[Presenter]
Presenter -->|updates| Model[Model]
Model -->|notifies| Presenter
Presenter -->|updates UI| View
I am not the biggest fan of MVP in Unity, but I cannot deny it has its place. If your UI is complex enough and you need to test it without opening a scene, MVP delivers. For simpler projects, though, the extra boilerplate can feel like ceremony without reward.
MVP divides a screen into three separate responsibilities:
The pattern can be summarized as:
Separate the UI into Model, View and Presenter, so the UI logic can be developed and tested independently from Unity.
MVP is most useful when the UI starts to grow. If a project only has a small prototype with a single button, this structure may be unnecessary. However, with multiple inputs, different screen states, validation steps or complex user flows, MVP makes the code easier to control.
It is a good choice when you want to test UI logic without opening Unity, since the Presenter can be a plain C# class. It also helps when the same game logic needs to be shown through different interfaces or when the team grows and designers and programmers need to work in parallel.
A simple coin counter demonstrates the pattern clearly:
public class CoinModel
{
public int Coins { get; private set; }
public void AddCoin() => Coins++;
}The View exposes input events and displays data:
public class CoinView : MonoBehaviour
{
[SerializeField] private Button addButton;
[SerializeField] private TMP_Text coinText;
public event Action OnAddCoinClicked;
private void Start()
{
addButton.onClick.AddListener(() => OnAddCoinClicked?.Invoke());
}
public void UpdateCoinDisplay(int coins)
{
coinText.text = $"Coins: {coins}";
}
}The Presenter orchestrates the flow:
public class CoinPresenter
{
private readonly CoinModel model;
private readonly CoinView view;
public CoinPresenter(CoinModel model, CoinView view)
{
this.model = model;
this.view = view;
view.OnAddCoinClicked += HandleAddCoin;
}
private void HandleAddCoin()
{
model.AddCoin();
view.UpdateCoinDisplay(model.Coins);
}
public void Dispose()
{
view.OnAddCoinClicked -= HandleAddCoin;
}
}A bootstrap wires everything together:
public class CoinScreen : MonoBehaviour
{
[SerializeField] private CoinView coinView;
private CoinPresenter presenter;
private void Start()
{
var model = new CoinModel();
presenter = new CoinPresenter(model, coinView);
}
private void OnDestroy()
{
presenter?.Dispose();
}
}Advantages:
Disadvantages:
MonoBehaviour. If you need lifecycle methods, manage them through the View.
graph TD
Client1[Client 1] -->|requests| Locator[Service Locator]
Client2[Client 2] -->|requests| Locator
Locator -->|returns| ServiceA[Service A]
Locator -->|returns| ServiceB[Service B]
I really love this one. It is simple, practical and hits a sweet spot between the rigidity of Singletons and the ceremony of full DI frameworks. A central registry where services register themselves and clients request them by interface, nothing more, nothing less.
The Service Locator pattern can be summarized as:
Provide a central registry where services register themselves, and clients request them by type, without knowing the concrete implementation.
At first it may look similar to Singleton, but the key difference is that Singleton creates a direct dependency on a concrete class, while Service Locator keeps the dependency flexible through interfaces. In Unity projects where scene management, object lifetimes and testing matter, that difference is huge.
Service Locator is useful when multiple systems need access to the same service. If different parts of the game need to play sounds, save data, read player input or send analytics events, managing these services from one place makes the project easier to control.
It is a more organized alternative when Singleton usage starts scattering across the project. Instead of making every system directly know about each other, services are accessed through the locator, reducing direct coupling.
It is also useful when you want to swap a service at runtime. The real game uses AudioService, while tests use NullAudioService or MockAudioService, without changing any client code.
A simple but effective implementation:
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> services = new();
public static void Register<T>(T service)
{
var type = typeof(T);
if (services.ContainsKey(type))
{
Debug.LogWarning($"{type.Name} already registered, overwriting.");
services[type] = service;
return;
}
services.Add(type, service);
}
public static T Get<T>()
{
var type = typeof(T);
if (!services.TryGetValue(type, out var service))
throw new Exception($"Service of type {type.Name} is not registered.");
return (T)service;
}
public static void Unregister<T>()
{
services.Remove(typeof(T));
}
public static void Clear()
{
services.Clear();
}
}Services register themselves early:
public class AudioService : MonoBehaviour
{
private void Awake()
{
ServiceLocator.Register<IAudioService>(this);
}
private void OnDestroy()
{
ServiceLocator.Unregister<IAudioService>();
}
}Clients request services when needed:
public class Player : MonoBehaviour
{
private IAudioService audio;
private void Start()
{
audio = ServiceLocator.Get<IAudioService>();
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
audio.PlaySfx("jump");
}
}Advantages:
Disadvantages:
Get<T>() calls.IAudioService instead of AudioService to make future changes easy.Awake() and clients should retrieve them in Start(). This avoids requesting a service before it has been registered.DontDestroyOnLoad, the same service may be registered again. Clear old services or warn on overwrite.OnDestroy(). Otherwise, the locator may hold references to destroyed objects, causing NullReferenceExceptions on scene changes.ServiceLocator.Get<T>() every frame. Retrieve the service once in Start() and store it in a field.
graph TD
Entity[Entity] -->|has| Component1[Component 1]
Entity -->|has| Component2[Component 2]
System1[System 1] -->|queries| Component1
System2[System 2] -->|queries| Component2
ECS, or Entity Component System, is an architectural pattern that structures code around data rather than objects. It flips the traditional MonoBehaviour approach on its head: instead of each object owning its behavior, data lives in components, behavior lives in systems, and entities are just IDs that group components together.
The pattern can be summarized as:
Structure code as data-oriented entities composed of components, processed by systems that operate on all matching entities at once.
Unity has its own high-performance ECS implementation: Unity DOTS (Data-Oriented Technology Stack). It includes the Entities package, the Job System, and the Burst Compiler. However, DOTS is not a replacement for MonoBehaviour, it is a specialized tool for CPU-bound scenarios with thousands of entities.
ECS shines when you need to process thousands of similar entities efficiently. Common use cases include large-scale simulations, crowd systems, bullet hell patterns, destructible environments, and any scenario where MonoBehaviours with individual Update() calls become a bottleneck.
For typical games with dozens or hundreds of objects, MonoBehaviour is simpler and more than sufficient. ECS adds significant complexity to the project structure and workflow.
A lightweight ECS-like structure without DOTS, just the mental model:
public struct HealthComponent
{
public int current;
public int max;
}
public struct MovementComponent
{
public Vector3 velocity;
public float speed;
}Systems process entities that have the right components:
public class MovementSystem
{
public void Update(List<int> entities,
Dictionary<int, MovementComponent> movements,
Dictionary<int, Transform> transforms,
float deltaTime)
{
foreach (var id in entities)
{
if (!movements.ContainsKey(id) || !transforms.ContainsKey(id))
continue;
var move = movements[id];
var tf = transforms[id];
tf.position += move.velocity * move.speed * deltaTime;
}
}
}
public class HealthSystem
{
public void Update(List<int> entities,
Dictionary<int, HealthComponent> healths)
{
foreach (var id in entities)
{
if (!healths.ContainsKey(id)) continue;
var health = healths[id];
if (health.current <= 0)
Debug.Log($"Entity {id} died");
}
}
}Advantages:
Disadvantages:
graph TD
A[Process Input] --> B[Update Game State]
B --> C[Render Frame]
C --> D[Wait for next frame]
D --> A
The Game Loop is the heartbeat of every real-time game. It decouples the passage of game time from user input and rendering speed, ensuring the simulation runs at a consistent rate regardless of frame rate.
The pattern can be summarized as:
Decouple the progression of game time from user input and frame rate, ensuring a stable and deterministic simulation.
In Unity, you do not write the game loop yourself, the engine provides it through Update, FixedUpdate and LateUpdate. Understanding how it works under the hood, however, is essential for building games that feel smooth and behave consistently.
If you are using Unity, the game loop is already there. You only need to implement your own when building an engine from scratch or when you need custom timing behavior that Unity’s built-in loop does not provide.
Understanding the game loop matters when you need to decide between Update (runs every rendered frame, variable delta time) and FixedUpdate (runs at a fixed timestep independent of frame rate). Physics goes in FixedUpdate, input and rendering-dependent logic go in Update.
The classic game loop decouples simulation from rendering using a fixed timestep with an accumulator, as described by Glenn Fiedler in his essential article Fix Your Timestep:
double t = 0.0;
double dt = 0.01;
double currentTime = Time.realtimeSinceStartupAsDouble;
double accumulator = 0.0;
while (running)
{
double newTime = Time.realtimeSinceStartupAsDouble;
double frameTime = newTime - currentTime;
currentTime = newTime;
accumulator += frameTime;
while (accumulator >= dt)
{
Integrate(state, t, dt);
accumulator -= dt;
t += dt;
}
double alpha = accumulator / dt;
state = Interpolate(previousState, currentState, alpha);
Render(state);
}The key ideas are:
dt, never more, never less. This guarantees deterministic, stable physics regardless of frame rate.alpha, the fraction of dt remaining in the accumulator. This produces smooth visuals even when the render rate and simulation rate do not align.Without this decoupling, a variable frame rate causes the simulation to behave differently every time it runs, fast machines and slow machines get different results, and physics can explode or objects can tunnel through walls.
Unity already implements a variant of this pattern:
FixedUpdate runs the physics at a fixed timestep, configurable in Edit > Project Settings > Time > Fixed Timestep (default 0.02s, 50Hz).Update runs every rendered frame at variable delta time, available via Time.deltaTime.LateUpdate runs after all Update calls, useful for camera follow and post-processing logic.The practical rule in Unity is: put physics and anything that needs deterministic behavior in FixedUpdate; put input, rendering-dependent logic and anything that benefits from variable frame rate in Update.
Advantages:
Disadvantages:
Update vs FixedUpdate correctly.Time.deltaTime in Update and Time.fixedDeltaTime in FixedUpdate for frame-independent calculations.Time.fixedDeltaTime in a loop inside Update with your own accumulator, following the pattern from the article above.
graph TD
Data[Data changes] -->|sets| Flag[Dirty Flag]
System[System] -->|checks| Flag
Flag -->|if dirty| Recalculate[Recalculate]
Recalculate -->|clears| Flag
Dirty Flag is a simple optimization pattern that tracks whether data has changed, so you can skip redundant expensive calculations. If the data has not changed, there is no need to recalculate.
The pattern can be summarized as:
Use a boolean flag to track unprocessed changes, and recalculate only when needed.
When the data changes, the flag becomes true. When the result is requested, the flag is checked: if dirty, the value is recalculated and cached, and the flag is reset. If clean, the cached value is returned immediately.
Dirty Flag is useful when a calculation is expensive but the data does not change very often. It works best when a value is read many times but updated only sometimes.
Common use cases include:
It also helps when you want to delay updates and process multiple changes at once, instead of recalculating after every small change. However, if the calculation is already cheap or if the data changes every frame, Dirty Flag adds complexity without benefit.
Property setters are the safest way to trigger the flag, avoiding the risk of forgetting to mark dirty when a field changes directly:
public class PlayerStats : MonoBehaviour
{
[SerializeField] private int strength = 10;
[SerializeField] private int agility = 5;
[SerializeField] private int intelligence = 3;
private int cachedPower;
private bool isDirty = true;
public int Strength
{
get => strength;
set
{
if (strength == value) return;
strength = value;
isDirty = true;
}
}
public int Agility
{
get => agility;
set
{
if (agility == value) return;
agility = value;
isDirty = true;
}
}
public int Intelligence
{
get => intelligence;
set
{
if (intelligence == value) return;
intelligence = value;
isDirty = true;
}
}
public int GetPower()
{
if (isDirty)
{
RecalculatePower();
isDirty = false;
}
return cachedPower;
}
private void RecalculatePower()
{
Debug.Log("Recalculating power...");
cachedPower = strength * 2 + agility * 3 + intelligence * 4;
}
}public class GameController : MonoBehaviour
{
[SerializeField] private PlayerStats player;
private void Update()
{
var power = player.GetPower();
if (Input.GetKeyDown(KeyCode.Alpha1))
player.Strength += 1;
if (Input.GetKeyDown(KeyCode.Alpha2))
player.Agility += 1;
}
}Advantages:
Disadvantages:
isDirty = true. Avoid changing backing fields directly.LateUpdate() rather than Update() when possible, so all changes for the frame have already been applied.OnValidate() to mark dirty when values are edited in the editor.transform.hasChanged and UI layout rebuild systems. Note that transform.hasChanged is not reset automatically after reading.true.
graph TD
Client[Client] -->|requests| Pool[Object Pool]
Pool -->|returns existing| Object1[Object]
Pool -->|creates new| Object2[Object]
Client -->|returns| Object1
Object1 --> Pool
Object Pooling is a pattern where objects that are expensive to create and destroy are prepared in advance and stored in a pool. When an object is needed, it is taken from the pool instead of being newly created. When done, it is returned to the pool instead of being destroyed. The idea is simple: stop throwing objects away and reuse them.
The pattern can be summarized as:
Maintain a pool of pre-instantiated objects, reusing them instead of constantly creating and destroying.
Unity has its own built-in pool system since 2021: UnityEngine.Pool.ObjectPool<T>. If you are using a recent version, you can use it directly instead of writing a custom one.
Object Pooling is useful when the same type of object is created and removed many times during gameplay. It is a good choice when:
Instantiate and Destroy calls cause frame drops or stutters.For objects that appear only once or a few times in a scene, pooling adds unnecessary complexity.
A simple generic pool implementation:
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int initialSize = 20;
private readonly Queue<GameObject> pool = new Queue<GameObject>();
private void Awake()
{
for (int i = 0; i < initialSize; i++)
{
var obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject Get()
{
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
var newObj = Instantiate(prefab);
newObj.SetActive(true);
return newObj;
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}A bullet that returns itself to the pool:
public class Bullet : MonoBehaviour
{
[SerializeField] private float lifetime = 2f;
private ObjectPool pool;
public void Init(ObjectPool parentPool)
{
pool = parentPool;
Invoke(nameof(ReturnToPool), lifetime);
}
private void ReturnToPool()
{
CancelInvoke();
pool.Return(gameObject);
}
private void OnDisable()
{
CancelInvoke();
}
}Using the pool to fire bullets:
public class Gun : MonoBehaviour
{
[SerializeField] private ObjectPool bulletPool;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
var bullet = bulletPool.Get();
bullet.transform.position = transform.position;
bullet.GetComponent<Bullet>().Init(bulletPool);
}
}
}Advantages:
Instantiate and Destroy, lowering Garbage Collector pressure.Disadvantages:
Instantiate and Destroy.UnityEngine.Pool.ObjectPool<T> provides a built-in, allocation-free pool. Prefer it over custom implementations unless you need specific behavior.
graph TD
World[World] -->|divides into| Grid[Grid / Quadtree]
Grid --> Cell1[Cell 1]
Grid --> Cell2[Cell 2]
Grid --> Cell3[Cell 3]
Entity1[Entity] --> Cell1
Entity2[Entity] --> Cell2
Spatial Partition is an optimization technique that organizes objects in a game world based on their physical position. Instead of checking every object against every other object, the world is divided into smaller regions. This is not just a nice idea, it is essential knowledge for game development. Without it, games with hundreds or thousands of objects would grind to a halt. Every open-world game, every RTS with unit selection, every collision-heavy action game relies on some form of spatial partitioning to stay performant.
The pattern can be summarized as:
Divide the game world into regions so that spatial queries only need to check nearby objects, not the entire scene.
When something happens in one area, the game only checks the objects near that area. Collision checks, distance checks, visibility checks and “who is near me?” searches become vastly cheaper, especially in scenes with many objects.
Spatial Partition is useful when a scene contains many dynamic objects that need proximity, collision or range checks. Common use cases include open-world object management, bullet-enemy collision systems, RTS unit selection, crowd simulations, flocking behavior and chunk-based world loading.
However, if a scene only has a few objects, a simple loop through all of them may be faster and easier to maintain.
A simple spatial grid that divides the world into cells:
public class SpatialGrid
{
private float cellSize;
private Dictionary<Vector2Int, List<Transform>> grid = new();
public SpatialGrid(float cellSize)
{
this.cellSize = cellSize;
}
public void Clear() => grid.Clear();
public void Add(Transform obj)
{
var cell = GetCell(obj.position);
if (!grid.ContainsKey(cell))
grid[cell] = new List<Transform>();
grid[cell].Add(obj);
}
public List<Transform> GetNearby(Vector3 position)
{
var nearby = new List<Transform>();
var centerCell = GetCell(position);
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
var cell = centerCell + new Vector2Int(x, y);
if (grid.TryGetValue(cell, out var objects))
nearby.AddRange(objects);
}
}
return nearby;
}
private Vector2Int GetCell(Vector3 position)
{
return new Vector2Int(
Mathf.FloorToInt(position.x / cellSize),
Mathf.FloorToInt(position.z / cellSize)
);
}
}Using the grid for efficient collision detection:
public class CollisionSystem : MonoBehaviour
{
[SerializeField] private float cellSize = 5f;
private SpatialGrid grid;
private void Awake()
{
grid = new SpatialGrid(cellSize);
}
private void Update()
{
grid.Clear();
foreach (var enemy in FindObjectsOfType<Enemy>())
grid.Add(enemy.transform);
foreach (var enemy in FindObjectsOfType<Enemy>())
{
var nearby = grid.GetNearby(enemy.transform.position);
foreach (var other in nearby)
{
// Only check collision with nearby objects
}
}
}
}While the uniform grid shown above works well for evenly distributed objects, different game scenarios demand different structures. Spatial partitioning in game development is a deep topic, and knowing when to use each structure is what separates a performant game from a stuttering one.
Quadtrees divide a 2D space recursively into four quadrants. Each node can hold a limited number of objects; when a node overflows, it splits into four children. This adapts naturally to object density: empty areas become coarse nodes, while dense areas refine further. Quadtrees are ideal for 2D games, top-down spatial queries, and terrain systems.
public class QuadTree
{
private const int MaxObjects = 4;
private const int MaxLevels = 5;
private int level;
private List<Transform> objects;
private Rect bounds;
private QuadTree[] children;
public QuadTree(int level, Rect bounds)
{
this.level = level;
this.bounds = bounds;
objects = new List<Transform>();
children = new QuadTree[4];
}
public void Clear()
{
objects.Clear();
for (int i = 0; i < children.Length; i++)
{
if (children[i] != null)
{
children[i].Clear();
children[i] = null;
}
}
}
private void Split()
{
float subWidth = bounds.width / 2f;
float subHeight = bounds.height / 2f;
float x = bounds.x;
float y = bounds.y;
children[0] = new QuadTree(level + 1, new Rect(x + subWidth, y, subWidth, subHeight));
children[1] = new QuadTree(level + 1, new Rect(x, y, subWidth, subHeight));
children[2] = new QuadTree(level + 1, new Rect(x, y + subHeight, subWidth, subHeight));
children[3] = new QuadTree(level + 1, new Rect(x + subWidth, y + subHeight, subWidth, subHeight));
}
private int GetIndex(Transform obj)
{
var pos = obj.position;
bool topHalf = pos.y > bounds.y + bounds.height / 2f;
bool rightHalf = pos.x > bounds.x + bounds.width / 2f;
if (rightHalf && !topHalf) return 0;
if (!rightHalf && !topHalf) return 1;
if (!rightHalf && topHalf) return 2;
return 3;
}
public void Insert(Transform obj)
{
if (children[0] != null)
{
children[GetIndex(obj)].Insert(obj);
return;
}
objects.Add(obj);
if (objects.Count > MaxObjects && level < MaxLevels)
{
if (children[0] == null) Split();
for (int i = objects.Count - 1; i >= 0; i--)
{
children[GetIndex(objects[i])].Insert(objects[i]);
objects.RemoveAt(i);
}
}
}
public List<Transform> Query(Rect area)
{
var result = new List<Transform>();
if (!bounds.Overlaps(area)) return result;
foreach (var obj in objects)
{
if (area.Contains(obj.position))
result.Add(obj);
}
if (children[0] != null)
{
for (int i = 0; i < 4; i++)
result.AddRange(children[i].Query(area));
}
return result;
}
}Octrees are the 3D counterpart of Quadtrees. Instead of four quadrants, each node splits into eight octants, covering a cubic volume. They are the go-to structure for 3D spatial queries in games with large, non-uniform 3D environments. Use them for 3D collision detection, visibility culling, and voxel-based systems like destructible terrain or Minecraft-style worlds.
Bounding Volume Hierarchies (BVHs) take a different approach. Instead of partitioning space itself, BVHs organize objects into a tree of bounding volumes. Each node stores a volume that encloses all its children. BVHs do not care about empty space, which makes them excellent for ray tracing, frustum culling of dynamic objects, and physics broad-phase detection. When objects move, only the bounding volumes need updating, not the entire spatial structure.
Choosing the right structure depends on your game’s needs. Uniform grids shine when objects are evenly distributed and queries are local. Quadtrees and Octrees excel when object density varies wildly across the world. BVHs dominate when you have many moving objects and need to rebuild or update the tree frequently. Many modern game engines combine multiple structures: a uniform grid for static world geometry, an octree for dynamic entities, and a BVH for the rendering pipeline.
Advantages:
Disadvantages:
Physics.OverlapSphere and Physics.OverlapBox already solve many proximity problems and are heavily optimized.NativeArray and NativeMultiHashMap for efficient multi-threaded processing.OnDrawGizmos with Gizmos.DrawWireCube to see if cells are covering the world correctly.