0
\$\begingroup\$

So I performed tests on calling coroutines from another script. I want to understand why the results aren't the expected behavior in one of the test case, and unfortunately the one that I needed to use (as it is more dynamic).

Tests:

// TEST 1: Running coroutines from passed IEnumerator
m_Tester.StartCoroutine(1);

// TEST 2: Running coroutines in this script through a delegate
m_TesterByFunc.StartCoroutineByFunc(1);
        
// TEST 3: Running coroutines by starting them publicly via this script 
m_TesterByCasting.StartCoroutineByCasting(1);

The idea is I'd like to call coroutines from another script - in this case, from a plain class. And I'm expecting that my coroutines would run normally. However, the problem is when passing IEnumerator to the Tester class, along with the owning Monobehavior object, then starting the coroutines from it (Tester class) only worked once. Calling StartCoroutine from CoroutineTest via a delegate function or publicly worked as intended.

I'm not sure if anyone has encountered this but I'd love to know why I'm getting the unexpected result particularly for Test #1.

  • Is there a problem with passing IEnumerator and caching them in the class and the reason why it only works once? Is it not passed by reference?

Please see the test scripts below.

Script 1:

public class Tester
{
    public delegate void Function();

    private MonoBehaviour m_Owner;
    private Function m_F1;
    private Function m_F2;
    private IEnumerator m_One;
    private IEnumerator m_Two;

    public Tester (MonoBehaviour owner)
    {
        m_Owner = owner;
    }

    public Tester(MonoBehaviour owner, IEnumerator one, IEnumerator two)
    {
        m_Owner = owner;
        m_One = one;
        m_Two = two;
    }

    public Tester(MonoBehaviour owner, Function f1, Function f2)
    {
        m_Owner = owner;
        m_F1 = f1;
        m_F2 = f2;
    }    

    public void StartCoroutine(int index)
    {
        if (index == 0)
        {
            m_Owner.StopCoroutine(m_Two);
            m_Owner.StartCoroutine(m_One);
        }
        
        if (index == 1)
        {
            m_Owner.StopCoroutine(m_One);
            m_Owner.StartCoroutine(m_Two);
        }
    }

    public void StartCoroutineByFunc(int index)
    {
        if (index == 0)
        {
            m_F1();
        }

        if (index == 1)
        {
            m_F2();
        }
    }

    public void StartCoroutineByCasting(int index)
    {
        var test = (m_Owner as CoroutineTest);

        if (index == 0)
        {
            test.StopCoroutine(test._TestTwoByCasting());
            test.StartCoroutine(test._TestOneByCasting());
        }

        if (index == 1)
        {
            test.StopCoroutine(test._TestOneByCasting());
            test.StartCoroutine(test._TestTwoByCasting());
        }
    }
}

Script 2: Attach to a game object in the scene

public class CoroutineTest : MonoBehaviour
{
    private Tester m_Tester;
    private Tester m_TesterByFunc;
    private Tester m_TesterByCasting;

    private void Awake()
    {
        m_Tester = new Tester(this, _TestOne(), _TestTwo());
        m_TesterByFunc = new Tester(this, TestOneByFunc, TestTwoByFunc);
        m_TesterByCasting = new Tester(this);
    }

    private void OnEnable()
    {
        // TEST 1: Running coroutines from passed IEnumerator
        m_Tester.StartCoroutine(1);

        // TEST 2: Running coroutines in this script through a delegate
        m_TesterByFunc.StartCoroutineByFunc(1);
        
        // TEST 3: Running coroutines by starting them publicly via this script 
        m_TesterByCasting.StartCoroutineByCasting(1);        
    }

    public IEnumerator _TestOne()
    {
        Debug.Log("Test one");

        yield return new WaitForSeconds(3f);

        m_Tester.StartCoroutine(1);
    }

    public IEnumerator _TestTwo()
    {
        Debug.Log("Test two");

        yield return new WaitForSeconds(3f);

        m_Tester.StartCoroutine(0);
    }

    private void TestOneByFunc()
    {
        StopCoroutine(_TestTwoByFunc());
        StartCoroutine(_TestOneByFunc());
    }

    private void TestTwoByFunc()
    {
        StopCoroutine(_TestOneByFunc());
        StartCoroutine(_TestTwoByFunc());
    }

    private IEnumerator _TestOneByFunc()
    {
        Debug.Log("Test one by func");

        yield return new WaitForSeconds(3f);

        m_TesterByFunc.StartCoroutineByFunc(1);
    }

    private IEnumerator _TestTwoByFunc()
    {
        Debug.Log("Test two by func");

        yield return new WaitForSeconds(3f);

        m_TesterByFunc.StartCoroutineByFunc(0);
    }

    public IEnumerator _TestOneByCasting()
    {
        Debug.Log("Test one by casting");

        yield return new WaitForSeconds(3f);

        m_TesterByCasting.StartCoroutineByCasting(1);
    }

    public IEnumerator _TestTwoByCasting()
    {
        Debug.Log("Test two by casting");

        yield return new WaitForSeconds(3f);

        m_TesterByCasting.StartCoroutineByCasting(0);
    }
}

Thanks!

\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

You can think of IEnumerator as a counter/sequence. It has an internal state to what position it reached. In the case of unity coroutines it is a sequence of waiting times. When you run it once you gone through the full sequence and it is at its end. I have not tested it but likely you can call Reset() on the enumerator before passing it again to StartCoroutine. BTW this makes me think that you should be careful to not run the same enumerator in parallel with itself. In the case of delegates a new enumerator is created each time you start a coroutine (or maybe even some unity magic to avoid the garbage).

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the reply. You are right, and I know that much about IEnumerator as a counter/sequence as you've said. And I suspected that maybe the IEnumerator isn't being restarted. I've tried Reset() before, but it I prolly need to implement something more since it throws a NotSupportedMethod exception. Test #1, and #3 are kinda similar except that on #1 I'm trying to run the IEnumerator I stored in the Tester class via the owning Monobehavior. So I guess, we need to answer why it behaves differently and what to do to fix it, what magic unity does when it's just inside the same script. \$\endgroup\$
    – Kintaro
    Commented Apr 26, 2021 at 8:13
  • \$\begingroup\$ I've also read from somewhere else about a new enumerator being created when called via delegate or method like you mentioned. So I'm thinking how do you renew the enumerator in the case that the IEnumerator is stored in a variable like in Tester class? Or how do you restart it putting the sequence back to the beginning? \$\endgroup\$
    – Kintaro
    Commented Apr 26, 2021 at 8:19
0
\$\begingroup\$

Okay, so after a few experiments I have found a solution to my issue. I just needed to create an IEnumerator delegate and pass that instead.

public delegate IEnumerator ICoroutine();

public ICoroutine m_One;
public ICoroutine m_Two;

public Tester(MonoBehaviour owner, ICoroutine one, ICoroutine two)
{
    ...
}

then update the calls to,

m_Owner.StartCoroutine(m_One());

and

m_Owner.StartCoroutine(m_Two());

Calling it as you would call it normally in the same script. It now works as intended.

Thanks to @Nikaas, I was reminded about enumerators getting renewed when called via delegates or methods.

\$\endgroup\$

You must log in to answer this question.

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