Building an Event Bus in Unity for Decoupled Communication
A step-by-step guide to creating a robust, centralized messaging system in Unity for cleaner, more scalable game development.
Here's the thing—when I first started building games at KIXEYE, I thought I was being efficient by having systems directly reference each other. My PlayerHealth script had a reference to the UIManager. The UIManager had references to the AudioManager. The AudioManager needed to know about the GameManager. It felt logical at the time. Then one day, I needed to change how the player died, and suddenly I was hunting through eight different scripts, fixing broken references, and wondering why my "simple change" had turned into a debugging nightmare. That's when I learned about the event bus Unity pattern, and honestly, it changed everything about how I architect games.
Table of Contents
- Why Your Game Systems Are Probably Talking to Each Other Wrong
- What Is This "Event Bus" Thing Everyone Keeps Mentioning?
- The Six Terms You Need to Know Before We Start Coding
- Here's How the Event Bus Unity System Actually Works Under the Hood
- Static Event Bus vs. UnityEvent: Which One Should You Use?
- Four Reasons I'll Never Go Back to Direct References
- The Three Rules I Follow Every Single Time
- How Hades, Enter the Gungeon, and Overwatch Use This Pattern
- Blueprint #1: Your First Complete Event Bus (Copy-Paste Ready)
- Blueprint #2: Making UI and Audio React to Player Death (No References Required)
- Blueprint #3: Passing Data Through Events (The Score System Example)
Why Your Game Systems Are Probably Talking to Each Other Wrong
Been there. You're building a game, and you need your UI to update when the player takes damage. The "obvious" solution? Give your PlayerHealth script a reference to the UIManager and call uiManager.UpdateHealthBar() directly. Simple, right?
Actually, wait. Now you also need to play a sound when the player gets hit. So you add a reference to the AudioManager. Then you need screen shake, so you add a reference to the CameraController. Before you know it, your PlayerHealth script has become a tangled mess of dependencies, and changing anything means touching five different files.
This is what we call "spaghetti code" in the industry, where different systems in your game become tightly tangled together with direct references, making them difficult to change or debug. The event bus Unity pattern solves this critical problem by allowing components to communicate with each other without needing to know who they are talking to, breaking these rigid connections.
Think of an event bus as a public announcement system at a train station. Instead of a station manager calling each passenger individually to tell them a train has arrived, they broadcast a single message like "Train 5 has arrived at Platform 2." Any passenger waiting for that train (listeners) will hear the announcement and react, while all other passengers will simply ignore it. The announcer (publisher) doesn't need to know who is listening.
What Is This "Event Bus" Thing Everyone Keeps Mentioning?
An event bus is a central, static class that manages the broadcasting and receiving of events in your game. It acts as the main hub for all communication, allowing you to create clean, modular, and scalable game architecture where systems like UI, audio, and gameplay logic can react to game events (like "PlayerDied" or "ItemCollected") independently.
Here's what makes it powerful: when your player takes damage, your PlayerHealth script simply announces "Hey, the player just took 10 damage" to the event bus. The event bus then notifies everyone who's interested—the UIManager updates the health bar, the AudioManager plays a hurt sound, and the CameraController adds screen shake. None of these systems need to know about each other. They're all just listening for the same announcement.
This makes it incredibly easy to add new features or change existing ones without causing a cascade of errors in other systems. Want to add a damage counter popup? Just create a new DamagePopup script that listens for damage events. You don't touch the PlayerHealth script at all.

The Six Terms You Need to Know Before We Start Coding
Let me break down the terminology so we're all on the same page. These six concepts are the foundation of understanding how the Unity event system works:
- Event Bus (or Event Aggregator): A central, static class that manages the broadcasting and receiving of events. It acts as the main hub for all communication.
- Publisher (or Broadcaster): Any script that triggers or raises an event. For example, a
PlayerHealthscript might publish aPlayerDiedEventwhen health reaches zero. - Subscriber (or Listener): Any script that registers itself with the event bus to be notified when a specific event occurs. A
UIManagermight subscribe to thePlayerDiedEventto show a "Game Over" screen. - Event: A message or data payload, often represented by a simple C# class or struct, that contains information about what happened. For instance, an
EnemyDefeatedEventmight contain the score value of the enemy. - Decoupling: The primary goal of an event bus. It means reducing the dependencies between different parts of your code so that a change in one part has little to no effect on others.
Action<T>: A generic delegate type in C# that is commonly used to define the signature of an event.Action<PlayerDiedEvent>represents a function that takes one parameter of typePlayerDiedEventand returns nothing.
Here's How the Event Bus Unity System Actually Works Under the Hood
Took me a good afternoon to fully understand this when I first implemented it at KIXEYE, so let me walk you through it step by step.
Step 1: Defining What an Event Looks Like
The first step is to define the event itself. This is typically a simple class or struct that acts as a data container for any information related to the event. Here's a straightforward example:
// This is a simple event that carries the amount of damage taken.public class PlayerTookDamageEvent{public readonly int DamageAmount;public PlayerTookDamageEvent(int damageAmount){DamageAmount = damageAmount;}}
This event is just a container that says "the player took damage, and here's how much." The readonly keyword means the damage amount can't be changed after the event is created, which is a good practice for event data.
Step 2: Building the Central Hub
This is where the magic happens. The event bus is a static class that contains a dictionary to store all the events and their subscribers:
using System;using System.Collections.Generic;public static class EventBus{private static Dictionary<Type, List<Action<object>>> _subscribers = new Dictionary<Type, List<Action<object>>>();}
This dictionary uses the event type (like PlayerTookDamageEvent) as the key, and stores a list of all the callback functions that want to be notified when that event happens. If you want to dive deeper into static classes in C#, check out the Microsoft Docs on Static Classes.
Step 3: Letting Systems Subscribe to Events
A listener script will call a Subscribe method on the event bus, providing the type of event it's interested in and the function (callback) to execute when the event is raised:
// Inside the EventBus classpublic static void Subscribe<T>(Action<T> callback){// ... logic to add the callback to the dictionary ...`}`// In a listener script (e.g., UIManager.cs)void OnEnable()`{`EventBus.Subscribe<PlayerTookDamageEvent>(OnPlayerDamaged);`}`void OnPlayerDamaged(PlayerTookDamageEvent e)`{`// Update health UI`}`
When the UIManager enables, it tells the event bus "Hey, whenever a PlayerTookDamageEvent happens, call my OnPlayerDamaged method."
Step 4: Broadcasting Events to Everyone Who's Listening
A publisher script will call a Publish method on the event bus, creating a new instance of the event. The event bus then notifies all registered subscribers:
// Inside the EventBus classpublic static void Publish<T>(T e)`{`// ... logic to find all subscribers for type T and invoke their callbacks ...`}`// In a publisher script (e.g., PlayerHealth.cs)public void TakeDamage(int amount)`{`health -= amount;EventBus.Publish(new PlayerTookDamageEvent(amount));`}`
When the player takes damage, the PlayerHealth script creates a new event with the damage amount and publishes it. The event bus automatically finds everyone who subscribed to PlayerTookDamageEvent and calls their callback functions.

Static Event Bus vs. UnityEvent: Which One Should You Use?
You know what's funny? I spent weeks using UnityEvents before I realized they weren't the right tool for system-level communication. Here's the breakdown that would've saved me that time:
| Criteria | Approach A: Static Event Bus | Approach B: UnityEvent / C# event |
|---|---|---|
| Best For | Global, system-level communication where many different, unrelated systems need to react to a central event (e.g., GamePaused, LevelCompleted). | Component-to-component communication where there is a clear, one-to-many relationship (e.g., a button notifying its listeners that it was clicked). |
| Performance | Can have minor overhead due to dictionary lookups and type casting, but is generally very fast for most use cases. | UnityEvent is slightly slower due to its serialization and Inspector integration. C# event is extremely fast, but less flexible than a full event bus. |
| Complexity | Requires writing a central static class from scratch. Offers the most flexibility for creating a decoupled architecture. | UnityEvent is very easy to use and assign in the Inspector. C# event requires manual subscription management in code, which can be error-prone. |
| Code Example | EventBus.Publish(new PlayerDiedEvent()); | public UnityEvent OnPlayerDied; OnPlayerDied.Invoke(); |
For most game architecture patterns involving multiple systems reacting to the same event, the static event bus is the clear winner. UnityEvents are great for UI buttons and similar component-specific interactions, but they don't scale well for complex game systems.
Four Reasons I'll Never Go Back to Direct References
After working on multiple Unity projects at KIXEYE and seeing how quickly codebases can become unmaintainable, here's why I always reach for the event bus pattern now:
1. Your Systems Become True Black Boxes
Your AudioManager doesn't need a reference to the player to play a death sound; it just needs to listen for the PlayerDiedEvent. This massively improved code modularity means each system is self-contained and can be tested independently.
2. Adding Features Becomes Ridiculously Easy
This one had me convinced. Want to add screen shake when the player gets hurt? Simply create a new ScreenShake script that subscribes to the PlayerTookDamageEvent. You don't have to touch the PlayerHealth script at all. No hunting through code, no adding new references, no risk of breaking existing functionality.
3. Debugging Goes from Nightmare to Manageable
Because systems are not directly calling each other, it's easier to trace the flow of logic. You can add a single Debug.Log in your EventBus.Publish method to see every single event that happens in your game in the order it occurs. This simplified debugging has saved me countless hours.
4. Your Game Can Actually Scale
This pattern is used in large-scale software development for a reason. It keeps your codebase clean and manageable as your game grows from a small prototype to a full-featured product. This promotes a scalable architecture that won't collapse under its own weight.
The Three Rules I Follow Every Single Time
Trust me, you'll thank me later for these tips. I learned all of these the hard way.
Rule #1: Always Subscribe in OnEnable and Unsubscribe in OnDisable
This is the most critical rule. Forgetting to unsubscribe can lead to memory leaks and "ghost" listeners—objects that have been destroyed but are still trying to receive events.
// Best Practice: Always clean up your subscriptions.private void OnEnable()`{`EventBus.Subscribe<GameStartedEvent>(OnGameStarted);`}`private void OnDisable()`{`EventBus.Unsubscribe<GameStartedEvent>(OnGameStarted);`}`
I spent a good chunk of time debugging why my game was calling functions on destroyed objects before I realized I wasn't unsubscribing properly. Don't be like early-me.
Rule #2: Use Structs for High-Frequency Events
If you are publishing an event many times per second (e.g., EnemyTookDamage), define the event as a struct instead of a class. This allocates it on the stack, avoiding heap allocation and reducing garbage collection pressure:
// Good for performance-critical events.public struct EnemyTookDamageEvent`{`// ... event data ...`}`
This is particularly important for mobile games where garbage collection can cause frame rate hiccups.
Rule #3: Consider a Base Event Type for Advanced Debugging
For advanced logging or debugging, you can have all your events inherit from a common base type:
public interface IEvent { }public class PlayerDiedEvent : IEvent { }public class ScoreUpdatedEvent : IEvent { }
This isn't necessary for basic implementations, but once your game has dozens of events, having a common interface makes it much easier to build debugging tools and event logging systems.

How Hades, Enter the Gungeon, and Overwatch Use This Pattern
Let me tell you about how I've seen this technique used brilliantly in some of my favorite games. After analyzing dozens of games, these implementations really stand out because they show the power of decoupled communication Unity in action.
What I Find Fascinating About Hades
In Hades, when Zagreus picks up a Boon from a god, multiple systems react simultaneously: the UI updates to show the new boon, a voice line from the god plays, and the player's stats are immediately modified.
From a developer's perspective, what makes this brilliant is the implementation: a central BoonCollectedEvent is likely published. The UIManager, AudioManager, and PlayerStatsController all subscribe to this one event and trigger their own specific logic without needing any references to each other.
The player experience this creates is immediate, multi-faceted feedback for player actions, making the experience feel incredibly responsive and polished. This is a direct result of decoupled systems all reacting to the same event in parallel. I always tell my students to look at how Hades handles this—it's textbook event-driven design.
Here's How Enter the Gungeon Solves This Problem
One of my favorite implementations of this is in Enter the Gungeon. When an enemy is killed, various things can happen: the player might gain currency, a key might drop, the room's doors might unlock after the last enemy is defeated, and a "kill count" objective might be updated.
What I find fascinating about this approach is that the enemy's health script would publish an EnemyDiedEvent containing information like the enemy type and its position. The LootDropper, RoomController, and QuestManager would all be subscribed to this event to perform their independent functions.
Here's how you can adapt this for your own game: the game's progression feels logical and seamless. The player is rewarded, the level opens up, and objectives are met, all triggered from the single, simple action of defeating an enemy. This is why I always recommend studying this game's approach to system communication.
The Overwatch Example That Convinced Me
This one really sold me on the pattern. In Overwatch, when a player captures an objective, the game provides massive feedback: the announcer shouts "Objective Captured," the game mode UI updates, all players see a notification, and the payload (if applicable) starts moving.
After analyzing this implementation, I realized a global ObjectiveCapturedEvent containing the team that captured it would be published. This single event would trigger the announcer system, the UI controllers for every player, and the payload's movement script, all of which are completely separate systems.
Let me tell you about the player experience this creates: key game moments are communicated clearly and effectively to all players at once, creating a cohesive and understandable team-based experience. This is the gold standard for event-driven multiplayer design.
Blueprint #1: Your First Complete Event Bus (Copy-Paste Ready)
Here's the exact method I use when implementing an event bus from scratch. Let's tackle this together and build a complete, type-safe, generic static event bus that can handle subscribing, unsubscribing, and publishing any class-based event.
Setting Up in Unity
- Create a new C# script named
EventBus - Create a new C# script for your first event,
GameStartedEvent
The Event Definition
Start with a simple event. It can even be empty if it's just a signal that something happened:
// In GameStartedEvent.cspublic class GameStartedEvent { } // Can be empty if it's just a signal.
The Complete Event Bus Implementation
This is the exact code I use in my projects. These are the tried-and-tested settings I've configured dozens of times:
// In EventBus.csusing System;using System.Collections.Generic;using UnityEngine;public static class EventBus`{`// Use object to store a list of any Action type.private static Dictionary<Type, List<object>> _subscribers = new Dictionary<Type, List<object>>();public static void Subscribe<T>(Action<T> callback)`{`Type eventType = typeof(T);if (!_subscribers.ContainsKey(eventType))`{`_subscribers[eventType] = new List<object>();`}`_subscribers[eventType].Add(callback);`}`public static void Unsubscribe<T>(Action<T> callback)`{`Type eventType = typeof(T);if (_subscribers.ContainsKey(eventType))`{`_subscribers[eventType].Remove(callback);`}` `}`public static void Publish<T>(T e)`{`Type eventType = typeof(T);if (_subscribers.ContainsKey(eventType))`{`// Cast and invoke each callback.foreach (var subscriber in _subscribers[eventType])`{`(subscriber as Action<T>)?.Invoke(e);`}` `}` `}` `}`
From my time at CMU, I learned that the dictionary-based approach is both flexible and performant for most game use cases. By removing the where T : class constraint that was in earlier versions, this implementation now supports both classes and structs, giving you maximum flexibility. The null-conditional operator (?.) prevents crashes if a subscriber is somehow invalid.
Verified: Source
Blueprint #2: Making UI and Audio React to Player Death (No References Required)
When I'm working on projects, I always start by implementing the player death scenario because it's such a clear demonstration of decoupling. Let me show you how I approach this: when the player dies, make the UIManager show a "Game Over" screen and the AudioManager play a death sound, without either system having a direct reference to the player.
Unity Editor Setup
- Create a "Player" GameObject with a
PlayerHealthscript - Create a "Managers" GameObject with
UIManagerandAudioManagerscripts - In your UI, create a "GameOverScreen" panel and assign it to a field in
UIManager
Step 1: Define the Event
// In PlayerDiedEvent.cspublic class PlayerDiedEvent { }
Step 2: The Publisher (Where the Player Dies)
Here's the exact implementation I use when handling player death:
// In PlayerHealth.csusing UnityEngine;public class PlayerHealth : MonoBehaviour`{`public void Die()`{`Debug.Log("Player has died!");EventBus.Publish(new PlayerDiedEvent());// Destroy the player object or disable it.gameObject.SetActive(false);`}` `}`
Notice how clean this is—the PlayerHealth script has zero knowledge about UI or audio. It just announces "the player died" and moves on.
Step 3: The Subscribers (UI and Audio Managers)
In my projects, I always implement these as separate, independent listeners:
// In UIManager.csusing UnityEngine;public class UIManager : MonoBehaviour`{`[SerializeField] private GameObject gameOverScreen;private void OnEnable() => EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);private void OnDisable() => EventBus.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);private void OnPlayerDied(PlayerDiedEvent e)`{`gameOverScreen.SetActive(true);`}` `}`// In AudioManager.csusing UnityEngine;public class AudioManager : MonoBehaviour`{`private void OnEnable() => EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);private void OnDisable() => EventBus.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);private void OnPlayerDied(PlayerDiedEvent e)`{`// Assume you have an AudioSource component to play the sound.Debug.Log("Playing death sound!");`}` `}`
Trust me, you'll appreciate this separation when you need to add screen shake, particle effects, achievement tracking, or any other system that needs to know when the player dies. Just create a new script that subscribes to PlayerDiedEvent, and you're done.
Blueprint #3: Passing Data Through Events (The Score System Example)
Here's the thing about events—they're not just signals. They can carry data, and that's when they become really powerful. Let me walk you through implementing a score system where enemies publish their score value when they die.
Unity Editor Setup
- Create an "Enemy" prefab with an
Enemyscript - Create a "Managers" GameObject with a
ScoreManagerscript
Step 1: Define an Event That Carries Data
// In EnemyDiedEvent.cspublic class EnemyDiedEvent`{`public readonly int ScoreValue;EnemyDiedEvent(int scoreValue)`{`ScoreValue = scoreValue;`}` `}`
This event contains the score value of the enemy that died. Different enemy types can have different values.
Step 2: The Enemy Publishing Its Score
When I'm working on enemy scripts, my process is to keep them simple and focused:
// In Enemy.csusing UnityEngine;public class Enemy : MonoBehaviour`{`[SerializeField] private int scoreValue = 10;public void Die()`{`EventBus.Publish(new EnemyDiedEvent(scoreValue));Destroy(gameObject);`}` `}`
The enemy doesn't care what happens with its score value—it just broadcasts "I died, and I was worth 10 points." Maybe the score gets added to the total. Maybe it triggers a combo counter. Maybe it unlocks an achievement. The enemy doesn't need to know.
Step 3: The Score Manager Listening
After working on multiple Unity projects, I've found this approach works best:
// In ScoreManager.csusing UnityEngine;public class ScoreManager : MonoBehaviour`{`private int _totalScore = 0;private void OnEnable() => EventBus.Subscribe<EnemyDiedEvent>(OnEnemyDied);private void OnDisable() => EventBus.Unsubscribe<EnemyDiedEvent>(OnEnemyDied);private void OnEnemyDied(EnemyDiedEvent e)`{`_totalScore += e.ScoreValue;Debug.Log("Score is now: " + _totalScore);// Here you would update a UI Text element.`}` `}`
This pattern scales beautifully. Want to add a combo multiplier? Create a ComboManager that also subscribes to EnemyDiedEvent. Want to track enemy kill counts for achievements? Add an AchievementManager subscriber. None of these systems need to know about each other.
Ready to Start Building Your First Game?
If you've made it this far, you understand one of the most important game architecture patterns in modern game development. The event bus Unity pattern is exactly the kind of foundational knowledge you need to build professional, scalable games.
At Outscal, I've built a complete game development course that takes you from the absolute basics to building professional-quality experiences. We cover not just event systems, but all the architectural patterns, optimization techniques, and practical skills you need to break into the game industry.
Whether you're working on your first Unity project or trying to level up from hobbyist to professional, the Mr. Blocks course will give you the exact roadmap I wish I had when I was starting out. No fluff, no outdated techniques—just the real-world skills studios are looking for.
Key Takeaways
- An event bus eliminates direct references between systems, solving the "spaghetti code" problem where changes cascade through your entire codebase.
- Always subscribe in
OnEnableand unsubscribe inOnDisableto prevent memory leaks and "ghost" listeners that cause crashes. - Use structs instead of classes for high-frequency events to reduce garbage collection pressure and improve performance.
- The event bus pattern uses three core components: the central EventBus class (hub), publishers (scripts that broadcast events), and subscribers (scripts that listen and react). * Static event buses are best for system-level communication where multiple unrelated systems react to the same event, while UnityEvents work better for component-specific interactions.
- Events can carry data by including fields in the event class, allowing systems to pass information without direct references.
- This pattern scales beautifully—adding new features means creating new subscribers without touching existing code.
- All events are defined as simple C# classes or structs that act as data containers for information about what happened in your game.
Common Questions
What is an event bus in Unity and why should I use it?
An event bus in Unity is a central messaging system that allows different parts of your game to communicate without needing direct references to each other. You should use it because it eliminates "spaghetti code," makes adding new features easier, simplifies debugging, and creates a scalable architecture that won't collapse as your game grows.
How does an event bus Unity system differ from UnityEvent?
An event bus is best for global, system-level communication where many unrelated systems need to react to the same event (like GamePaused or LevelCompleted). UnityEvent is better for component-to-component communication with clear one-to-many relationships (like a button click). The event bus requires more initial setup but offers much more flexibility for complex game architectures.
What does it mean to subscribe and unsubscribe to events?
Subscribing means registering a function (callback) with the event bus to be notified when a specific event occurs. Unsubscribing means removing that registration. Think of it like signing up for notifications (subscribe) and turning them off (unsubscribe). You must always unsubscribe when your object is destroyed or disabled to prevent memory leaks.
When should I use a struct vs a class for events?
Use a struct for events that are published many times per second (like EnemyTookDamage or PlayerMovedEvent) because structs are allocated on the stack, avoiding garbage collection pressure. Use a class for events that happen less frequently (like PlayerDiedEvent or LevelCompletedEvent) where the small performance difference doesn't matter.
Why is it important to unsubscribe in OnDisable?
If you don't unsubscribe when your object is disabled or destroyed, the event bus will still try to call methods on that destroyed object when the event is published, causing "ghost" listeners and null reference errors. Unsubscribing in OnDisable ensures clean memory management and prevents these crashes.
How do I pass data through events in Unity?
Define your event as a class or struct with fields that hold the data you want to pass. For example, EnemyDiedEvent might have a ScoreValue field. When you publish the event, create it with the data: EventBus.Publish(new EnemyDiedEvent(10)). Subscribers receive the event object and can access its fields: e.ScoreValue.
What is decoupling in game development?
Decoupling means reducing the dependencies between different parts of your code so that a change in one part has little to no effect on others. For example, with an event bus, your PlayerHealth script doesn't need to know about your UIManager—they're decoupled. This makes your code more maintainable and easier to modify.
Can I use the same event bus for different event types?
Yes! That's the beauty of using a generic event bus with Action<T>. The same EventBus class handles all your event types—PlayerDiedEvent, EnemyDiedEvent, ScoreUpdatedEvent, and any other events you define. The dictionary inside the event bus keeps track of which subscribers care about which event types.
What games use the event bus pattern in their architecture?
Many professional games use event-driven architecture. Games like Hades use it for boon collection (UI updates, audio, and stat changes all react to one event). Enter the Gungeon uses it for enemy death (loot drops, door unlocking, quest updates). Overwatch uses it for objective capture (announcer, UI updates, payload movement). It's a standard pattern in the industry.
How do I debug my event bus if events aren't being received?
Add a Debug.Log statement in your EventBus.Publish method to see every event that's published and when. Check that your subscriber is calling Subscribe in OnEnable before the event is published. Verify that your subscriber object is actually enabled in the scene. Make sure the event type matches exactly (including namespace) between publisher and subscriber. These steps will help you trace where the communication is breaking down.