Search Unity

Extending IEnumerator for "custom" coroutines

Discussion in 'Scripting' started by JohnnyA, Jul 7, 2012.

  1. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Heaps of topics about coroutines lately so I though I would post a little snippet. The TimedAction class triggers an action after a certain amount of seconds. What's interesting is that you can query its state as it executes so you can drive a timer, progress bar, wrist watch, or whatever else you want. Of course you could do this in your coroutine but then it wouldn't be very generic.

    Code (csharp):
    1.  
    2. public class TimedAction: IEnumerator {
    3.     float delay;
    4.     Action action;
    5.    
    6.     private bool startedDelay;
    7.     private DateTime endTime;
    8.    
    9.     public TimedAction(float delay, Action action) {
    10.         this.delay = delay;
    11.         this.action = action;
    12.     }
    13.    
    14.     public void Reset() {
    15.         startedDelay = false;
    16.     }
    17.    
    18.     public bool MoveNext() {
    19.         // If this is the first invocation we want to set our end time.
    20.         if (!startedDelay) {
    21.             endTime = DateTime.Now.AddSeconds(delay);  
    22.             return true;
    23.         } else {
    24.             //On the second invocation do the action.
    25.             action();  
    26.         }
    27.         // All done.
    28.         return false;
    29.     }
    30.    
    31.     public object Current {
    32.         get {
    33.             if (!startedDelay) {
    34.                 startedDelay = true;
    35.                 // Return WaitForSeconds, Unity will handle the delay for us.
    36.                 return new WaitForSeconds(delay);
    37.             }
    38.         // If we have already delayed do nothing.
    39.             return null;
    40.         }
    41.     }
    42.    
    43.     public DateTime EndTime {
    44.         get {
    45.             return endTime;
    46.         }
    47.     }
    48.    
    49.     public TimeSpan TimeRemaining {
    50.         get {
    51.             TimeSpan result = endTime - DateTime.Now;
    52.             // The frame times probably won't lineup exactly with our delay, lets not allow negatives
    53.             if (result < TimeSpan.Zero) result = TimeSpan.Zero;
    54.             return result;
    55.         }
    56.     }
    57.    
    58.     public float Percentage {
    59.         get {
    60.             TimeSpan span = endTime - DateTime.Now;
    61.             if (span < TimeSpan.Zero) span = TimeSpan.Zero;
    62.             return 1.0f - (float)(span.TotalSeconds / delay);
    63.         }
    64.     }
    65. }
    66.  
    And a behaviour to test it:

    Code (csharp):
    1.  
    2. public class StartWorker : MonoBehaviour {
    3.    
    4.     public TimedAction action;
    5.    
    6.     void Start(){
    7.         action = new TimedAction(4.0f, new Action(SayBoom));
    8.         StartCoroutine(action);
    9.     }
    10.    
    11.     public void Update(){
    12.         // Of course you probably don't want to check this every frame, just an example
    13.         Debug.Log (string.Format("Time: {0} ({1:##}%)", action.TimeRemaining, action.Percentage * 100.0f));
    14.     }
    15.    
    16.     public void SayBoom(){
    17.         Debug.Log ("Boom");
    18.     }
    19. }
    20.  
    Although this example is pretty trivial you can use a similar sort of approach for all kinds of cool stuff :)


    EDIT:

    To be clear there is nothing (functionally) this approach enables that can't be achieved with "normal" coroutines wrapped in another class. This approach tends to lead to neater code when you want to do things like control and interrupt the execution flow (e.g. pause) or provide results from the coroutine at any point during its execution (if you have an interface why not use it). This approach also allows you to inherit behaviour between coroutines including the functions Reset() and MoveNext() and the property Current. This is something you cannot do when using the auto-generated code of coroutines generated with yield.
     
    Last edited: Jul 11, 2012
    jpthek9 likes this.
  2. Jessy

    Jessy

    Joined:
    Jun 7, 2007
    Posts:
    7,327
    What's the point of this?
    Code (csharp):
    1. public delegate void ActionDelegate()
     
  3. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Not quite sure if you are asking what a delegate is or why I'm using my own one (instead of Func or Action).

    Presumably the second ... this is just a matter of me cutting this code down from a more complicated example I'm actually using (in which the delegate has a more specific signature). In this case I could have (and should have) used Action given that it is already defined.

    Just in case you are asking first (or someone else does), delegates work like function pointers in C, they allow you to pass a function as an argument. This way I can call my SayBoom() function or any other with a matching signature as a TimedAction.
     
    Last edited: Jul 7, 2012
  4. Jessy

    Jessy

    Joined:
    Jun 7, 2007
    Posts:
    7,327
    Yes.
     
  5. Jaimi

    Jaimi

    Joined:
    Jan 10, 2009
    Posts:
    4,895
    This is a cool sample, so don't get me wrong. But you're not actually extending IEnumerator - it's just an interface, it has no class to extend. You have written a class that implements IEnumerator. Still a good example of how interfaces are used.
     
  6. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Indeed, I'm well aware of that, will update. I had trouble with the title so I think as soon as I had something vaguely conveying what I wanted I hit enter :)

    The point is not really to demonstrate interfaces; the point is that most people use functions containing yields for their coroutines almost exclusively (meaning that they are auto-generated). This approach gives you an alternative which has a lot of flexibility (e.g. you could implement a handler for a sequence of actions which you could pause, reverse, etc).

    There's nothing complex about it, but it's just not something people seem to think of using very often.
     
  7. npsf3000

    npsf3000

    Joined:
    Sep 19, 2010
    Posts:
    3,832
    I like the idea... but I think it's overly verbose for what you're trying to do :p

    I'd look into 'token' objects [no idea if that's what they are actually called] and possibly using your own 'StartCoroutine' that accepts IEnumerator<T>.

    That said, I've not done this at a high level before.
     
  8. Ocid

    Ocid

    Joined:
    Feb 9, 2011
    Posts:
    476
    Nice post. Not taken the time to look at it properly yet, I only just quickly skimmed it but just also wanted to point you towards a post Renauld Benard made about custom coroutines in Unity over here in case you want to mash up something between what he posted and you have here.
     
  9. Tseng

    Tseng

    Joined:
    Nov 29, 2010
    Posts:
    1,217
    I think it's pretty bad design, to access the fields directly. Adding an Update events where interested objects can subscribe for seems more reasonable approach.
     
  10. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    I completely disagree. Events make sense for well ... "events", some kind of important state change. Events might make sense for a sequence of actions (e.g. event when each finishes), or you might use an event at the end of this instead of taking a delegate approach. But to use events to do something like a timer seems crazy. You want to send an event every tenth of a second to drive a stop watch readout?

    It doesn't allow you to "access the field directly", it provides read-only properties which describe the state of the enumerator.
     
  11. stimarco

    stimarco

    Joined:
    Oct 17, 2007
    Posts:
    721
    There's no variable named "TimeRemaining" or "Percentage". They're both getter functions that returned calculated values. As such, there's no problem accessing them directly. Furthermore, events aren't guaranteed to arrive within a set time. You might get nothing for a whole 10th of a second, then two events arrive at once in a single frame. For a timer display, that's not ideal.

    Nevertheless, it's wise to stick with the "KISS" principle. Using an additional event / observer mechanism is all well and good, but it's overkill for an illustration of the core technique. JohnnyA clearly didn't intend for this to be used in production.


    That said, "Percentage" is definitely misnamed. It should return an actual percentage, not a value between 0 and 1!
     
    Last edited: Jul 10, 2012
  12. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Off topic but isn't percentage usually expressed as a value between 0 and 1 (until display time). I usually see it done that way and have always done it that way. Presumably so if you use it for calculations you get a correct result. Obviously production code would have this documented.

    i.e. 10 * 50% = 5 not 500

    EDIT: Also +1 to this "JohnnyA clearly didn't intend for this to be used in production". I just wanted to illustrate a simple technique that I think is easily forgotten because of the typical way coroutines are used.

    EDIT 2: The reason I added the observer pattern was to provide some kind of use case which made sense in a gaming context. I figured if I just demonstrated the core technique in isolation I would have written 40 lines of code to do what could have been done with 5, which might put people off a little :)
     
    Last edited: Jul 10, 2012
  13. Tseng

    Tseng

    Joined:
    Nov 29, 2010
    Posts:
    1,217
    That's exactly what happens, as the time passes the event (percentage) changes. But instead of rising an event, you access the fields directly from the TimedAction object. The event can be useful if you want to create casting/progress bar, you would simply

    Code (csharp):
    1.  
    2. TimedAction action = ...;
    3. action.ProgressUpdate += delegate(float percentage, float timeLeft, float totalTime) { // or use the default EventHandler object/EventArgs if you like
    4.     // update the progress bar here
    5. }
    6.  
    And you even don't have to store a reference of TimedAction, as long as you unsubscribe all listeners when you're done (so it can be claimed by the GC).
     
  14. npsf3000

    npsf3000

    Joined:
    Sep 19, 2010
    Posts:
    3,832
    Then the way you have seen it done is wrong. Your example is flawed because you do not return a percentage [fraction of 100]. If a function advertised that it returned a speed in m/s, but gave you km/s would you be happy? While it's not drastically different, and can be easily modified, it's not what the function claims to be.

    Moving on.

    While I certainly see what you're trying to do, I've answered similar questions using a completely different line of reasoning and thought I'd post a quick example:

    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections;
    4. using UnityEngine;
    5.  
    6. public class AdvancedCoroutines : MonoBehaviour
    7. {
    8.     IEnumerator Start()
    9.     {
    10.         var isDone = new IsDoneObject();
    11.         StartCoroutine(MyActions.DelayedAction(1, () => print("HUZZAH!"), timeRemaining => print(timeRemaining), isDone));
    12.         while (!isDone) yield return null;
    13.         print("Finished");
    14.     }
    15. }
    16.  
    17. public static class MyActions
    18. {
    19.     public static IEnumerator DelayedAction(float delayTime, Action action, Action<float> timeRemaingUpdate = null, IsDoneObject isDone = null)
    20.     {
    21.         while (delayTime > 0)
    22.         {
    23.             if (timeRemaingUpdate != null) timeRemaingUpdate(delayTime -= Time.deltaTime);
    24.             yield return null;
    25.         }
    26.  
    27.         action();
    28.         if (isDone != null) isDone.SetDone();
    29.     }
    30. }
    31.  
    32. public class IsDoneObject
    33. {
    34.     private bool isDone = false;
    35.  
    36.     public void SetDone()
    37.     {
    38.         isDone = true;
    39.     }
    40.  
    41.     public static implicit operator bool(IsDoneObject x)
    42.     {
    43.         return x.isDone;
    44.     }
    45. }
    I use an Action to demonstrate active updates [think 'events'] and an isDone object for passive updates ['querying']. I even rewrote it do be more generic 'DelayedAction' rather than the original 'DelayedShout'.

    What I like about this particular solution is that it's shorter, and more to the point 44LOC vs 82LOC.

    I used very similar code to actually run a 'multithreaded' downloaded in unity - where I could limit how many threads run at once, have a nice updating gui etc. without having to worry about threading etc.
     
    Last edited: Jul 10, 2012
  15. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Cant beleive we are still arguing about this trivial example, I think its time for me to stop trying to be useful here.

    @Tseng, you don't seem to understand (or don't want to understand) what myself and Stimarco (one of the best and most helpful coders on the forums) have pointed out. Yes an event might make sense if you have a fixed set of intervals, however the provided approach is much more appropriate in most use cases involving timers and progress bars (for the reasons mentioned by myself and Stimarco).

    Not to mention the first post clearly states this is a "pretty trivial" example.

    @NPSF First comment not aimed at you, I don't mind reasonable discussion, however:

    1) I'm well aware that percentage are generally displayed as a value between 0 and 100 followed by a trailing % sign. However I was being a little tongue in cheek when I asked "but isn't percentage usually expressed as a value between 0 and 1". The answer is yes it is. This is not me introducing some strange convention. When coding it's more common to express percentages as a value between 0 and 1 than a value between 0 and 100. Google it or see for example this stack overflow post.

    A few examples: The C# string formatter expects values between 0 and 1 for a percentage, (from the MSDN doc "P -> Result: Number multiplied by 100 and displayed with a percent symbol.") . Even MS Excel, aimed at a pretty general audience, uses 0 - 1 for its storage value for percentages (write 0.1, 10, 0.5, 50 in a column in Excel and convert them to a percentage).

    2) Your example doesn't actually demonstrate what I was trying to demonstrate. You are just writing a normal coroutine and wrapping it in another coroutine. The point was never that you can't write this example with less lines of code. Notice how the provided example doesn't use yields, yet I'm still able to return WaitForSeconds and have it handled correctly by Unity.
     
    Last edited: Jul 11, 2012
  16. npsf3000

    npsf3000

    Joined:
    Sep 19, 2010
    Posts:
    3,832
    I have no problem with using decimals 0-1, but it's *not* a percentage - as per the definition of the term. The MSDN doc you linked does NOT use a percentage as an input - it accepts a number that it then converts to a percentage. Anywho, it's a minor point.

    While my code doesn't demonstrate what you demonstrate, it does achieve the same end goal. I would point out that there's only one coroutine in my example [same as yours]. And while I do you use 'yield' - you do to albeit with the more verbose 'MoveNext' and 'Current' functions.

    Basically I'm trying to point out that since the compiler will already do this rewriting for you, why not use it? I find the use of token objects or events to lead to shorter code that achieves the same objective. It's also more open to reuse - though i guess you could use interfaces or inheritance to allow multiple 'actions' to have a 'percent completed' function.

    At the end of the day though, it's just and alternative - feel free to ignore :p
     
    Last edited: Jul 11, 2012
  17. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    Yeah MSDN example wasn't the best however it really is common to use 0-1 not 0-100. I tutored and later lectured programming for quite a while, and been in the industry for nearly 15 years, 10 of those in development and most of that as a development lead with 5-10 people reporting in to me. In that time I'd say that 0-1 has been used for functions labelled as percentage around .9 of the time (grin)*. It really is common, and I don't think just its just in the areas I'm most familiar with. The fact the Stimarco and yourself think otherwise just points out how dangerous even the simplest of assumptions can be, document, document, document!

    In terms of the example, it is difficult to find a clear cut case for this method being better than another (although I think the code comes out a little easier to understand in some cases). One quality, that I have pointed out in the first post, is that this lets you use inheritance to extend the enumerator. This is actually pretty handy and something you can't do very easily using the "normal" way. Consider for example building up a library of handy IEnumerators like SequenceOfActions or DelegatedWork, etc. You could very easily extend these classes just overriding one or two methods to make a ReversibleSequenceOfActions or a PausableDelegatedWork.

    In the end though, this was merely about saying "Hey guys, have you thought about doing this, it might help you elegantly solve a problem one day".

    * Not trying to big note, just saying I have sat through a lot of code reviews and API selection panels... I've seen a lot of code :)
     
    Last edited: Jul 11, 2012
  18. npsf3000

    npsf3000

    Joined:
    Sep 19, 2010
    Posts:
    3,832
    This whole thing is quite interesting, because it demonstrates how programmers think of percentage as 'number between 0 and 1' when it's not - as 200% is perfectly valid. The best match for what we actually mean that I could fines is 'Unit Interval' - but that's verbose and not many people would intuitively know what it means. Anywho...

    I would point out that both solutions can do this, you'd override the 'IEnumerator' while I'd override the 'token object'. For example:

    Code (csharp):
    1.  
    2. //untested
    3. var sequence = new SequenceToken(()=> print("one"}, () => print("two"), ()=> print("three"));
    4. StartCoroutine(Actions.Sequance(sequence));  //Accepts ISequenceToken
    5. print(sequence.Peek());
    6.  
    7. var reversible = new ReversibleToken(()=> print("one"}, () => print("two"), ()=> print("three"));
    8. StartCoroutine(Actions.Sequance(reversible));   //Accepts ISequenceToken
    9. reversible.Reverse();
    10. print(reversible.Peek());
    11.  
    Could be valid code.

    Regardless of how you do it, this level of control and flexibility of how our code runs is fascinating.
     
  19. JohnnyA

    JohnnyA

    Joined:
    Apr 9, 2010
    Posts:
    4,654
    I guess the key is 50% = 50/100 = 0.5 (or 200% = 2). Its a mathematical way of thinking and the reason its nice in code is mainly for calculation(10 * 50% = 5). I think its clear there are many and varied solutions to any problem :)
     
  20. Nunez-Torrijos

    Nunez-Torrijos

    Joined:
    Mar 3, 2016
    Posts:
    19
    Hi, a bit late, but this was very helpful.
    I was looking for a way to make characters display a dialog just once, or finite amount of times, without having a lot of trackers inside other classes. Thanks to your tips I was able to make my own class implementing IEnumerator.
    Thx a lot!