I have discovered some interesting behavior regarding the use of closures within coroutines.
It seems that C# closures do not work if I use them within a coroutine, unless the closure is included within a separate function from the coroutine.
In the attached example, three buttons do basically the same thing. Each spawns a list of buttons, and attaches lambda expressions to the spawned buttons that do nothing but log out their (supposedly free) associated index.
[44098-coroutinesclosurestestunitypackage.zip|44098]
Just calling MakeButtons works like it should, with each spawned button logging out 0,1,2,3 depending on which is clicked.
However, calling MakeButtonsCoroutine makes each spawned button act as if it were using an index that is still bound to the calling method’s loop index (just logs out 3).
Through a bit of experimentation, I then discovered that encapsulating the button spawning into a separate method causes the index to once again act like a free variable, with 0,1,2,3 logged out. That is illustrated in MakeButtonsCoroutineSubroutine.
Can someone tell me if this is a unity bug or is there just something I do not understand about the subtleties of coroutines and C# closures?
Here is the test script that contains the bulk of my logic:
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
public class ClosureTest : MonoBehaviour {
public GameObject madeButtonPrefab;
public Transform madeButtonParent;
public int buttonNumber = 4;
public void MakeButtonsCoroutine()
{
StartCoroutine(WaitMakeButtons());
}
public void MakeButtonsCoroutineSubroutine()
{
StartCoroutine(WaitMakeButtonsSubroutine());
}
public void MakeButtons()
{
//Clear out all existing buttons first
foreach (Transform child in madeButtonParent) {
GameObject.Destroy(child.gameObject);
}
for(int i = 0; i < buttonNumber; i++)
{
GameObject temp = null;
int captured = i;
temp = Instantiate(madeButtonPrefab, Vector3.zero, Quaternion.identity) as GameObject;
temp.GetComponentInChildren<Text>().text = "Subroutine Button Log " + captured;
temp.transform.SetParent(madeButtonParent, false);
Button madeButton = temp.GetComponentInChildren<Button>();
madeButton.onClick.AddListener(() => {Debug.LogWarning("Captured index: " + captured);});
}
}
IEnumerator WaitMakeButtons()
{
yield return new WaitForSeconds(1.0f);
Debug.Log ("After coroutine");
//Clear out all existing buttons first
foreach (Transform child in madeButtonParent) {
GameObject.Destroy(child.gameObject);
}
for(int i = 0; i < buttonNumber; i++)
{
GameObject temp = null;
int captured = i;
temp = Instantiate(madeButtonPrefab, Vector3.zero, Quaternion.identity) as GameObject;
temp.GetComponentInChildren<Text>().text = "No Subroutine Button Log " + captured;
temp.transform.SetParent(madeButtonParent, false);
Button madeButton = temp.GetComponentInChildren<Button>();
madeButton.onClick.AddListener(() => {Debug.LogWarning("Captured index: " + captured);});
}
}
IEnumerator WaitMakeButtonsSubroutine()
{
yield return new WaitForSeconds(1.0f);
Debug.Log ("After coroutine call subroutine");
MakeButtons();
}
}