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");
}
}
}
CodePudding user response:
As Hans pointed out, the issue was stemming from a captured variable.
As our variable was iterating up, the lambda expression wasn't correctly capturing it and was therefore giving the error mentioned above.
Below is the updated BuildActions function, with a temporary variable which holds the value of i temporarily to allow it to be used within the lambda expression, even if the iterator changes value between assignment and execution, the temporary variable will still allow the lambda expression to function correctly.
public void BuildActions(TestHUDPanel panel)
{
for (int i = 0; i < panel.buttonActions.Count; i )
{
var tempIDX = i;
GameObject panelObject = Instantiate(functionPrefab);
panelObject.transform.SetParent(functionPanel.transform);
Button button = panelObject.GetComponent<Button>();
panelObject.GetComponentInChildren<TextMeshProUGUI>().text = panel.buttonLabels[i];
button.onClick.AddListener(() => panel.buttonActions[tempIDX]());
}
}
I'll also link the documentation for captured variables in lambda expressions for further explanation.
Capture of outer variables and variable scope in lambda expressions