C# ARCHITECTURE • DESIGN PATTERNS
SCRIPT COMMUNICATION IN UNITY: EVENTS, DELEGATES & SINGLETONS
Eliminate tight coupling and spaghetti references by comparing Singletons, C# System.Action Events, Direct Injection, and ScriptableObject channels.
1. The Four Ways Scripts Talk to Each Other
1. Direct Referencing
Script A holds a direct
[SerializeField] private ScriptB target;. Best for closely related components on the same GameObject (e.g. GunController talking to AmmoSystem).
2. Singleton Pattern
Global static instance accessible via
GameManager.Instance. Best for true global systems (Audio Manager, Save System, Scene Loader).
3. C# Event Delegates (Action)
Publisher/Subscriber pattern using
public static event Action OnPlayerDeath;. The player doesn't care who is listening (UI, SFX, Analytics).
4. ScriptableObject Events
Asset-based event channels that bridge scenes without code singletons or static variables.
2. Production Event-Driven Pattern (C# Action)
Here is how to implement clean, decoupled event communication when an enemy is destroyed:
using System;
using UnityEngine;
// 1. THE PUBLISHER (Enemy)
public class EnemyTarget : MonoBehaviour
{
// Static event broadcasted to any interested listener in the game
public static event Action<int> OnEnemyDestroyed; // passes score value
[SerializeField] private int scoreValue = 250;
public void DestroyTarget()
{
// Safe null-conditional invocation
OnEnemyDestroyed?.Invoke(scoreValue);
Destroy(gameObject);
}
}
// 2. THE SUBSCRIBER (UI Scoreboard)
public class ScoreboardUI : MonoBehaviour
{
private int currentScore = 0;
// ALWAYS subscribe in OnEnable and unsubscribe in OnDisable to prevent memory leaks!
private void OnEnable()
{
EnemyTarget.OnEnemyDestroyed += AddScore;
}
private void OnDisable()
{
EnemyTarget.OnEnemyDestroyed -= AddScore;
}
private void AddScore(int points)
{
currentScore += points;
Debug.Log("New Score: " + currentScore);
}
}
3. Generic Persistent Singleton Pattern
For global managers (like AudioEngine or SaveManager) that must persist across scene changes:
using UnityEngine;
public class PersistentSingleton<T> : MonoBehaviour where T : Component
{
public static T Instance { get; private set; }
protected virtual void Awake()
{
if (Instance == null)
{
Instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject); // Prevent duplicate managers on scene reload
}
}
}
Related Architecture Guides & Error Fixes
Frequently Asked Questions
Static C# events hold strong references to subscriber methods. If a UI object subscribes to an event and the scene changes (destroying the UI GameObject), the static event continues holding a reference to the dead object in memory. This causes MissingReferenceException errors when the event next fires, as well as silent memory leaks.
Singletons are not inherently bad, but overusing them is. Use Singletons sparingly for true singleton services (Audio Mixer, Save/Load Manager, Input Manager). Avoid creating Singletons for gameplay entities like
PlayerController or individual enemy squads.