How to detect interactable state change in selectable objects (Buttons, etc.)

Pitching this one out there to the community - let’s say, for the sake of argument here, I have a button on-screen.

I have some additional behaviors that I’d like to trigger upon changing a selectable GUI control’s interactable state. I don’t want to disable the button object, but I do want to change it’s text color, for example, when the button becomes non-interactable.

I understand that I could just test to see if the interactable boolean has changed within the update loop, and trigger off of that - but I really don’t think this would be the optimal way to do so, considering multiple buttons would likely be pounding the crap out of the Update loop in the process, needlessly checking for its state change.

I also understand that I could write a custom function that performs these actions in addition to setting that variable - but I was hoping to have it more reactive to the state, rather than driving it.

Does anyone have some suggestions or tips on how I might achieve this sort of detect-and-react approach? Or should I just begin writing my custom function to handle this?

Thanks for any suggestions!

Hi.

Try this.

public class MyButton : Button
{
    protected override void DoStateTransition(SelectionState state, bool instant)
    {
        if (state == SelectionState.Disabled)
        {

        }
        else if (state == SelectionState.Normal)
        {
            
        }
    }

typical rule of thumb I go by is that other game objects shouldn’t know that a UI even exists (unless its also a UI and only if there’s no better way). This rule of thumb is there to help prevent deadlocks in logic behavior, or some nasty hard to trace bugs.

For example, I wouldn’t have my AudioController ask if the UI mute button is turned on or off, I do it the other way around… my AudioController tracks the mute state and the button checks what state the mute is. If muted show the unmute icon, otherwise show the mute icon. when the button is pressed it triggers a call to the AudioController to change its state via a public function in the AudioController. The AudioController has no knowledge whatsoever that a mute button even exists, nor does it need to.

so to answer your question, I would instead have whatever you have checking to state of the button check the source of what ever is affecting that button’s state. if the logic you have that determines the state of the button self contained in the script connected to the button, I would push it out to an external controller class.

And in this external class set the state change to an event so what ever needs to work of it can just subscribe themselves to the event. This will further decouple your code (classes know less about other classes and thus are less prone to break from code changes) and keeps it fast (instead of having to ask that class every frame what it’s state is, the class will simply tell all that cares to listen when it’s state has changed).

I don’t know exactly what you need to track the button state, but this is how I would code it, going back to my previous example…

    public class AudioController : Monobehavior
    {
        public delegate void MuteEvent();
    	public static event MuteEvent OnMute;
        public static AudioController instance;
    
        private bool _isMuted = false;
        public bool IsMuted
        {
              get { return _isMuted;}
              set
              {
                     _isMuted = value;

                     // only call if someone is listening, 
                     // otherwise you'll get a crash 
                     if(OnMute!=null) 
                     {
                            OnMute();
                     }
              }
        }
    
        void Awake()
        {
              // set the static reference so that other 
              // scripts don't have to do any complex searching
              instance = this;
        }
    }
    
    public class MuteButtonScript : Monobehavior
    {
            public Button btn;
            public Sprite muteSprite;
            public Sprite unmuteSprite;
    
           void Awake()
           {
                  Button btn = GetComponent<Button>();
           }
           void OnEnable()
           {
                  //subscribe to the AudioController's On Mute event
                  AudioController.OnMute += OnMute;
           }
           void OnDisable()
           {
                  //unsubscribe from the AudioController's On Mute event
                  AudioController.OnMute -= OnMute;
           }

          // this function will always run whenever the class is subscribed 
          // to the AudioController and the AudioController runs "OnMute()"
          void OnMute()
          {
                 //update the button sprite depending on the AudioController's Mute State
                 btn.image.sprite = AudioController.instance.IsMuted ? muteSprite : unmuteSprite;
          }
    }

here the button’s image is changing state depending on when the AudioController issues the OnMute event. So instead of having whatever you’re planning to listen to this button, have it listen to the event from the audio controller. Neither the AudioController nor the MuteButtonScript needs to know what this script is, which makes it very easy and lightweight to extend functionality.

by using events you can have other classes (subscribers) attach themselves to an event that is issued by a single source (provider) and the subscribers don’t have to constantly check every update loop, the provider simply tells the subscribers when the event happens.

so whatver event that is causing your button to become non-interactible have your other UI stuff also listen to that same event and have then change their color based on the same decision making

Looks like I am super late to the party but I’d like to share my solution as it is a combination of @dkjunior’s and @free-divbyzero’s solutions. Extend the Button class, add an event and invoke that event on the DoStateTransition override. Have a member variable to track the previous state of the Button class’ interactable property so that you only invoke the event when the property has actually changed. You can then have any other scripts that need to know about the state change subscribe to that event.

using System;
using UnityEngine;
using UnityEngine.UI;

public class ButtonInteractable : Button
{
	public event Action<bool> InteractableChanged;

	protected SelectionState prevState;

	protected override void DoStateTransition(SelectionState state, bool instant)
	{
		base.DoStateTransition(state, instant);
		if(state != prevState)
		{
			InteractableChanged?.Invoke(interactable);
			prevState = state;
		}
	}
}

Not exactly the answer to your question, but you can customize button’s appearance in non-interactable state using Transitions. Check them out in the inspector. If color change is all you need than it’s pretty simple - keep the default Color Tint transition type and change the Disabled color property (which corresponds to non-interactable state). You can customize appearance even further by selecting Sprite Swap or Animation transition types.