+1
Answered

Coroutines

NeedsLoomis 2 years ago updated by Lazlo Bonin (Lead Developer) 2 years ago 6

Im having trouble with coroutines.  99% of the stems from the fact that I want to use StopCoroutine, which requires a stored reference to work when not using strings.

So heres my code:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    
    private void Start()
    {
        coroutine = SomeCoroutine(someObject);
    }
    public IEnumerator SomeCoroutine(GameObject obj)
    {
        while (true)
        {
            Debug.Log(obj.name);
            yield return null;
        }
    }
}

And my FSM


Which throws an error (someObject is null).

However, when I setup the Machine like this:


Everything works fine, so Im guessing there is an execution order issue.  Is there an easy way to resolve this, like a set method for Ienumerator references that allow parameters?

Bolt Version:
Unity Version:
Platform(s):
Scripting Backend:
.NET Version (API Compatibility Level):

Answer

+1
Answer
Answered

Here's the issue: when you create an enumerator and store its reference in start, this enumerator stores the reference value of its parameter in its own object. Initially, that value is null. Then, even if you change the source of this value later, it will not update the enumerator's parameter, because references are copied, not delegated.

This has nothing to do with Bolt, and it's rather an issue of C# and object lifecycle.

You have three solutions here:

1. Don't pass an obj parameter, and use the member someObject directly:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    
    private void Start()
    {
        coroutine = SomeCoroutine();
    }
    public IEnumerator SomeCoroutine()
    {
        while (true)
        {
            Debug.Log(someObject.name);
            yield return null;
        }
    }
}


2. Instead of creating and storing that reference on start, create it directly from Bolt, by calling SomeCoroutine as a unit and specifying the obj parameter from the node. Then, store the return value of that node in your coroutine member using a set member unit. Your code would then be only:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    
    public IEnumerator SomeCoroutine(GameObject obj)
    {
        while (true)
        {
            Debug.Log(obj.name);
            yield return null;
        }
    }
}


3. Do part of #2 in script by using a CreateSomeCoroutine method, and then using that as a node in the graph:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    public void CreateSomeCoroutine(GameObject obj)
    {
        coroutine = SomeCoroutine(obj);
    }
    public IEnumerator SomeCoroutine(GameObject obj)
    {
        while (true)
        {
            Debug.Log(someObject.name);
            yield return null;
        }
    }
}


Let me know if this makes sense to you.

Pending Review

This doesn't seem to be related to coroutines, just to execution order.

What error do you get exactly when you say:

Which throws an error (someObject is null).

I don't see how your component itself could be null... Do you mean that accessing the coroutine field throws a NullReferenceException? In this case it might just be a script execution order issue, where the flow machine starts before your component, and putting your Start code in Awake would fix it.

Edit:  To clarify, the coroutine runs, but it can't find the the GameObject "someObject" that is passed to it as a parameter.

"UnassignedReferenceException: The variable someObject of CoroutineTest has not been assigned.

You probably need to assign the someObject variable of the CoroutineTest script in the inspector."


I can assign someObject in start if I wanted to, or drag and drop it in the inspector, but I can't set it in Bolt like I would assume I could, as in the first example.  When I do that I can see it being set in the inspector when the state activates, but it doesnt seem to set before the coroutine executes (giving me that error) unless I set it way ahead of time, like in a completely different state, as in example 2.

Nevermind my past reply. I see the issue now, I'll look into it.

+1
Answer
Answered

Here's the issue: when you create an enumerator and store its reference in start, this enumerator stores the reference value of its parameter in its own object. Initially, that value is null. Then, even if you change the source of this value later, it will not update the enumerator's parameter, because references are copied, not delegated.

This has nothing to do with Bolt, and it's rather an issue of C# and object lifecycle.

You have three solutions here:

1. Don't pass an obj parameter, and use the member someObject directly:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    
    private void Start()
    {
        coroutine = SomeCoroutine();
    }
    public IEnumerator SomeCoroutine()
    {
        while (true)
        {
            Debug.Log(someObject.name);
            yield return null;
        }
    }
}


2. Instead of creating and storing that reference on start, create it directly from Bolt, by calling SomeCoroutine as a unit and specifying the obj parameter from the node. Then, store the return value of that node in your coroutine member using a set member unit. Your code would then be only:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    
    public IEnumerator SomeCoroutine(GameObject obj)
    {
        while (true)
        {
            Debug.Log(obj.name);
            yield return null;
        }
    }
}


3. Do part of #2 in script by using a CreateSomeCoroutine method, and then using that as a node in the graph:

public class CoroutineTest : MonoBehaviour {
    public IEnumerator coroutine;
    public GameObject someObject;
    public void CreateSomeCoroutine(GameObject obj)
    {
        coroutine = SomeCoroutine(obj);
    }
    public IEnumerator SomeCoroutine(GameObject obj)
    {
        while (true)
        {
            Debug.Log(someObject.name);
            yield return null;
        }
    }
}


Let me know if this makes sense to you.

+1

Ah interesting, so the whole reference is cached, gameobject and all.  I can make those work.  Out of curiosity, why does it work when you set the GameObject in a previous state in the FSM?  Is the FSM state triggering before the Start function of the script?  

Yes, Bolt starts its state machines on OnEnable, which happens before Start in the Unity order of events:

https://docs.unity3d.com/Manual/ExecutionOrder.html

Your transition in your second example happens on the first Update, which happens after Start, so that explains why it worked. :)