3
\$\begingroup\$

Normally the game speed with pygame is locked with the fps, so I attempted making a simple two classes that'd be super easy to use, to allow for the same game speed no matter what the fps is.

For each frame, it calculates the difference between the amount of ticks that have passed, and how many should have passed (I get that this way could be better, but people don't change this mid game right?). For example, the difference at 60fps at 60 ticks per second is 1, at 30fps, the difference becomes 2, at 120fps, one frame will have a difference of 0, and the next 1. I did my own version of ticks since I locked the actual ticks to the fps.

With the frames per second, I set it to calculate it every x seconds, so it's a bit more accurate and avoids times so small it causes zero division errors (0.1 seconds for 1753 frames is more accurate that 0.00005704506 seconds for 1 frame). If you don't limit the fps, the inbuilt fps function just returns 0.

There does seem to be a bit of a problem though, and I'm not sure what's causing it. I'm sure I've done the framerate calculation correctly (with a minimum time so it doesn't end up working with tiny floats), but setting the fps to 500 reads as 333, and 1000 reads as 500, which is really strange. Setting it to something really high or quite low returns something that looks a lot more correct.

I've not really used a with with a class containing another class before, so I'm not sure if I've done it right (I originally only had the one class, but realised __enter__ and __exit__ needed the class called each time), so any feedback would be good as to what I didn't do well.

from __future__ import division
import pygame
import time
import random

class GameTime(object):
    def __init__(self, desired_fps, desired_ticks, clock):
        self.start_time = time.time()
        self.desired_fps = desired_fps
        self.desired_ticks = desired_ticks
        self.clock = clock

        self.ticks = 0

        self.framerate_counter = 1
        self.framerate_time = 0

    def get_ticks(self, current_time):
        """Ensure the correct number of ticks have passed since the 
        start of the game.
        This doesn't use the inbuilt pygame ticks.
        """
        time_elapsed = current_time - self.start_time

        total_ticks_needed = int(time_elapsed * self.desired_ticks)

        ticks_this_frame = total_ticks_needed - self.ticks
        self.ticks += ticks_this_frame

        return ticks_this_frame

    def get_fps(self, current_time, update_time=0.1):
        """Calculate the FPS from actual time, not ticks.

        It will return a number every update_time seconds, and will
        return None any other time.
        """
        frame_time = current_time - self.framerate_time

        if frame_time < update_time:
            self.framerate_counter += 1

        else:
            self.framerate_time = current_time
            fps = self.framerate_counter / frame_time
            self.framerate_counter = 1
            return int(fps)

    def limit_fps(self, alternate_fps=None):

        wanted_fps = alternate_fps or self.desired_fps
        if wanted_fps:
            self.clock.tick(wanted_fps)

class GameTimeLoop(object):
    """This gets called every loop but uses GameTime."""

    def __init__(self, GTObject):

        self.GTObject = GTObject
        GTObject.loop_start = time.time()

        #Run the code once now so the result can be called multiple times
        self.ticks = GTObject.get_ticks(GTObject.loop_start)
        self.fps = GTObject.get_fps(GTObject.loop_start)

        self.temp_fps = None

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.GTObject.limit_fps(self.temp_fps)
        self.temp_fps = None

    def set_fps(self, fps):
        self.GTObject.desired_fps = fps

    def update_fps(self, fps):
        self.temp_fps = fps

An example pygame function using it:

def game():

    pygame.init()
    screen = pygame.display.set_mode((640, 480))

    clock = pygame.time.Clock()
    max_fps = None
    ticks_per_second = 60
    FrameRate = GameTime(max_fps, ticks_per_second, clock)

    move_speed = 0.175
    move_total = 0

    while True:
        with GameTimeLoop(FrameRate) as frame_time:

            #Display the current fps
            if frame_time.fps is not None:
                pygame.display.set_caption(str(frame_time.fps))

            #Calculations to be done once per tick
            #Can multiply by the number of ticks or do a loop
            ticks_this_frame = frame_time.ticks
            for tick in xrange(frame_time.ticks):
                pass


            #Do normal stuff here
            move_total += move_speed * ticks_this_frame
            print move_total  #Just print this to show it's correctly staying at the same speed

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

                #Set a new fps
                if event.type == pygame.MOUSEBUTTONDOWN:
                    new_fps = random.choice((None, 1, 5, 10, 30, 60, 120, 1000, 10000))
                    frame_time.set_fps(new_fps)
                    print 'set fps to: {}'.format(new_fps)

                #Temporarily set a new fps while the mouse moves
                if event.type == pygame.MOUSEMOTION:
                    frame_time.update_fps(30)
\$\endgroup\$
2
\$\begingroup\$

It'd be easy to add default values to desired_fps and desired_ticks rather than requiring they be manually supplied.

get_ticks is a bit of a misleading name. It implies a getter, but actually you're modifying an attribute as well as returning a value. I'd personally call it calculate_ticks so that it's less surprising to see both results happening. You should definitely write an explanation of what it does in the docstring though. You explained the function but didn't actually tell the user what it gives them or what effect it has.

def calculate_ticks(self, current_time):
    """Returns ticks from this frame and updates self.ticks

    Ensure the correct number of ticks have passed since the 
    start of the game.
    This doesn't use the inbuilt pygame ticks.
    """

And I'd do similar with your FPS calculation.

\$\endgroup\$
  • \$\begingroup\$ Thanks, that makes sense, I was a bit stuck on what to call the functions but calculate sounds good, I renamed it through like .fps, .get_fps, .current_fps etc, but nothing seemed right haha. I also totally forgot to update the docstrings, so will get that done. \$\endgroup\$ – Peter Nov 30 '15 at 18:04
  • \$\begingroup\$ Yeah I generally use get for basic returns, update for just adjusting values and returning nothing and then calculate when it does both. \$\endgroup\$ – SuperBiasedMan Nov 30 '15 at 18:04
  • \$\begingroup\$ Ah thanks, I'll keep that in mind. The other one I wasn't sure on was set_fps and update_fps. set_fps actually sets a new fps, and update_fps sets a new fps for only one frame (with the intention of using it every frame it's needed). What would you use in that case? \$\endgroup\$ – Peter Nov 30 '15 at 18:07
  • \$\begingroup\$ @Peter That is tougher. Maybe something like refresh_fps perhaps? To indicate how it's a temporary value that will be set over and over. It would be worth adding a docstring to clarify it better too. \$\endgroup\$ – SuperBiasedMan Nov 30 '15 at 22:50

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.