0
\$\begingroup\$

Currently writing a script that builds a HUD for testing some functions in a project. It automatically builds buttons, one of which works, while the other gives the error indicated in the title and error message below.

The function the button is calling works when called by a button manually placed in the scene, indicating that the issue isn't with the function itself but the way the button is being built.

The script compiles successful and are rendered correctly in the edtior. Clicking the radio button (which builds the button that is giving the error) works correctly, building the button and running the debug logs.

The error occurs when clicking on the "next song" button, which is generated by clicking on the radio button.

Here is the error:

ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
System.Collections.Generic.List`1[T].get_Item (System.Int32 index) (at <6073cf49ed704e958b8a66d540dea948>:0)
TestHUD+<>c__DisplayClass12_0.<BuildActions>b__0 () (at Assets/Scripts/TestHUD.cs:86)
UnityEngine.Events.InvokableCall.Invoke () (at <f1212ad1dec44ce7b7147976b91869c3>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <f1212ad1dec44ce7b7147976b91869c3>:0)
UnityEngine.UI.Button.Press () (at Library/PackageCache/[email protected]/Runtime/UI/Core/Button.cs:70)
UnityEngine.UI.Button.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Library/PackageCache/[email protected]/Runtime/UI/Core/Button.cs:114)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at Library/PackageCache/[email protected]/Runtime/EventSystem/ExecuteEvents.cs:57)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at Library/PackageCache/[email protected]/Runtime/EventSystem/ExecuteEvents.cs:272)
UnityEngine.EventSystems.EventSystem:Update() (at Library/PackageCache/[email protected]/Runtime/EventSystem/EventSystem.cs:501)

Here are the Debug Logs

Length of buttonActions is 1.

Function button created

Here is the script as it currently exists:

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

public class TestHUD : MonoBehaviour
{
    GameObject testHUD;

    public List<TestHUDPanel> testHUDPanels = new();

    public GameObject sectionPanel;
    public GameObject functionPanel;

    public GameObject sectionPrefab;
    public GameObject functionPrefab;

    //Color32 activeColor = new Color32(255, 201, 191, 255);
    //Color32 inactiveColor = new Color32(191, 255, 241, 255);

    SoundManager soundManager;

    public class TestHUDPanel
    {
        public string name;
        public GameObject linkedObject;
        public GameObject panelObject;
        public List<Action> buttonActions = new();
        public List<string> buttonLabels = new();

        public TestHUDPanel(string panelName)
        {
            name = panelName;
        }

        public void AddButton(string buttonName, Action buttonFunc)
        {
            buttonActions.Add(buttonFunc);
            buttonLabels.Add(buttonName);
        }
    }

    private void Awake()
    {
        testHUD = gameObject;
        soundManager = GameObject.Find("SoundManager").GetComponent<SoundManager>();
    }

    private void Start()
    {
        BuildPanelList();
        BuildPanels();
    }

    public void BuildPanelList()
    {
        testHUDPanels.Add(new TestHUDPanel("Radio"));
        testHUDPanels[0].AddButton("Next Song", soundManager.PlayNextSong);
        testHUDPanels.Add(new TestHUDPanel("Typing"));
        testHUDPanels.Add(new TestHUDPanel("Audio"));
    }


    public void BuildPanels()
    {
        foreach (TestHUDPanel panel in testHUDPanels)
        {
            GameObject panelObject = Instantiate(sectionPrefab);
            panelObject.transform.SetParent(sectionPanel.transform);
            Button button = panelObject.GetComponent<Button>();
            panelObject.GetComponentInChildren<TextMeshProUGUI>().text = panel.name;
            button.onClick.AddListener(() => BuildActions(panel));
        }
    }

    public void BuildActions(TestHUDPanel panel)
    {
        for (int i = 0; i < panel.buttonActions.Count; i++)
        {
            GameObject panelObject = Instantiate(functionPrefab);
            panelObject.transform.SetParent(functionPanel.transform);
            Button button = panelObject.GetComponent<Button>();
            panelObject.GetComponentInChildren<TextMeshProUGUI>().text = panel.buttonLabels[i];
            Debug.Log(string.Format("Length of buttonActions is {0}, ", panel.buttonActions.Count));
            >>> THIS IS LINE 86 <<< : button.onClick.AddListener(() => panel.buttonActions[i]());
            Debug.Log("Function button created");
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Are you referring to the one that isn't present at the end of this line: "testHUDPanels[0].AddButton("Next Song", soundManager.PlayNextSong);" ? If I add "()" here, I get the error "Cannot convert from void to System.Action", because the function is a void with no return. \$\endgroup\$
    – EvelynSays
    Jun 9, 2022 at 19:02

1 Answer 1

2
\$\begingroup\$

This is a common coding pitfall: you are using a loop increment in a callback function inside a loop construct. The callback function records the reference of the local variable i and reads it when it is called, but at this time i has already increased to the exit threshold.

Try this:

    for (int i = 0; i < panel.buttonActions.Count; i++)
    {
        var temp = i;
        ...
        button.onClick.AddListener(() => panel.buttonActions[temp]());
    }

Deeper explanation:

When a local variable is referenced in the delegate, it becomes a captured variable. this is a good explanation.

The for loop is actually a syntactic sugar, the scope of i is outside the for structure:

    for (int i = 0; i < 10; i++)
    {
        //your code
    }

is actually equivalent to:

    int i = 0;
    for (;;)
    {
        if(i < 10) break;
        //your code
        i++;
    }

that is to say, the i used in each round of the loop points to the same local variable. The captured variable is also the same one. If a new variable(copied of i) scoped to the loop structure is used in each round of the loop, then the captured variable is different.

\$\endgroup\$
1
  • \$\begingroup\$ This worked and fixed the issue. Is there somewhere I can learn more about what went wrong here? I think I understand why on a surface level but would like to understand fully. \$\endgroup\$
    – EvelynSays
    Jun 9, 2022 at 19:12

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .