Generic Scriptable Object Events

Going off Ryan Hipple's fantastic demonstration of using Scriptable Objects for handling events, I attempted to make it generic so I could start passing args with it. Here's the code for reference.

public abstract class BaseGameEventListener<T> : MonoBehaviour
{
    public BaseGameEvent<T> GameEvent;
    public UnityEvent<T> Response;

    private void OnEnable()
    {
        GameEvent.RegisterListerner(this);
    }

    private void OnDisable()
    {
        GameEvent.UnregisterListener(this);
    }

    public void RaiseEvent(T _t)
    {
        Response.Invoke(_t);
    }
}
public abstract class BaseGameEvent<T> : ScriptableObject
{

    private List<BaseGameEventListener<T>> listeners = new List<BaseGameEventListener<T>>();
    public void RegisterListerner(BaseGameEventListener<T> listener)
    {
        listeners.Add(listener);
    }

    public void UnregisterListener(BaseGameEventListener<T> listener)
    {
        listeners.Remove(listener);
    }

    public void Raise(T _t)
    {
        for (int i = listeners.Count - 1; i >= 0; --i)
        {
            Debug.Log(_t.ToString());
            listeners[i].RaiseEvent(_t);
        }
    }
}

Then I create derived classes so I can create the actual instances.

[CreateAssetMenu()]
public class IntGameEvent : BaseGameEvent<int>{}


public class IntGameEventListener : BaseGameEventListener<int>{}

And now I have my nice cozy game events.
7370453--898484--upload_2021-7-29_0-37-43.png

However, there's a bit of an usability issue here, demonstrated in the below gif.
7370453--898487--GIF.gif

Unity is serializing my GameEvent correctly in the inspector, but in the object selection window it doesn't show my custom GameObjectGameEvent scriptable objects, despite having the same signature(?). Dragging and dropping the event still works perfectly fine. It's just not convenient. Especially in my larger project.

What could I do to make this generic take on Scriptable Object Events more convenient? Is there a better solution out there already? I could use some feedback here. My priority for this feature is convenience.

You might need to specialize them with generic arguments:

    public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour
        where TGameEvent : BaseGameEvent<TParameter>
        where TUnityEvent : UnityEvent<TParameter>
    {
        public TGameEvent  GameEvent;
        public TUnityEvent  Response;
    }

Then you need to do public class IntGameEventListener : BaseGameEventListener<int, IntGameEvent, IntUnityEvent> { }

2 Likes

The object picker bugs out quite often when it comes to assets (maybe it’s even by design to encourage the purchase of thirdparty assets?). One way to fix it (atleast it works for me almost every time) is to rightclick → reimport the scriptable object assets, which i guess triggers an assetdatabase re-registration with the correct metadata. Maybe that works in your case too.

This is SO CLOSE. Fixes the issue with selecting the SO in the object picker. I’m stuck on the last piece of C# though.

public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour
    where TGameEvent : BaseGameEvent<TParameter>
    where TUnityEvent : UnityEvent<TParameter>
{
    public TGameEvent GameEvent;
    public TUnityEvent Response;

    //Error CS1503  Argument 1: cannot convert from 'BaseGameEventListener<TParameter, TGameEvent, TUnityEvent>' to 'BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEngine.Events.UnityEvent<TParameter>>'
    private void OnEnable() => GameEvent.RegisterListerner(this);

    //Error CS1503  Argument 1: cannot convert from 'BaseGameEventListener<TParameter, TGameEvent, TUnityEvent>' to 'BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEngine.Events.UnityEvent<TParameter>>'
    private void OnDisable() => GameEvent.UnregisterListener(this);

    public void RaiseEvent(TParameter _t)
    {
        Response.Invoke(_t);
    }
}
public abstract class BaseGameEvent<TParameter> : ScriptableObject
{

    private List<BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>>> listeners;
    public void RegisterListerner(BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>> _listener)
    {
        listeners.Add(_listener);
    }

    public void UnregisterListener(BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>> _listener)
    {
        listeners.Remove(_listener);
    }

    public void Raise(TParameter _t)
    {
        for (int i = listeners.Count - 1; i >= 0; --i)
        {
            Debug.Log(_t.ToString());
            listeners[i].RaiseEvent(_t);
        }
    }
}

Looking at the error message (commented above in BaseGameEventListener), the generic arguments do not appear to be the same signature as my RegisterListener() and UnregisterListener(). Does this mean this particular solution is a dead end?

RegisterListener woulld need to take a BaseGameEventListener<TParameter, TGameEvent, TUnityEvent>, meaning BaseGameEvent would need all those generic parameters as well.

Try making an interface for BaseGameEventListener to implement: IEventListener<T> { void RaiseEvent(T parameter); }. Then RegisterListener should be able to take that interface and keep a list of it instead of needing all the generic parameters.

1 Like

Ooh, interfaces were the right move! Works brilliantly now. Thanks for the help! I’ll include the finished scripts below for anyone that wants them.

public abstract class BaseGameEvent<TParameter> : ScriptableObject
{

    private List<IEventListener<TParameter>> listeners = new List<IEventListener<TParameter>>();
    public void RegisterListerner(IEventListener<TParameter> _listener)
    {
        listeners.Add(_listener);
    }

    public void UnregisterListener(IEventListener<TParameter> _listener)
    {
        listeners.Remove(_listener);
    }

    public void Raise(TParameter _t)
    {
        for (int i = listeners.Count - 1; i >= 0; --i)
        {
            listeners[i].RaiseEvent(_t);
        }
    }
}
public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour, IEventListener<TParameter>
    where TGameEvent : BaseGameEvent<TParameter>
    where TUnityEvent : UnityEvent<TParameter>
{
    public TGameEvent GameEvent;
    public TUnityEvent Response;

    private void OnEnable() => GameEvent.RegisterListerner(this);

    private void OnDisable() => GameEvent.UnregisterListener(this);

    public void RaiseEvent(TParameter _t)
    {
        Response.Invoke(_t);
    }
}
[CreateAssetMenu()]
public class IntGameEventListener : BaseGameEventListener<int, IntGameEvent, UnityEvent<int>> {}

If UnityEvent works, then I would assume you could also use BaseGameEvent instead of IntGameEvent.

@FlounderBox I am trying to understand how you did this, but my knowledge of interfaces is very limited. I get this error
Error CS0246 The type or namespace name 'IEventListener<>' could not be found (are you missing a using directive or an assembly reference?).

How and where should the IEventListener interface be added to the above code?

@Tinsa - just came upon this. Almost certainly too late, but for future generations, I just put the interface into the same .cs file as the baseGameEventListener class. It could also be in its own file. So, here is the full code I am using:

BaseGameEventListener.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public interface IEventListener<T>
{
    void RaiseEvent(T parameter);
}
public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour, IEventListener<TParameter>
    where TGameEvent : BaseGameEvent<TParameter>
    where TUnityEvent : UnityEvent<TParameter>
{
    public TGameEvent GameEvent;
    public TUnityEvent Response;

    private void OnEnable()
    {
        GameEvent.RegisterListener(this);
    }

    private void OnDisable()
    {
        GameEvent.UnRegisterListener(this);
    }

    public void RaiseEvent(TParameter t)
    {
        Response.Invoke(t);
    }

}

BaseGameEvent.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public abstract class BaseGameEvent<TParameter> : ScriptableObject
{
    private List<IEventListener<TParameter>> _listeners = new List<IEventListener<TParameter>>();

    public void Raise(TParameter t)
    {
        for (int i = _listeners.Count - 1; i >= 0; i--)
        {
            _listeners[i].RaiseEvent(t);
        }
    }
    public void RegisterListener(IEventListener<TParameter> listener)
    {
        if (!_listeners.Contains(listener)) { _listeners.Add(listener); }
    }
    public void UnRegisterListener(IEventListener<TParameter> listener)
    {
        if (_listeners.Contains(listener)) { _listeners.Remove(listener); }
    }
}

IntGameEvent.cs: (sets the model for using the class with a specific data type)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu()]
public class IntGameEvent : BaseGameEvent<int>
{

}

IntGameEventListener.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class IntGameEventListener : BaseGameEventListener<int, BaseGameEvent<int>, UnityEvent<int>>
{
}
1 Like

You sir are a saint for actually including the working fix at the end of your post! I tried for hours to make this happen myself and finally admitted defeat and found this!

On this same subject I made a far simpler far more "oldskool" package for data storage and interprocess communication. I call it Datasacks and it is based on ScriptableObjects. It is far more of a programmer's tool, and it includes code generation to reduce typos.

Here's the overview:

8687265--1171653--20180724_datasacks_overview.png
Datasacks is presently hosted at these locations:

https://bitbucket.org/kurtdekker/datasacks

https://github.com/kurtdekker/datasacks

https://gitlab.com/kurtdekker/datasacks

https://sourceforge.net/projects/datasacks/