3
\$\begingroup\$

I have a script which has a Queue datastructure (code below). This queue holds more than 100 Actions. So currently my script does a yield return null after every Dequeue, which means that one Dequeue() call is executed every frame.

private static readonly Queue<Action> DataQueue = new Queue<Action>();
private bool _isHandlingQueue; // Helper bool to check if Coroutine is active

private IEnumerator HandleQueue()
{
    _isHandlingQueue = true;

    while (DataQueue.Count > 0)
    {
        DataQueue.Dequeue().Invoke();
        yield return null;
    }

    _isHandlingQueue = false;

    yield return null;
}

I'm trying to achieve that multiple Dequeue and Invoke actions take place in the same frame. One Dequeue and Invoke takes between x and y ms, this varies depending on the Action itself.

Example: Lets say the Queue has 100 entries and the game runs at 60 frames per second (minimum). One iteration (Dequeue + Invoke) in the while loop takes about 5 ms in this example. Here is the thing: In this case it is essentially waiting for 11.67 ms for the next frame (16.67 ms each frame at 60fps). I don't want the system to wait, but use the remaining 11.67 ms to Dequeue and Invoke 2 more Actions, so it only waits for 1.67ms each frame. In this ideal case the Queue get's cleared in 34 frames instead of 100 frames.

All help to point me in the right direction is appreciated!

\$\endgroup\$
3
\$\begingroup\$

The obvious solution would of course be to not yield after every queue entry but only after every n entries, where n is a number you need to figure out through experimentation.

But that will of course lead to irregularities if some actions take much longer to invoke than others. An ideal solution would not process n queue entries but rather process queue entries for n milliseconds.

Most functionality of the Unity Time class won't help you here, because it represents the state at the beginning of the current frame. However, there is Time.realTimeSinceStartup which is a thin wrapper around the system clock. It should keep progressing during the frame.

private static readonly Queue<Action> DataQueue = new Queue<Action>();
private bool _isHandlingQueue; // Helper bool to check if Coroutine is active

private static readonly float QUEUE_PROCESSING_TIME = 0.01167f;

private IEnumerator HandleQueue()
{
    _isHandlingQueue = true;

    float frameStartTime = Time.realTimeSinceStartup;

    while (DataQueue.Count > 0) 
    {
        DataQueue.Dequeue().Invoke();

        if (frameStartTime + QUEUE_PROCESSING_TIME < Time.realTimeSinceStartup) 
        {            
            yield return null;
            frameStartTime = Time.realTimeSinceStartup;
        }
    }

    _isHandlingQueue = false;

    yield return null;
}

Note that this will process the queue for at least 0.01167 seconds per frame (or until it is empty). Processing it for exactly 0.01167 seconds would be rather difficult because you can't predict how long each task will take.

Also keep in mind that not all platforms will provide you with the same clock accuracy. So if you want to support multiple platforms, then this is one thing you need to keep an eye on during cross-platform QA.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the info! Very helpful to know about the Time wrapper. I indeed already tried to keep track of the time, but now I see why that didn't work. I will implement your solution with realTimeSinceStartup tomorrow and will report back :) \$\endgroup\$ – Patrick2607 Mar 26 '18 at 14:48
  • \$\begingroup\$ You could use StopWatch too, you start an instance when the coroutine starts and check against it with the ElapsedMilliseconds property. \$\endgroup\$ – Galandil Mar 27 '18 at 9:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.