4

Notice the two extensions, one for float, one for Vector3.

Notice there's only a slight difference in the var( call.

In c# could these be written as one as a generic??

The essence of my question is:

within a generic, can you branch on the nature of the type?

public static IEnumerator Tweeng( this float duration,
         System.Action<float> vary, float aa, float zz )
{
    float sT = Time.time;
    float eT = sT + duration;

    while (Time.time < eT)
    {   
        float t = (Time.time-sT)/duration;
        vary( Mathf.SmoothStep(aa,zz, t) ); // slight difference here
        yield return null;
    }

    vary(zz);
}

public static IEnumerator Tweeng( this float duration,
      System.Action<Vector3> vary, Vector3 aa, Vector3 zz )
{
    float sT = Time.time;
    float eT = sT + duration;

    while (Time.time < eT)
    {
        float t = (Time.time-sT)/duration;
        vary( Vector3.Lerp(aa,zz, t) ); // slight difference here
        yield return null;
    }

    vary(zz);
}

(BTW for any c# gurus reading, the code example is in Unity, where you access the frame system in a coroutine.)

For any Unity devs reading, examples of how you call Tweeng

// tweeng z to 20 degrees in .12 seconds
StartCoroutine(.12f.Tweeng( (t)=>transform.Eulers(0f,0f,t), 0f,20f) );
// fade in alpha in .75 seconds
StartCoroutine(.75f.Tweeng( (u)=>{c.a=u;s.color=c;}, 0f,1f) );

(If you're new to Unity and not familiar with the basic concept of extensions, here's an intro.)

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Yes, you could add a generic argument `T` (`float`/`Vector3`), and then pass in `Lerp/SmoothStep` as `Func` – Rob Mar 20 '16 at 23:59
  • ah, basically adding another argument. it would be great to make the extension figure it out, I guess one could press down that division to another generic function – Fattie Mar 21 '16 at 02:18

3 Answers3

3

You can do it if you make an extra Func<T,T> that performs transformation before calling the var action (which you should rename, because var is a C# keyword).

Here is one approach that you could take:

public static IEnumerator Tweeng<T>(
    this float duration
,   System.Action<T> varAction
,   T aa
,   T zz
) {
    Func<T,T,float,T> transform = MakeTransform<T>();
    float sT = Time.time;
    float eT = sT + duration;
    while (Time.time < eT) {   
        float t = (Time.time-sT)/duration;
        varAction(transform(aa, zz, t));
        yield return null;
    }
    varAction(zz);
}

private static Func<T,T,float,T> MakeTransform<T>() {
    if (typeof(T) == typeof(float)) {
        Func<float, float, float, float> f = Mathf.SmoothStep;
        return (Func<T,T,float,T>)(Delegate)f;
    }
    if (typeof(T) == typeof(Vector3)) {
        Func<Vector3, Vector3, float, Vector3> f = Vector3.Lerp;
        return (Func<T,T,float,T>)(Delegate)f;
    }
    throw new ArgumentException("Unexpected type "+typeof(T));
}

It can even be done inline:

public static IEnumerator DasTweeng<T>( this float duration, System.Action<T> vary, T aa, T zz )
    {
    float sT = Time.time;
    float eT = sT + duration;

    Func<T,T,float,T> step;

    if (typeof(T) == typeof(float))
        step = (Func<T,T,float,T>)(Delegate)(Func<float, float, float, float>)Mathf.SmoothStep;
    else if (typeof(T) == typeof(Vector3))
        step = (Func<T,T,float,T>)(Delegate)(Func<Vector3, Vector3, float, Vector3>)Vector3.Lerp;
    else
        throw new ArgumentException("Unexpected type "+typeof(T));

    while (Time.time < eT)
        {
        float t = (Time.time-sT)/duration;
        vary( step(aa,zz, t) );
        yield return null;
        }
    vary(zz);
    }

Perhaps a more natural idiom is

    Delegate d;

    if (typeof(T) == typeof(float))
        d = (Func<float, float, float, float>)Mathf.SmoothStep;
    else if (typeof(T) == typeof(Vector3))
        d = (Func<Vector3, Vector3, float, Vector3>)Vector3.Lerp;
    else
        throw new ArgumentException("Unexpected type "+typeof(T));

    Func<T,T,float,T> step = (Func<T,T,float,T>)d;
Fattie
  • 27,874
  • 70
  • 431
  • 719
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • @JoeBlow You can switch on the type by doing `if (typeof(T) == typeof(float))...` (I'm sorry about the mistake in my code above, it is fixed now). You could certainly do it inside the loop, but then you would have to make the same decision on each iteration. Storing a `Func` upfront lets you do an `if` once, and then reuse the result on each iteration. – Sergey Kalinichenko Mar 21 '16 at 02:39
  • right, but what I mean is, can one put the code from `MakeTransform` simply inside `Tweeng` (sure, at the top to avoid doing it every loop) ... I will try it! – Fattie Mar 21 '16 at 12:09
  • @JoeBlow Absolutely, one could inline that code inside `Tweeng`. I separated it out because to me the condition on `typeof(T)` is somewhat "unclean", while the rest of `Tweeng` is nice and clean. – Sergey Kalinichenko Mar 21 '16 at 12:18
  • @JoeBlow Please try the fix with an explicit cast from the last edit. – Sergey Kalinichenko Mar 21 '16 at 13:32
  • @JoeBlow The edit should work, or at least compile now. – Sergey Kalinichenko Mar 21 '16 at 13:51
  • incredible, that's it :O Thanks. It's like you have to cast it "through a delegate". You can even do it inline .. I'll paste it in for your editing!! – Fattie Mar 21 '16 at 14:01
  • an amazing answer, thanks again. Do you think the `Delegate d` style I pasted in seems the most natural? I guess one would just have a pile of those for each conceptually matching function :O cheers... – Fattie Mar 21 '16 at 14:11
  • @JoeBlow Yes, the last bit that you pasted seems very easy to read and understand. – Sergey Kalinichenko Mar 21 '16 at 14:13
  • I didn't even know what a "float" was a few minutes ago – Fattie Mar 21 '16 at 14:15
3

You can define your method as follows:

public static IEnumerator Tweeng<T>(this float duration,
         System.Action<T> var, T aa, T zz, Func<T,T,float,T> thing)
{
    float sT = Time.time;
    float eT = sT + duration;

    while (Time.time < eT)
    {
        float t = (Time.time - sT) / duration;
        var(thing(aa, zz, t));
        yield return null;
    }

    var(zz);
}

And then using it:

float a = 5;
float b = 0;
float c = 0;
a.Tweeng(q => {}, b, c, Mathf.SmoothStep);

Or:

float a = 0;
Vector3 b = null;
Vector3 c = null;
a.Tweeng(q => {}, b, c, Vector3.Lerp);

Alternatively, if you want to get rid of the method passing, you can have simple overloads to handle it:

public static IEnumerator Tweeng(this float duration, System.Action<float> var, float aa, float zz)
{
    return Tweeng(duration, var, aa, zz, Mathf.SmoothStep);
}
public static IEnumerator Tweeng(this float duration, System.Action<Vector3> var, Vector3 aa, Vector3 zz)
{
    return Tweeng(duration, var, aa, zz, Vector3.Lerp);
}

private static IEnumerator Tweeng<T>(this float duration,
         System.Action<T> var, T aa, T zz, Func<T,T,float,T> thing)
{
    float sT = Time.time;
    float eT = sT + duration;

    while (Time.time < eT)
    {
        float t = (Time.time - sT) / duration;
        var(thing(aa, zz, t));
        yield return null;
    }

    var(zz);
}

And then using it:

float a = 5;
float b = 0;
float c = 0;
a.Tweeng(q => {}, b, c);

Or:

float a = 0;
Vector3 b = null;
Vector3 c = null;
a.Tweeng(q => {}, b, c);


Stub methods to compile in LINQPad/without unity:
public class Mathf { public static float SmoothStep(float aa, float zz, float t) => 0; }
public class Time { public static float time => DateTime.Now.Ticks; }
public class Vector3 { public static Vector3 Lerp(Vector3 aa, Vector3 zz, float t) => null; }
Rob
  • 26,989
  • 16
  • 82
  • 98
0

I liked the Tweeng thing, but why extend float if the Coroutine can only be used on MonoBehaviours? You should do the extensions for MonoBehaviour so, for example I did a extension to do Interpolation:

public static void _Interpolate(this MonoBehaviour monoBehaviour, float duration,
    Action<float, bool> callback, float from, float to, Interpolator interpolator)
{
    monoBehaviour.StartCoroutine(ExecuteInterpolation(interpolator, duration, callback, from, to));
}

So I just Started a Coroutine inside the extension:

private static IEnumerator ExecuteInterpolation(Interpolator interpolator, float duration,
    Action<float, bool> callback, float from, float to)
{
    float sT = Time.time;
    float eT = sT + duration;
    bool hasFinished = false;
    while (Time.time < eT)
    {
        float t = (Time.time - sT) / duration;
// ----> my logic here with callback(to, false)
        yield return null;
    }

    hasFinished = true;
    callback(to, hasFinished);
}

Note that I have a boolean to say that the interpolation finished, this happens because is not best practice to rely on float comparison to check end of a flow, if it rounds for the max result the result before the last we are going to have the call back for the last interaction called twice.

Fernando Bonet
  • 576
  • 5
  • 13
  • hey Fernando! of course you can "re-base" any extension as you see fit, it's only syntactic candy - it has no reality. Really "Tweeng" is very widely used as is. (Example, https://stackoverflow.com/a/37228628/294884 ) Since it's an animation, it's "about" a time so (for me) that's the logical "base" of the syntax. As you mention it can only be used in a MonoBehavior - naturally, that's true of many/most things in Unity. Really it's just a convention to have it come off the seconds. You can surely have the arguments in a different order if preferred. Enjoy! – Fattie Feb 20 '18 at 16:46
  • Hi Fattie, "Tweeng" itself is a really useful idea, my code is nothing but a Tweeng for Monobehaviour instead of float, and I could refactor the Tweeng with MonoB. Still, not only the syntax is not nice to put .75.Tweeng(params[]) but is not right once the action can be only executed in MonoB. The better one Function describe itself the best the code is, to create an extension with no definition, poor syntax and out of context is not nice, is a fast trick, but not a good solution.(Respectfully, I don't want to argue with you, but just point the weakness on this solution that I spotted) – Fernando Bonet Feb 21 '18 at 14:13
  • for sure @fernanobonet, I totally understand what you mean!! I respectfully disagree, the operation happens "on" seconds so it seems natural to me - but I totally get what you're saying!!! I would just say, it's pretty common in unity code to see extensions on float, so even if you disagree - you may see it in other code bases! Heh! Cheers!!! – Fattie Feb 21 '18 at 14:17
  • Nothing against to extend float or neither any other primitives, is a matter of context, usage and naming, but I got your point. If you got what I meant and still disagreeing, soooo... I respect even more hahaha, this crazy world people just shout their own opinion without consider any other, quite rare discussions like this one, isn't? xD – Fernando Bonet Feb 21 '18 at 14:28