8
\$\begingroup\$

I am having trouble setting up the frame rate in my first game. I know that I can set it up with:

clock = pygame.time.Clock()
clock.tick(60)

but this is not producing very desirable results. If I move a character 1 pixel per frame, it will move smoothly but jitter about once a second. If I print(clock.get_fps()) it tells me that my frame rate is about 59.8. Since it never hits 60, it never syncs up with my monitor's refresh rate and the annoying jittering continues.

That I know of, this is not a problem with the game's performance, since it is in a very early stage of development. If I cap the FPS at 1000 then I can get a maximum FPS of about 270.7. Is there any way I can give my game a continuous frame rate of 60?

P.S. I don't mind that my movement code is tied to FPS. I tried making it FPS-independent using this code, but this introduced some floating-point inaccuracies to my movement. It wouldn't let me set the FPS to exactly 60, either.

\$\endgroup\$

2 Answers 2

12
\$\begingroup\$

If you read the pygame.time.Clock.tick() and pygame.time.Clock.get_fps() documents, you'll find that:

  • tick() works with milliseconds - it returns the number of milliseconds since the last call
  • get_fps() returns an average of the last 10 calls to tick(). If you do the math, that's pretty damn good that you're getting 59.8 most of the time, since with 60 FPS you'll be ticking 16 or 17ms per frame, and 59.88 is the closest number you can get ((16*3 + 17*7) / 10 = 59.88)

That explains why you're not seeing 60FPS from get_fps(). The jitter is a different matter though; it has to do with how tick() is implemented.

It uses SDL_Delay, which in turn uses various platform-specific sleep functions, which depending on the OS can be quite inaccurate at the millisecond level. What's probably going on is, a popular OS's sleep function has a granularity of 15ms, whereas your target FPS of 60 requires 16.66...ms sleeps. One of your frames is going to stick around for 30ms, maybe every second or so.

Basically if you want smooth frames, you'll need to avoid methods that exclusively use SDL_Delay. You'll want to read Fix Your Timestep, but here are your options:

  • Use tick_busy_loop(), which doesn't sleep but makes your CPU spin until enough time has passed. The downside is increased power usage, a big no-no for battery-powered devices.
  • Track the number of milliseconds you need to delay yourself, and use a combination of pygame.time.wait() and pygame.time.delay() to precisely control timing, without doing a busy-wait all the time.
  • Divorce game time from draw time, and draw interpolated states based on delta time. What that means is, instead of drawing things at say x + speed, you draw them at x + speed * delta, where delta is the amount of time actually taken to draw that frame. This masks a lot of jittering problems.
  • This option is probably out for you, but there are more precise sleeps available that are platform-specific. Sadly they are not available to SDL/pygame.
\$\endgroup\$
3
  • \$\begingroup\$ The problem with these approaches are that they are tied to milliseconds. In order to achieve a framerate of exactly 60, you would need to wait 16.666 milliseconds. My program needs to sleep and then blit everything to the screen whenever the monitor refreshes. \$\endgroup\$ Jun 26, 2015 at 19:20
  • \$\begingroup\$ @DragonDePlatino in that case you are looking for "Vsync". \$\endgroup\$ Jun 27, 2015 at 1:28
  • \$\begingroup\$ @congusbongus hey, do you have any examples of a complicated PyGame project running at 60fps? \$\endgroup\$ May 1, 2019 at 3:01
3
\$\begingroup\$

clock.tick() only sets the loops at intervals of milliseconds, so at 60 fps, it will make each game loop at 16 milliseconds, not 16.6666 (which is what you need) that causes the jittering.

The below code will ensure that your game runs at the frame cap exactly since time.perf_counter() gives very precise timing. How it works is that if the time passed from the last frame (unprocessed) is over or equal to the the frame cap, then the frame will render. Also note that if the frame renders, the frame cap is subtracted from the unprocessed time, making the frames more stable since the errors don't accumulate.

import pygame
import time

frame_cap = 1.0/60
time_1 = time.perf_counter()
unprocessed = 0

while True:
    can_render = False
    time_2 = time.perf_counter()
    passed = time_2 - time_1
    unprocessed += passed
    time_1 = time_2

    while(unprocessed >= frame_cap):
        unprocessed -= frame_cap
        can_render = True

    if can_render:
        # put everything inside here

        pygame.display.update()
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .