Search Unity

[C#] Passing and Changing Variables Using A Coroutine

Discussion in 'Scripting' started by SwaggyMcChicken, Sep 30, 2015.

  1. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    I'm developing and AI and I'm wanting to delay the decision making process, so I'm using a coroutine. It's meant to check the distance of the Player, and edit an int to determine if it should move or not. The coroutine is supposed to edit a variable named "MoveZ" but whenever I check the value after the coroutine runs, it doesn't change the value. Here's my code:

    Declaring the method
    Code (CSharp):
    1. IEnumerator FindRun(int Z){
    2.  
    3.         if (Distance > 3){
    4.            
    5.             Z = 1;
    6.            
    7.         }
    8.        
    9.         else if (Distance < 2){
    10.            
    11.             Z = -1;
    12.            
    13.         }
    14.        
    15.         else{
    16.        
    17.             Travelling = false;
    18.        
    19.         }
    20.  
    21.         yield return new WaitForSeconds(.1f);
    22.    
    23.     } // Closes FindRun
    In Update
    Code (CSharp):
    1. StartCoroutine ("FindRun", MoveZ);
    What am I doing wrong? I'm not sure what "return" does, does it have anything to do with that?
     
  2. EETechnology

    EETechnology

    Joined:
    Aug 15, 2015
    Posts:
    185
    all the code in couroutine should go after "yield return new WaitForSeconds(.1f);"

    -Edmond--
     
  3. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    Thanks for the tip, but it doesn't solve my problem x-x
     
  4. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Try using the non-string version of StartCoroutine:
    Code (csharp):
    1. StartCoroutine(FindRun(MoveZ));
    I prefer to use the non-string version because it's clearer about what's happening. Plus, it allows you to use coroutines with more complex parameter lists than just the one.

    If you want your coroutine to delay what it does, then you need to put the yield return line at the top of the function.

    Ordinarily, in a function, as soon as you hit a 'return' statement, it's over. Nothing after that line is ever executed. A coroutine is a little different. Once it hits that 'yield return' line, it DOES return, BUT it also marks that spot.

    It returns an object called an IEnumerator which contains the current running state of the coroutine, including the line at which it stopped. When StartCoroutine gets that object, it stashes the IEnumerator in Unity's "list of coroutines". Once per frame, Unity runs through this entire list and kicks up each function where it left off, one at a time; if the function comes across another 'yield return' line, it again jumps out of the function, and Unity holds onto it in its queue of coroutines. That's what happens when you use 'yield return null;' in a coroutine; effectively, it tells Unity to wait for one frame.

    There are special commands you can give it when returning, the most common of which is 'yield return new WaitForSeconds(x);' This makes Unity, instead of tossing it on the 'next frame' queue, toss it on a different particular pile based on the command you gave it. This makes 'yield return new WaitForSeconds(0.1f);' do exactly what its name says, and at exactly the place where you put it.

    The important takeaway here is that yield returns are special, and unlike normal returns, they don't always come at the end of the function.
     
    gaggedegg likes this.
  5. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    Thanks for the clarifications over coroutines, I appreciate it greatly. This community is really helpful for the confused.
    However, I still can't figure out why my MoveZ isn't being changed outside of the function. Whenever I use Debug.Log(MoveZ); it says "0" in the console where the values should have been "1" or "-1" and I can't figure out why.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    MoveZ will not change. It is passed by value and becomes the local copy called "Z," which IS being changed.

    If you want your MoveZ to change, you can either reference it explicitly from the coroutine (not a great way to go about it), or use a closure (functor) to allow the coroutine to modify it by callback. I'm not familiar with functor syntax in Unityscript, only C#, but there are plenty of examples out there. You are essentially creating a small anonymous function that is a block of code that takes the new value you want to put into MoveZ and sticks it into MoveZ, which ought to get you to where you want to be. You would pass this functor into your coroutine, which is yet another reason to use the non-string version.
     
    Kiwasi likes this.
  7. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    I'm unable to find much documentation over the closures, other than one tutorial which didn't make too much sense to me, and used syntax I've never seen before. Is there a Unity tutorial or anything else for this?
     
  8. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    Also, would there be anyway to avoid doing this in another fashion? I'm still learning how to code, so I'm not sure if I'm passing anything simpe up here.
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Whoops, somehow I thought you were using javascript. Never mind. Yes, here's the syntax snippet you're looking for in C#...

    Code (csharp):
    1. IEnumerator FindRun( System.Action<int> callback){
    2.  
    3.         yield return new WaitForSeconds(.1f);
    4.  
    5.         if (Distance > 3){
    6.          
    7.             callback(1);
    8.          
    9.         }
    10.      
    11.         else if (Distance < 2){
    12.          
    13.             callback(-1);
    14.          
    15.         }
    16.      
    17.         else{
    18.      
    19.             Travelling = false;
    20.      
    21.         }
    22.  
    23.     } // Closes FindRun
    24.  
    25. // At your call site:
    26.  
    27. StartCoroutine ( FindRun( (i) => { MoveZ = i; } ) );
    The closure is the little snippet "(i) => { MoveZ = i; }" blurb, which is a tiny little anonymous function that accepts a single variable of type int and assigns it to MoveZ in the call site's context. Closures are neat and useful in many Coroutine and other semi-asynchronomous contexts.
     
    khaled24, owyang, Marty_Gra and 5 others like this.
  10. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    I appreciate this. Some intimidating code though, hahaha. I'll try to work this out. Thanks!
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Yeah, I went through the same "why would I want to do that?!" phase with functors/closures, and then one day I saw the light. Coroutines can really be my friend now...

    Be sure you grasp the actual functionality going on there. It's sort of a lambda, but more because it can capture local variable context and state.

    There are times when writing a particular thing with closures allows you to completely "turn the code inside-out," and make it all much simpler and cleaner as you go.
     
    SwaggyMcChicken likes this.
  12. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    I'm confused by this "lamba syntax" I'm seeing everywhere, could you explain the

    StartCoroutine ( FindRun( (i) => { MoveZ = i; } ) );

    and the

    IEnumerator FindRun( System.Action<int> callback)

    please? It's kinda confusing, not a lot of documentation on it, it seems. At least, I cannot find it.
     
  13. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Code (csharp):
    1. (i) => { MoveZ = i; } )
    This is pretty much equivalent to this

    Code (CSharp):
    1. void SomeMethod (int i) {
    2.     MoveZ = i;
    3. }
    It's often referred to as an anonymous function, because we haven't given it an explicit name. Anonymous functions are generally used for light weight methods that won't hang around for long. You could also use the named method if you liked, like this

    Code (csharp):
    1. StartCoroutine(FindRun(SomeMethod));
    System.Action<int> is shorthand for writing out a delegate. It's entirely syntatic sugar. Read up on delegates to get the general concept.
     
    SwaggyMcChicken and Kurt-Dekker like this.
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Adding to @BoredMormon, another term for a delegate is a function pointer with a context, i.e., which object do you want this function to be called on. If it is static, then there is no object, just the static context of the class itself.

    Functor syntax just saves you having to come up with different function names for each little function that you want to pass around like this, plus makes the code tidier because it keeps all the logic localized to the call site.
     
    Kiwasi likes this.
  15. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    Thanks a ton guys, you really helped with simplifying the logic of the code into understandable bits. Often when I look at examples and explanations, they use a lot of terms I don't really understand, and when I look up those terms, I typically don't get what I'm looking for, or they use more terms that I don't understand. Don't know if I'm the only one, lol, but thanks
     
    Kiwasi likes this.
  16. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Definitely not. I frequently come here to ask for step by step instructions on a coding concept I couldn't quite grasp.

    Glad we could help.
     
    SwaggyMcChicken likes this.
  17. SwaggyMcChicken

    SwaggyMcChicken

    Joined:
    Apr 13, 2015
    Posts:
    108
    So, just a quick clarification, is this functor supposed to change the variable outside of the function? I've been fiddling with it, and it won't do that. I have the same issue I had originally, once it leaves the function it drops the value

    Edit: Now that I reread the code, I suppose it shouldn't be changing the value outside of the function. How could I get it to do this?
     
    Last edited: Oct 5, 2015
  18. RendCycle

    RendCycle

    Joined:
    Apr 24, 2016
    Posts:
    330
    Found any solution? I know this is an old thread but I am experiencing a similar problem and could not find clear explanation anywhere. The value being thrown from the Coroutine, even when passed through callback does not reflect outside that routine.
     
  19. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    What is the problem you're having? See enclosed project for fully-functioning example of my original 2015 post including scene and code.
     

    Attached Files:

  20. RendCycle

    RendCycle

    Joined:
    Apr 24, 2016
    Posts:
    330
    I've finally solved my problem. I was using the wrong variable name! I was trying to hijack a value being thrown from an open source script and change it in real-time. But before I got it, I tested your code and it worked like a charm. I had better understanding now on Coroutine callbacks. Thank you very much! :)
     
    Kurt-Dekker likes this.
  21. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Excellent! They are super-powerful, super-useful, but make sure you use them with discipline because they can quickly lead to very complicated code flow paths.
     
    RendCycle likes this.
  22. RendCycle

    RendCycle

    Joined:
    Apr 24, 2016
    Posts:
    330
    Hmm... I have another question. Would it be possible to pass an array outside an IEnumerator? If yes, how would it be called by StartCoroutine? I tried the code below but I couldn't figure out how to call the four values it generates.

    Code (CSharp):
    1. IEnumerator WavesChanger (float transitionSpeed, float fromScale, float toScale, float fromChoppy, float toChoppy, float fromSpeed, float toSpeed, float fromDensity, float toDensity, System.Action<Array> callback = null) {
    2.  
    3.         _isChangingWaves = true;
    4.         float timeToStart = Time.time;
    5.         wavesProperties = new float [4];
    6.  
    7.         while (scale != toScale && speed != toSpeed && choppy_scale != toChoppy && waveDistanceFactor != toDensity) {
    8.  
    9.             scale = Mathf.Lerp (fromScale, toScale, (Time.time - timeToStart) * transitionSpeed);
    10.             choppy_scale = Mathf.Lerp (fromChoppy, toChoppy, (Time.time - timeToStart) * transitionSpeed);
    11.             speed = Mathf.Lerp (fromSpeed, toSpeed, (Time.time - timeToStart) * transitionSpeed);
    12.             waveDistanceFactor = Mathf.Lerp (fromDensity, toDensity, (Time.time - timeToStart) * transitionSpeed);
    13.          
    14.             wavesProperties [0] = scale;
    15.             wavesProperties [1] = choppy_scale;
    16.             wavesProperties [2] = speed;
    17.             wavesProperties [3] = waveDistanceFactor;
    18.  
    19.             callback (wavesProperties);
    20.             yield return null;
    21.  
    22.         }
    23.  
    24.         _isChangingWaves = false;
    25.  
    26. }
    This one does not seem to work because it does not recognize the wavesProperties[] variable:

    Code (CSharp):
    1. StartCoroutine (WavesChanger (0.5f, scale, maxWaveScale, choppy_scale, maxChoppyScale, speed, maxSpeed, waveDistanceFactor, maxWaveDistanceFactor, returnValue => wavesProperties[] = returnValue));
    2.  
    3. scale = wavesProperties [0];
    4. choppy_scale = wavesProperties [1];
    5. speed = wavesProperties [2];
    6. waveDistanceFactor = wavesProperties [3];
     
  23. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Take a look at the CallbackDemo.unitypackage I posted above. In it you pass a System.Action<int> callback into the coroutine.

    To handle an array of floats, you would instead type it to be System.Action<float[]> and then give it a function (or anonymous function) that accepts an array of floats and does whatever you want with it, probably assign it to some other local storage.

    Basically you can type the callback arguments to be whatever you like, and you can actually give it a bunch of callbacks if you want. A common use for this is a callback for success and then a callback for failure that takes a string "reason" for why it failed.
     
    RendCycle likes this.
  24. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Any reason not to pass the array directly?
     
    RendCycle likes this.
  25. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    I was thinking of the use case of you don't already have the array, or you don't know the size of it and the coroutine is going to know, basically the generic "data backflow" problem.
     
    RendCycle and Kiwasi like this.
  26. RendCycle

    RendCycle

    Joined:
    Apr 24, 2016
    Posts:
    330
    Thanks again! That answered my question. :)
     
    Kurt-Dekker likes this.
  27. unity_rUXG6TRGKyfxuQ

    unity_rUXG6TRGKyfxuQ

    Joined:
    Nov 28, 2019
    Posts:
    5
    This is a great solution and it helps on many of my problems.
    But I got to a new problem: what if I want to change the value of the variable and read it in the same coroutine?

    Here is my problem:

    Code (CSharp):
    1. IEnumerator PlayDialogueAfterWaiting(System.Action<bool> myCondition, int waitTime) {
    2.         myCondition(true);
    3.         yield return new WaitForSeconds(waitTime);
    4.         if (/* I want to read the value of my bool here, because it might have been changed by other functions during the wait time */) {
    5.             // Do something
    6.         }
    7. }
    Thanks!
     
  28. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    A
    System.Action<int>
    can call functions to send out integers. (Or any other type you want)

    A
    System.Func<int>
    can get them back!

    In fact a
    System.Func<T1,T2>
    can send and receive information, basically pass arguments into a function and get data back.
     
    unity_rUXG6TRGKyfxuQ likes this.
  29. unity_rUXG6TRGKyfxuQ

    unity_rUXG6TRGKyfxuQ

    Joined:
    Nov 28, 2019
    Posts:
    5
    Thanks for the help! This was just what I needed.
    In case someone wants to see the code, here it is.

    Coroutine:
    Code (CSharp):
    1. IEnumerator PlayDialogueAfterWaiting(int waitTime, System.Action<bool> WaitCondition, System.Func<bool> ConditionChecker) {
    2.         WaitCondition(true);
    3.         yield return new WaitForSeconds(waitTime);
    4.         if (ConditionChecker()) {
    5.             SoundAction();
    6.         }
    7. }
    And the call:
    Code (CSharp):
    1. yield return StartCoroutine(PlayDialogueAfterWaiting(waitTimeForButton, (myBool) => { isStillWaiting = myBool; }, () => isStillWaiting));
    2.        
    I was not sure if the lambda expression to get the boolean value was correct, but this is working well over here.
     
    Kurt-Dekker likes this.
  30. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,674
    Looks good to me! Glad you got it sorted. That construct enables really interesting programming constructs.