One of the biggest problems I used to run into while developing my game in Unity were referencing scripts and objects. Unless it's just a small project that can be handled by a handful of scripts, if your game gets any larger you quickly realize this can become a big issue. Before you know it, you're dealing with a ton of spaghetti code referencing scripts all over the place. That's where game architecture comes into play.
As you continue your game development career, you'll notice that game architecture becomes more and more important. It's something you probably didn't give much thought to when you started out, but now it's super important to understand which game architecture path you want to implement in your game.
Does the UI panel need to know if your player takes damage? Or does your Audio Manager need to know if the state of your game changes? Already you can see there is some referencing involved.
Let me show you the "old way" I used to decouple scripts from one another, and the new way — a technique I'm using now to make it even more expandable.
The old way featured a static class that acted like a middleman between scripts that needed information from each other.
The script was simply called GameEvents.cs
. and looked like this with an Audio event as an example:
using System;
using UnityEngine;
namespace Munnica.Events
{
public static class GameEvents
{
///
/// If audio needs to be played
///
public static Action<AudioClip> OnAudioPlay = delegate { };
}
}
Then in another script, let's say AudioManager.cs
, you would then subscribe to, and unsubscribe from, this GameEvent
:
namespace Munnica.Managers
{
public class AudioManager : MonoBehaviour
{
private void OnEnable()
{
GameEvents.OnAudioPlay += PlayAudio;
}
private void OnDisable()
{
GameEvents.OnAudioPlay -= PlayAudio;
}
private void PlayAudio(AudioClip clip)
{
// Cached references from AudioManager would then be called
// e.g. _audioSource.clip = clip; _audioSource.Play();
}
}
}
Audio, for example, would then be triggered from another script by means of GameEvents.OnAudioPlay
, which in turn would play the passed through audioclip
The problem that I realized (after having about 20 GameEvents in the script), was that this technique wasn't very scalable. What if I also wanted to control the volume of the audio, or the pitch? GameEvents that took care of th UI were even worse! Images, text components etc, have a lot more possibilities. This would all have to be restructured and refactored if I wanted to add extra things.
The solution? An EventBus!!
A bus, not to be confused with the vehicle :P, in computing terms is a communication system that transfers data between components. Exactly what we need for our game development project that's in dire need of decoupling!
Understanding how the 'old-event' system works (as described above), is a huge plus since it behaves similarly - but not necessary to implement this new system in your own project.
Let’s walk through how you can set up your own EventBus system step by step.
The EventBus system looks like this:
Step 1: Create a static class EventBus
using System;
using System.Collections.Generic;
namespace Munnica.Events
{
public static class EventBus
{
private static readonly Dictionary _eventTable = new();
public static void Subscribe(Action callback)
{
if (_eventTable.TryGetValue(typeof(T), out var existingDelegate))
{
_eventTable[typeof(T)] = (Action)existingDelegate + callback;
}
else
{
_eventTable[typeof(T)] = callback;
}
}
public static void Unsubscribe(Action callback)
{
if (_eventTable.TryGetValue(typeof(T), out var existingDelegate))
{
_eventTable[typeof(T)] = (Action)existingDelegate - callback;
}
}
public static void Publish(T eventData)
{
if (_eventTable.TryGetValue(typeof(T), out var del))
{
((Action)del)?.Invoke(eventData);
}
}
public static void DebugSubscribers()
{
foreach (var kvp in _eventTable)
{
var type = kvp.Key;
var del = kvp.Value;
UnityEngine.Debug.Log($"Event Type: {type.Name}");
if (del != null)
{
foreach (var d in del.GetInvocationList())
{
UnityEngine.Debug.Log($" -> {d.Method.Name} from {d.Target}");
}
}
}
}
}
}
As you can see, there are a few additions compared to the previous GameEvent system.
The static 'Subscribe' method is similar to the += subscribe in GameEvents. This time with the addition that we're now using Generics to implement the TYPE we're going to communicate with. The subscription is then stored in an event table (dictionary). In the same class, we're also implementing 'Unsubscribe', this ensures that we can easily unsubscribe from the EventBus in the method OnDisable in any script!
Step 2: (this is where the magic is going to happen!) Create structs: special data containers tailored to your project.
In my game development project, I use all kinds of structs for EventBus, the following example is how I play audio by means of a struct:
using UnityEngine;
namespace Munnica.Events
{
public struct PlaySoundEvent
{
public AudioClip Clip;
public Vector3 Position;
public float Volume;
public int Channel;
public bool Stop;
public PlaySoundEvent(AudioClip clip, Vector3 position, int channel = 1, float volume = 1f, bool stop = false)
{
Clip = clip;
Position = position;
Channel = channel;
Volume = volume;
Stop = stop;
}
}
}
As you can see, it's totally customizable and expandable now! This struct 'PlaySoundEvent' has a constructor that assigns the parameters that need to be implemented! I can now not only decide which Audioclip to play, also I can assign the position, the channel it needs to play through (I have 3 different AudioSources), the volume level and if the audio needs to stop at any given moment. Let's say later on I want to also include the pitch level, or something else, it can be easily added on without breaking any code!
Now, how do we call this Event? Remember in our static class EventBus we wrote the Publish method? Exactly, that's what we're going to use to 'invoke' this event!
Step 3: Publish the event!
From any script, we can invoke this event by:
EventBus.Publish(new PlaySoundEvent(_scannerLoadingSFX, Vector3.zero, 2, 1f));
By means of customized structs, perfectly tailored to any small- midsized project, we've now completely decoupled our game system! AudioManager, for example, doesn't need to know which script is playing an audioclip, it just knows it needs to play the clip that's coming in. The same goes for the script pushing the audioclip, it doesn't need to know which script is responsible for playing the clip. It just needs to 'submit' the clip to a subscriber. I recently completely overhauled my project to implement the EventBus system. I've been working with this new system for quite some time now, and I can't go back anymore!
Final note about subscribers:
One last note, and that's about the EventBus subscribers. At some point, your going to have more than one event. In fact, it's likely you'll be ending up with lots of custom structs that all do something in your project. With the 'old-game event system', you could just look at the list GameEvents and see how many events there were in your game (although you still weren't able to see which scripts were assigned to that event unless you did a work around). Now, with EventBus, there is a special included method 'DebugSubscribers' that will allow you to see which game objects (scripts) are subscribed to which event!
In my GameManager class (which is a persistent singleton), I just have a simple boolean _showEventSubscribers, that (when ticked) prints out all the actives subscribers to the console in Unity by means of this code:
if (_showEventBusSubscribers) { EventBus.DebugSubscribers(); }
I hope that you can apply this technique in your own project. I found it the best way to decouple scripts in Unity game development! If you have any questions, (or perhaps know even a better way!) do not hesitate to reach out.
This article may contain typos or grammarly incorrect sentence structures, I'm a gamedeveloper, not an editor or proofreader :P
Happy coding!