Search Unity

[FREE] More Effective Coroutines

Discussion in 'Assets and Asset Store' started by Trinary, Feb 23, 2016.

  1. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Hello @RazaTech

    I would indeed use just one coroutine for that. A good way to structure it might be to add each task to a .new Queue object, and then in the coroutine you can check "while(taskQueue.count > 0)" and pop an item from the queue. Sometimes you might want to make sure you only load a certain number per frame, and if so you can use a counter. If you want to save processor cycles and not check the queue every frame then I suggest running it in the SlowUpdate time segment.
     
  2. codemonkeynorth

    codemonkeynorth

    Joined:
    Dec 5, 2017
    Posts:
    13
    If anyone is interested, I made a post about implementing DOTween's YieldInstruction with MEC (as it doesn't support CustomYieldInstruction yet). And also calling standard Unity coroutines/yields from MEC

    https://github.com/Demigiant/dotween/issues/213#issuecomment-393657794

    My workaround is possibly horribly problematic, as I'm new to C# and Unity but for a quickfix there's something usable for certain situations. It also possibly defeats the point of moving over to MEC in the first place. But for those situations where you absolutely need to call some standard functionality just to get a yield, please take a look

    Maybe someone can make something more useful from this, and hopefully DOTween's author is looking into further integration.

    But for now it allows me to do the following, which is useful within a limited scope (ie StopCoroutine etc is going to be problematic as it doesn't affect MEC so you'll need to manage the different systems yourself)

    (see the GitHub comment for the actual class code)

    Code (CSharp):
    1.  
    2. // MEC Coroutine, standard DOTween YieldInstruction
    3. private IEnumerator<float> MECCoroutine()
    4. {
    5.     Tween myTween = transform.DOMoveX(45, 1);
    6.     yield return Timing.WaitUntilDone(
    7.         new WaitForStandardYieldInstruction(this, myTween.WaitForCompletion())
    8.     );
    9. }
    10.  
    11. public class WaitForStandardYieldInstruction : CustomYieldInstruction { ... }
    12.  
    Code (CSharp):
    1.  
    2. // MEC coroutine wiating on Unity standard coroutine
    3. private IEnumerator<float> MECCoroutine()
    4. {
    5.    yield return Timing.WaitUntilDone(
    6.        new WaitForStandardCoroutine(this, SomeStandardUnityCoroutine())
    7.    );
    8. }
    9. private IEnumerator SomeStandardUnityCoroutine() { ... }
    10. public class WaitForStandardYieldInstruction : CustomYieldInstruction { ... }
    11.  
     
    Last edited: Jun 1, 2018
  3. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    My suggestion, if you want to use DOTween, is to just use a Unity coroutine for that part. MEC doesn't disable Unity's coroutines so you can still use them for compatibility with things like that, and you can use MEC coroutines everywhere else.
     
  4. codemonkeynorth

    codemonkeynorth

    Joined:
    Dec 5, 2017
    Posts:
    13
    the problem is (at least for me) once you're inside a MEC Coroutine and you want to yield from a standard one you can't (or use a
    YieldInstruction
    ) .. you've got to track back through your application finding the first point where there's just a standard
    StartCoroutine
    or
    RunCoroutine
    without a yield.

    I realise it's an edge case to some extent.... I just hit it when moving quite a small application over to MEC.

    I believe Demigiant is going to look at an implementation over the weekend at least for converting his
    YieldInstruction
    methods to
    CustomYieldInstruction
    that can be used with MEC, which will be useful.

    specifically it was me trying to port this over
    yield return myTween.WaitForCompletion()


    thanks for the feedback
    J

    PS I added my other method
    WaitForStandardCoroutine : CustomYieldInstruction
    here
    https://github.com/Demigiant/dotween/issues/213#issuecomment-393737584

    I'm sure it has all sorts of problems when trying to interact with the coroutine, but it serves a specific edge case purpose

    it was more just a simple example of how it could be used to integrate into MEC if there's no easy way around it - eg a Coroutine method from a 3rd party utility etc. You'd need to implement some kind of CoroutineManager to make it stoppable/killable though as obviously it's 2 completely different systems

    Like i said i'm new to coding in C#/Unity and don't really understand the systems I'm using but they work up unto a point. Your videos on the subject have been really useful though, so thanks!
     
    Last edited: Jun 1, 2018
  5. codemonkeynorth

    codemonkeynorth

    Joined:
    Dec 5, 2017
    Posts:
    13
    @chyen @Trinary

    as noted above, Demigiant is working on a potential update for us so DOTween's YieldInstructions eg WaitForCompletion have an alternative CustomYieldInstruction version to worrk with MEC
    https://github.com/Demigiant/dotween/issues/213

    the current syntax would be
    yield return Timing.WaitUntilDone(myTween.WaitForCompletionCY());
    but i'm wondering if it's possible to overload with something like
    yield return Timing.WaitUntilDone(myTween.WaitForCompletion<CustomYieldInstruction>());

    could that work? (He needs to keep back compatibility so can't change the original YieldInstruction version of the function directly)

    thanks for any suggestions
    J

    Update: Demigiant has decided on just a true parameter. it's actually
    myTween.WaitForCompletion(returnCustomYieldInstruction: true)
    but you can omit the parameter name

    you can now do this for example
    Code (CSharp):
    1. myTween = transform.DOMoveX(10f, 10f);
    2. // this test function will cause the Tween to complete early (eg after 100 frames instead of 10 seconds)
    3. Timing.RunCoroutine(TestDOTweenEarlyComplete());
    4. // now wait for our Tween to complete using MEC. (which will then complete early as per above eg with a Kill on the tween)
    5. // true param returns a MEC-compatible CustomYieldInstruction, instead of a YieldInstruction
    6. yield return Timing.WaitUntilDone(myTween.WaitForCompletion(true));
    obviously it's not the same as porting DOTween to MEC , as it'll still run standard Unity coroutines for the Tweens, but it is useful for integration as per @chyen's original request
     
    Last edited: Jun 2, 2018
    jeromeWork likes this.
  6. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    Is it possible to use `Timing.CallDelayed()` with `Segment.RealtimeUpdate`?
     
  7. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Yes :)

    You would catch the handle returned and switch the segment on it.
    Code (CSharp):
    1. CoroutineHandle handle = Timing.CallDelayed(...);
    2. handle.segment = Segment.RealtimeUpdate;
    Or if you like combining lines into one..
    Code (CSharp):
    1. Timing.CallDelayed(...).segment = Segment.RealtimeUpdate;
     
    hungrybelome likes this.
  8. hungrybelome

    hungrybelome

    Joined:
    Dec 31, 2014
    Posts:
    336
    Thanks! This helps a lot.
     
    Trinary likes this.
  9. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    First off, Trinary, this is an awesome asset!! MEC Pro is one of my best purchases yet. Without it, I don't think my ScriptableObject based coroutine system would have even been possible.

    I added some custom functionality that might be useful in the official release. Basically I wanted to be able to access the LinkCoroutines functionality to wait out the linked coroutines before the master coroutine itself finishes.

    I added the following function to the Timing class:

    Code (CSharp):
    1. static public HashSet<CoroutineHandle> GetLinks( ref CoroutineHandle master )
    2. {
    3.     if ( Links?.ContainsKey( master ) == true )
    4.     {
    5.         return Links[master];
    6.     }
    7.     else
    8.     {
    9.         return null;
    10.     }
    11. }
    In my master coroutine, I have the following:

    Code (CSharp):
    1. HashSet<MEC.CoroutineHandle> slaves = MEC.Timing.GetLinks( ref chainHandle );
    2.    
    3. if ( slaves != null )
    4. {
    5.     // Since this Master Coroutine runs before the Linked Coroutines,
    6.     // it would only detect a Linked Coroutine finishing on the following frame.
    7.     // So instead run in the LateUpdate Segment, allowing detection
    8.     // on the same frame that the Linked Coroutines finish.
    9.  
    10.     yield return MEC.Timing.WaitUntilDone(
    11.         WaitForSlaves( slaves ), MEC.Segment.LateUpdate );
    12. }
    And the WaitForSlaves implementation:

    Code (CSharp):
    1. private IEnumerator<float> WaitForSlaves( HashSet<MEC.CoroutineHandle> slaves )
    2. {
    3.     bool isStillExecuting = false;
    4.  
    5.     do
    6.     {
    7.         foreach ( var handler in slaves )
    8.         {
    9.             isStillExecuting = handler.IsRunning;
    10.  
    11.             if ( isStillExecuting )
    12.             {
    13.                 yield return MEC.Timing.WaitForOneFrame;
    14.                 break;
    15.             }
    16.         }
    17.     } while ( isStillExecuting );
    18.  
    19.     slaves.Clear( );
    20.  
    21.     yield break;
    22. }
    If we could get a new yield return instead like MEC.Timing.WaitUntilLinksAreDone or something similar (and maybe there already is and I just missed it), that would be awesome!!
     
    Last edited: Jul 8, 2018
    Trinary likes this.
  10. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Hi @mykillk

    I'm glad to see that you are using (or trying to use) links. I put those in recently, and wasn't sure anyone would want to use them. They were just something I needed to put together in order to implement the delay functions properly. I'd be interested to know how they work for you (if you're using them).

    I don't think that links should be used for waiting though. I have a system that makes coroutines wait for other coroutines under Timing.WaitForOtherHandles. I would use that. You can pass in a list of coroutine handles to that function and it will wait for all of them to be done before continuing.
     
  11. giraffe1

    giraffe1

    Joined:
    Nov 1, 2014
    Posts:
    302
    Is there any easy way to make a mec coroutine wait for an unity coroutine to finish before proceeding?

    I use A* Pathfinding Project and he has a built-in function for waiting for the path to be calculated. It uses a regular unity coroutine and I didn't want to modify his stuff.
     
  12. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    That's not easy. You have to get to a CustomYieldInstruction. The Coroutine class that unity uses for a handle can be cast into a YieldInstruction, but those have no managed interface so MEC can't react to them being done. Unity hasn't provided a way that I know of so I can't do much to bridge that gap.
     
  13. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    Exactly the kind of feedback I was hoping for! :) I will look into the WaitForOtherHandles functionality. I noticed it before but only saw the version that takes in a single CoroutineHandle to wait on.

    I'll likely still also be using the Link Coroutines functionality because I have to account for both scenarios:

    1) Linked coroutines all finish naturally, and then the Master coroutine ends (which it sounds like WaitForOtherHandles can do).

    2) Master coroutine can be aborted early, which then Kills the Linked Coroutines (which the Links functionality handles).

    Thinking about it though, it just seems a little redundant/inefficient to have both the Links storing the linked coroutines, and then to create an array with the same set of coroutines to pass to WaitForOtherHandles. Extra GC allocations too. Per-coroutine allocations are one of my main concerns since I am intending on making heavy use of coroutines in my game architecture. That's one of the things I'm working on right now actually, is to see if and where caching and pooling could be used to reduce play-time allocations.
     
    Last edited: Jul 10, 2018
    Trinary likes this.
  14. stgs73

    stgs73

    Joined:
    Mar 23, 2015
    Posts:
    59
    Hi, we're running into an odd problem and only on specific devices..just want to confirm

    This is within a coroutine launched via MEC...using the <> syntax..

    Within the MEC routine, i want to yield with the assetbundle load..

    AssetBundleCreateRequest ab = AssetBundle.LoadFromFileAsync (url);
    yield return Timing.WaitUntilDone(ab);

    It seems to be exiting out prematurely....and yielding until the operation is complete...

    Is this expected?

    2018.1.8
    iOS
     
  15. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I've seen this before. Unity has some sort of error with it's CustomYieldExpression that causes it to show isDone as true if you test it immediately after creation before its first use. I think it should work around the issue if you add a yield return Timing.WaitForOneFrame between those two lines.
     
    stgs73 likes this.
  16. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I understand what you are saying about combining two features being inefficient. In the use case I was using links for I didn't want the coroutines to wait for each other, so I know there's at least one case where making that the default behavior would be undesirable. I could theoretically come up with some kind of interface with different types of links, but I would need to hear from a couple more people before I would try to implement something like that.. it could get confusing quickly.
     
  17. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    Not worries. What I'm working on is still very much a work in progress anyway. What I end up doing could be completely different than what I'm currently doing. I'll feed back on what kind of crazy schemes I come up with :D
     
    Trinary likes this.
  18. stgs73

    stgs73

    Joined:
    Mar 23, 2015
    Posts:
    59
    Thanks for the clarification!
     
  19. giraffe1

    giraffe1

    Joined:
    Nov 1, 2014
    Posts:
    302
    Hi,

    I must be doing something wrong, I can't figure out why my Stop() function runs but the coroutines continue to run. What am I doing wrong? I want to stop the coroutines on this game object only.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using MEC;
    4.  
    5. public class Test: MonoBehaviour
    6. {
    7.     void Start ()
    8.     {
    9.         Timing.RunCoroutine(Coroutine(), "coroutine");
    10.     }
    11.  
    12.     void Update()
    13.     {
    14.         if (Input.GetButtonDown("Fire1"))
    15.         {
    16.             Stop();
    17.         }
    18.     }
    19.  
    20.     private IEnumerator<float> Coroutine()
    21.     {
    22.         Debug.Log("started");
    23.  
    24.         yield return Timing.WaitUntilDone(Wait(20f), "wait");
    25.  
    26.         yield return Timing.WaitForSeconds(20f);
    27.  
    28.         Debug.Log("finished");
    29.     }
    30.  
    31.     private IEnumerator<float> Wait(float duration)
    32.     {
    33.         float startTime = Time.time;
    34.  
    35.         while (Time.time < (startTime + duration))
    36.         {
    37.             yield return Timing.WaitForOneFrame;
    38.         }
    39.     }
    40.  
    41.     void Stop()
    42.     {
    43.         Timing.KillCoroutines(this.gameObject, "coroutine");
    44.         Timing.KillCoroutines(this.gameObject, "wait");
    45.  
    46.         Debug.Log("stopped");
    47.     }
    48. }
     
  20. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Hi giraffe1!

    You have to associate the coroutine with this gameObject when it's ran if you want that info to be something that you can call KillCoroutines based on later:

    Timing.RunCoroutine(Coroutine(), this.gameObject, "coroutine");
     
  21. giraffe1

    giraffe1

    Joined:
    Nov 1, 2014
    Posts:
    302
    That stopped the main coroutine. How would I associate the game object with the second coroutine? Specifically this line:

    Code (CSharp):
    1. yield return Timing.WaitUntilDone(Wait(20f), "wait");
    Thanks,
     
  22. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Oh, well when you do
    Code (CSharp):
    1. yield return Timing.WaitUntilDone(Wait(20f), "wait");
    that is a shorcut for
    Code (CSharp):
    1. yield return Timing.WaitUntilDone(Timing.RunCoroutine(Wait(20f), "wait"));
    so in this case you have to use the long form and add the gameObject parameter to that since it seems to have been left out. I'll add putting it into that overload to my TODO list.
     
  23. giraffe1

    giraffe1

    Joined:
    Nov 1, 2014
    Posts:
    302
    Perfect, that is exactly what I was looking for.

    Thanks a lot for your help!
     
  24. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    Is it possible to have coroutines execute in reverse order? I know that on the first execution (i.e. prewarm) of the coroutines, they execute immediately (FIFO), so of course they execute in the order that you call RunCoroutine. That works as expected.

    But on the subsequent Segment executions, I would sometimes like the ability to have them execute in reverse order (LIFO), while other times keeping the FIFO order.

    Looking at the code, it appears that the coroutines are stored in arrays which do not play well with mixed insertions. Could the coroutines instead be stored in a List, and a new argument passed into RunCoroutine for selecting between appending or prepending?

    Right now I am doing a decent bit of layering (i.e. coroutines running other coroutines in a hierarchy), and the system would work better if the children coroutines execute first, as the parent coroutines are waiting for them to finish before continuing. Executing in FIFO order is adding an extra frame of delay before a parent coroutine detects that the child coroutine has completed.

    I'm not super familiar with the underlying implementation of how coroutines work, so I wanted to get your thoughts on whether reverse ordering is actually possible at all. Maybe there's some quirky behavior to IEnumerators that I don't understand. I'm looking at implementing this myself since as far as I can tell it would be a fairly straightforward change. I'll definitely share back my results!

    EDIT 1:

    Ok, I went ahead and took a stab at this. It wasn't quite as straightforward as I was hoping but I think I was able to get it worked out! What I came up with is not an ideal solution as it adds more overhead than I'd like, but I wanted it to involve as few modifications to the code base as possible.

    The general idea is to separate the logical ordering of the coroutines from the physical index ordering. That way, regardless of how the coroutines are serially ordered in the Array (determined by the order of RunCoroutine calls), you can have an arbitrary execution order of the coroutines. I actually wrote a C++ collection framework years back that would be perfect for this. The design goals were two-fold: All collections are stored in contiguous memory allotments and can be accessed by index, and, the index location of an entry never changes once it is added. It would store "vacancies" in a separate linked list so on insertion it would re-use vacated slots as a form of garbage collection (negating the need for something like your RemoveUnused method). It was very minimal and efficient, and the Linked List would be perfect for UpdateProcesses (instead of a C# Array) as it has both the benefits of an array (access-by-index) and a linked list (separate logical ordering & O(1) insertion to either end). But for now, would involve too many changes to the code base, which is what I wanted to avoid. If you're interested though I would definitely share! Cutting out the RemoveUnused process could see some decent performance gains.

    So instead, I left the UpdateProcesses Array as-is, and added an additional C# Array called UpdateProcessesLogical:

    Code (CSharp):
    1. private ProcessIndexListNode[] UpdateProcessesLogical =
    2.     new ProcessIndexListNode[InitialBufferSizeLarge];
    Instead of storing the IEnumerator<float> coroutines, it stores a new class ProcessIndexListNode for the doubly-linked list:

    Code (CSharp):
    1. class ProcessIndexListNode
    2. {
    3.     public int Index = -1;
    4.     public ProcessIndexListNode Next;
    5.     public ProcessIndexListNode Prev;
    6. }
    7.  
    8. ProcessIndexListNode _headNode;
    9. ProcessIndexListNode _tailNode;
    Next up were some standard helper methods to handle insertion and removal to the linked list ordering of the nodes:

    Code (CSharp):
    1. public void PrependProcessIndexNode( int index )
    2. {
    3.     ProcessIndexListNode node = new ProcessIndexListNode( );
    4.     ProcessIndexListNode head = _headNode;
    5.     ProcessIndexListNode tail = _tailNode;
    6.  
    7.     if ( head != null ) { node.Next = head; head.Prev = node; }
    8.     if ( tail == null ) { _tailNode = node; }
    9.  
    10.     node.Index = index;
    11.     _headNode = node;
    12.     UpdateProcessesLogical[index] = node;
    13. }
    14.  
    15. public void AppendProcessIndexNode( int index )
    16. {
    17.     ProcessIndexListNode node = new ProcessIndexListNode( );
    18.     ProcessIndexListNode head = _headNode;
    19.     ProcessIndexListNode tail = _tailNode;
    20.  
    21.     if ( tail != null ) { tail.Next = node; node.Prev = tail; }
    22.     if ( head == null ) { _headNode = node; }
    23.  
    24.     node.Index = index;
    25.     _tailNode = node;
    26.     UpdateProcessesLogical[index] = node;
    27. }
    28.  
    29. public void RemoveProcessIndexNode( int index )
    30. {
    31.     ProcessIndexListNode node = UpdateProcessesLogical[index];
    32.     ProcessIndexListNode next = node.Next;
    33.     ProcessIndexListNode prev = node.Prev;
    34.  
    35.     if ( next != null ) { next.Prev = node.Prev; }
    36.     if ( prev != null ) { prev.Next = node.Next; }
    37.     if ( node == _headNode ) { _headNode = next; }
    38.     if ( node == _tailNode ) { _tailNode = prev; }
    39.  
    40.     UpdateProcessesLogical[index] = null;
    41. }
    Handling the UpdateProcessesLogical array pretty much mirrors that of the UpdateProcesses array since the indexes match. In RunCoroutineInternal, it resizes the Array as necessary and calls the PrependProcessIndexNode method to add it to the beginning of the logical ordering, and storing the physical ordering:

    Code (CSharp):
    1. if (_nextUpdateProcessSlot >= UpdateProcesses.Length)
    2. {
    3.     ProcessIndexListNode[] oldProcLogicalArray = UpdateProcessesLogical;
    4.     IEnumerator<float>[] oldProcArray = UpdateProcesses;
    5.     bool[] oldPausedArray = UpdatePaused;
    6.  
    7.     UpdateProcessesLogical = new ProcessIndexListNode[UpdateProcesses.Length + (ProcessArrayChunkSize * _expansions)];
    8.     UpdateProcesses = new IEnumerator<float>[UpdateProcesses.Length + (ProcessArrayChunkSize * _expansions++)];
    9.     UpdatePaused = new bool[UpdateProcesses.Length];
    10.  
    11.     for (int i = 0;i < oldProcArray.Length;i++)
    12.     {
    13.         UpdateProcessesLogical[i] = oldProcLogicalArray[i];
    14.         UpdateProcesses[i] = oldProcArray[i];
    15.         UpdatePaused[i] = oldPausedArray[i];
    16.     }
    17. }
    18.  
    19. if (UpdateTimeValues(slot.seg))
    20.     _lastUpdateProcessSlot = _nextUpdateProcessSlot;
    21.  
    22. slot.i = _nextUpdateProcessSlot++;
    23. UpdateProcesses[slot.i] = coroutine;
    24. PrependProcessIndexNode( slot.i );
    RemoveUnused slightly changed:

    Code (CSharp):
    1.  
    2. for (outer.i = inner.i = 0; outer.i < _nextUpdateProcessSlot; outer.i++)
    3. {
    4.     if (UpdateProcesses[outer.i] != null)
    5.     {
    6.         if (outer.i != inner.i)
    7.         {
    8.             UpdateProcessesLogical[inner.i] = UpdateProcessesLogical[outer.i];
    9.             UpdateProcessesLogical[inner.i].Index = inner.i;
    10.             UpdateProcesses[inner.i] = UpdateProcesses[outer.i];
    11.             UpdatePaused[inner.i] = UpdatePaused[outer.i];
    12.  
    13.             if (_indexToHandle.ContainsKey(inner))
    14.             {
    15.                 RemoveGraffiti(_indexToHandle[inner]);
    16.                 _handleToIndex.Remove(_indexToHandle[inner]);
    17.                 _indexToHandle.Remove(inner);
    18.             }
    19.  
    20.             _handleToIndex[_indexToHandle[outer]] = inner;
    21.             _indexToHandle.Add(inner, _indexToHandle[outer]);
    22.             _indexToHandle.Remove(outer);
    23.         }
    24.         inner.i++;
    25.     }
    26. }
    27. for (outer.i = inner.i; outer.i < _nextUpdateProcessSlot; outer.i++)
    28. {
    29.     UpdateProcessesLogical[outer.i] = null;
    30.     UpdateProcesses[outer.i] = null;
    31.     UpdatePaused[outer.i] = false;
    32.     if (_indexToHandle.ContainsKey(outer))
    33.     {
    34.         RemoveGraffiti(_indexToHandle[outer]);
    35.         _handleToIndex.Remove(_indexToHandle[outer]);
    36.         _indexToHandle.Remove(outer);
    37.     }
    38. }
    The Nullify method is slightly changed to call RemoveProcessIndexNode:

    Code (CSharp):
    1. case Segment.Update:
    2.     retVal = UpdateProcesses[coindex.i] != null;
    3.     UpdateProcesses[coindex.i] = null;
    4.     RemoveProcessIndexNode( coindex.i );
    5.     return retVal;
    Then, for iteration during the Update Segment, it iterates over the linked list, using the stored Index of the ProcessIndexListNode entry to access the correct slot in the UpdateProcesses array:

    Code (CSharp):
    1. if (_nextUpdateProcessSlot > 0)
    2. {
    3.     ProcessIndex coindex = new ProcessIndex { seg = Segment.Update };
    4.     if (UpdateTimeValues(coindex.seg))
    5.         _lastUpdateProcessSlot = _nextUpdateProcessSlot;
    6.  
    7.     for (ProcessIndexListNode walker = _headNode; walker != null; walker = walker.Next)
    8.     {
    9.         coindex.i = walker.Index;
    10.  
    11.         if (!UpdatePaused[coindex.i] && UpdateProcesses[coindex.i] != null && !(localTime < UpdateProcesses[coindex.i].Current))
    12.         {
    13.             if (ProfilerDebugAmount != DebugInfoType.None && _indexToHandle.ContainsKey(coindex))
    14.             {
    15.                 Profiler.BeginSample(ProfilerDebugAmount == DebugInfoType.SeperateTags ? ("Processing Coroutine, " +
    16.                         (_processLayers.ContainsKey(_indexToHandle[coindex]) ? "layer " + _processLayers[_indexToHandle[coindex]] : "no layer") +
    17.                         (_processTags.ContainsKey(_indexToHandle[coindex]) ? ", tag " + _processTags[_indexToHandle[coindex]] : ", no tag"))
    18.                         : "Processing Coroutine");
    19.             }
    20.  
    21.             if (!UpdateProcesses[coindex.i].MoveNext())
    22.             {
    23.                 if (_indexToHandle.ContainsKey(coindex))
    24.                     KillCoroutinesOnInstance(_indexToHandle[coindex]);
    25.             }
    26.             else if (UpdateProcesses[coindex.i] != null && float.IsNaN(UpdateProcesses[coindex.i].Current))
    27.             {
    28.                 if (ReplacementFunction != null)
    29.                 {
    30.                     UpdateProcesses[coindex.i] = ReplacementFunction(UpdateProcesses[coindex.i], _indexToHandle[coindex]);
    31.                     ReplacementFunction = null;
    32.                 }
    33.                 //coindex.i--;
    34.             }
    35.  
    36.             if (ProfilerDebugAmount != DebugInfoType.None)
    37.                 Profiler.EndSample();
    38.         }
    39.     }
    40. }
    The only thing left, which I haven't implemented yet, would be to add an argument to RunCoroutine and RunCoroutineInternal to control whether the coroutine will be Prepended to the beginning of the logical order, or Appended to the end. Probably have it default to Append to match the current behavior.

    EDIT 2:

    One thing that could be improved with this implementation is to pre-allocate all the ProcessIndexListNodes and to re-use them on insertions, instead of calling new and allocating additional GC on every RunCoroutine.
     
    Last edited: Sep 2, 2018
  25. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    See my above post, I'm also very interested in the execution order of nested coroutines. Your tests show something I didn't realize, it seems that Unity's default coroutine behavior is LIFO execution of nested coroutines. So prepending coroutines to the execution list should actually be the default behavior to match right?

    I am thinking this is the right approach:

    RunCoroutine, intended for standard, non-hierarchical coroutines, and appends to the execution list for FIFO ordering.

    A new RunNestedCoroutine, intended for parent/child relationships, which inserts directly before the parent for LIFO ordering between the children and parent, but maintains FIFO ordering of siblings.

    Here's a little bit of a "diagram"


    Letters indicate execution order
    Numers indicate creation order

    First frame of exeuction (i.e. prewarm)

    A-1
    B-2 E-5
    C-3 D-4 F-6 G-7

    Second+ frame of execution (i.e. Segment Tick)

    G-1
    C-2 F-5
    A-3 B-4 D-6 E-7

    1 = RunCoroutine( ) ----> Appends (1)
    2 = 1.RunNestedCoroutine( ) ----> Inserts before 1 (21)
    3 = 2.RunNestedCoroutine( ) ----> Inserts before 2 (321)
    4 = 2.RunNestedCoroutine( ) ----> Inserts before 2 (3421)

    5 = 1.RunNestedCoroutine( ) ----> Inserts before 1 (34251)
    6 = 5.RunNestedCoroutine( ) ----> Inserts before 5 (342651)
    7 = 5.RunNestedCoroutine( ) ----> Inserts before 5 (3426751)


    and a new Container class implementing the mentioned Array-Backed Linked List. I added in an optional pre-allocation of the linked list nodes so the GC doesn't happen during gameplay per RunCoroutine:

    Code (CSharp):
    1. class CoroutineList
    2. {
    3.     class CoroutineListNode
    4.     {
    5.         public readonly int Index;
    6.         public IEnumerator<float> Coroutine;
    7.         public CoroutineListNode Next;
    8.         public CoroutineListNode Prev;
    9.  
    10.         public CoroutineListNode( int index )
    11.         {
    12.             Index = index;
    13.         }
    14.     }
    15.  
    16.     CoroutineListNode[] _coroutines = new CoroutineListNode[InitialBufferSizeLarge];
    17.     CoroutineListNode _head;
    18.     CoroutineListNode _tail;
    19.     CoroutineListNode _vacancyHead;
    20.     CoroutineListNode _walker;
    21.     int _count;
    22.     bool _doPreAllocate;
    23.  
    24.     public void PreAllocate( )
    25.     {
    26.         if ( _doPreAllocate || _count > 0 )
    27.             return ;
    28.  
    29.         _doPreAllocate = true;
    30.  
    31.         int count = _coroutines.Length;
    32.         for ( int i = 0; i < count; ++i )
    33.             _coroutines[i] = new CoroutineListNode( i );
    34.     }
    35.  
    36.     public IEnumerator<float> Next( )
    37.     {
    38.         _walker = (_walker == null) ? _head : _walker.Next;
    39.  
    40.         return (_walker == null) ? null : _walker.Coroutine;
    41.     }
    42.  
    43.     public int Append( IEnumerator<float> coroutine )
    44.     {
    45.         CoroutineListNode booking = Book( coroutine );
    46.  
    47.         if ( _head == null )
    48.             _head = booking;
    49.  
    50.         if ( _tail != null )
    51.             _tail.Next = booking;
    52.  
    53.         booking.Prev = _tail;
    54.         _tail = booking;
    55.  
    56.         return booking.Index;
    57.     }
    58.  
    59.     public int InsertBefore( IEnumerator<float> coroutine, int parentIndex )
    60.     {
    61.         CoroutineListNode booking = Book( coroutine );
    62.         CoroutineListNode parent = _coroutines[parentIndex];
    63.         CoroutineListNode parentPrev = parent.Prev;
    64.  
    65.         if ( parentPrev != null )
    66.         {
    67.             parentPrev.Next = booking;
    68.             booking.Prev = parentPrev;
    69.         }
    70.  
    71.         if ( parent == _head )
    72.             _head = booking;
    73.  
    74.         parent.Prev = booking;
    75.         booking.Next = parent;
    76.  
    77.         return booking.Index;
    78.     }
    79.  
    80.     public void Remove( int index )
    81.     {
    82.         CoroutineListNode node = _coroutines[index];
    83.         CoroutineListNode next = node.Next;
    84.         CoroutineListNode prev = node.Prev;
    85.  
    86.         if ( next != null )
    87.             next.Prev = prev;
    88.  
    89.         if ( prev != null )
    90.             prev.Next = next;
    91.  
    92.         if ( node == _head )
    93.             _head = next;
    94.  
    95.         if ( node == _tail )
    96.             _tail = prev;
    97.  
    98.         --_count;
    99.         node.Coroutine = null;
    100.         node.Prev = null;
    101.         node.Next = _vacancyHead;
    102.         _vacancyHead = node;
    103.     }
    104.  
    105.     public IEnumerator<float> Retrieve( int index )
    106.     {
    107.         return _coroutines[index].Coroutine;
    108.     }
    109.  
    110.     private CoroutineListNode Book( IEnumerator<float> coroutine )
    111.     {
    112.         CoroutineListNode booking;
    113.  
    114.         if ( _vacancyHead != null )
    115.         {
    116.             booking = _vacancyHead;
    117.             _vacancyHead = _vacancyHead.Next;
    118.             booking.Next = null;
    119.         }
    120.         else
    121.         {
    122.             if ( _count == _coroutines.Length )
    123.                 ResizeArray( );
    124.  
    125.             if ( !_doPreAllocate )
    126.                 _coroutines[_count] = new CoroutineListNode( _count );
    127.  
    128.             booking = _coroutines[_count];
    129.         }
    130.  
    131.         booking.Coroutine = coroutine;
    132.         ++_count;
    133.  
    134.         return booking;
    135.     }
    136.  
    137.     private void ResizeArray( )
    138.     {
    139.         int currentCount = _coroutines.Length;
    140.         int newCount = currentCount + (currentCount / 2);
    141.  
    142.         CoroutineListNode[] oldArray = _coroutines;
    143.         _coroutines = new CoroutineListNode[newCount];
    144.  
    145.         for ( int i = 0; i < currentCount; ++i )
    146.             _coroutines[i] = oldArray[i];
    147.  
    148.         if ( _doPreAllocate )
    149.         {
    150.             for ( int i = currentCount; i < newCount; ++i )
    151.                 _coroutines[i] = new CoroutineListNode( i );
    152.         }
    153.     }
    154. }
    This code is totally untested so far because I don't yet have an overloaded [] operator to return the IEnumerator<float> directly as a (mostly) plug-in replacement for the existing Processes Arrays. I will get to that tomorrow.

    While it adds a small amount overhead on top of the C# Array, I think it more than makes up for that for two reasons: First, it prioritizes recycling removed nodes on insertion instead of appending to the end, so no need for compaction in the RemoveUnused( ) method which looks fairly expensive. Second, because the index position of a CoroutineListNode never changes, it can be reliably used as an ID number to access by index. It would replace the need for the handleToIndex dictionary lookup, instead just storing the ProcessIndex value in the CoroutineHandles directly. It would also further simplify RemoveUnused as there wouldn't be any need to synchronize changes to the index location across ProcessIndex values.
     
    Last edited: Sep 3, 2018
    nicloay likes this.
  26. mykillk

    mykillk

    Joined:
    Feb 13, 2017
    Posts:
    60
    Here's version 1 of my CoroutineList class. I've made quite a few updates/fixes since the version above. The big thing is that the CoroutineList.Node class now tracks additional info like Segment and CoroutineHandle and Pause/Running state, as well as has support for parent/child relationships. There's still some work to go before it's fully usable (most noticeably it is hard-coded for the Update Segment).

    I am continuing to work on integrating with the Timing class. Currently focused on the Update Segment as that's the only one I use right now. So far the functionality of the CoroutineList has been able to replace the _nextUpdateProcessSlot and _lastUpdateProcessSlot member fields, as well as the UpdatedPaused array. By adding a reference to the CoroutineList.Node in the CoroutineHandle, I've been able to remove the _handleToIndex dictionary, and with the CoroutineHandle referenced in the CoroutineList.Node I'll soon be able to remove the _indexToHandle dictionary too. The new parent/child relationship support is also supplanting the Links functionality. Most of RemoveUnused() is unnecessary as well now.

    But, obviously, I won't post the modified MEC Pro Timing.cs file here. If you are interested @Trinary though I can show what I have in the hopes you may like it and integrate into the official release. Very open to collaboration :D
     

    Attached Files:

  27. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Hi @mykillk I'm sorry I didn't see your posts earlier. Unity's fourms stopped notifying me.. as they occasionally seem to do. Anyway, I'm here now.

    I ran into the issue that you're trying to solve here. I was thinking of solving it by having the waiting method go through the list of coroutines and swap it's index with the last coroutine in the list of coroutines waiting for it (unless it's index is already later). Coroutines can swap positions in the arrays so long as the dictonaries are kept up to date.

    I'm sorry, but I can't insert new coroutines into the vacant slots left by the old ones. The problem is that some people's code relies on coroutines always executing in the order that they were created in. Some people's code relies on this always being the case.

    For instance, if you create a coroutine that moves a button to some place on the screen and then get a little sloppy and define a second coroutine that moves that same button the second coroutine will always overwrite the first because it will execute every frame after the first. This isn't really an ideal situation but it's better than the one where coroutines are being inserted into the gaps because then sometimes the first coroutine would be the one executed after the second. It's far better if this situation is handled consistently.
     
  28. sathya

    sathya

    Joined:
    Jul 30, 2012
    Posts:
    297
    @Trinary
    Please add support for new Unity 2018.3. WWW is removed in this version
     
  29. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Ok, I'll do that in the next update. Until then it's safe to just delete that one function that allows you to Timing.WaitUntilDone(WWWObject)
     
  30. Michael-Ryan

    Michael-Ryan

    Joined:
    Apr 10, 2009
    Posts:
    184
    @Trinary

    I added the following methods to Timing.cs to allow an action to be invoke the next frame. The behavior is similar to Timing.CallDelayed(float, Action), but uses WaitOneFrame instead of WaitForSeconds() for the delay.

    We're currently on MEC Pro 3.01.5, and I'm not sure if something like this has already been added to the API, but I find it useful when I really just want to invoke something the next frame as opposed to some duration later.

    Anyhow, if you're interested in adding something like this to the MEC project, feel free to use:

    Code (CSharp):
    1. /// <summary>
    2. /// Calls the specified action after one frame.
    3. /// </summary>
    4. /// <param name="action">The action to call.</param>
    5. /// <returns>The handle to the coroutine that is started by this function.</returns>
    6. public static CoroutineHandle CallNextFrame(System.Action action)
    7. {
    8.     return action == null ? new CoroutineHandle() : RunCoroutine(Instance._DelayedCall(action, null));
    9. }
    10.  
    11. /// <summary>
    12. /// Calls the specified action after one frame.
    13. /// </summary>
    14. /// <param name="action">The action to call.</param>
    15. /// <param name="gameObject">A GameObject that will be tagged onto the
    16. /// coroutine and checked to make sure it hasn't been destroyed before
    17. /// calling the action.</param>
    18. /// <returns>The handle to the coroutine that is started by this function.</returns>
    19. public static CoroutineHandle CallNextFrame(System.Action action, GameObject gameObject)
    20. {
    21.     return action == null
    22.        ? new CoroutineHandle()
    23.        : RunCoroutine(Instance._DelayedCall(action, gameObject), gameObject);
    24. }
    25.  
    26. private IEnumerator<float> _DelayedCall(System.Action action, GameObject cancelWith)
    27. {
    28.     yield return WaitForOneFrame;
    29.  
    30.     if (ReferenceEquals(cancelWith, null) || cancelWith != null)
    31. action();
    32. }
     
  31. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Thank you Michael. CallDelayed waits for one frame when you pass in a delay time of 0. It looks like that isn't documented anywhere, so I'll add that to the context help in the next update.
     
  32. Suzuka91

    Suzuka91

    Joined:
    May 7, 2014
    Posts:
    39
    Hi

    If I'm not wrong, in older MEC (free) versions I could check if the coroutinehandle (wich previously was an IEnumerator?) was null, in order to to avoid calling again a coroutine that was runing.

    This does not work
    Code (CSharp):
    1. if (aCoroutineHandle== null)
    2. {
    3. //do things
    Is not possible anymore to check if handle is null?
     
  33. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    That is right. Only reference types can be null, and I changed the CoroutineHandle to a value type because the value type is 4 bytes in memory instead of 20. This is a GC alloc, so it's important to use the minimum possible.

    In order to work around that I also defined the handle.IsValid property. An uninitialized handle will return false for IsValid.
     
    Suzuka91, manpower13 and Alverik like this.
  34. ratking

    ratking

    Joined:
    Feb 24, 2010
    Posts:
    350
    MEC produces three warnings with 2018.3, something about the WWW class being deprecated.
     
  35. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    The new version will be out next week and it removes those warnings.
     
    ratking likes this.
  36. ratking

    ratking

    Joined:
    Feb 24, 2010
    Posts:
    350
    Is it somehow possible to change CallDelayed()'s timing segment? I'd like to change between scaled and unscaled time.
     
  37. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Yes, you can catch the handle returned and change the segment that way. Or you can crunch it into one line.

    Code (CSharp):
    1. Timing.CallDelayed(2f, delegate { Timing.RunCoroutine(_RunFor5Seconds(handle)); }).Segment = Segment.RealtimeUpdate;
     
    sarynth and ratking like this.
  38. NotEvenTrying

    NotEvenTrying

    Joined:
    May 17, 2017
    Posts:
    43
    Hi, I just wanted to ask if there are any issues with using a static IEnumerator method from a static class; I need to use a coroutine from within a static class, but wasn't sure if MEC supports this.
     
  39. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    There are no issues with that. Static functions, static classes, non-monobehaviors.. all of those are fine. It's one of the reasons that people use MEC.
     
    NotEvenTrying likes this.
  40. bugfinders

    bugfinders

    Joined:
    Jul 5, 2018
    Posts:
    1,720
    I could really use an example for the WaitUntilTrue/False examples where for an example, Im waiting on a number of items to turn once a class variable of counter reaches a number, i want the timer to move on..

    as clearly

    bool checkcounter(int value)
    {
    return counter==value;
    }

    ....

    yield return Timing.WaitUntilTrue(checkcounter); // (or even checkcounter(g))

    doesnt work, checkcounter says cannot convert from method group to func<bool>, and checkcounter(g) says cannot convert from bool to system.func<bool> ..

    the doc half has an example it it shows waituntiltrue being called, but not clearly for sure what its calling.. and its late so I maybe also being dumb.
     
  41. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    WaitUntilTrue has to take a function that has no parameters and returns a bool. The framework wouldn't know what to pass in for value on that one. If you made value into a class variable so that you didn't have to pass it in then it would work.
     
  42. bugfinders

    bugfinders

    Joined:
    Jul 5, 2018
    Posts:
    1,720
    Thanks Trinary, thats frustrating, but OK, I can work with that
     
  43. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    If you want to use a function that receives parameters then you can wrap that function in a delegate (or a lambda expression):
    Code (CSharp):
    1. yield return Timing.WaitUntilTrue(delegate{ return checkcounter(5); });
     
    bugfinders likes this.
  44. DwinTeimlon

    DwinTeimlon

    Joined:
    Feb 25, 2016
    Posts:
    300
    I am using Timing.CallDelayed which does return a CoroutineHandle, but it does not behave the way I would expect as Timing.KillCoroutine() does not kill it properly, as it still invokes the delegate. Any ideas what I am doing wrong?
     
    Last edited: Mar 7, 2019
  45. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I can't tell from the description. Perhaps you could send me a message with some of the details and/or put the relevant code into pastebin.
     
  46. justtime

    justtime

    Joined:
    Oct 6, 2013
    Posts:
    424
    Hi there! How to show progress when using Timing.WaitUntilDone ?
     
  47. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I'm assuming you are waiting for a UnityWebRequest object. If you want to show progress you should use a while loop rather than a WaitUntilDone.

    Code (CSharp):
    1. while(webRequest.downloadProgress < 1.0f)
    2. {
    3.     // update your UI element with the progress value here.
    4.     yield return Timing.WaitForOneFrame;
    5. }
    If you want to be as efficient as possible and you are updating a text box you would run that block inside the SlowUpdate segment.
     
  48. greengremline

    greengremline

    Joined:
    Sep 16, 2015
    Posts:
    183
    I'm getting a lot of errors on Unity 2019.2 and it doesn't seem like things are cleaning up correctly

    For instance, apparently the TimingController game object isn't getting cleaned up which causes missing references when I stop playing in editor and then start playing again

    This is using the latest version of MEC Pro

    Code (CSharp):
    1. Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
    2. The following scene GameObjects were found:
    3. Timing Controller
    4.  
    5. 0x00007FF60EE81BCC (Unity) StackWalker::GetCurrentCallstack
    6. 0x00007FF60EE850A1 (Unity) StackWalker::ShowCallstack
    7. 0x00007FF60D7250C5 (Unity) GetStacktrace
    8. 0x00007FF60FA2DEED (Unity) DebugStringToFile
    9. 0x00007FF60E8C8D54 (Unity) ValidateNoSceneObjectsAreLoaded
    10. 0x00007FF60D3F9671 (Unity) EditorSceneManager::RestoreSceneBackups
    11. 0x00007FF60CE8743E (Unity) PlayerLoopController::ExitPlayMode
    12. 0x00007FF60CE9A277 (Unity) PlayerLoopController::SetIsPlaying
    13. 0x00007FF60CE9D137 (Unity) Application::TickTimer
    14. 0x00007FF60D72C5E0 (Unity) MainMessageLoop
    15. 0x00007FF60D72EFFF (Unity) WinMain
    16. 0x00007FF6103C91B2 (Unity) __scrt_common_main_seh
    17. 0x00007FFF91737974 (KERNEL32) BaseThreadInitThunk
    18. 0x00007FFF922FA271 (ntdll) RtlUserThreadStart
     
  49. stevenatunity

    stevenatunity

    Joined:
    Apr 17, 2015
    Posts:
    114
    @zornor90 if it's a game breaker then I've known @Trinary to be super quick in answering messages if Unity doesn't show an unread forum post ;)

    (would like to know the answer as well as I've moved to 2019.2)
     
  50. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I've just run the latest version in 2019.2.1f1 and I didn't see any errors. Can you give me more information about the problem so I can figure out how to reproduce it?