Search Unity

Coroutine WaitForSecond is fps dependant

Discussion in 'Scripting' started by imtrobin, Aug 27, 2011.

Thread Status:
Not open for further replies.
  1. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    This causes quite a lot of problems in low fps, as things are fired in wrong order. It should be fps independent, or is there something like WaitForSecondFixedUpdate.
     
  2. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,755
    Why don't you use something like invokerepeating?
     
  3. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    Does it make a difference? It is still dependant on fps.
     
  4. Jasper-Flick

    Jasper-Flick

    Joined:
    Jan 17, 2011
    Posts:
    959
    The granularity of WaitForSeconds in limited by the framerate, as they're checked once per frame loop. If you require very precise timing then you're stuck depending on FixedUpdate in some way. But if you're running into very low FPS you might want to fix that first.
     
  5. amit1532

    amit1532

    Joined:
    Jul 19, 2010
    Posts:
    305
    you can lock the frame rate to 75.
    thats what i do to fix it :\
     
  6. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    I'm developing a big scene, so I cannot guarantee high fps all the time. It can have spikes at moments which brings down fps. WaitForSecond that depend real time and not game time is not reliable. so to have correct and predictable behavior, I have to use my own timer.
    How do u lock the frame rate?
     
  7. jcarpay

    jcarpay

    Joined:
    Aug 15, 2008
    Posts:
    561
  8. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    Thanks, but I don't think locking the fps is the "correct" solution
     
  9. Rafes

    Rafes

    Joined:
    Jun 2, 2011
    Posts:
    764
    Aren't these two conflicting?

    Real-time means that even if the FPS spikes (up or down) there is no change to the "timer" if your game runs slow or fast, 3 real-time seconds is 3 real-time seconds. Real-time IS FPS independent. However, WaitForSeconds() is dependent on Time.scale, right? So it is not real-time.

    For example, we use Time.scale to pause our game, or change the speed (fast forward or slow motion). However, we don't use WaitForSeconds() anywhere in our GUI or else pausing the game would also pause the GUI (same thing for deltaTime/fixedDeltaTime respectively).

    I'm not sure what you are trying to do exactly, but there is absolutely nothing wrong with rolling your own timer in a co-routine to work with any time requirements you have. It shouldn't be more than 2 or 3 lines of code.
     
  10. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    I'm using WaitForSecond to trigger an animation, and when the fps is low, my animation did not complete before the WaitForSecond triggered again. I don't think WaitForSeconds is dependent on Timescale.

    Yes, I can roll out my own timer, then I wouldn't really need to use coroutines. The WaitForSecond time should be using game update loop timing, or else it can't be reliabily depended on to do things with physics etc.
     
  11. fholm

    fholm

    Joined:
    Aug 20, 2011
    Posts:
    2,052
    The way co-routines (and WaitForSecond) work, they are checked once per frame, which means that the max resolution (or rather, precision) you can get is the time it takes (on average) for one frame to render. Let's make an example:


    You call WaitForSecond(1), and each one of your frames take 300ms to render (this is very slow, but easy to illustrate my point with), this is what will happen:

    1. Frame N+1 renders, the WaitForSecond timer is now at 300ms.
    2. Frame N+2 renders, the WaitForSecond timer is now at 600ms.
    3. Frame N+3 renders, the WaitForSecond timer is now at 900ms.
    4. Frame N+4 renders, the WaitForSecond timer is now at 1200ms and is now activated, this is 200ms to late, but there is nothing you can do about this.

    WaitForSecond should really be called WaitForMinimumSeconds.

    There are a couple of way to solve this:

    1. Implement your own timer, it will be a bit more precise but if the frame-rate is low enough you will still over-shoot your mark with a few ms
    2. Add a check to the animation and don't start it if it's already running, use some sort of "animation queue" which you de-queque things of once the current animation is playing
     
    Khena_B and Deleted User like this.
  12. imtrobin

    imtrobin

    Joined:
    Nov 30, 2009
    Posts:
    1,548
    Thanks but the issue is not about the timer overshooting. It is about time elapsed added to WaitForSecond is the real clock time, not game loop time (Time.deltaTime)
     
  13. Dreamora

    Dreamora

    Joined:
    Apr 5, 2008
    Posts:
    26,601
    yield Waits are as some mentioned, only tested in the update - fixed update - late update loops so you can't get any more granular than that. Thats why they will also stop to fire with timescale 0

    Unsure what imtrobin wanted to express with his last posting above, it doesn't make any time to me what I'm reading there as unity does not use clock times at all.

    Time.deltaTime is no game loop time, its the time since the last round of Update, more accurately the simulated realtime since last Update (so realTime * timeScale where as realtime is dependent on the app running at all not being paused).


    If you really need to be more granular or time precise, not unity dependent, you need to use the .NET Invokes and timers not the unity ones
     
  14. deram_scholzara

    deram_scholzara

    Joined:
    Aug 26, 2005
    Posts:
    1,043
    I think you might be missing something here. Let's assume that WaitForSeconds is entirely based on "real" time. If the framerate is at, say a steady 9.2fps, then that means that WaitForSeconds(1) will finish somewhere between the 9th and 10th frames - not directly on either of them. So anything that is supposed to be run when WaitForSeconds completes, will not actually appear for the user until frame 10 anyway. So regardless of whether WaitForSeconds triggers after 1 second, or on the next frame after/during 1 second, the frame its result triggers on will be the same.

    If you really absolutely must get the exact time, then you're going to need to make sure that all your code is designed to count on the fact that it is run only after the previous frame has completed.

    The only other possibility I can think of would be to use a timer variable and repeatedly include the excess time when it triggers - something like this:
    Code (csharp):
    1. float timer = 0.0f;
    2. float cycleTime = 1.0f;
    3.  
    4. void Update () {
    5.     timer += Time.deltaTime;
    6.    
    7.     if (timer >= cycleTime) {
    8.         timer -= cycleTime;
    9.        
    10.         //Do whatever you want to happen every cycle
    11.         CycleEvent();
    12.     }
    13. }
    14.  
    15. void CycleEvent () {
    16.     //Stuff you want to do goes in here
    17. }
    This makes it so that if you were to overshoot the cycle time, it would start counting the next cycle timer from the overshot amount. So in that example, if you were to overshoot the 1 second cycle and ended up at say 1.02 seconds, then your code run run and the next cycle would begin at 0.02 seconds and count up to 1 second again.

    Obviously the CycleEvent stuff wouldn't usually happen exactly dead-on every second, but the margin of error wouldn't accumulate after many multiple cycles. You would always be at around 1, or 2, or 15,000 seconds from your starting time of 0. If you wanted, you could even keep track of the number of cycles that have passed by creating an int that does ++ in the CycleEvent.
     
    Thorlar likes this.
  15. arzi

    arzi

    Joined:
    Apr 6, 2009
    Posts:
    154
    If your actual problem is event firing in the wrong order, you should just just make sure it's not possible. It's not really related to FPS at all.

    Eg.

    Code (csharp):
    1.  
    2.  
    3. // Can fire in wrong order:
    4. void SomeFunction()
    5. {
    6.    StartCoroutine(Routine1());
    7.  
    8.    StartCoroutine(Routine2());
    9. }
    10.  
    11. IEnumerator Routine1()
    12. {
    13.    yield return WaitForSeconds(0.5f);
    14.  
    15.    DoFirstStuff();
    16. }
    17.  
    18. IEnumerator Routine2()
    19. {
    20.    yield return WaitForSeconds(1.0f);
    21.    DoSecondStuff();
    22. }
    23.  
    24. // Can't be fired in wrong order:
    25.  
    26. void DoStuff()
    27. {
    28.    StartCoroutine(Routine());
    29. }
    30.  
    31. IEnumerator Routine()
    32. {
    33.  
    34.    yield return WaitForSecond(0.5f);
    35.  
    36.    DoFirstStuff();
    37.  
    38.    yield return WaitForSeconds/0.5f);
    39.  
    40.    DoSecondStuff();
    41. }
     
  16. ABigElephant

    ABigElephant

    Joined:
    Mar 2, 2021
    Posts:
    3
    Hi. I know this is an old threat, but I had a similar problem.

    I needed to sync music and a cut scene that worked with a script, not with the time liner.

    My problem was that the music and the animation worked good in the editor but in the build the animation was too slow.

    I used coroutines to do that. In the editor it ran faster than in the build with "Waitforseconds". I tried with "yield return null" and custom counters but it didn't worked (obviously).

    I finally brought it to work with WaitForFixedUpdate() and adjusting the value of my loop factor till animation and music were synced:

    Code (CSharp):
    1.     IEnumerator UIGetsSmaller(RectTransform canvasText)
    2.     {  
    3.      
    4.         float small = 1;
    5.         while(small > 0)
    6.         {
    7.           yield return new WaitForFixedUpdate();
    8.  
    9.           canvasText.localScale = new Vector3(small, small, small);
    10.           small -= 0.01f;
    11.         }
    12.  
    13.     }
     
  17. Lo-renzo

    Lo-renzo

    Joined:
    Apr 8, 2018
    Posts:
    1,511
    Unfortunately WaitForFixedUpdate is not very efficient because it allocates every frame.

    If you're in 2023.2+, you might look at the Awaitables API, which allows you to rewrite it like this:
    Code (CSharp):
    1. private async Awaitable UIGetsSmaller(RectTransform canvasText)
    2. {
    3.       float small = 1;
    4.       while(small > 0)
    5.       {
    6.           await Awaitable.FixedUpdateAsync();
    7.  
    8.           canvasText.localScale = new Vector3(small, small, small);
    9.           small -= 0.01f;
    10.       }
    11. }

    There are also other asset-based solutions out there that let you run the coroutine in a specific loop, like FixedUpdate, or using unscaled time.
     
  18. AcidArrow

    AcidArrow

    Joined:
    May 20, 2010
    Posts:
    11,755
Thread Status:
Not open for further replies.