Search Unity

Frame rate user input issues >60hz monitors

Discussion in 'Scripting' started by WhyCantIUseMyLogin, May 1, 2020.

  1. WhyCantIUseMyLogin

    WhyCantIUseMyLogin

    Joined:
    Oct 1, 2013
    Posts:
    14
    Hi!

    I sent my first build (of my early stage 2D sidescroller but with a perpspective camera game) to a friend to test, and he noticed something really strange. He has a 144hz monitor and a 60hz monitor running on the same machine.
    He was able to do a particular maneouver regularly on the 144 but never on the 60.

    All my physics code is in FixedUpdate() as it should be, and all objects move at the same speed regardless of frame rate (this can be observed both with VSync enabled, and with VSync disabled and the TargetFrameRate set to various values) so I don't think it is that causing the problem.

    My user input code is all in the Update() function.
    My suspicion is that, because the Update() is being called either 60 times a second or at 144 times a second (because VSync is turned on), then the input code being run more frequently is what causes the problem.
    Turning off VSync and setting the TargetFrameRate to over 100 or so does remove the difference between the two monitors, but turning off VSync is not something I wish to do (and I don't see professional games requiring that).

    If it were just a more responsive feel that was the difference, then that is to be expected... unfortunately he can (using the 144hz monitor) get to areas of the level that he can't on the other... so that is something I definitely can't allow.

    Of course I first tried using FixedUpdate() for input code, but my investigations on these forums and other places have made me realise that while FixedUpdate() is called x times per second regardless of frame rate, those iterations are not necessarily called in an evenly spaced pattern, and therefore is unsuitable for user input.

    I have looked at all sorts of solutions to try to make this go away; I assume I need a framerate independant method that is called x times per second, but I can't find a reliable way to do this that works consistantly across different machines. I have tried simple InvokeRepeating, coroutines, through to looking at the new PlayerLoop system.

    I know about a million sidescrollers have been made using Unity; how has everybody else who has released these games done it?

    Thank you for reading! :)

    --Mike--
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    The general strategy is to capture user input in Update(), store what you need in member variables, then use that captured input data in FixedUpdate() where relevant.
     
    Joe-Censored likes this.
  3. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Can you show your current input and character moving code?
     
  4. WhyCantIUseMyLogin

    WhyCantIUseMyLogin

    Joined:
    Oct 1, 2013
    Posts:
    14
    PraetorBlue: That exactly what I am doing. The problem is, I think, that there are extremely subtle differences caused by the frequency of the Update().
    Joe-Censored: Due to so many revisions trying to sort this, it's now in an awful mess. I will try to get it back into some sort of order and post it later.

    If you were to look at the game running, you would be adamant that there is no difference between the two refresh rates.

    I will have to explain the maneuver, I am afriad, for it to make any sense.
    The game does NOT allow double jumping, BUT you can jump continously if you are touching a wall beside the character.

    In my case there is a particular place where the player jumps against a wall, jumps again while moving slightly away from the wall, but on the upwards phase of the second jump brushes an outcrop. This registers as 'grounded' allowing the player to jump a third time, and over the outcrop.
    This wasn't by design, and can't be done at 60FPS. However, it CAN be done when running at higher frame rates.
    The guy testing this for me can always do it on the 144hz monitor, but has never managed it on his 60hz monitor.





    The problem with this is, how do I develop when a) I can't be sure that the level I create can't be 'broken' by players running at higher framerates or b) If I develop on a higher refresh rate monitor, how can I be sure that players with lower refresh rate monitors can perform what is required?


    The only thing I can sumise could be happening is that, given there are approximately 2 Updates for every FixedUpdate when running at 144Hz, that an input is being caught on say the second Update()
    that (on the lower FPS) has wouldn't be registerd at that precise real world moment, because Unity has already gone on the FixedUpdate().


    Like this: (A being FixedUpdate, B being Update())
    ABABAB

    whereas at 120 (I know my real world problem is with 144, but 120 is easier to conceptualise) is more like:
    ABBABBABB

    Is it not conceivable that an input is caught in the second B at 120, but when its run at a lower FPS, the system has already moved to the next FixedUpdate (A) so the input is then only acted upon in the subsequent FixedUpdate, so the player is in a slightly different place?

    To me this makes perfect sense why this could be ocurring, but I can't believe I am the only person to have this happen.

    Sorry for so many words used!

    But assuming I am correct in my deduction, this would be solved with another Update-like method which would be called at a given fixed interval.
    As described in various articles I have read, FixedUpdate() is not that, because (despite the fact it is guaranteed to run x times per second) it is not guaranteed to run those calls in an evenly-spaced manner.
    Which is presumable why so many people advise against putting input code in FixedUpdate, despite it being (logically) the correct place.

    I investigated the new PlayerInput system (because it allows the developer to add new Update()-like functions), but it doesn't (that I can see) offer the ability to specify the interval at which they are called.
    I have also tried InvokeRepeating, which does seem to offer what is required, but that (when measured) seems to not behave consistantly when called more frequently.
    (For example, InvokeRepeating("Test", 0.0333333333333333f, 0.0333333333333333f) is consistant, but NOT InvokeRepeating("Test", 0.0166666666666667f, 0.0166666666666667f);
    I have also tried Coroutines, but the time to Yield for varys based on the frame rate.
    My final hope is multi-threading; but I am sure that this must be overkill?

    Am I going mad here? Is this even a thing, or do developers not worry about it and not worry that some players can do things others can't because of their frame rates?

    Sorry for the (again) overly long post.

    Cheers,

    --Mike--
     
  5. WhyCantIUseMyLogin

    WhyCantIUseMyLogin

    Joined:
    Oct 1, 2013
    Posts:
    14
    Sorry, I meant PlayerLoop towards the bottom of my post, not PlayerInput; unfortuately when I go to edit my post, it complains that it is spam-like... I guess I DO use to many words! ;)
     
  6. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Where are you doing your "player is grounded" check? Update() or FixedUpdate()? It seems to me if that check is in Update () that could cause this problem.

    The other question is are you using GetKeyDown or MouseButtonDown or any of the "one-time" input checks? It's possible you need to deduplicate these so they don't get processed twice.
     
  7. tessiof

    tessiof

    Joined:
    Dec 6, 2017
    Posts:
    25
    If you can jump a third time in high fps, is because you register the third input before the physics engine has time to move the player, so it still registers as grounded.
    In situations like this you can use a cooldown system. Declare a bool canJump an set to true. Then set it to false as soon as you register a jump input. Then start a coroutine that will wait for, say, .067f seconds before setting canJump to true again. Only register a jump input if canJump is true.

    Code (CSharp):
    1. if (canJump && Input.getButtonDown("Jump"))
    2. {
    3.     Jump();
    4.     canJump = false;
    5.     StartCoroutine(JumpCoolDown());
    6. }
     
    Last edited: May 2, 2020
  8. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Can you elaborate which exact approach you've chosen in order to communicate your current state between Update and FixedUpdate? I know you're caching the values, but does FixedUpdate cache its values for Update to consume, or vice versa? Who's the consumer of the cached data?

    It's both possible but you could indeed encounter some issues if you do not "consume" the cached values correctly, or when you override them.
    That is especially important when you have different rates of Update and FixedUpdate, like basically always.... The examples you've given were most-likely supposed to point that out.

    Yes, that's true. FixedUpdate is a simulation. It does not run at fixed points in time, it only simulates that.

    Note that InvokeRepeating is bound to the Update cycle, as well as many of the yield instructions in coroutines. So it'll suffer from the same issues. It's just a different step in the corresponding phase of the frame.

    Also, they do not wait exactly the time you specify, that's only the duration they need to wait at the very least. If you do not take overshooting into account, the error accumulates over time.
     
  9. WhyCantIUseMyLogin

    WhyCantIUseMyLogin

    Joined:
    Oct 1, 2013
    Posts:
    14
    Wow guys, thank you all for your comments.

    I had a little time away because it was driving me mad, so apologies for the delay in responding.

    Firstly, PraetorBlue: I am doing the isGrounded check in Update. Your suggestion sounds really promising. I will try it out in a sec.

    Tessiof: That very much sounds like something I should try out, unless the suggestion above sorts it completely.

    Suddoha: Thank you for a) confirming what I believed about FixedUpdate b) also for confirming that InvokeRepeating is in fact a blind alley. And to your question: I am setting the state in Update which is then consumed in FixedUpdate().

    Cheers for your replies; I will implement this stuff, and then I will need my pet tester to try it out for me! Fingers crossed....

    --Mike--
     
  10. WhyCantIUseMyLogin

    WhyCantIUseMyLogin

    Joined:
    Oct 1, 2013
    Posts:
    14
    Hi again,

    Well, I think you nailed it, PraetorBlue. I had put the grounded check in the Update, as opposed the the FixedUpdate. I say think because (instead of stopping the unwanted behaviour), it has made it happen consistantly at all (sensible) frame rates. But thats great! It is kind of cool, and I can block it from happening where I don't want it to through other means (possibly invisible blocks or merely making the obstacle bigger than one block).

    So I haven't really proved the solution, but it seems to do the trick!

    I do intellectually worry that (due to the inconsistant way FixedUpdate is called at fixed intervals, as confirmed in Suddoha's post) there are still possibly problems, but it feels right when played, and across both refresh rates, which is all that really matters to me.

    Tessiof: I may yet include your suggestion as well, because that would allow me to (say) allow the wall jump only as a power-up or similar.

    Many, many thanks to you guys for your help!

    Cheers,

    --Mike--
     
    PraetorBlue likes this.