UI buttons not working

UI buttons are not clickable
I was able to tweak them to highlight when moused over, but clicking them does nothing
Can I contact you on discord so I could have a proper back and forth?
If not, what information do you need from me? This seems to be a very common issue, and the other solutions posted do not work for me.

Let’s start with a look at your UnitActionSystemUI and ActionButtonUI scripts.

unit action system:
https://hatebin.com/dcsaoptlvf

unit action system UI:
https://hatebin.com/orehqmhybz

actionbuttonUI:
https://hatebin.com/rufheykkla

Thanks again for taking a look

For future code pastes, please paste the text here in the forums, it’s a lot easier to help, and the code doesn’t dissappear for future readers who may have the same issue in six months. The Forum user guide post I linked above displays how, or I can reformat it manually.

As near as I can tell, the code looks correct.

Are the buttons being created with the correct names in their text component? To test this, make sure that the buttons in the game scene have some name that doesn’t correspond like “Placeholder”.

Try adding

button.interactable = true;

after assigning the OnClick += event.

Didn’t seem to work.

The buttons are spawned with the correct names.

On the note of code blocks, I’m used to linking things to avoid walls of text. Would it be better to collapse code into a spoiler? or just do this:

UnitActionSystemUI:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UnitActionSystemUI : MonoBehaviour
{
    [SerializeField] private Transform actionButtonPrefab;
    [SerializeField] private Transform actionButtonContainerTransform;
    private void Start()
    {
        // why do we += this?! -Dino
        UnitActionSystem.Instance.OnSelectedUnitChanged += UnitActionSystem_OnSelectedUnitChanged;

        CreateUnitActionButtons();
    }
    private void CreateUnitActionButtons()
    {
        // nuke any existing buttons
        foreach(Transform buttonTransform in actionButtonContainerTransform)
        {
            Destroy(buttonTransform.gameObject);
        }

        // get the selected unit
        Unit selectedUnit = UnitActionSystem.Instance.GetSelectedUnit();

        // create the appropriate buttons for this unit.
        foreach(BaseAction baseAction in selectedUnit.GetBaseActionArray())
        {
            Transform actionButtonTransform = Instantiate(actionButtonPrefab, actionButtonContainerTransform);
            ActionButtonUI actionButtonUI = actionButtonTransform.GetComponent<ActionButtonUI>();
            actionButtonUI.SetBaseAction(baseAction);

            // This didn't change anything. -Dino
            actionButtonUI.GetComponent<Button>().interactable = true;
        }

    }

    private void UnitActionSystem_OnSelectedUnitChanged(object sender, EventArgs e)
    {
        CreateUnitActionButtons();
    }
}

ActionButtonUI:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class ActionButtonUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI textMeshPro;
    [SerializeField] private Button button;

    private void Update()
    {
        // Debug.Log("buttonUI is running");
    }

    public void SetBaseAction(BaseAction baseAction)
    {
        textMeshPro.text = baseAction.GetActionName().ToUpper();

        // we're using a different method than this. -Dino
        // button.onClick.AddListener(MoveActionBtn_Click);

        button.onClick.AddListener(() => {
            Debug.Log("was clicked");
            UnitActionSystem.Instance.SetSelectedAction(baseAction);
        });
    }

    // this is for the commented out method. -Dino
/*
    private void MoveActionBtn_Click()
    {
    }
    */
}

UnitActionSystem:

using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

public class UnitActionSystem : MonoBehaviour

{

    // This property can only be set by this class because of private set;

    // but it can be read by any other class because of get;

    // -Dino

    public static UnitActionSystem Instance { get; private set; }

    public event EventHandler OnSelectedUnitChanged;

    [SerializeField] private Unit selectedUnit;

    [SerializeField] private LayerMask unitLayerMask;

    private BaseAction selectedAction;

    private bool isBusy;

    private void Awake()

    {

        if(Instance != null)

        {

            Debug.LogError("There is more than one UnitActionSystem! " + transform + " - " + Instance);

            Destroy(gameObject);

            return;

        }

        Instance = this;

    }

   

    void Start()

    {

        SetSelectedUnit(selectedUnit);

    }

    // Update is called once per frame

    void Update()

    {

        if(isBusy) { return; }

        // Debug.Log(selectedAction.ToString());

        HandleUnitSelection();

        if(selectedUnit) {

            HandleSelectedAction();

        }

        // we're going to try to handle everything with clicks. -Dino

        /*

        // If we get a hard coded input, we move to a location. -Dino

        if(Input.GetMouseButtonDown(1))

        {

            if(selectedUnit != null)

            {

                GridPosition mouseGridPosition = LevelGrid.Instance.GetGridPosition(MouseWorld.GetPosition());

               

                if(selectedUnit.GetMoveAction().IsValidActionGridPosition(mouseGridPosition))

                {

                    SetBusy();

                    selectedUnit.GetMoveAction().Move(mouseGridPosition, ClearBusy);

                }

                // selectedUnit.GetMoveAction().Move(MouseWorld.GetPosition());

            }

            else

            {

                Debug.Log("No unit selected... idiot.");

            }

        }

        if(Input.GetButtonDown("Jump"))

        {

            SetBusy();

            selectedUnit.GetSpinAction().Spin(ClearBusy);

        }

        */

    }

    private void HandleSelectedAction()

    {

        if(Input.GetMouseButtonDown(0))

        {

            GridPosition mouseGridPosition = LevelGrid.Instance.GetGridPosition(MouseWorld.GetPosition());

            switch (selectedAction)

            {

                case MoveAction moveAction:

                    if(moveAction.IsValidActionGridPosition(mouseGridPosition))

                    {

                        SetBusy();

                        moveAction.Move(mouseGridPosition, ClearBusy);

                    }

                    break;

                case SpinAction spinAction:

                    SetBusy();

                    selectedUnit.GetSpinAction().Spin(ClearBusy);

                    spinAction.Spin(ClearBusy);

                    break;

                default:

                    break;

            }

        }

    }

    private void SetBusy()

    {

        isBusy = true;

    }

    private void ClearBusy()

    {

        isBusy = false;

    }

    private void HandleUnitSelection()

    {

        if(Input.GetMouseButtonDown(0)) {

            //raycast method -Dino

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            if (Physics.Raycast(ray, out RaycastHit raycastHit, float.MaxValue, unitLayerMask))

            {

                if(raycastHit.transform.TryGetComponent<Unit>(out Unit unit))

                {

                    SetSelectedUnit(unit);

                }

            }

            // fancy method that was fun to try.

            /*

            // If we click on the floor without selecting a unit...

            // the floor is still a game object, but it does not have a "unit" component.

            // This script will run, and set the selectedUnit field to the "unit"

            // component of the floor, which is 'null'. -Dino

            if(MouseWorld.GetObject().GetComponent<Unit>() != null)

            {

                SetSelectedUnit(MouseWorld.GetObject().GetComponent<Unit>());

            }

            */

            /*

            else

            {

                // To make this clean, we can set the selected unit to null by default

                // if we don't get a unit when clicking.

                Debug.Log("Setting selected unit to null deliberately");

                selectedUnit = null;

            }

            */

            // This code does the same thing as the conditional below. -Dino

            OnSelectedUnitChanged?.Invoke(this, EventArgs.Empty);

            // This code does the same thing as the line above. -Dino

            /*

            if(OnSelectedUnitChanged != null)

            {

                OnSelectedUnitChanged(this, EventArgs.Empty);

            }

            */

        }

    }

    private void SetSelectedUnit(Unit unit)

    {

        selectedUnit = unit;

        SetSelectedAction(unit.GetMoveAction());

        //SetSelectedAction(unit.GetSpinAction());

        OnSelectedUnitChanged(this, EventArgs.Empty);

    }

    public void SetSelectedAction(BaseAction baseAction)

    {

        selectedAction = baseAction;

    }

    public Unit GetSelectedUnit()

    {

        return selectedUnit;

    }

}

That’s actually perfect.

I did update this to include the change you suggested where I think you were wanting me to place it.

So the buttons are created, with the correct names, but not functioning, even when Interactible is set to true… Are you getting the Debug when you click?

when I enable the

    private void Update()
    {
        // Debug.Log("buttonUI is running");
    }

It gets spammed in the console as expected, though I should probably have set this debug to report a name and probably some kind of game object ID

this debug message however:

    public void SetBaseAction(BaseAction baseAction)
    {
        textMeshPro.text = baseAction.GetActionName().ToUpper();

        // we're using a different method than this. -Dino
        // button.onClick.AddListener(MoveActionBtn_Click);

        button.onClick.AddListener(() => {
            Debug.Log("was clicked");
            UnitActionSystem.Instance.SetSelectedAction(baseAction);
        });
    }

The “Was Clicked” never appears in the console, hence why I think the buttons just aren’t working for some reason.

The buttons should be working (let’s just call that zero chance that buttons in Unity are buggy after 10 years)… It’s possible another GameObject is consuming the click.

I believe I’ve seen this before when the Turn Number text had it’s size set so that it covered the entire screen (even though the text itself was just in the top, the component was covering everything). This was causing it to intercept clicks, to block them.

Without worrying about changing this, simply open your Turn Number Text component, and in the TextMeshPro, click on Extra Settings. Try unchecking the box that reads “Raycast Target”.

If that doesn’t do it, some other UI component is masking the buttons… Now the Busy and the Enemy turn are SUPPOSED to do that, but when they’re deactivated by the script, they shouldn’t be blocking. Might be a bit of seeking through the components looking for overlap. it’ll be a component lower in the heirarchy than the buttons themselves.

If I have a turn number text component, I don’t know what or where it is. I’ve picked through all this stuff but I can’t find any such thing.

I altered the mouseworld code to do this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MouseWorld : MonoBehaviour
{
    private static MouseWorld instance;

    [SerializeField] private LayerMask mousePlaneLayerMask;

    // BeginPlay
    private void Awake() {
        instance = this;
    }

    public static Vector3 GetPosition()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit raycastHit, float.MaxValue, instance.mousePlaneLayerMask);
        return raycastHit.point;
    }

    public static GameObject GetObject()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Physics.Raycast(ray, out RaycastHit raycastHit, float.MaxValue, instance.mousePlaneLayerMask);
        return raycastHit.collider.gameObject;
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        // Debug.Log(Physics.Raycast(ray, out RaycastHit raycastHit, float.MaxValue, instance.mousePlaneLayerMask));
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, float.MaxValue))
            Debug.Log(hit.transform.ToString());
    }
}

my console prints this:

Unit (UnityEngine.Transform)
UnityEngine.Debug:Log (object)
MouseWorld:Update () (at Assets/Scripts/MouseWorld.cs:44)

Plane (UnityEngine.Transform)
UnityEngine.Debug:Log (object)
MouseWorld:Update () (at Assets/Scripts/MouseWorld.cs:44)

Unit (1) (UnityEngine.Transform)
UnityEngine.Debug:Log (object)
MouseWorld:Update () (at Assets/Scripts/MouseWorld.cs:44)

when moving the mouse over the respective game objects

the buttons do nothing.

The buttons are handled by the EventSystem, which is unrelated to MouseWorld. (If you don’t have an Eventsystem in your scene, this may be the issue)

You may not be to turn numbers yet…

Zip up your project and upload it to https://gdev.tv/projectupload Be sure to remove the Library folder to conserve space. I’ll see if I can find what’s blocking the buttons.

I have submitted it, hopefully everything necessary is there

Took a little digging, but I finally put my finger on it. The clue was in the EventSystem, which is present. The trouble was that the methods handling unit selection were in a race condition with the button script. We prevent this by adding

if(EventSystem.current.IsPointerOverGameObject()) return;

before HandleUnitSelection() in UnitActionSystem.Update(). IsPointerOverGameObject() is a strangely named method, because what it is really meaning is “Is the pointer over a piece of UI”. When the cursor is overtop a button, we want the UnitActionSystem to not handle any mouse activity at all. Without that line, the mouse click gets processed by UnitActionSystem at about the same time as the EventSystem says “hey, let’s see what we clicked on”, and by the time it sends an OnClick event to the button, the UnitActionSystem has already done something that makes the button go away.

    void Update()
    {

        if(isBusy) { return; }

        // Debug.Log(selectedAction.ToString());

        if (EventSystem.current.IsPointerOverGameObject()) return; //Add this line here
        
        HandleUnitSelection();

That seems to correct the problem as it exists now.
In the course, Eventsystems or and the library they come from hasn’t been discussed and wasn’t implemented in this method.

Why does it work in the videos?
Is this the best way to handle this? Or is this just a useful method to isolate the problem?

It works because Hugo added it in this lecture (screenshot of the course repo changes for this lecture)

This is the best way to handle this.

Update Hugo actually adds this in the next lecture at around 7:40. The two lectures started out as one lecture, and were split to conform to our time standards, so it appears in this lecture’s project changes.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms