1
\$\begingroup\$

I'm trying to implement an observable pattern on variables, so that I can hook up UI components, etc. to change displayed values or other actions when their value changes.

I wanted three features that the built-in Delegate system doesn't satisfy:

  • (optional) try/catch calls so that some not important UI element doesn't break the whole game loop
  • having the variables in ScriptableObjects for easy storage and inspection
  • having a list of listeners in the inspector for debugging and transparency

Here is the code I finally came up with, and it does what I need. But I'm relatively new to C# so I'm looking forward to improvements and advise:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
using Sirenix.OdinInspector;


[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {

    [System.Serializable]
    public class Listener {
        [DisplayAsString, HideInInspector] public int ID;
        [DisplayAsString, HideLabel, HorizontalGroup] public string Observer;
        [DisplayAsString, HideLabel, HorizontalGroup] public string Method;
        [HideInInspector] public UnityAction<float> Callback;
        
        public Listener(UnityAction<float> cb) {
            ID = cb.GetHashCode();
            Observer = cb.Target.ToString();
            Method = cb.Method.ToString();
            Callback = cb;
        }
    }

    [Delayed]
    [OnValueChanged("NotifyListeners")]
    [SerializeField] private float m_Value;
    public float Value {
        get {
            return m_Value;
        }
        set {
            m_Value = value;
            NotifyListeners();
        }
    }

    [Tooltip("Log Invoke() calls")]
    [SerializeField] bool Trace;

    [Tooltip("Use try/catch around Invoke() calls so events continue to other listeners even if one fails")]
    [SerializeField] bool CatchExceptions;

    [ListDrawerSettings(DraggableItems = false, Expanded = true, ShowIndexLabels = false, ShowPaging = false, ShowItemCount = true)]
    [SerializeField] List<Listener> listeners = new List<Listener>();


    void Awake() {
        // clear out whenever we start - just in case some observer doesn't properly remove himself
        // maybe later I'll also add persistent listeners, but for now I don't see the use case
        listeners = new List<Listener>();
    }

    void NotifyListeners() {
        foreach (Listener listener in listeners) {
            if (Trace) {
                Debug.Log("invoking "+listener.Observer+" / "+listener.Method+ " / value = "+m_Value);
            }
            if (CatchExceptions) {
                try {
                    listener.Callback.Invoke(m_Value);
                } catch (System.Exception exception) {
                    Debug.LogException(exception, this);
                }
            } else {
                listener.Callback.Invoke(m_Value);              
            }
        }
    }

    public void AddListener(UnityAction<float> func) { listeners.Add(new Listener(func)); }
    public void RemoveListener(UnityAction<float> func) { listeners.RemoveAll(l => l.ID == func.GetHashCode()); }
}

Note that I'm using a few OdinInspector tags to make the inspector display a bit more beautiful, but it's not required for functionality.

Note 2: Yes, I plan to turn this into a generic eventually.

\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.