[Solved] Adding listeners with for loop issues

Does anyone know why the following doesn’t work:

for (int i = 0; i < (int)TileUITabs.Count; i++)
{
    m_tileUITabsToggle[i].onValueChanged.AddListener((tog) => { ToggleValueChanged(i); });
}

Whereas this does work:

m_tileUITabsToggle[0].onValueChanged.AddListener((tog) => { ToggleValueChanged(0); });
m_tileUITabsToggle[1].onValueChanged.AddListener((tog) => { ToggleValueChanged(1); });

Other code:

    public enum TileUITabs
    {
        CurrentTileInfo,
        CurrentTileResourceInfo,
        Count
    }

    public GameObject[] m_tileUITabs = new GameObject[(int)TileUITabs.Count];
    public Toggle[] m_tileUITabsToggle = new Toggle[(int)TileUITabs.Count];

    bool ToggleValueChanged(int tog)
    {
        Toggle toggle = m_tileUITabsToggle[tog];

        if (toggle.isOn)
        {
            // Loop through and turn other toggles off
            for (int i = 0; i < (int)TileUITabs.Count; i++)
            {
                // If we are the toggle that should be on
                if (m_tileUITabsToggle[i] == toggle)
                {
                    // Set our gameobject on
                    m_tileUITabs[i].SetActive(true);
                }

                // If we should be off
                else
                {
                    // if GO is on
                    if (m_tileUITabs[i].activeInHierarchy)
                    {
                        m_tileUITabs[i].SetActive(true);
                    }

                    if (m_tileUITabsToggle[i].isOn)
                    {
                        m_tileUITabsToggle[i].isOn = false;
                    }
                }
            }
        }
        else
        {
            for (int i = 0; i < (int)TileUITabs.Count; i++)
            {
                // If we are the toggle that should be on
                if (m_tileUITabsToggle[i] == toggle)
                {
                    // Set our gameobject on
                    m_tileUITabs[i].SetActive(false);
                }
            }
        }

        return true;
    }

When I try using the first, I get this error, although not when this is called, but when the listeners triggers:

IndexOutOfRangeException: Index was outside the bounds of the array.
UIController.ToggleValueChanged (System.Int32 tog) (at Assets/Controllers/UIController.cs:37)
UIController+<>c__DisplayClass3_0.<Start>b__0 (System.Boolean tog) (at Assets/Controllers/UIController.cs:22)
UnityEngine.Events.InvokableCall`1[T1].Invoke (T1 args0) (at <5e35e4589c1948aa8af5b8e64eea8798>:0)
UnityEngine.Events.UnityEvent`1[T0].Invoke (T0 arg0) (at <5e35e4589c1948aa8af5b8e64eea8798>:0)
UnityEngine.UI.Toggle.Set (System.Boolean value, System.Boolean sendCallback) (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Toggle.cs:281)
UnityEngine.UI.Toggle.set_isOn (System.Boolean value) (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Toggle.cs:244)
UnityEngine.UI.Toggle.InternalToggle () (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Toggle.cs:314)
UnityEngine.UI.Toggle.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/UI/Core/Toggle.cs:325)
UnityEngine.EventSystems.ExecuteEvents.Execute (UnityEngine.EventSystems.IPointerClickHandler handler, UnityEngine.EventSystems.BaseEventData eventData) (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:50)
UnityEngine.EventSystems.ExecuteEvents.Execute[T] (UnityEngine.GameObject target, UnityEngine.EventSystems.BaseEventData eventData, UnityEngine.EventSystems.ExecuteEvents+EventFunction`1[T1] functor) (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/ExecuteEvents.cs:261)
UnityEngine.EventSystems.EventSystem:Update() (at C:/Program Files/Unity/Hub/Editor/2019.3.0b6/Editor/Data/Resources/PackageManager/BuiltInPackages/com.unity.ugui/Runtime/EventSystem/EventSystem.cs:377)

If I put in a breakpoint where the loop is happening, it does loop the correct amount of times, however when looking at variables, ‘i’ cannot be found, and no value is shown. It is fine in any other loop of the same, and only seems to break when inside the for loop contains this line:

m_tileUITabsToggle[i].onValueChanged.AddListener((tog) => { ToggleValueChanged(i); });

Does anyone have any information or insight as to why this is?

Thanks.

1 Like

EDIT: disregard this approach; read @Antistone 's reply below for a much simpler approach. - Kurt

This is called variable capture. It works differently in different languages.

You are “capturing” the variable i in your lambda but because C# captures the VARIABLE, not the VALUE at that moment, all of those lambdas, when run, use the final post-for-loop exit value of i, which is beyond the index range.

One way is to define another lambda that captures the variable AND the value, and then immediately execute that lambda right then and there. Later versions of C# let you specify how to capture but I’m not sure that has come to Unity yet.

Try this approach:

for (int i= 0; i < max; i++)
{
 // temp lambda to capture...
 System.Action<int> action = (capturedi) => {
  m_tileUITabsToggle[capturedi].onValueChanged.AddListener(
    (tog) => {
       ToggleValueChanged(capturedi);
    }
 );

 // capture variable and value
 action(i);
}

(I was lazy and didn’t copy all the code but you can figure it out)

(oop, edit, left out your addlistener call)

2 Likes

You can also just declare a new variable that is scoped inside of the loop:

for (int i = 0; i < (int)TileUITabs.Count; i++)
{
    int tempVar = i;
    m_tileUITabsToggle[i].onValueChanged.AddListener((tog) => { ToggleValueChanged(tempVar); });
}
3 Likes

Wow, I swear at one time this did not work in C#… it would share the locally-scoped variable for the life of the loop.

But you’re right, it works like a charm today.

OP: scratch my highly over-engineered nightmare of lambdas jumping through their own keisters and go with @Antistone 's much-simpler approach above. TIL!

1 Like

Thank you for this info! Definitely helped me get my head around why this was all happening.

Thank you, this worked a charm!

1 Like