Jump to content
  • Advertisement
Sign in to follow this  

How can I speed up my Python+Pygame game?

This topic is 1192 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've created a game in Python+Pygame, but on some (older) computers it's running very slow. The code is on Github. How can I speed up the game?

Share this post

Link to post
Share on other sites

The file with the code of the game is singleplayer.py. I don't care if the menu (menu.py) is slow. The multiplayer mode isn't finished yet, so you don't have to look at the multiplayer.py and server.py. The file settings.py is just some code to read and write the settings.txt file. And the main.py is to launch the menu. When I run `python -m cProfile main.py` and play the game I get this.

Share this post

Link to post
Share on other sites

Profile seems to point to 'blit'.

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000  122.073  122.073 main.py:2(<module>)
  95466   87.969    0.001   87.969    0.001 {method 'blit' of 'pygame.Surface' objects}
    259    3.744    0.014    3.744    0.014 {method 'fill' of 'pygame.Surface' objects}

First a little about what things mean here


The long list are all the functions in your code, as well as in the pygame code.


The "ncalls" column lists how often the function is used, obviously "main.py" is used exactly once. The "percall" (5th column) is how much time is spend in each call. "cumtime" is the cumulative time for all calls together. Eg the "fill" call takes (on average) 0.014, but it's called 259 times, and needs 3.744.

"time spend" here is the time between entering the function and leaving it. For main it is 122.073, which is about 2 minutes.


While main is running all the time (as in, you call it at startup, and it finishes at the end of the game), the function itself doesn't do much, it just calls "launch", and waits until it returns.

The 122.073 thus does not say what it was actually doing those two minutes.

That information is in the 2nd and 3rd column. It works just like above, but it only measures time you spend calculating in the function itself, leaving out the time needed by functions that it calls (that time is listed at another line). So you can see, main used 0.000 time itself, which is what you expect.


Now if you scan the list, you see "blit" jumping out. It is fast (0.001), but as you call it almost 100,000 times, you spend almost 88 seconds there.

'fill' also takes some time.

A second one (but you don't gain much), is 'fill'.


The way to go thus seems to reduce the number of 'blit' calls. And this is the complicated part of speed-up, you have to figure out how to avoid calling this function.


I scanned your code, and you seem to build the entire display from scratch, for each frame.

If that is true, why do you 'fill'? The next thing you do is blit the background all over it.


A simple way to reduce the number of calls to 'blit' is to blit larger areas. You could for example make a larger background to blit, Doing a blit will take longer, but you need less calls to fill the entire background. I expect it to be faster, but you have to try. Make the change, run the profiler again, and check how much it improved.



If I remember correctly, pygame remembers the previous image if you don't blit everywhere. (try this!)

If that is true, there is another way, namely by dirtying areas. The basic idea is that most of the display is not changing, just the areas where the bad guys, balloons, and arrows are changes (since they move each time). You only need to draw those areas again, and can skip all areas that have not changed at all. Pygame (I hope) displays the blit you did the previous time at the skipped areas.

That should drastically reduce the number 'blit' calls. It's a challenge to implement though.



As for your code, some recommendations


- You comment a lot, which is good. A lot of comment (especially at the start) however seems to say what the code line below it does. If someone is reading code, it is safe to assume they can read the code, they know Python, and they know pygame. As such, what the program does can be read in the code lines already, no need to repeat that in the comment. Instead, use the comment to explain why this action must be done at this point, or explain what is happening at a more global level, eg line 64 "# Load images" says what the next 20 lines do. The other "# load FOO image" can be left out, as the code already says what happens. In the main loop, your comments seem fine.


- You have a very long main loop, try to break it into separate functions. A function attaches a name to some code which is useful for reading. Also, you can re-use the same code by calling the function from several places in the code.


- In settings.py, you remove the settings file (remove("settings.txt")), and use an operating system call to create a new empty file (execute("type NUL > settings.txt")). The open function can do both things for you: open("settings.txt", "w")  deletes the old content, and creates a blank new file, where you can write to (but not read).



Finally some code fragments that you can simplify/improve

        # Change the image of the badgers
        if badguyimg == badguyimg1:
            badguyimg = badguyimg2
        elif badguyimg == badguyimg2:
            badguyimg = badguyimg3
        elif badguyimg == badguyimg3:
            badguyimg = badguyimg4
        elif badguyimg == badguyimg4:
            badguyimg = badguyimg1

can be rewritten to

bad_guys = [badguyimg1, badguyimg2, badguyimg3, badguyimg4]
current_badguy = 0  # displayed bad guy is bad_guys[current_badguy]

current_badguy = currentbadguy + 1
if current_badguy >= len(bad_guys):
    current_badguy = 0

Make a lis the bad guy images, and an integer that points to the currently displayed bad guy. Instead of checking which bad guy image you display, just increment the integer, so it points to the 'next' bad guy image. At the end of the list, jump back to the beginning.



The key handling can also be improved a bit

                # Move up
                if event.key==K_w:
                elif event.key==K_UP:

The index 0 seems to be associated with "up", you can express that more clearly. Also, Python has a little trick for the comparison you do here.

# At start of the file:
KEY_UP = 0

# In the loop:

# Move up
if event.key in (K_w, K_UP):     # Test whether event.key is one of the values.
    keys[KEY_UP] = True          # Use the KEY_UP number instead of 0.

Share this post

Link to post
Share on other sites

Yeah, blit is the problem. I maked the grass texture bigger, but it doesn't help very much. So how can I speed up the blits? I removed the fill function as you suggested. The code to change the badguy texture which you suggested doesn't work, what do I have to change for that? Now I get this with cProfile. It seems that flip() also takes some time.

Share this post

Link to post
Share on other sites

"doesn't work" is quite vague, it's hard to diagnose that smile.png

Random guess however, are you drawing the "bad_guys[current_badguy]" sprite, instead of  "badguyimg" sprite? (could be done as simple as "badguyimg = bad_guys[current_badguy]") just after changing "current_badguy").


Right, bigger blit isn't working thus sad.png


Dropping blits is much more difficult unfortunately, but here goes.

Let's use the background image as unit of measure. In the loop, before you move anything, you get the background images that need to be changed:


is a very bad sketch smile.png


Each cell in the grid is one background sprite. Its height is by, and its width is bx.

The orange rectangles are things that move in the next frame you're going to draw.


Before the movement, you know where the rectangles are, since you have the coordinates of them. I assume a moving rectangle is smaller than a background image. The problem that you need to handle first is find out which background images are (partly) covered by an orange rectangle.

You do that by computing the following for each corner of the rectangle.

tx = corner_x // bx
ty = corner_y // by

That should give the x and y coordinate (numbered as shown in the image) of the background image where the corner is. You do that for each corner of each orange rectangle, and collect the (tx, ty) pairs.

collected = set()
for rect in [(x, y, width, height), ... ]:  # xpos, ypos, width, and height for every orange rectangle that you have
  for corner_x, corner_y in [(rect.x, rect.y), (rect.x, rect.y + rect.height), (rect.x+rect.width, rect.y), (rect.x + rect.width, rect.y + rect.height)]: # For every corner of that rectangle
    tx = corner_x // bx
    ty = corner_y // by
    collected.update((tx, ty))

Note the double parentheses in the "update" line.

A set stores values you give to it, but it automatically deletes doubles.


I would suggest that you implement this first, and then check whether "print collected" outputs pairs of numbers that you expect.



Assuming you got the numbers that you expect, the next step is to move the moving things in your game. They now have new positions.

Since these new coordinates may be at a different background image, you need to repeat the above trick, except don't clear "collected", ie leave out "collected = set()" the second time.

You are doing the same computation again, finding background images that are partly covered by a corner. Anything that you already collected before movement is still in the set, and the set will make sure you don't get double values.


So that's it. Now you have the set of background images that need to be redrawn, as the things above it moved. Now you have to draw everything

for x, y in collected:
  posx = x * bx
  posy = y * by
  # Check that the image is visible, otherwise you can completely skip it.
  if posx >= 0 and posx <= ... and posy >= 0 and posy <= ...:
     blit(..., posx, posy)  # Don't know the syntax exactly, but that shouldn't be too hard.

Now the background is updated, only the backgrounds where moving things are above are refreshed, everything else should not change.


Next, draw the moved guys, balloons, etc.




Share this post

Link to post
Share on other sites

Thanks for what you mentioned! At the beginning you define a window (in my case called "screen"), on which you must blit things, so that blit command becomes screen.blit(image, [xpos, ypos]). I've added to the instances of image = pygame.image.load("image.png") a convert_alpha() call, which really speed it up. I'm going to implement that what you mentioned.

Share this post

Link to post
Share on other sites

once you've applied all the obvious optimizations, such as dirty rectangles, if you find its still not fast enough, you may need to port to a faster graphics library.


over the years, as i've wanted to do more in game's, i've gone from basic, to pascal, to c and assembly code, to c++ with hardware acceleration in my quest for speed.


a quick google returned these hits:






didn't check them out myself though.


sorry, just noticed they happen to be the same links Spidi posted.

Edited by Norman Barrows

Share this post

Link to post
Share on other sites
Sign in to follow this  

  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!