Master time with PyGame
A novice games programmer learns he can move a sprite by adding a constant to its position every frame. For a while he is happy and mocks the master games programmer for having wasted many years learning. But soon the novice realises his sprites are unpredictable and move at different rates on different machines. The novice is despondent and visits the master for advice.
"Master. Why do my sprites not obey my commands? They jump when I ask them to slide."
"Frame rate is like the wolf, it can not be tamed", replies the master.
The novice returns to his code and a revelation comes to him. "I must move my sprites with the frame rate!" The novice changes his sprite code to move a distance relative to the time since the previous frame, and his sprites become at one with the frame rate. "I must truly be a master", thinks the novice.
For a while the novice is happy, and produces many simple games. But as the novice's logic gets more complex he is faced with a problem. "My game updates are tied to the frame rate!", thinks the novice. He realises that his game will run differently on other machines and yearns for the simpler days when he thought frame rate was constant. Despondent, he visits the master for advice.
"Master. How can I make my game run predictably on all machines?"
"Time is not rigid as a tree. Time is supple, as a reed", replies the master.
The novice goes back to his code and ponders the master's advice. A revelation comes to him, "Game logic can run in it's own time!" With that thought in mind, he separates his game logic from the render code and soon his game logic runs independently of the frame rate.
The novice is humbled and thanks the master for his help.
"Truly, you are a master now", replies the master.
To become a master you can either meditate in a cave in Tibet or use the GameClock
class in the Game Objects library. GameClock
manages game time, which marches on at a perfectly constant frame rate, no matter how many frames you can actually render. The main advantage of this approach is that the game will run the same on a fast computer, as it does a slow computer, which is extremely useful for recording demos or adding network play. It also makes the math for moving sprites (2D or 3D) a little simpler, because you can just add constants each frame.
Here's a snippet from the example project that shows how to separate the render loop in to an update phase and a render phase. The call to game_clock.update
is an iterator that returns the frame count and time for each game logic update. It will return as many updates as necessary for game time to 'catch up' with the real time.
# Update phase for frame_count, game_time in game_clock.update(): for ball in balls: ball.update() # Render phase for ball in balls: ball.render(screen)
The update
function calculates the position of the sprite at the current update, and the position it will be at the next update. This is because the render phase will likely occur at some point between game logic updates, and the render position should be adjusted proportionally. The method get_between_time
returns a value between 0 and 1 that you can use to interpolate value between two game logic updates.
For example, if the sprite contains its current position (self.position
) and its position at the next update (self.next_position
), then you could find the render position with the following code (lerp
is a function in gameobjects.util
).
x, y = lerp( self.position, self.next_position, game_clock.get_between_frame() )
So what is a good rate for the game logic update? In my experience 20 logic updates per second is about right for most games. You can increase this rate if you want finer control, but it is probably not a good idea to go much lower as it will reduce the responsiveness of the game.
An additional advantage of GameClock is that you can control the speed of time with little effort. If you call game_clock.set_speed(0.5)
, then half as many game logic updates will be issued and the game will run at half speed. You can also make the game run faster by setting the speed to a value greater than 1.0. Alas, you can't set the speed to a negative value, as this would violate the laws of the cosmos.
You can experiment with the game logic update rate and time speed with the GameClock
sample code. You will need PyGame of course, and Game Objects 0.0.2. Game Objects can be easy installed with the command: easy_install gameojects
.
Here's a screenshot of the sample code running. I really must try to write samples that don't feature shiny spheres!
phaero@jsbig ~/tmp/gametime $ python gametimesample.py
Traceback (most recent call last):
File "gametimesample.py", line 180, in ?
run()
File "gametimesample.py", line 148, in run
for frame_count, game_time in game_clock.update():
File "build/bdist.linux-i686/egg/gameobjects/gametime.py", line 142, in update
ZeroDivisionError: float division
Great class, and fun article. I like the implementation with generators.
Awesome, elegant solution to one of the most common game programming problems, with the added bonus of being able to go all max payne style :)
I get this error :
for frame_count, game_time in game_clock.update():
File "/usr/lib/python2.5/site-packages/gameobjects/gametime.py", line 142, in update
self.fps = 1.0 / self.real_time_passed
ZeroDivisionError: float division
I add a check before this division and it works :
if self.real_time_passed != 0:
self.fps = 1.0 / self.real_time_passed
else:
self.fps = 1.0
Thanks for the bug report. Strange that that can occur. It must be down to the granularity of time.clock.
Fix in SVN.
I noticed that when you create alot of balls at like speed 200% and let them move around at the bottom for a while, the frame rate keeps linearly decreasing, until it becomes intolerably slow.
It seems that once the balls settle the frame rate should increase... hm..
More bouncing means that the collision detection code is firing more often. Although I suspect that the slow down is just due to the number of sprites being rendered.
Um... no. What I mean is this:
The framerate is fine with I spawn alot of balls. Eventually all the balls don't bounce as high and come down.
When all the balls are done bouncing (i.e. they are at the very bottom - just hopping a little and going one wall to another) the frame rate keeps dropping (and does not stay constant).
Could it be because the bounces are begging shorter? Doesn't seem so.
Strange, I don't see that here. The frame rate is always consistent with the number of sprites on screen.
What platform are you running it on? And are you using the latest code, with the fix that Batiste suggested?
Hmm. I'm getting weird performance on python 2.5.1/pygame 1.7.1.
If I just add one ball, it's laggy and looks like 20FPS (but the indicator says 800fps)
if I go crazy and add 100 objects, the displayed FPS goes to 50 but it gets very smooth.
I don't see that behavior (same Python and Pygame). It actually sounds like a v-sync issue. What platform are you running it on?
Try changing it to full-screen...
Fullscreen didn't help. I'm running on Ubuntu 7.04
Switching to double buffered (and making pygame.display.update => pygame.display.flip) didn't help either
oh, and plugging in a frame count (not frame rate count) indicator shows that the screen is actually updating at full speed, just the ball isn't moving smoothly.
hey,
I am using a Mac and I was wondering how to get GameObjects to work. I downloaded the .0.0.3 version in the .zip file (cause .exe doesn't work of course...)
I tried running the setup.py file and got :
Traceback (most recent call last):
File "/Users/ryansweeney/Sites/gameobjects-0.0.3/setup.py", line 31, in -toplevel-
classifiers = classifiers.splitlines(),
File "/Library/Frameworks/Python.framework/Versions/2.4//lib/python2.4/distutils/core.py", line 137, in setup
raise SystemExit, gen_usage(dist.script_name) + "\nerror: %s" % msg
SystemExit: usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: setup.py --help [cmd1 cmd2 ...]
or: setup.py --help-commands
or: setup.py cmd --help
error: no commands supplied
You should be able to install it with "easy_install gameobjects". You can get easy_install from (http://peak.telecommunity.com/DevCenter/EasyInstall). I'm assuming it works on Mac -- I'd be shocked if it didn't!
Alternatively, since you have downloaded the zip file, you can install it with the following command:
python setup.py install
Hope that helps!
Will
Alright... I have easy_install and I have easy_install-2.4 here's what happened when I tried what you said:
-bash: /Library/Frameworks/Python.framework/Versions/Current/bin/easy_install: "/Applications/MacPython: bad interpreter: No such file or directory
it always comes up with that last line, even if I state which directory "gameobjects" is in.
I tried that other method "python setup.py install" and that tells me a bunch of other stuff that doesn't produce the preferred result I even put directories in front of setup.py and it does more but not what I think should happen. Sorry most of this is vague. It usually comes down to "no such file or directory"
any ideas?
I'm afraid I'm not sure what is going on there. Could be some kind of Mac Python configuration error. The comp.lang.python newsgroup should be able to help you...
Will, thanks for this article and your latest gameobjects library. I revised a game of mine to use your GameClock class. Now it works properly on slower machines, and it also has a working pause feature. I was stumped on how to implement this before studying this article and your code.
I recommend to other readers that they consider how to implement game time logic before they start their own designs. If you have to retrofit your own game to implement virtual time correctly using Will's techniques, you will have to rework a lot of the internals to get your game to flow correctly again. However, it's worth it for the resulting polish and consistency in your game across all classes of machines.
An updated information.
To install Game Objects in MAC OS X Leopard, I downloaded the .zip file, unpacked it, and in the Console - reached the unpacked Game Object folder - , i've typed "python setup.py install", just like Will says... and it works!
I'm using an iMac Intel based, with Leopard, and Python2.5.
that's it!
Ah... Will, your book is awesome! Thanks.
What if I wanted to limit the frames as well? Is it really necessarry to render more than 60 or 120 fps?