Wakeup
Wakeup

Reputation: 165

Detect changes in the value of other objects referenced in unity?

I'm trying to implement a feature that detects a change in the value of another field object reference.

For example, there is an A object, and the B object contains A. The A object is notified by OnValidate when the field of the A object has changed. At this time. Is there any way that "B" can detect the change of "A"?

This feature is only required in the Editor and does not include code in Runtime.

public class A : ScriptableObject
{
    [SerializeField]
    public string team;

    private void OnValidate()
    {
        Debug.Log($"My team name has been changed to {team}.");
    }
}

public class B : ScriptableObject
{
    [SerializeField]
    public A a;

    private void OnValidate()
    {
        Debug.Log($"A changed.");
    }

    // How can I detect changes to A and call this functions?
    private void OnATeamChanged()
    {
        Debug.Log($"A's team name has been changed to {a.team}.");
    }

    private void OnADestroyed()
    {
        Debug.Log($"A is missing.");
    }
}

Upvotes: 2

Views: 4371

Answers (2)

derHugo
derHugo

Reputation: 90580

I would also propose to use events but be careful how and when you register and remove listeners!

public class A : ScriptableObject
{
    // You don't need [SerializeField] 
    // public properties are serialized anyway
    public string team;

    public event Action onChanged;
    public event Action onReset;
    public event Action onDestroyed;

    private void OnValidate()
    {
        Debug.Log($"My team name has been changed to {team}.");

        if(onChanged == null) return;
        onChanged.Invoke();
    }

    // This is called on reset
    // But if you override this you have to set
    // The default values yourself!
    private void Reset()
    {
        team = "";

        if (onReset == null) return;
        onReset.Invoke();
    }

    // Called when object is destroyed
    private void OnDestroyed()
    {
        if(onDestroyed == null) return;
        onDestroyed.Invoke();
    }
}

But now in B I would not add the listener on Awake since this adds it after every recompile and multiple times! Instead don't forget to "clean up" all listeners you ever add to avoid getting NullReferenceExceptions:

public class B : ScriptableObject
{
    // You don't need [SerializeField] since public properties
    // are serialized anyway
    public A a;

    // Internal for removing listeners later
    private A lastA;

    private void OnValidate()
    {
        // Apparently something else was changed
        if(a == lastA) return;

        Debug.Log("reference a changed");

        // Unregister listeners for old A reference
        if(lastA)
        {
            lastA.onChanged -= OnAChanged;
            lastA.onReset -= OnAReset;
            lastA.onDestroyed -= OnADestroyed;
        }

        // Register listeners for new A reference
        // Note that it is allways possible to remove listeners first
        // even if they are not there yet
        // that makes sure you listen only once and don't add multiple calls
        if(a)
        {
            a.onChanged -= OnAChanged;
            a.onReset -= OnAReset;
            a.onDestroyed -= OnADestroyed;

            a.onChanged += OnAChanged;
            a.onReset += OnAReset;
            a.onDestroyed += OnADestroyed;
        }

        lastA = a;
    }

    // Make allways sure you also remove all listeners on destroy to not get Null reference exceptions
    // Note that it is allways possible to remove listeners even if they are not there yet
    private void OnDestroy()
    {
        if(!a) return;

        a.onChanged -= OnAChanged;
        a.onReset -= OnAReset;
        a.onDestroyed -= OnADestroyed;
    }

    private void OnAChanged()
    {
        Debug.Log("A was changed");
    }

    private void OnAReset()
    {
        Debug.Log("A was reset");
    }

    private void OnADestroyed()
    {
        Debug.Log("a was destroyed");
    }
}

Small optional change if you want

If you want additionally to be able to register other listeners in the Inspector the same way you do for Buttons you could simply exchange the

public event Action xy;

with

public UnityEvent xy;

and remove the if(xy == null) checks.

You would than also replace

a.onChanged -= OnAChanged;
a.onChanged += OnAChanged;

by

a.onChanged.RemoveListener(OnAChanged);
a.onChanged.AddListener(OnAChanged);

Upvotes: 1

Everts
Everts

Reputation: 10701

You could try to add an event

public class A : ScriptableObject
{
    [SerializeField]
    public string team;
    public Action RaiseChangeName;
    private void OnValidate()
    {
        Debug.Log($"My team name has been changed to {team}.");
        if(RaiseChangeName != null) { RaiseChangeName(); }
    }
}

public class B : ScriptableObject
{
    [SerializeField]
    public A a;
    void Awake()
    {
         a.RaiseChangeName += OnATeamChanged;
    }

    // How can I detect changes to A and call this functions?
    private void OnATeamChanged()
    {
        Debug.Log($"A's team name has been changed to {a.team}.");
    }
}

Upvotes: 1

Related Questions