#ScriptableObject Events

1 messages · Page 1 of 1 (latest)

ivory epoch
#

HI, I recently started looking into scriptable objects, and found a highly referenced video from one of Unity's tedtalks a few years ago about scriptableobject events. Ive also looked at a few different videos that basically explained the same thing, but I wanted to see if they had something new to add, and surprisingly no, it's generic code that is basically a universal "event" component that has an event that triggers, and what the response will be; looks very flexible. I personally always struggled with the concept of normal unity events, both simantics and logic flow in general. Code below.

A few questions:
Why doesnt unity just keep this code as a draggable component in the inspector from unity's out-the-box code? The code is so generic and reusable since the set up is done, now u just have to connect the actual event and response functions that you just drag and drop, similar to other components.

I also noticed that one of the videos I saw had used the same GameEventListener.cs multiple times, so I'm assuming this requires that each event needs its own GameEventListener.cs component ? So basically unlike scripting where u can keep different systems, (ex.: character controller input event > playerobject action delegate in controller.cs, player.cs health drop event > UI slider delegate, etc). So with scriptableobject events, specifically this script, I could reduce code without needing those examples in the previous sentence, but the player object would neeeed to have a bunch of different GameEventListener.cs components attached per event right ? Since only one event can "invokes" the response() list in the script?

Regarding the last question, is this a relatively efficient way of doing this? I'm just surprised since at face value the code makes sense, but I'm not used to seeing a GO have multiple of the same components, and with this method, it looks like a singular GO could have MANY of the same GameEventListener.cs, one per event (and then one or multiple responses).

// ----------------------------------------------------------------------------
// Unite 2017 - Game Architecture with Scriptable Objects
// 
// Author: Ryan Hipple
// Date:   10/04/17
// ----------------------------------------------------------------------------

using System.Collections.Generic;
using UnityEngine;

namespace RoboRyanTron.Unite2017.Events
{
    [CreateAssetMenu]
    public class GameEvent : ScriptableObject
    {
        /// <summary>
        /// The list of listeners that this event will notify if it is raised.
        /// </summary>
        private readonly List<GameEventListener> eventListeners = 
            new List<GameEventListener>();

        public void Raise()
        {
            for(int i = eventListeners.Count -1; i >= 0; i--)
                eventListeners[i].OnEventRaised();
        }

        public void RegisterListener(GameEventListener listener)
        {
            if (!eventListeners.Contains(listener))
                eventListeners.Add(listener);
        }

        public void UnregisterListener(GameEventListener listener)
        {
            if (eventListeners.Contains(listener))
                eventListeners.Remove(listener);
        }
    }
}
timber cradle
karmic vault
#

Unity wouldn't add this as part of the engine because:

  • it enforces a certain project scope architecture. Something an engine shouldn't implement usually.
  • it goes a bit against the intended use case of the ScrriptableObject as Unity envisions it - an immutable data container, not something to be mutated at runtime.
  • It's too simple and obvious to be a separate editor feature.

Regarding several events, yes, it seems like with the provided implementation, you'd need a listener for each event.

Regarding efficiency, you'll need to elaborate on what you're talking about. If it's performance, it should hardly be a concern in this context, unless you're having thousands of such events invoked every frame with multiple listeners each.

ivory epoch
#

For small games I understand of course, but as the project grows, I was just curious how this scales

timber cradle
#

it doesn't scale at all

karmic vault
#

It's just a little trick/hack that can work well in some scenarios, but definitely not something that should be a "default feature"

ivory epoch
#

Because my lack of understanding with normal unityevents, where each script you'll have to hook up their events/delegates, while this seemed plentiful generic and simple, but I guess its not exactly equal

timber cradle
karmic vault
#

There are many tricks like that and many ways to implement stuff. If you found this one amazing, you probably just scratched the surface of what's possible in programming

ivory epoch
#

So I should stick with the usual UnityEvent system of OnEnable(), OnDisable(0, in a certain function specify event?.invoke(), etc ? :C

#

ty

timber cradle
karmic vault
ivory epoch
#

I see

timber cradle
#

unity events, when configured in the editor, are nothing like actual c# events, they are serialized method calls. They do not implement the observer pattern and do not decouple anything. When you use them outside triggering effects, your game-logic will become very brittle and breaks very easily. You can also not debug or refactor any logic your have implemented with them

ivory epoch
#

(╯°□°)╯︵ ┻━┻

#

dafuk. All ive heard is that events decouple

timber cradle
ivory epoch
#

so it does help with decoupling? Its just how u use it

#

cuz this statement kinda confused me: "They do not implement the observer pattern and do not decouple anything."

timber cradle
#

events are an implementation of the observer pattern. it is easily possible to use them in a way that they make things more difficult

#

you should try to avoid events until you clearly understand that you do not actually need them for anything. you need to understand the problem they solve before using them. It also helps to know a few related patterns like inversion-of-control, command, mediator and event-queue.

steady raft
#

I recommend reading through Game Programming Patterns. But don't get too hung up on using them. It's easy to start over engineering or fall into premature optimization

wheat eagle
#

To add to some of the replies here, I've used this architecture for a few released games and made a semi popular package for this. I agree with most replies in this thread.

Such an architecture only works for small games or game jam games. It esp works well if you heavily rely on splitting stuff into prefabs and use additive scene loading - useful if your team is having issues with git confclits. E.g., in one of our smaller games which took us 2 months to release on Steam, this was very useful as I could quickly disable chunks of the map checking if a player is in a trigger etc, this was useful as we split areas of the map into prefabs/scenes and each of us worked on a separate area - no git conflicts this way and I just add a listener on the prefab root and don't have to fiddle with scenes. Performance I don't think is an issue, unless you fire thousands of such events. It only works if you use it sparingly.

It can also be useful in bigger games if you need a quick hack, but other than that you'll end up with a mess which is hard to debug, unless you're super duper tidy with your naming and how you reference these events, or you're the only dev on the project.

The biggest issue will arise during refactoring. You'll eventually end up with a bunch of event assets which are not used and you'll have trouble figuring what can be deleted and what is using what, until you actually run the game. You'll get events which raise other events, recursive loops and similar. Async code with this approach is a suicide. Additionally, I found designers love this approach, perhaps too much, which means that you'll eventually get a lot accidental bugs in your prod code/assets and will have no idea this happened, you'll spend ages tracing such issues.

#

Some of my friends recently released a game where they used this architecture for pretty much everything, I plan to interview them on how it went. So far I've heard from them some time ago they had issues with saving/restoring game state, which I can imagine could be caused by overusing this architecture.

I've also heard some talks from devs using Unity Atoms and they recommend using additional inspector tools to workaround the debugging issues, but that seems to me like the wrong approach. Also what I've seen, devs who praise this the most are mostly artists/designers, so I'd take such praises with a grain of salt...

frosty meadow
#

In general much has been said above, and I agree. ScriptableObjects are best used as definitions in a project, not as something we dynamically update and create at runtime (although we can). The safest approach is to operate on copies of them. This fits naturally into patterns like DTO, adapter, or prototype – the OS acts as the definition/prototype, and in runtime we work on an instance of a C# class that represents the actual state.

The event system is often considered redundant and problematic but that due to the over-intention to use something modular. This doesnt mean that events or observer patterns are useless. Theyre very situational, and you can use them when you truly need and understand them.

If you really have to use events, following previous logic a much better solution for events is to base them on pure cshrp code rather than assets. For example, my event system is based on a list of channels (EventChannel<TEventArgs>), where the key is a token (associated with a specific argument type – public sealed class ChannelToken<TEventArgs>) and each channel has a hashset of listeners sorted by priority. Each event has clearly defined EventArgs that carry the data. Subscribers register and respond only to channels they are interested in, while publishers simply "throw" an event with a token and argument. The entire event flow is visible in the code, easy to search and refactor.

  • clear typing – each event carries specific data (EventArgs), there are no empty"events
  • true decoupling – the publisher doesnt know the subscribers, operating only on tokens and data.
  • testability , you can write unit tests for event calls because its pure C#.
  • control – the system is easily extended with logging, priorities, onetime listeners, throttling etc
  • scalability – even in a large project you have a central channel registry which helps maintain order.