Search Unity

Feedback Generic Scriptable Object Events

Discussion in 'Scripting' started by FlounderBox, Jul 29, 2021.

  1. FlounderBox

    FlounderBox

    Joined:
    May 5, 2015
    Posts:
    13
    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.

    Code (Csharp):
    1. public abstract class BaseGameEventListener<T> : MonoBehaviour
    2. {
    3.     public BaseGameEvent<T> GameEvent;
    4.     public UnityEvent<T> Response;
    5.  
    6.     private void OnEnable()
    7.     {
    8.         GameEvent.RegisterListerner(this);
    9.     }
    10.  
    11.     private void OnDisable()
    12.     {
    13.         GameEvent.UnregisterListener(this);
    14.     }
    15.  
    16.     public void RaiseEvent(T _t)
    17.     {
    18.         Response.Invoke(_t);
    19.     }
    20. }
    Code (Csharp):
    1. public abstract class BaseGameEvent<T> : ScriptableObject
    2. {
    3.  
    4.     private List<BaseGameEventListener<T>> listeners = new List<BaseGameEventListener<T>>();
    5.     public void RegisterListerner(BaseGameEventListener<T> listener)
    6.     {
    7.         listeners.Add(listener);
    8.     }
    9.  
    10.     public void UnregisterListener(BaseGameEventListener<T> listener)
    11.     {
    12.         listeners.Remove(listener);
    13.     }
    14.  
    15.     public void Raise(T _t)
    16.     {
    17.         for (int i = listeners.Count - 1; i >= 0; --i)
    18.         {
    19.             Debug.Log(_t.ToString());
    20.             listeners[i].RaiseEvent(_t);
    21.         }
    22.     }
    23. }
    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.
    upload_2021-7-29_0-37-43.png

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

    Unity is serializing my GameEvent<T> 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.
     
  2. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    You might need to specialize them with generic arguments:
    Code (CSharp):
    1.     public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour
    2.         where TGameEvent : BaseGameEvent<TParameter>
    3.         where TUnityEvent : UnityEvent<TParameter>
    4.     {
    5.         public TGameEvent  GameEvent;
    6.         public TUnityEvent  Response;
    7.     }
    8.  
    Then you need to do
    public class IntGameEventListener : BaseGameEventListener<int, IntGameEvent, IntUnityEvent> { }
     
    Bunny83 and FlounderBox like this.
  3. Flavelius

    Flavelius

    Joined:
    Jul 8, 2012
    Posts:
    945
    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.
     
  4. FlounderBox

    FlounderBox

    Joined:
    May 5, 2015
    Posts:
    13
    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.
    Code (CSharp):
    1. public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour
    2.     where TGameEvent : BaseGameEvent<TParameter>
    3.     where TUnityEvent : UnityEvent<TParameter>
    4. {
    5.     public TGameEvent GameEvent;
    6.     public TUnityEvent Response;
    7.  
    8.     //Error CS1503  Argument 1: cannot convert from 'BaseGameEventListener<TParameter, TGameEvent, TUnityEvent>' to 'BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEngine.Events.UnityEvent<TParameter>>'
    9.     private void OnEnable() => GameEvent.RegisterListerner(this);
    10.  
    11.     //Error CS1503  Argument 1: cannot convert from 'BaseGameEventListener<TParameter, TGameEvent, TUnityEvent>' to 'BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEngine.Events.UnityEvent<TParameter>>'
    12.     private void OnDisable() => GameEvent.UnregisterListener(this);
    13.  
    14.     public void RaiseEvent(TParameter _t)
    15.     {
    16.         Response.Invoke(_t);
    17.     }
    18. }
    Code (CSharp):
    1. public abstract class BaseGameEvent<TParameter> : ScriptableObject
    2. {
    3.  
    4.     private List<BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>>> listeners;
    5.     public void RegisterListerner(BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>> _listener)
    6.     {
    7.         listeners.Add(_listener);
    8.     }
    9.  
    10.     public void UnregisterListener(BaseGameEventListener<TParameter, BaseGameEvent<TParameter>, UnityEvent<TParameter>> _listener)
    11.     {
    12.         listeners.Remove(_listener);
    13.     }
    14.  
    15.     public void Raise(TParameter _t)
    16.     {
    17.         for (int i = listeners.Count - 1; i >= 0; --i)
    18.         {
    19.             Debug.Log(_t.ToString());
    20.             listeners[i].RaiseEvent(_t);
    21.         }
    22.     }
    23. }
    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?
     
  5. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    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.
     
    FlounderBox likes this.
  6. FlounderBox

    FlounderBox

    Joined:
    May 5, 2015
    Posts:
    13
    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.

    Code (CSharp):
    1. public abstract class BaseGameEvent<TParameter> : ScriptableObject
    2. {
    3.  
    4.     private List<IEventListener<TParameter>> listeners = new List<IEventListener<TParameter>>();
    5.     public void RegisterListerner(IEventListener<TParameter> _listener)
    6.     {
    7.         listeners.Add(_listener);
    8.     }
    9.  
    10.     public void UnregisterListener(IEventListener<TParameter> _listener)
    11.     {
    12.         listeners.Remove(_listener);
    13.     }
    14.  
    15.     public void Raise(TParameter _t)
    16.     {
    17.         for (int i = listeners.Count - 1; i >= 0; --i)
    18.         {
    19.             listeners[i].RaiseEvent(_t);
    20.         }
    21.     }
    22. }
    Code (CSharp):
    1. public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour, IEventListener<TParameter>
    2.     where TGameEvent : BaseGameEvent<TParameter>
    3.     where TUnityEvent : UnityEvent<TParameter>
    4. {
    5.     public TGameEvent GameEvent;
    6.     public TUnityEvent Response;
    7.  
    8.     private void OnEnable() => GameEvent.RegisterListerner(this);
    9.  
    10.     private void OnDisable() => GameEvent.UnregisterListener(this);
    11.  
    12.     public void RaiseEvent(TParameter _t)
    13.     {
    14.         Response.Invoke(_t);
    15.     }
    16. }
    Code (CSharp):
    1. [CreateAssetMenu()]
    2. public class IntGameEventListener : BaseGameEventListener<int, IntGameEvent, UnityEvent<int>> {}
     
  7. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    If UnityEvent<int> works, then I would assume you could also use BaseGameEvent<int> instead of IntGameEvent.
     
  8. Tinsa

    Tinsa

    Joined:
    Jul 24, 2018
    Posts:
    3
    @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?
     
  9. briankauf

    briankauf

    Joined:
    Jun 21, 2017
    Posts:
    1
    @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:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5.  
    6. public interface IEventListener<T>
    7. {
    8.     void RaiseEvent(T parameter);
    9. }
    10. public abstract class BaseGameEventListener<TParameter, TGameEvent, TUnityEvent> : MonoBehaviour, IEventListener<TParameter>
    11.     where TGameEvent : BaseGameEvent<TParameter>
    12.     where TUnityEvent : UnityEvent<TParameter>
    13. {
    14.     public TGameEvent GameEvent;
    15.     public TUnityEvent Response;
    16.  
    17.     private void OnEnable()
    18.     {
    19.         GameEvent.RegisterListener(this);
    20.     }
    21.  
    22.     private void OnDisable()
    23.     {
    24.         GameEvent.UnRegisterListener(this);
    25.     }
    26.  
    27.     public void RaiseEvent(TParameter t)
    28.     {
    29.         Response.Invoke(t);
    30.     }
    31.  
    32. }
    BaseGameEvent.cs:


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public abstract class BaseGameEvent<TParameter> : ScriptableObject
    7. {
    8.     private List<IEventListener<TParameter>> _listeners = new List<IEventListener<TParameter>>();
    9.  
    10.     public void Raise(TParameter t)
    11.     {
    12.         for (int i = _listeners.Count - 1; i >= 0; i--)
    13.         {
    14.             _listeners[i].RaiseEvent(t);
    15.         }
    16.     }
    17.     public void RegisterListener(IEventListener<TParameter> listener)
    18.     {
    19.         if (!_listeners.Contains(listener)) { _listeners.Add(listener); }
    20.     }
    21.     public void UnRegisterListener(IEventListener<TParameter> listener)
    22.     {
    23.         if (_listeners.Contains(listener)) { _listeners.Remove(listener); }
    24.     }
    25. }
    IntGameEvent.cs: (sets the model for using the class with a specific data type)

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu()]
    6. public class IntGameEvent : BaseGameEvent<int>
    7. {
    8.  
    9. }
    10.  
    IntGameEventListener.cs:


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Events;
    5.  
    6. public class IntGameEventListener : BaseGameEventListener<int, BaseGameEvent<int>, UnityEvent<int>>
    7. {
    8. }
    9.  
     
    Frttpc likes this.
  10. mcarr390

    mcarr390

    Joined:
    Aug 28, 2017
    Posts:
    1

    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!
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,744
    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:

    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/