Doubts about Scriptable Object Events

Hello guys.

This last week I have been checking about Scriptable Objects, Unity actions and Unity events to improve my projects code arquitecture and avoid dependencies at all cost.

I have watched Ryan´s classic talk in Unite 2017 and other tutorials to see how they work. Even though I understand actions (delegates) and the general idea of the SO events I dont really understand where those last ones can be actually used or how to mix both of them.

What I mean with that is that almost all the examples I have found are very “general” or they are mostly local functions that cannot be moved to another scenes without having to manually settled them again.

For example the most common example is the “OnPlayerDeath”, that takes away the player input, places a sound and changes the UI. But this SO event I cannot use it “again” unless I settle it again manually in other scene which creates dependencies.

I would appreciate deeply if you guys could explain to me when and how could I implement SO events with some examples so I can add them in my next projects.

Thank you so much

A scriptable object event is just a simple wrapper around a delegate, to make it assignable into serialized fields:

[CreateAssetMenu]
public class Event : ScriptableObject
{
	private Action listeners = null;

	/// <summary>
	/// Subscribe a method to be executed whenever the event is raised.
	/// </summary>
	public void AddListener(Action method) => listeners += method;

	/// <summary>
	/// Unsubscribe a method from being executed whenever the event is raised.
	/// </summary>
	public void RemoveListener(Action method) => listeners -= method;

	/// <summary>
	/// Raise the event, causing all listener methods to be executed.
	/// </summary>
	public void Raise() => listeners?.Invoke();
}

You can then create multiple assets from this same class, to represent different events, like “OnPlayerDeath”.

If you need to pass an argument with the event, then you do just need to create a separate class for each argument type.

// "OnPlayerDeath", "OnPlayerDamaged" etc...
[CreateAssetMenu]
public class PlayerEvent : Event<Player> { }

// "OnEnemyDeath", "OnEnemyDamaged" etc...
[CreateAssetMenu]
public class EnemyEvent : Event<Enemy> { }

public abstract class Event<T> : ScriptableObject
{
	private Action<T> listeners = null;

	/// <summary>
	/// Subscribe to receive a callback whenever the event is raised.
	/// </summary>
	/// <param name="method"> Method to execute whenever the event is raised. </param>
	public void AddListener(Action<T> method) => listeners += method;

	/// <summary>
	/// Unsubscribe from receiving a callback whenever the event is raised.
	/// </summary>
	/// <param name="method"> Method to no longer execute whenever the event is raised. </param>
	public void RemoveListener(Action<T> method) => listeners -= method;

	/// <summary>
	/// Raise the event, causing all listener methods to be executed.
	/// </summary>
	public void Raise(T argument) => listeners?.Invoke(argument);
}

Then you drag the same event asset to a serialized field on the even raiser and all the listeners, and latter subscribe to it, and the prior raise it.

1 Like

Events are not about reducing dependencies. The number of dependencies you need without events is the same as with events. The only exception might be a class that acts as a mediator, but even this class may or may not use events. So, the amount of dependencies you have remains unchanged just because you’re using events. Instead, events are about inverting dependencies and providing a notification when something happens.

There are three key topics to address here: events, events with Scriptable Objects (SO), and Ryan’s video.

First, about events:
Take the example of OnPlayerDeath without using an SO. You need something to happen when the player dies, let’s say the screen flashes. Instead of writing code inside the player class that directly depends on a ScreenFlash class and calls its flash method, you invert the dependency. The ScreenFlash class subscribes to the OnPlayerDeath event, so it listens for when the player dies.

This inversion is beneficial because now the UI class, ScreenFlash, depends on the player class, which is less likely to change. The player’s death mechanic is a core part of your game’s logic and is unlikely to be modified frequently, whereas the visual effect of the screen flash could change more often.

For instance, if you later decide to add a sound effect when the player dies, you can create a new class that subscribes to the same OnPlayerDeath event and plays the sound. If you then replace the screen flash with a death counter, you can simply swap out the ScreenFlash class or keep both without altering the player class at all.

Without using events, you would have to modify the player class each time, risking introducing bugs or breaking unrelated functionality. Events allow your code to mirror the requirements more cleanly: when you need a new feature (e.g., a counter), you add a corresponding class; when you want to remove a feature (e.g., screen flash), you simply remove the related class, without touching the player script.

You could achieve something similar by having UI classes poll the player’s state every few seconds (e.g., checking Player.IsDead), but polling is less efficient than event-driven code.

Second, events with Scriptable Objects (SO):
As sisus_co explained, you can use a Scriptable Object as a wrapper for events. Since SOs are assets, they can be accessed from anywhere by dragging them into a field in the Unity editor, which makes them convenient for event management.

Third, Ryan’s video:
In my experience, people often overlook the most important point Ryan makes at the start of his video: he and his team are programmers who need to hand off projects to game designers and then move on to the next project. In this context, the SO architecture Ryan presents works well because it allows game designers to make changes without interfering with the programmers’ code or needing the programmers to come back for small adjustments.

However, in teams where programmers are available throughout the project or for solo developers, I’ve found that this SO architecture can introduce unnecessary complexity and make the project overly reliant on that structure. it may not be the best fit for those scenarios.

Lastly, on mixing Actions and events:
Actions and events are not different or directly comparable things. Events are essentially a wrapper around delegates/Actions. The reason to use events is that they only allow other classes to subscribe or unsubscribe to a delegate, preventing accidental modifications like clearing all subscriptions. You can think of events as similar to properties in C#: just as properties encapsulate fields, events encapsulate delegates. Properties control getting and setting values, while events control subscribing and unsubscribing to a delegate.

6 Likes

I have some questions: what is the advantage of using delegates? Can I use UnityEvent instead? Something like:

[CreateAssetMenu]
public class PlayerEventsContainer : ScriptableObject
{
	[HideInInspector] public UnityEvent OnPlayerDeath;
	[HideInInspector] public UnityEvent<float> OnPlayerAttack; //Pass in player's damage as argument for example
	[HideInInspector] public UnityEvent OnPlayerSpawn;
}

So I can have containers for different types of events (such as one for the player, and one for enemy). Will this work?

And one more question: as for the listeners to subscribe to the event and the raiser to invoke the event, you need to drag the SO to their serialized field in the inspector. Is there any way to skip this step as it becomes more cumbersome when you have more and more events? Thanks a lot!

No, that wouldn’t work, because:

  1. You can’t drag-and-drop scene objects into serialized fields on scriptable object assets in Edit Mode.
  2. If you drag-and-drop a prefab asset into an UnityEvent field on scriptable object asset, the method would get executed on the prefab asset itself, not instances of it that have been Instantiated into scenes.

This is why you drag the scriptable object event asset into scene objects in Edit Mode, but only have the scene objects subscribe to the event later in Play Mode. Scriptable object assets can hold references to scene objects in Play Mode, they just can’t save those to disk, so they won’t survive the editor being restarted etc.

You can use the Default References UI that is shown when you view your script asset in the Inspector.

Once your event has been added there, it will automatically get assigned to all newly created components when you attach them to game objects in the editor.

1 Like

I dont really see the benefit of making messages into SOs.

However a message can hold a reference to a SO and the publisher hooks it up when publishing a message on the event buss.

1 Like