Skip to content
This repository was archived by the owner on Aug 14, 2019. It is now read-only.

Creating a Scriptable Object Event system

Erik edited this page Apr 29, 2019 · 9 revisions

There are a few modifications I have made to the scripts provided by Unity, such as enabling the GameEventListeners to listen to multiple different events. For direct reference here are the 2 customized GameEvent scripts: Game Event Listener Script, Game Event Script

I have since removed these modifications from the master branch, since they soon became a replacement for standard subscribable events and removed reflection/good code maintenance practices from many of the use cases.

Why use an event system?

Why not just have your scripts coupled and calling one another directly? An event system doesn't seem like it will save me much time.

The purpose of an event system isn't to save you time on your first development cycle. It's one of the most significant examples of delayed gratification in game development.

Let's imagine a scenario where we have a PlayerHealth script and a PlayerRespawn script tightly coupled together:

When my player's health reaches below 0, my player dies. I could simply cache my playerRespawn component and trigger a "QueueRespawn" method from my PlayerHealth script and that would take care of the issue:

PlayerRespawn respawn = GetComponent<PlayerRespawn>();
if(health <= 0)
{
respawn.QueueRespawn();
}

There's nothing broken in this solution. Once a player dies, we trigger another script's respawn method. The problem comes from coupling 2 scripts together like this. The PlayerHealth script has now adopted the responsibility of handling respawn logic. There's a direct dependency built between the scripts and if I want to start adding other things that trigger off of a death event (like, a score change, sound effects, difficulty changes, saving, and much much more) I need to continue to extend that logic. Eventually I'll end up with something that looks like this:

PlayerRespawn respawn = GetComponent<PlayerRespawn>();
SoundEffects sound = GetComponent<SoundEffects>();
Score score = GetComponent<Score>();
.
..
...
if(health <= 0)
{
respawn.QueueRespawn();
sound.PlayDeathSound();
score.ResetScore();
.
..
...
}

Each time I couple more logic to this code I do 2 things:

  1. I increase the responsibility of the PlayerHealth script
  2. I increase the complexity of managing the overall game

In many scenarios, the scripts that are being triggered aren't going to be directly attached to the GameObject, so you'll need to set them up as public or serialized fields and configure them through the inspector.

Eventually you'll have inspectors that look like this for all of your GameObjects, which is going to be very tedious to maintain and will require much more mental capacity to keep track of overall.

Ugly Inspector for a monolithic script

Now instead, let's look at a scenario that uses an event system (either ScriptableObjects or events/delegates):

The beauty of an event system is that it enables you to trigger an event and effectively "forget it". The script triggering the event should be agnostic to any of its listeners (meaning it knows they exist, but it doesn't really care about them or what they're doing). You may have noticed in our prior milestone we used a delegate and event for managing health changes. This still involved a dependency on CharacterHealth for CharacterUI, but it's a 1-way dependency that doesn't affect the triggering script CharacterHealth.

Let's repeat our mock example from before:

if(health <= 0)
{
//Trigger all of our death scripts here
}

Both scenarios are going to do what is listed above, but here we are going to use a GameEvent scriptable object event system.

Let's add 2 GameEvents. Since these shouldn't really be touched by any other scripts, we will leave the access modifier as private, and instead [SerializeField] to expose them to the inspector.

public class CharacterHealth : MonoBehaviour
{
    [SerializeField]
    GameEvent deathEvent = null;

    [SerializeField]
    GameEvent healEvent = null;

Then, since we've created the GameEvent and the GameEventListener scripts from Unity's Architect your code with Scriptable Objects article, we can create 2 new assets called "Death Event" and "Heal Event".

Creating a new GameEvent

We can now drag and drop these events into the inspector:

Events in Inspector

and within our CharacterHealth script we can raise those events anywhere that we deem necessary:

deathEvent.Raise();

The CharacterHealth script is now "alerting" any listeners to the deathEvent. If you're following along, we don't have a listener set up yet so it won't do anything. Let's set one up right now. Create a new GameObject and add an AudioSource to it. Pick a sound to play and uncheck "Play on Awake":

Audio Source

We are going to trigger this AudioSource with a GameEventListener. Add a GameEventListener to the AudioSource and drag the Death Event into the Events list. Then add a new response and call the AudioSource object's Play method:

GameEventListener triggering the play method

When the enemie's death goes below 0, a sound should play. Consider trying out other things, such as triggering a particle affect on death, making the enemy object disappear, triggering an animation, or something else! You can add as many events to this version of the listener, as well as trigger as many other methods as you like. Currently the event does not pass any parameters, but we will look into adding that functionality in the next milestone!

The old way to trigger events

Prior to the advent of this Scriptable Object event system, the most common way to keep code reasonably decoupled was through singleton managers with delegates/events. Things like inventory change events, enemy deaths, score systems, and more were created through this model, but it required liberal use of singletons and presented its own issues.

Clone this wiki locally