Table of Contents

Unity Patterns — Event-Driven Architecture

Core rule: event-driven, not polling. React to state changes via events, delegates, and callbacks. Never poll in Update() for something that can be an event.


Event-Driven vs Polling

Polling (BAD) Event-Driven (GOOD)
How Check every frame Fire when state changes
CPU Runs every frame even if nothing changed Zero cost when idle
Coupling Listener knows about source internals Source broadcasts, listeners subscribe
Scale O(n) checks per frame O(1) per state change

Polling — Do NOT Do This

// BAD — checking a bool every single frame
private void Update()
{
    if (_door.IsOpen)
    {
        EnterRoom();
    }
}

Event-Driven — Do This Instead

// GOOD — react when the door actually opens
private void OnEnable()
{
    _door.Opened += OnDoorOpened;
}

private void OnDisable()
{
    _door.Opened -= OnDoorOpened;
}

private void OnDoorOpened()
{
    EnterRoom();
}

C# Events & Delegates (Preferred)

Zero-allocation, type-safe, fastest option.

using System;
using PixelEngine.Architecture;

public class HealthSystem : IDisposable
{
    [Serializable]
    public readonly struct Configuration : IConfiguration
    {
        public readonly int maxHealth;

        public static readonly Configuration Default = new Configuration(100);

        public Configuration(int maxHealth)
        {
            this.maxHealth = maxHealth;
        }
    }

    [Serializable]
    public readonly struct Reference : IReference
    {
        public static readonly Reference Default = new Reference();
    }

    public struct State : IState
    {
        public int currentHealth;
        public bool isDead;
    }

    public class Components : IComponents { }

    private Configuration _configuration;
    private Reference _reference;
    private State _state;

    public Configuration configuration => _configuration;
    public Reference reference => _reference;
    public State state => _state;
    public Components components { get; private set; }

    public event Action<int> HealthChanged;
    public event Action Died;

    public HealthSystem() : this(Configuration.Default, Reference.Default) { }

    public HealthSystem(in Configuration configuration, in Reference reference)
    {
        _configuration = configuration;
        _reference = reference;
        _state = new State();
        this.components = new Components();
        Init();
    }

    public void SetConfiguration(in Configuration configuration)
    {
        _configuration = configuration;
    }

    public void TakeDamage(int amount)
    {
        if (_state.isDead) return;

        _state.currentHealth = Math.Max(0, _state.currentHealth - amount);
        HealthChanged?.Invoke(_state.currentHealth);

        if (_state.currentHealth <= 0)
        {
            _state.isDead = true;
            Died?.Invoke();
        }
    }

    private void Init()
    {
        _state.currentHealth = _configuration.maxHealth;
    }

    public void Dispose() { }
}

Subscribe/unsubscribe in OnEnable / OnDisable to avoid leaks:

private void OnEnable()
{
    _healthSystem.Died += OnPlayerDied;
}

private void OnDisable()
{
    _healthSystem.Died -= OnPlayerDied;
}

UnityEvent (Inspector-Wirable)

Use UnityEvent when designers need to wire callbacks in the Inspector. Slower than C# events — do not use in hot paths.

using UnityEngine.Events;

public class Door : MonoBehaviour
{
    [SerializeField]
    private UnityEvent _onOpened;

    public void Open()
    {
        // ... door logic ...
        _onOpened?.Invoke();
    }
}
C# event UnityEvent
Performance Fast (delegate invoke) Slower (reflection, serialization)
Inspector Not visible Configurable in Inspector
Use when Code-to-code Designer-to-code wiring

Lifecycle Method Ordering

Order your MonoBehaviour event functions in the same order Unity calls them (execution order):

public class Example : MonoBehaviour
{
    private void Awake() { }       // 1. Called once, before Start
    private void OnEnable() { }    // 2. Subscribe to events here
    private void Start() { }       // 3. Called once, after all Awake
    private void FixedUpdate() { } // 4. Physics tick
    private void Update() { }      // 5. Per-frame (use sparingly)
    private void LateUpdate() { }  // 6. After all Update calls
    private void OnDisable() { }   // 7. Unsubscribe from events here
    private void OnDestroy() { }   // 8. Cleanup
}

When Update() IS Acceptable

Use Case Why It's OK
Continuous input (movement, camera) No discrete event to react to
Animation blending / procedural motion Needs per-frame interpolation
Physics queries (Raycast) per frame Driven by player aim
Frame-rate-dependent visual effects Inherently per-frame

Everything else should be event-driven.


Input — Use Action Callbacks

Use Input System action callbacks, not per-frame polling.

// BAD — polling
private void Update()
{
    if (UnityEngine.Input.GetKeyDown(KeyCode.Space))
    {
        Jump();
    }
}

// GOOD — event callback via Input System
private void OnEnable()
{
    _controls.Player.Jump.performed += OnJump;
    _controls.Enable();
}

private void OnDisable()
{
    _controls.Player.Jump.performed -= OnJump;
    _controls.Disable();
}

private void OnJump(InputAction.CallbackContext ctx)
{
    Jump();
}

Coroutines & Async — When to Use

Mechanism Use For Reference
Coroutine Sequences tied to MonoBehaviour lifetime StartCoroutine
Awaitable Async operations with cancellation await Awaitable.NextFrameAsync()
UniTask Zero-alloc async, broad await support Third-party, performant
// Coroutine — wait for event, not polling
private IEnumerator WaitForDoor()
{
    yield return new WaitUntil(() => _door.IsOpen);
    EnterRoom();
}

ScriptableObject as Event Channel

Decouple systems with a SO-based event bus. Zero scene references needed.

[CreateAssetMenu(menuName = "Events/Void Event")]
public class VoidEventChannel : ScriptableObject
{
    private Action _listeners;

    public void Raise()
    {
        _listeners?.Invoke();
    }

    public void Subscribe(Action listener)
    {
        _listeners += listener;
    }

    public void Unsubscribe(Action listener)
    {
        _listeners -= listener;
    }
}

References