- Home /
The question is answered, right answer was accepted
Cannot null Coroutine variable if it calls an interface
Hi there!
I am creating a simple AI system for my game, and I ran into a weird issue. In my implementation the AI decides every once in a while to take an action (represented by a GameAction object), and I pass that action to a routine to be executed when certain conditions are met. I store the Coroutine object resulting from the StartCoroutine() to avoid the AI overriding an already started action:
void Update () {
if (currentCoroutine != null) { return; }
elapsedTime += Time.deltaTime;
if (elapsedTime >= coroutineCallingPeriod) {
elapsedTime -= coroutineCallingPeriod;
currentCoroutine = StartCoroutine (GameActionCallerRoutine(new ExampleGameAction()));
}
}
private IEnumerator GameActionCallerRoutine(GameAction gameActionToExecute) {
// some irrelevant condition before calling Execute()
gameActionToExecute.Execute();
currentCoroutine = null;
yield return null;
}
The variable currentCoroutine is set to null at the end of GameActionCallerRoutine, however, it does not work! I can notice this by no more actions being called ever again! Upon further research it turns out that the problem is related to the routine having as a parameter an interface or abstract class (GameAction of which ExampleGameAction is a concrete implementor).
The weirdest part is that if instead of a Coroutine I store the routine before StartCoroutine() into an IEnumerator, then it can be nulled:
// it works this way!
currentCoroutine = GameActionCallerRoutine(null);
StartCoroutine (currentCoroutine);
So I know I already have a solution for this, but can someone explain what's going on here? I suspect that something stores a reference to the Coroutine object, but why can't a variable I have complete control over set to be null just because the routine has an abstract parameter?
Thanks! Harinezumi
Answer by Bunny83 · Sep 05, 2015 at 12:33 PM
You have several issues here. First of all, why are you use a coroutine at all? Your actual action method seems to be a "normal" method. So it will complete immediately within the same frame it's called.
Your actual problems comes from the fact that when you start a coroutine Unity immediately runs the coroutine up to the next yield before the StartCoroutine call even returns. Since your first and only yield is at the end of your coroutine, you "reset" the currentCoroutine variable before it's actually assigned.
So at the yield StartCoroutine will actually return and you assign the Coroutine object to your variable. At this point the coroutine has already done it's "magic". Once frame later the coroutine is done.
The way you use a coroutine here makes no sense. What does your actual "ExampleGameAction" contain? The way you've setup your stuff you can remove the whole coroutine stuff as it has no purpose at the moment.
If you want your GameAction to be able to execute things over several frames it should be a coroutine itself.
Just want to add that you can "make it work" just by adding a yield return null; at the very beginning of your coroutine. However as i said i don't see the point of using a coroutine at all in your case.
Hi Bunny83!
Thanks for your reply, but I think there is a misunderstanding of the issue. The actual issue is that the currentCoroutine variable does not become null if you pass the routine an object that implements an interface or abstract class as parameter. I did not post my actual code, only the $$anonymous$$imum code that does show this behaviour.
To make it clear, I use a coroutine for executing a GameAction, because a GameAction has a range, and the executing GameObject first needs to get into position (so a GameAction is basically a callback). I do the approaching the target within the routine, which may take some time, which is in the omitted part marked with "some irrelevant condition before calling Execute()", but as the issue happened without it, I just removed it.
So I'm sorry if my code example was confusing, but I am looking for an answer to something different. Anyway, if what you said would solve the problem, then I would get the passed GameAction.Execute() call repeatedly, which does not happen.
@Harinezumi:
Sorry but if that's not your actual code, why have you posted it when it doesn't reflect what you're actually doing? Have you actually understood why the variable wasn't "reset" in the code you posted in your question?
If you already yield on something before you reset your variable everything should be fine. It absolutely doesn't matter what parameters you pass to the generator method. That certainly won't change the way the coroutine works ^^. Again, it's important that you yield at least once before you reset your variable. Otherwise StartCoroutine will rush through your code and resets the variable before you actually set it.
btw a yield at the end of a coroutine is quite pointless unless someone is waiting for the coroutine and you want to extend it's lifetime for a frame.
With the information provided i can't reproduce your problem. If your code stands as it is shown in your question plus you have an unconditional yield before you reset the variable everything works just fine.
I've used a lot coroutines with and without callbacks and complex objects passed around. I litterally know coroutines inside out. So without more information this is a dead end...
Just to make my point clear. Here i've implemented the smallest possible example with your setup:
// ActionScheduler.cs
using UnityEngine;
using System.Collections;
public class ActionScheduler : $$anonymous$$onoBehaviour
{
Coroutine currentCoroutine = null;
float elapsedTime = 0f;
float coroutineCallingPeriod = 2f;
void Update()
{
if (currentCoroutine != null) { return; }
elapsedTime += Time.deltaTime;
if (elapsedTime >= coroutineCallingPeriod)
{
elapsedTime -= coroutineCallingPeriod;
currentCoroutine = StartCoroutine(GameActionCallerRoutine(new ExampleGameAction()));
}
}
private IEnumerator GameActionCallerRoutine(GameAction gameActionToExecute)
{
for (float t = 0; t < 1f; t += Time.deltaTime )
{
// do something ...
yield return null;
}
gameActionToExecute.Execute();
currentCoroutine = null;
yield return null;
}
}
Further more i have those two classes:
public abstract class GameAction
{
public abstract void Execute();
}
public class ExampleGameAction : GameAction
{
public override void Execute()
{
Debug.Log("Aaand, action!");
}
}
I get reliable "Aaand, action!" printed about every 3 seconds (2 seconds wait time + 1 second setup time inside the coroutine). So your problem is not reproducible with the code you've provided.
How do you know that your GameAction isn't executed? Do you have a Debug.Log inside Execute? Are you sure you don't have set your console to "collapse"? That would merge all messages that contains the same text into one entry in the console.
Answer by Harinezumi · Sep 05, 2015 at 03:22 PM
@Bunny83: Thanks for the thorough response. I was also writing an answer and it is now clear what you mean: the StartCoroutine(), even before returning a Coroutine object, will run until the first yield, which means it will set currentCoroutine to null, but to no avail, because the returned Coroutine will be assigned to it. This also means that an empty yield does solve the issue.
To be honest, your somewhat offensive / contemptuous tone made me reluctant to read and analyse your answer thoroughly, but you did answer the question, and quite thoroughly. Maybe you could be a bit friendlier in the future?
I'm sorry if i sounded contemptuous. We often have to deal with questions (and answers) that are missing essential information, are "badly worded" or just make absurd claims. A lot (i mean really a lot) of users here have up to no program$$anonymous$$g experience and often it's not enough to just "point them into a direction".
Actually i skip a lot of question because often title already gives a good idea what madness will expect you when you're going to read the actual question. So i usually pick questions that stand above those and your title sounded interesting ^^. Questions like this one are always welcome as they help to improve the quality of questions / topics and counter act that downward spiral we have since a few years here on UA.
Again i'm sorry if it sounded offensive. That certainly wasn't my intention ^^. btw. english isn't my first language and i'm still trying to improve it.
Follow this Question
Related Questions
Return result of the IEnumerator. 2 Answers
StartCoroutine important for using yield? 1 Answer
A loop FOR waits for a coroutine to end 1 Answer
Coroutine freeze other animation & UI part on service request 1 Answer
Item duration doesn't stack 3 Answers