Search Unity

  1. Are you interested in providing feedback directly to Unity teams? Sign up to become a member of Unity Pulse, our new product feedback and research community.
    Dismiss Notice

Generic Coroutines and Actions

Discussion in 'Scripting' started by Xorxor, Jan 7, 2016.

  1. Xorxor

    Xorxor

    Joined:
    Oct 15, 2014
    Posts:
    24
    I find myself writing similar coroutines over and over again. Typically I animate a value, whether it's a float, Vector2, Vector3 etc. And then often times I do something with that value. In this case I'm setting a transform's position to that value.

    Here's an example:
    Code (csharp):
    1. StartCoroutine(AnimateTransformPositionThenSetIsHittable(xform, fromValue, toValue, duration, animationCurve, isHittable));
    2.  
    3. IEnumerator AnimateTransformPositionThenSetIsHittable(Transform xform, Vector3 fromValue, Vector3 toValue, float duration, AnimationCurve animationCurve)
    4. {
    5.     float elapsedTime = 0.0f;
    6.     // isAnimating = true;
    7.     while (elapsedTime < duration)
    8.     {
    9.         yield return new WaitForEndOfFrame();
    10.         elapsedTime += Time.deltaTime;
    11.         float t = Mathf.Clamp01(elapsedTime / duration);
    12.         float tt = animationCurve.Evaluate(t);
    13.         Vector3 v = Vector3.Lerp(fromValue, toValue, tt);
    14.         xform.localPosition = v;
    15.     }
    16. }
    What I'd like is something more generic. Perhaps I can lerp some generic value in a coroutine and pass it a function to apply that value? Perhaps something like this:

    Code (csharp):
    1. StartCoroutine(Animate(fromValue, ToValue, duration, animationCurve, ()=>transform.position = value));
    2.  
    3. IEnumerator Animate(T fromValue, T ToValue, float duration, float animationCurve, System.Action operation)
    4. {
    5.     float elapsedTime = 0.0f;
    6.     // isAnimating = true;
    7.     while (elapsedTime < duration)
    8.     {
    9.         yield return new WaitForEndOfFrame();
    10.         elapsedTime += Time.deltaTime;
    11.         float t = Mathf.Clamp01(elapsedTime / duration);
    12.         float tt = animationCurve.Evaluate(t);
    13.         T v = T.Lerp(fromValue, toValue, tt);
    14.         operation(value);
    15.     }
    16. }
    My syntax is totally wrong, but Is something like this even possible? Thanks!
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    7,748
    You can't do the generic thing, since T does not define 'Lerp'.

    But you really are only handling a few types (float, Vector2, Vector3, so on)... just create some overloads:
    Code (csharp):
    1.  
    2. IEnumerator Animate(float fromValue, float ToValue, float duration, AnimationCurve animationCurve, System.Action<float> operation)
    3. {
    4.     //code
    5. }
    6.  
    7. IEnumerator Animate(Vector2 fromValue, Vector2 ToValue, float duration, AnimationCurve animationCurve, System.Action<Vector2> operation)
    8. {
    9.     //code
    10. }
    11.  
    12. IEnumerator Animate(Vector3 fromValue, Vector3 ToValue, float duration, AnimationCurve animationCurve, System.Action<Vector3> operation)
    13. {
    14.     //code
    15. }
    16.  
    Also, make sure the 'operation' callback includes the type.

    Another option, if you wanted to... but would be slower... is you could reflect from T which type it is and Lerp based on it. But you'd probably end up doing some boxing and the sort to do this...

    For example, I have a very basic 'TryLerp' method at the bottom of this class:
    https://github.com/lordofduct/space...ob/master/SpacepuppyBase/Dynamic/Evaluator.cs
    But it's used in a presumed situation where your values are already boxed and you don't know what they are. Rather than as a generic 'T'. It would work in your situation of the T, as you'd just box it on the way in. But it's slow compared to overloading for each type.
     
  3. Xorxor

    Xorxor

    Joined:
    Oct 15, 2014
    Posts:
    24
    This is pretty amazing! Thanks so much for your help! Here's what I put together:
    Code (CSharp):
    1.  
    2. IEnumerator Animate(float fromValue, float toValue, float duration, AnimationCurve animationCurve, System.Action<float> operation)
    3. {
    4.     float elapsedTime = 0.0f;
    5.     while (elapsedTime < duration)
    6.     {
    7.         yield return new WaitForEndOfFrame();
    8.         elapsedTime += Time.deltaTime;
    9.         float t = Mathf.Clamp01(elapsedTime / duration);
    10.         float tt = animationCurve.Evaluate(t);
    11.         float value = Mathf.Lerp(fromValue, toValue, tt);
    12.         // Do something
    13.         operation(value);
    14.     }
    15. }
    16.  
    I'm calling it like this:
    Code (CSharp):
    1.  
    2. StartCoroutine(Animate(fromValue: 0f, toValue: 1f, duration: 0.5f, animationCurve: animationCurve, operation: x =>
    3. {
    4.     myController.SetFloat(x);
    5. }));
    6.  
    I'm running into a snag when operating on a List. This wasn't an issue before when I wasn't using lambdas. For example if I do this:
    Code (CSharp):
    1.  
    2. foreach (SuperController myController in myListOfSuperControllers)
    3. {
    4.     StartCoroutine(Animate(fromValue: 0f, toValue: 1f, duration: 0.5f, animationCurve: animationCurve, operation: x =>
    5.     {
    6.         Debug.Log(myController.name + x.toString());
    7.         myController.SetFloat(x);
    8.     }));
    9. }
    10.  
    The only myController.name that gets printed is the last one. Any idea what might be wrong? Thanks!!
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    7,748
    You'll notice that for each update it's actually printing out the last myController multiple times for that update (the number of times as there are items in myListOfSuperControllers).

    This has to do with the way that anonymous methods and lambdas compile. See really what happens is that the compiler generates a type with a weird name in the class you defined this all in. If that anonymous function refers to variables declared outside of itself ('myController' in your case), the type created is a class in which the state information is stored so it can be accessed in the anonymous function.

    Now here in lies a problem, and it has to do with the C# compiler that is used by Unity. It's based on an outdated compiler for mono, and contains a bit of a bug. In an attempt to optimize the state information, it'll reuse it if it thinks it can... and well this compiler thinks it can here. It doesn't distinguish between the state information for each iteration of the for loop. So unfortunately you get the state information of the most recently defined state (the last element in the for loop). I don't know the compiler, but I have the feeling what they do is instead generate a state for the entire function in which the anon method is defined, rather than one for each new anon function.

    You can see this by building similar code in Visual Studio using the Microsoft .Net compiler, it'll distinguish the states just fine. It should even work if you write the same code and place the generated dll into a unity project.


    Keep in mind, the version of Mono used in Unity targets support for .Net 2.0. Which is before unity 3.5 when anonymous functions were released.

    Because mono versions don't actually follow .net versions directly. It has semi-support for some .Net 3.5 features. This is why it has anonymous functions and lambdas, despite the target support is .Net 2.0. Thing is, because it's only partially supported... it's not considered to work correctly... hence the bug.

    So, really, features like this should be considered risky.

    Of course there's a way to trick the compiler into creating the correct state information.

    Code (csharp):
    1.  
    2.     void Foo()
    3.     {
    4.         foreach (var myController in myListOfSuperControllers)
    5.         {
    6.             this.StartAnimation(myController);
    7.         }
    8.     }
    9.  
    10.     private void StartAnimation(SuperController  myController)
    11.     {
    12.         StartCoroutine(Animate(fromValue: 0f, toValue: 1f, duration: 0.5f, animationCurve: animationCurve, operation: (x) =>
    13.         {
    14.             Debug.Log(myController.name + x.toString());
    15.             myController.SetFloat(x);
    16.         }));
    17.     }
    18.  
     
    Last edited: Feb 2, 2016
  5. A.Killingbeck

    A.Killingbeck

    Joined:
    Feb 21, 2014
    Posts:
    483
    I'm sure I've read somewhere that this "bug" wasn't actually a bug and foreach was designed in a way similar to for loops. The actual loop variable is not local to each iteration. It's "scope" is outside the loop.

    What's actually happening is with each iteration of the for loop, myController is being re-assigned.No new variable is created. Which means if you are trying to capture the loop variable in a closure(i.e. a lambda) it will always use the last value it was set to.

    Another workaround in addition to lordofduct response, is to create a copy of the loop variable in local scope:

    Code (CSharp):
    1. foreach (SuperController theController in myListOfSuperControllers)
    2. {
    3. var myController = theController;
    4.     StartCoroutine(Animate(fromValue: 0f, toValue: 1f, duration: 0.5f, animationCurve: animationCurve, operation: x =>
    5.     {
    6.         Debug.Log(myController.name + x.toString());
    7.         myController.SetFloat(x);
    8.     }));
    9. }
     
    lordofduct likes this.
  6. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    5,540
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    7,748
    Nice article.

    I did not know this was how .Net treated the anonymous functions in loops back when it was created. That would explain why this older mono compiler would do that then. I only had assumed it was a bug.

    I just saw .Net and Mono acting different... and as the article said "this would be a breaking change, and we hates them, my precious". I assumed .Net wouldn't have changed it if it were the other way prior, because changing it would be "breaking". But I guess they did.
     
unityunity