10 C# Patterns Every Unity Developer Should Know

From Singleton to Observer, master the design patterns that will level up your Unity projects.

Design patterns are proven solutions to recurring problems in software development. In Unity, using the right pattern can mean the difference between a maintainable codebase and spaghetti code. Let's explore the 10 most useful patterns for Unity developers.

These patterns aren't just academic exercises — they're battle-tested tools used in production games of every scale. Whether you're building a mobile puzzler or an open-world RPG, internalizing these patterns will help you write code that's easier to debug, extend, and hand off to teammates. Each pattern below includes a practical Unity example you can drop straight into your project.

1. Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global access point. In Unity, this is commonly used for managers like AudioManager, GameManager, and UIManager. The pattern is simple: a static reference to the single instance, a check in Awake to enforce uniqueness, and optionally DontDestroyOnLoad so the instance survives scene transitions.

While Singletons are convenient, overuse leads to hidden dependencies and difficult testing. Treat them as a last resort for truly global services — audio, save data, input — rather than as a default architecture choice. If you find yourself creating more than three or four Singletons, consider a Service Locator or Dependency Injection approach instead (both covered below).

C#
public class GameManager : Singleton<GameManager>
{
    public int Score { get; private set; }
    
    public void AddScore(int points)
    {
        Score += points;
    }
}

When to use: Global managers that must persist across scenes and be accessible from anywhere — AudioManager, GameManager, SaveManager. Avoid using Singletons for systems that could benefit from multiple instances or polymorphism.

2. Observer Pattern (Events)

The Observer pattern lets objects subscribe to events without tight coupling. Unity's built-in UnityEvent and C# events/delegates make this pattern natural to implement. Instead of object A directly calling methods on objects B, C, and D, object A fires an event and any interested party listens.

This is the single most important decoupling pattern in game development. A health system can broadcast OnPlayerDied without knowing that the UI, the audio system, and the analytics tracker are all listening. You can add or remove listeners without touching the publisher's code at all.

C#
// Publisher
public static event System.Action<int> OnScoreChanged;

public void AddScore(int points)
{
    Score += points;
    OnScoreChanged?.Invoke(Score);
}

// Subscriber
void OnEnable() => GameManager.OnScoreChanged += UpdateScoreUI;
void OnDisable() => GameManager.OnScoreChanged -= UpdateScoreUI;

private void UpdateScoreUI(int newScore)
{
    scoreText.text = $"Score: {newScore}";
}

When to use: Any time one system needs to notify others without knowing who is listening — score changes, player death, level completion, achievement triggers. Always unsubscribe in OnDisable to prevent memory leaks.

3. Object Pool Pattern

Object pooling reuses inactive objects instead of creating and destroying them. This is critical for performance in games with many spawning objects like bullets, particles, or enemies. Every call to Instantiate and Destroy generates garbage that eventually triggers the garbage collector, causing frame hitches.

The pool pre-allocates a collection of objects at startup, deactivates them, and hands them out on request. When an object is "destroyed" it's simply returned to the pool and deactivated again. Unity 2021+ includes a built-in ObjectPool<T> class, but understanding the manual implementation gives you more control over warm-up, growth policies, and per-pool limits.

C#
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 20;

    private Queue<GameObject> pool = new Queue<GameObject>();

    private void Awake()
    {
        for (int i = 0; i < initialSize; i++)
        {
            GameObject obj = Instantiate(prefab, transform);
            obj.SetActive(false);
            pool.Enqueue(obj);
        }
    }

    public GameObject Get(Vector3 position, Quaternion rotation)
    {
        GameObject obj = pool.Count > 0 ? pool.Dequeue() : Instantiate(prefab, transform);
        obj.transform.SetPositionAndRotation(position, rotation);
        obj.SetActive(true);
        return obj;
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        pool.Enqueue(obj);
    }
}

When to use: Bullets, VFX particles, enemies, collectibles — anything spawned and despawned frequently. If you see frame spikes in the profiler's GC Alloc column, pooling is usually the fix.

4. State Machine Pattern

State machines organize behavior into discrete states with clear transitions. Perfect for player controllers, AI, and UI flow. Instead of a tangle of booleans (isJumping, isAttacking, isDashing), you define each state as a class and let the machine manage transitions between them.

Each state encapsulates its own Enter, Update, and Exit logic. The state machine delegates calls to whichever state is current. Adding a new state — say, a wall-slide — means creating a single new class without touching existing states.

C#
public abstract class State
{
    public virtual void Enter() { }
    public virtual void Update() { }
    public virtual void Exit() { }
}

public class StateMachine
{
    private State currentState;

    public void ChangeState(State newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter();
    }

    public void Update() => currentState?.Update();
}

// Usage: player states
public class IdleState : State
{
    private readonly PlayerController player;
    public IdleState(PlayerController player) => this.player = player;

    public override void Update()
    {
        if (Input.GetAxisRaw("Horizontal") != 0)
            player.Machine.ChangeState(new RunState(player));
    }
}

When to use: Player controllers with multiple movement modes, enemy AI with patrol/chase/attack phases, menu systems with distinct screens. If you have more than three booleans controlling behavior, refactor to a state machine.

5. Command Pattern

The Command pattern encapsulates actions as objects, enabling undo/redo, input rebinding, and replay systems. Each command is a self-contained object that knows how to execute itself and, optionally, how to undo itself. A command history stack makes features like multi-level undo trivial.

This pattern shines in strategy games, level editors, and turn-based games where players expect to undo moves. It's also the foundation for input replay: record the stream of commands with timestamps, then play them back to reproduce the exact same game session.

C#
public interface ICommand
{
    void Execute();
    void Undo();
}

public class MoveCommand : ICommand
{
    private Transform unit;
    private Vector3 previousPosition;
    private Vector3 targetPosition;

    public MoveCommand(Transform unit, Vector3 target)
    {
        this.unit = unit;
        this.targetPosition = target;
    }

    public void Execute()
    {
        previousPosition = unit.position;
        unit.position = targetPosition;
    }

    public void Undo()
    {
        unit.position = previousPosition;
    }
}

// Command invoker with undo stack
public class CommandInvoker
{
    private Stack<ICommand> history = new Stack<ICommand>();

    public void Execute(ICommand cmd)
    {
        cmd.Execute();
        history.Push(cmd);
    }

    public void Undo()
    {
        if (history.Count > 0)
            history.Pop().Undo();
    }
}

When to use: Undo/redo in level editors, turn-based game move history, input recording and playback, rebindable controls. Any time you need to treat "an action" as a first-class object.

6. Factory Pattern

Creating Objects Without Exposing Instantiation Logic

The Factory pattern centralizes object creation so that calling code never needs to know the concrete class being instantiated. Instead of scattering new Enemy() or Instantiate(prefab) calls throughout your codebase, you call EnemyFactory.Create(EnemyType.Goblin) and the factory handles prefab lookup, initialization, and pooling behind the scenes.

This is especially powerful in Unity because prefab selection, component configuration, and object pooling can all be hidden inside the factory. Your gameplay code only knows about the abstract product (e.g., IEnemy), not the concrete prefab or setup steps. Swapping a Goblin prefab for an updated version becomes a one-line change inside the factory, not a project-wide find-and-replace.

Factories pair naturally with ScriptableObjects. You can define enemy stats, prefab references, and spawn weights in a ScriptableObject, then hand that data to the factory. This lets designers create new enemy types without touching C# code — they just create a new EnemyConfig asset in the Project window.

C#
using UnityEngine;

public enum EnemyType { Goblin, Skeleton, Dragon }

[CreateAssetMenu(menuName = "Game/Enemy Config")]
public class EnemyConfig : ScriptableObject
{
    public EnemyType type;
    public GameObject prefab;
    public int health;
    public float speed;
}

public class EnemyFactory : MonoBehaviour
{
    [SerializeField] private EnemyConfig[] configs;

    public GameObject Create(EnemyType type, Vector3 position)
    {
        EnemyConfig config = System.Array.Find(configs, c => c.type == type);
        if (config == null)
        {
            Debug.LogError($"No config for enemy type: {type}");
            return null;
        }

        GameObject enemy = Instantiate(config.prefab, position, Quaternion.identity);
        var health = enemy.GetComponent<HealthComponent>();
        health.SetMaxHealth(config.health);

        var movement = enemy.GetComponent<MovementComponent>();
        movement.SetSpeed(config.speed);

        return enemy;
    }
}

When to use: Spawning enemies, projectiles, loot drops, or UI elements where the concrete type varies. Factories shine when creation logic is complex (multi-step setup, pooling, config lookup) or when you want designers to define new variants via ScriptableObjects without code changes.

7. Builder / Fluent Pattern

Constructing Complex Objects Step by Step

The Builder pattern constructs complex objects incrementally. Instead of a constructor with 15 parameters — half of which are optional — you chain descriptive method calls. In Unity, this is ideal for assembling things like character loadouts, procedural levels, or UI dialogs where each piece is optional and order-independent.

The "fluent" variant returns this from each builder method, enabling readable call chains like builder.SetName("Fire Sword").SetDamage(50).AddEnchantment(burn).Build(). The final Build() call validates the accumulated state and returns the finished product. If a required field is missing, you can throw a clear error at build time rather than discovering a broken object at runtime.

Builders are especially useful for procedural generation. A DungeonBuilder might expose methods like WithRooms(12), WithBossRoom(), WithTreasureRooms(3), and WithDifficulty(Difficulty.Hard). Each method configures an aspect of the dungeon, and Build() runs the generation algorithm with all the accumulated settings.

C#
using UnityEngine;

public class CharacterBuilder
{
    private string characterName;
    private int health = 100;
    private float speed = 5f;
    private GameObject weaponPrefab;
    private Material skinMaterial;
    private bool hasShield;

    public CharacterBuilder SetName(string name)
    {
        characterName = name;
        return this;
    }

    public CharacterBuilder SetHealth(int hp)
    {
        health = hp;
        return this;
    }

    public CharacterBuilder SetSpeed(float spd)
    {
        speed = spd;
        return this;
    }

    public CharacterBuilder WithWeapon(GameObject weapon)
    {
        weaponPrefab = weapon;
        return this;
    }

    public CharacterBuilder WithSkin(Material mat)
    {
        skinMaterial = mat;
        return this;
    }

    public CharacterBuilder WithShield()
    {
        hasShield = true;
        return this;
    }

    public GameObject Build(GameObject basePrefab, Vector3 spawnPos)
    {
        GameObject character = Object.Instantiate(basePrefab, spawnPos, Quaternion.identity);
        character.name = characterName ?? "Unnamed Hero";

        var stats = character.GetComponent<CharacterStats>();
        stats.Initialize(health, speed);

        if (weaponPrefab != null)
        {
            var weapon = Object.Instantiate(weaponPrefab, character.transform);
            character.GetComponent<CombatController>().EquipWeapon(weapon);
        }

        if (skinMaterial != null)
            character.GetComponentInChildren<Renderer>().material = skinMaterial;

        if (hasShield)
            character.GetComponent<DefenseController>().EnableShield();

        return character;
    }
}

// Usage
var hero = new CharacterBuilder()
    .SetName("Aragorn")
    .SetHealth(200)
    .SetSpeed(6f)
    .WithWeapon(swordPrefab)
    .WithShield()
    .Build(heroPrefab, spawnPoint.position);

When to use: Any object with many optional configuration parameters — character creation, procedural level generation, UI dialog construction, quest definitions. If your constructor or factory method has more than 4-5 parameters, a builder makes the code far more readable.

8. Strategy Pattern

Swappable Behaviors at Runtime

The Strategy pattern defines a family of interchangeable algorithms and lets you swap them at runtime. In Unity, this means you can change how an enemy pathfinds, how damage is calculated, or how a sort algorithm works — without touching the class that uses it. The consuming class holds a reference to an IStrategy interface and delegates the work.

Consider an AI system where enemies can patrol, flee, or rush the player. Without the Strategy pattern you'd write a monolithic switch statement. With it, each behavior is its own class implementing IAIBehavior. Swapping from patrol to chase is a single line: currentBehavior = new ChaseBehavior(target). You can even blend strategies or compose them — a flanking behavior that combines pathfinding with line-of-sight checks.

The Strategy pattern also enables data-driven design. You can store strategy references in ScriptableObjects, letting designers configure which movement algorithm an enemy uses directly in the Inspector. An EnemyConfig asset might reference a FlyingMovement strategy for a bat and a GroundPatrol strategy for a skeleton, all without changing the enemy controller code.

C#
using UnityEngine;

public interface IMovementStrategy
{
    void Move(Transform transform, float speed, float deltaTime);
}

public class PatrolMovement : IMovementStrategy
{
    private Vector3[] waypoints;
    private int currentIndex;

    public PatrolMovement(Vector3[] waypoints)
    {
        this.waypoints = waypoints;
    }

    public void Move(Transform transform, float speed, float deltaTime)
    {
        Vector3 target = waypoints[currentIndex];
        transform.position = Vector3.MoveTowards(transform.position, target, speed * deltaTime);

        if (Vector3.Distance(transform.position, target) < 0.1f)
            currentIndex = (currentIndex + 1) % waypoints.Length;
    }
}

public class ChaseMovement : IMovementStrategy
{
    private Transform target;

    public ChaseMovement(Transform target)
    {
        this.target = target;
    }

    public void Move(Transform transform, float speed, float deltaTime)
    {
        Vector3 direction = (target.position - transform.position).normalized;
        transform.position += direction * speed * deltaTime;
    }
}

// Enemy controller using strategy
public class EnemyController : MonoBehaviour
{
    [SerializeField] private float speed = 3f;
    private IMovementStrategy movementStrategy;

    public void SetStrategy(IMovementStrategy strategy)
    {
        movementStrategy = strategy;
    }

    void Update()
    {
        movementStrategy?.Move(transform, speed, Time.deltaTime);
    }
}

When to use: Enemy AI behaviors, damage calculation formulas, sorting or filtering logic, movement modes (walking, flying, swimming). Any time you have multiple interchangeable algorithms and want to select or swap them without modifying the consuming class.

9. Dependency Injection

Decoupling Dependencies for Testability

Dependency Injection (DI) is the practice of providing a class with its dependencies from the outside rather than letting it create or find them internally. Instead of a WeaponController calling FindObjectOfType<AudioManager>() or referencing a Singleton, it receives an IAudioService through its constructor or a public method. This makes the class easier to test, easier to reuse, and completely decoupled from the concrete implementation.

In Unity, constructor injection isn't always practical because MonoBehaviours are created by the engine. Instead, you can use method injection (an Initialize method called by a setup script), Inspector injection (serialized interface fields — available in Unity 2023.1+ with [SerializeReference]), or a DI framework like VContainer or Zenject that hooks into Unity's lifecycle automatically.

The payoff is enormous for testability. With DI, you can write unit tests that inject mock services — a silent IAudioService, a deterministic IRandomProvider, a fast ISaveService that writes to memory instead of disk. Your gameplay code is none the wiser, and your tests run in milliseconds without spinning up a full Unity scene.

C#
// Define abstractions
public interface IAudioService
{
    void PlaySFX(AudioClip clip);
    void PlayMusic(AudioClip clip, float fadeTime);
}

public interface ISaveService
{
    void Save(string key, string json);
    string Load(string key);
}

// Concrete implementation
public class UnityAudioService : MonoBehaviour, IAudioService
{
    [SerializeField] private AudioSource sfxSource;
    [SerializeField] private AudioSource musicSource;

    public void PlaySFX(AudioClip clip) => sfxSource.PlayOneShot(clip);

    public void PlayMusic(AudioClip clip, float fadeTime)
    {
        musicSource.clip = clip;
        musicSource.Play();
    }
}

// Consumer — depends on interfaces, not concrete classes
public class WeaponController : MonoBehaviour
{
    private IAudioService audio;
    private ISaveService save;

    // Method injection — called by a setup/bootstrap script
    public void Initialize(IAudioService audioService, ISaveService saveService)
    {
        audio = audioService;
        save = saveService;
    }

    public void Attack()
    {
        // Use injected services
        audio.PlaySFX(attackClip);
        save.Save("lastAttackTime", Time.time.ToString());
    }
}

When to use: Any class that depends on external services — audio, saving, analytics, networking. DI is especially valuable when you plan to write unit tests or when multiple implementations exist (e.g., a real save service vs. a cloud save service). For small projects, manual method injection works fine; for large projects, consider VContainer or Zenject.

10. Service Locator

A Global Access Point for Services

The Service Locator pattern provides a central registry where systems can look up services by interface type. It's a middle ground between raw Singletons (which expose the concrete class globally) and full Dependency Injection (which requires a DI framework or manual wiring). You register services at startup and retrieve them anywhere via ServiceLocator.Get<IAudioService>().

The key advantage over Singletons is that the consumer depends on an interface, not a concrete class. You can swap implementations at runtime — register a MutedAudioService during testing, a UnityAudioService in production, and a RemoteAudioService for cloud gaming — all without changing a single line in the consuming code.

Critics point out that Service Locator hides dependencies (unlike DI, which makes them explicit in the constructor signature). This is a valid concern. The best practice is to use Service Locator as a stepping stone: start with it for rapid prototyping, then migrate to full DI as your project grows and you need clearer dependency graphs and automated testing.

C#
using System;
using System.Collections.Generic;

public static class ServiceLocator
{
    private static readonly Dictionary<Type, object> services = new();

    public static void Register<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public static T Get<T>()
    {
        if (services.TryGetValue(typeof(T), out object service))
            return (T)service;

        throw new InvalidOperationException(
            $"Service {typeof(T).Name} not registered. " +
            "Call ServiceLocator.Register<T>() in your bootstrap script.");
    }

    public static void Reset() => services.Clear();
}

// Bootstrap script — runs first via Script Execution Order
public class GameBootstrap : MonoBehaviour
{
    [SerializeField] private UnityAudioService audioService;
    [SerializeField] private PlayerSaveService saveService;

    private void Awake()
    {
        ServiceLocator.Register<IAudioService>(audioService);
        ServiceLocator.Register<ISaveService>(saveService);
    }
}

// Any consumer, anywhere in the project
public class EnemyAI : MonoBehaviour
{
    private IAudioService audio;

    private void Start()
    {
        audio = ServiceLocator.Get<IAudioService>();
    }

    public void OnDeath()
    {
        audio.PlaySFX(deathClip);
    }
}

When to use: When you need global access to services but want to depend on interfaces rather than concrete Singletons. Great for rapid prototyping, game jams, and mid-size projects. For large-scale projects with automated testing, consider graduating to a full DI framework like VContainer.

Choosing the Right Pattern

No single pattern is a silver bullet. The best Unity codebases combine several patterns: a Service Locator or DI container wires up global services, Observers decouple cross-system communication, State Machines manage complex behavior, Object Pools handle performance-critical spawning, and Factories centralize creation logic. Start with the pattern that solves your most pressing problem, then layer in others as complexity grows.

Remember that patterns are tools, not goals. If a Singleton solves your problem cleanly and you don't need testability or swappability, use a Singleton. If your creation logic is a single Instantiate call, you don't need a Factory. The hallmark of a senior developer isn't knowing every pattern — it's knowing when not to use one.