Dynamic and reusable way to bind Behaviour.enabled to a global state

Problem Recap

I wanted a dynamic and reusable way to bind AudioSource.enabled (or any Behaviour.enabled) to a global state managed by PersistManager. We explored:

  1. Creating a generic static method (BindToSoundState) in PersistManager.
  2. Using extension methods to encapsulate binding logic (Enable).
  3. Safeguarding against null references with concise syntax.

Approach Overview

1. Using an Extension Method

public static class BehaviourExtensions
{
    public static void Enable(this Behaviour self, ref Action<bool> callback, bool initialState)
    {
        void SetEnable(bool enable) => self.enabled = enable;
        callback += SetEnable;
        self.enabled = initialState; // Sync initial state
    }
}

This encapsulates the binding logic:

  • Attaches a method (SetEnable) to a callback (onSoundStateChange).
  • Syncs the initial state of the component immediately.

Usage:

_audioSource.Enable(ref PersistManager.Instance.onSoundStateChange, PersistManager.Instance.SoundsEnabled);

Performance and Usefulness

Performance

  1. Callback Overhead:
  • Adding a delegate to Action has negligible overhead.
  • Invoking the Action when the state changes is efficient for handling UI or state synchronization tasks.
  1. Null Checks:
  • Using _audioSource?.Enable(...) is efficient and safe for optional components.
  1. Initial State Synchronization:
  • The immediate state synchronization (self.enabled = initialState) ensures consistency without waiting for the first event.

Usefulness

  • The extension method approach is highly reusable for any Behaviour, not just AudioSource.
  • It’s clean, concise, and centralizes logic, avoiding repetitive code in every component.

Improvements

  1. Optional Logging: Add a debug log in the extension to warn if the component is null or already bound:
public static void Enable(this Behaviour self, ref Action<bool> callback, bool initialState)
{
    if (self == null)
    {
        Debug.LogWarning("Attempted to bind a null Behaviour to the state callback.");
        return;
    }

    void SetEnable(bool enable) => self.enabled = enable;
    callback += SetEnable;
    self.enabled = initialState;
}

  1. Unbinding Support (Optional): If you later need to unbind components, store the delegate reference:
public static void Enable(this Behaviour self, ref Action<bool> callback, bool initialState)
{
    if (self == null) return;

    Action<bool> setEnable = enable => self.enabled = enable;
    callback += setEnable;
    self.enabled = initialState;

    // Optional unbinding logic
    // callback -= setEnable;
}

  1. More Generic Method: A variation could work for any property or action beyond enabled:
public static void BindProperty(this Behaviour self, ref Action<bool> callback, Action<bool> propertySetter, bool initialState)
{
    if (self == null) return;

    callback += propertySetter;
    propertySetter(initialState);
}

Usage:

`_audioSource.BindProperty(ref PersistManager.Instance.onSoundStateChange, enable => _audioSource.enabled = enable, PersistManager.Instance.SoundsEnabled);`

This same misunderstanding comes up again and again and again:

Using the ? or ?? operator is not safe and is not compatible with Unity’s objects, either optional, destroyed, or anything.

In your extension method you are internally guarding the self object against null, so you might as well simply omit the ? operator at the callsite, since it isn’t doing anything but confusing what your code is doing.

If you wrote the extension method expecting the invocations to be guarded because of your ? operator, they will not be guarded against null.

This applies to any reference to a class derived from UnityEngine.Object, which is basically everything in Unity.

Try it yourself.

	// Using ? and ?? is not helpful in Unity to detect nullness of ALL objects!

	// drag it in, or not, both are NOT detected by the ? operator
	public AudioSource _audioSource;

	IEnumerator Start()
	{
		_audioSource?.Enable(true);

		yield return null;

		Destroy(_audioSource);

		// allow destruction to happen (happens at end of frame remember!)
		yield return null;

		// this will still invoke .Enable(), which will
		// throw an exception because the
		// C# object is not null, but the underlying
		// UnityEngine.Object has been destroyed:
		_audioSource?.Enable(false);
	}

Although irrelevant to my point above, the extension method .Enable(bool) above is simply:

	public static void Enable( this AudioSource az, bool onoff)
	{
		Debug.Log("Enable(): onoff:" + onoff);
		az.enabled = onoff;
		Debug.Log("Enable(): success!");
	}

and the actual result of running the above code and expecting ? to protect you from Destroy()-ed objects:

If you provide an AudioSource, it works the first call, then fails on the Destroy()-ed object:

If you don’t provide an AudioSource, it immediately fails:

1 Like

seems unnecessarily complicated, I don’t see what problem it solves. I’d rather use singleton or write boilerplate code to register listener for any classes that need to sync enabled state with other classes.

1 Like

@Kurt-Dekker I agree, I actually realised that rigth after I made the post in my project. I forgot to come back here fix it. Thanks

I made it just because I didn’t want to use more lines for such components. I agree it may not be necessary at all, I just wanted to achieve it with only a method call.