【Unity】Unity C# Design Patterns: Decoupled Code with Events and Delegates

Created: 2025-12-07

Overusing GetComponent and FindObjectOfType makes code complex and fragile. Learn to use C#'s powerful delegates and events to break object dependencies and design loosely coupled, extensible, reusable systems.

Overview

As game development progresses, inter-object communication grows increasingly complex. Consider: "When the player takes damage, update the UI health bar, play a scream through the audio manager, shake the camera, and notify the game manager of player death."

The naive implementation has the player script directly reference UIManager, AudioManager, CameraManager, GameManager, calling their methods. But this approach (tight coupling) has many problems:

  • Complex dependencies: The player script assumes knowledge of UI, audio, and other systems it shouldn't need to know about.
  • Poor extensibility: Adding "notify achievement system" requires modifying the player script.
  • Poor reusability: Reusing this player system for enemies brings unwanted UI and camera dependencies.

The powerful mechanism for solving these problems and writing clean, extensible code is event-driven design using C#'s delegates and events.

The core idea: completely separate "the side declaring something happened (event)" from "the side that wants to do something when it happens." The player just shouts "I took damage!"—it doesn't need to know who's listening. UI and audio hear that shout and do their own jobs.

What are Delegates?

Delegates are simply "types that can hold references to methods." Similar to C/C++ function pointers but safer and more object-oriented. Delegates let you pass methods around like variables, hand them to other methods, or store them in class fields.

// First, define the delegate "type"
// This defines "a method type with void return and one int parameter"
public delegate void MyDelegate(int number);

public class DelegateExample
{
    public void Run()
    {
        // Assign a method to delegate variable
        MyDelegate myDelegate = PrintNumber;
        myDelegate += PrintDoubleNumber; // Multicast delegate: register multiple methods

        // Invoke delegate (all registered methods are called)
        myDelegate(5);
        // Output:
        // Number: 5
        // Double Number: 10
    }

    void PrintNumber(int num) { Debug.Log($"Number: {num}"); }
    void PrintDoubleNumber(int num) { Debug.Log($"Double Number: {num * 2}"); }
}

What are Events?

Delegates are powerful, but public delegate variables can be invoked freely from outside (myDelegate(5)) or cleared (myDelegate = null)—poor encapsulation.

Events wrap delegates, restricting external access to only subscription (+=) and unsubscription (-=). Event invocation can only occur from within the declaring class. This enables safe notification patterns.

Unity Practical Example: Decoupled Health System

Let's implement a player health system with event-driven design.

1. Event Publisher (PlayerHealth.cs)

PlayerHealth notifies damage and death as events. This class knows nothing about UI or audio.

using System;
using UnityEngine;

public class PlayerHealth : MonoBehaviour
{
    // Event definitions
    // Action<T> is a convenient delegate type built into .NET/Unity
    public static event Action<int, int> OnHealthChanged; // Args: current health, max health
    public static event Action OnPlayerDied;

    public int maxHealth = 100;
    private int currentHealth;

    private void Start()
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int damage)
    {
        currentHealth -= damage;
        if (currentHealth < 0) currentHealth = 0;

        // Publish event (subscribers get notified)
        // ?.Invoke() is safe call that prevents NullReferenceException when no subscribers
        OnHealthChanged?.Invoke(currentHealth, maxHealth);

        if (currentHealth <= 0)
        {
            OnPlayerDied?.Invoke();
        }
    }
}

2. Event Subscribers (UIManager.cs, AudioManager.cs)

UIManager and AudioManager subscribe to PlayerHealth events and execute their respective processing when notified.

// UIManager.cs
using UnityEngine;
using UnityEngine.UI; // Required for Text

public class UIManager : MonoBehaviour
{
    public Text healthText;

    private void OnEnable() // When object becomes active
    {
        // Subscribe to PlayerHealth event
        PlayerHealth.OnHealthChanged += UpdateHealthUI;
    }

    private void OnDisable() // When object becomes inactive
    {
        // Always unsubscribe (prevents memory leaks)
        PlayerHealth.OnHealthChanged -= UpdateHealthUI;
    }

    private void UpdateHealthUI(int current, int max)
    {
        healthText.text = $"HP: {current} / {max}";
    }
}

// AudioManager.cs
public class AudioManager : MonoBehaviour
{
    private void OnEnable()
    {
        PlayerHealth.OnPlayerDied += PlayDeathSound;
    }

    private void OnDisable()
    {
        PlayerHealth.OnPlayerDied -= PlayDeathSound;
    }

    private void PlayDeathSound()
    {
        // Play death sound processing
    }
}

Using static events enables subscription by class name without instance references, allowing manager classes to communicate easily without singletons.

Summary

Event-driven design is a fundamental, powerful technique for writing clean, scalable Unity code.

  • Tight coupling through direct references makes code difficult to modify and reuse.
  • Delegates are "method references"; events are "safe delegate wrappers."
  • Publishers just declare "what happened." They don't need to know about subscribers.
  • Subscribers subscribe (+=) to events of interest and must always unsubscribe (-=) when done.
  • This pattern enables independent, loosely coupled, extensible system design.

Before using GetComponent or FindObjectOfType to hunt for other objects, ask yourself: "Could this be notified via events?" That habit will elevate your code to professional levels.