Fixed Frame Rates in Unity

Many people will tell you to make your game frame rate independent. While there’s certainly a lot of merit to this, a fixed frame rate is desirable for some games, most notably deterministic pixel art games.

Why a fixed frame rate?

If you want your game to be deterministic, a fixed frame rate is probably the best option. If a jump can play out over a varying amount of frames, the jump could be slightly different for different frame rates, in part because of floating point precision, but also because the frame where you reached the peak could be skipped at lower frame rates.

Another reason to have a fixed frame rate would be pixel art games. As an example, let’s assume your character walks at a pace of 4 pixels per frame at 60fps. At 75fps, you’d need to move at 3.2 frames per frame. In both cases, your character would walk 240 pixels per second.

But in a pixel-perfect game, moving something 3.2 frames is problematic. If you snap everything to the pixel grid, this would result in visibly moving 3 pixels for the first 4 frames, and then 4 pixels on the next. So the movement wouldn’t be constant or smooth.

So this is why for 2D pixel art games, I opt for a fixed frame rate of 60fps.

60 is the obvious choice — anything lower doesn’t look as good, and every screen supports 60hz, at least these days.

The issues with fixing the frame rate in Unity

Unity is in essence a 3D engine. And while it’s certainly possible to make 2D games with it, and they now offer a pixel-perfect 2D camera solution, Unity wasn’t built for 2D pixel art games, and there are some issues we’ll have to solve.

The main issue is related to vSync.

There are two fullscreen modes, Exclusive Fullscreen and Fullscreen Window. Fullscreen Window is the default, but this mode will use the current refresh rate of the operating system. So if the user has their OS set to 144hz, Unity will use that refresh rate.

So one option would be to go for Exclusive Fullscreen, so you can force the refresh rate to 60hz. But the problem is that in Windowed mode (non-fullscreen), the refresh rate can still be different from 60hz.

You also probably want to give your players the option to toggle vSync off and on.

This leads to 3 scenarios :

  • vSync is off
    In this case, you can just set Application.targetFrameRate to 60 and your game will run at close to 60 fps.
  • vSync is on, and the refresh rate is a multiple of 60 (or close enough… 59 also counts)
    In those cases, you can set QualitySettings.vSyncCount according to the refresh rate. If the refresh rate is close to 60, you set vSyncCount to 1. If the refresh rate is close to 120, you set it to 2.
  • vSync is on, and the refresh rate is undesirable.
    This is a bit of a pickle. The easiest solution would be to automatically disable vSync and just rely on targetFrameRate. This is certainly a decent solution, but it can lead to tearing. It’s up to you to decide whether you find that acceptable or not.

My solution

First, I want to stress that there may be much better solutions that I’m simply not aware of. If that’s the case, I’d definitely want to hear them!

I’d also like to point out that just disabling vSync when the refresh rate doesn’t match your game’s desired frame rate is a much simpler solution.

With that said, my solution is based on something Unity posted on their blog :

But their solution has a couple of issues.

  • It allocates a new WaitForEndOfFrame object every frame, which will lead to garbage collection. Easy fix, just reuse the WaitForEndOfFrame object. But instead, I opted to not use coroutines (see below).
  • The code works by basically scheduling frames. If we’re ahead of time, we spin the CPU for a bit until the right time (with a clever solution to not use too much CPU time). But what happens if we’re behind schedule? To try this, I used Thread.Sleep to hat the main thread for 10 seconds. After that, we’re behind schedule, and the coroutine would allow frames to run at full speed until we were caught up. This can also happen if the player leaves the game and comes back to it. So to solve this, we’ll have to reset the schedule when we’re behind too much.
  • Another issue was that on my iMac, sometimes it would lead to jittery movement for a bit. My theory is that Unity does some things between WaitForEndOfFrame and actually rendering (maybe GC? I’m not sure). To deal with this, I moved the code from the coroutine to a Camera.onPreRender callback, which I run on my camera. That way the spinlock code stabilizes the actual rendering. If you have multiple cameras, you may want to experiment with letting it run on your first or last camera that renders, I’m actually not sure which would be best (probably the last one though).

Here’s my modified version of Unity’s solution :

If you have any questions about this or would like to discuss, you can find me in my Discord server :

Thanks!

Games made by sentient beings currently residing on planet earth. https://gamesfrom.earth/