Using Pygame to Build an Asteroids Game (Summary)
Congratulations, you just built a clone of the Asteroids game using Python! With Pygame, your Python knowledge can be directly translated into game development projects.
In this course, you’ve learned how to:
- Load images and display them on the screen
- Add input handling to your game
- Implement game logic and collision detection in Python
- Play sounds
- Display text on the screen
For more information on concepts covered in this lesson, you can check out:
- The Pygame Documentation | pygame.org
- Make a 2D Side-Scroller Game With PyGame | Real Python Video Course
Congratulations, you made it to the end of the course! What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment in the discussion section and let us know.
00:00 In the previous lesson, I showed you how to add text to the screen. In this lesson, I’ll wrap up the course.
00:07 This course has been all about creating your first game in Python using the Pygame library. By building an Asteroids clone called Space Rocks, you’ve learned about game loops and the main Pygame library methods, what sprites are and how to blit them, rotate them, and move them around.
00:25 You’ve also learned how to respond to keyboard input, how to play sounds, all about loading fonts and displaying text, and general game writing awesomeness.
00:37
Here are some ideas you can try to improve your game even further. In the lesson where ship collisions were added, I pointed out that using None
for a destroyed ship meant there was a danger of crashing your code. I walked you through fixing a couple of spots but left one out. If you didn’t find it, I’ll tell you now. Fire the gun after the ship is destroyed, and the code will crash.
00:59
The simple way would be adding a None
check inside the keyboard handler that fires the gun. Or, the better long-term way, that involves adding an alive and dead state to the ship class.
01:12 You would then modify the class further to behave correctly after the ship is in the dead state. This method takes a bit more work, but is probably the better solution, particularly if you’re going to… add more lives to the game!
01:27
This can be about more than just allowing the player to restart three times. You can also add a "press space to start"
message with the game paused after each time the player respawns. Right now, it is possible if you accelerate in just the right way, for the ship to overtake the bullets. If you manage this, you’ll discover that these are magic bullets that only destroy rocks.
01:48 You could add some code to make them fatal to the ship.
01:52
Or, what’s a game without a score? Add something to track the score and display scoring text. You’ll need to update print_text()
to do this, as right now, it only displays text as a centered message on the screen.
02:05 You can also add a play again key. When the game is done, the player should be able to play again without having to close the window and restart the program. And finally, if you want a real challenge, add an instant replay movie. Show the player what happened and how they lost.
02:22 This one could get messy. You could store all the positions of all the objects in order to create the new frames for, say, the last three seconds. Or, in a more memory-intensive version, you could store copies of the frames in buffers. When a user dies or wins, you then create a playback using the position information or the buffers.
02:40 Of course, you’d also want the player to be able to skip past this playback and get on with the game. This one could be fun to code.
02:49 Here are two great sources for more Pygame goodness. This is a link to the Pygame documentation, which is a great way to find all the ins and outs of writing Pygames.
02:59 And this is Christopher Bailey’s course on writing a 2D side-scroller in Pygame. It covers some of the same basics as this course in more detail, and it also talks about sprite groups, playing background music, and more.
03:13 That’s it for Space Rocks. I hope you enjoyed the course. Thanks for your attention. Leave a comment below bragging about any neat features you’ve added to your version of the game and challenge your fellow Pythonistas.
Christopher Trudeau RP Team on Oct. 26, 2021
Glad you enjoyed it Mike! It was a fun course to build. ..ct
Rok Kužner on Dec. 31, 2021
Thank you all for making such a great tutorial! I imporove my game by ading sounds for weening anl losing. I also add score: Utils.py:
def print_score_text(surface, text, font, color=Color('green')):
text_surface = font.render(text, True, color)
rect = text_surface.get_rect()
surface.blit(text_surface, rect)
and game.py in function draw:
print_score_text(self.screen, f'Score: {self.score}', pygame.font.Font(None, 40))
I also add Do you like to play again by adding some lines of code in function draw:
if self.message:
if self.message == 'You lost! Press r to play again.':
print_text(self.screen, self.message, self.font)
you_lost_sound_f()
is_key_pressed = pygame.key.get_pressed()
if is_key_pressed[pygame.K_r]:
from game import SpaceRocks
space_rocks = SpaceRocks()
space_rocks.main_loop()
else:
print_text(self.screen, self.message, self.font, color=Color('green'))
you_ween_sound_f()
is_key_pressed = pygame.key.get_pressed()
if is_key_pressed[pygame.K_r]:
from game import SpaceRocks
space_rocks = SpaceRocks()
space_rocks.main_loop()
I also like to add instant replay but I don’t know how. Can you help me with this?
Thanks for everything Rok.
Christopher Trudeau RP Team on Dec. 31, 2021
Hi Rok,
I haven’t tried it myself, but I can think of two ways it could be done:
1) Keep a list of screen images and each time you do your blit to the screen, store a copy on the list. The list should be limited in size: once it reaches a certain number of items, add one and remove one. The number of images in the list would be related to the amount of time in your replay. If you’re doing 20 frames-per-second and you want 3 seconds of replay, you need to store 60 images.
2) Or, instead of storing the screen images, you can store the state of the system – the positions of all the rocks, bullets, and your ship. I’d create an object that kept this and then keep a list of those storage objects.
The first case is likely easiest to code but would take up more memory. The images to the screen aren’t compressed so each image would take up at least a byte per pixel (it might be more than that) and you have 800x600 pixels. Multiply this by 60 and you’re going to be at least 28MB of memory. Modern computers can handle this fine, but this isn’t how it would have been done in olden times.
The second case will require less memory but more work. You’re only storing the positions of the objects, which of course varies by how many objects, but lets say 50. The positions of them are a couple of bytes, let’s keep the math simple and say 20 bytes per object. Still need 60 frames’ worth, so that’s around 60KB. Much less memory. But, now you have to write code that is similar to the playing code to replay the game using this data instead of what the user input. If you’re careful with your method calls, it shouldn’t be much different than the code you currently have – in addition to move()
you might want move_to(pos)
. Depending on how the code shakes out, move()
could call move_to(pos)
, but I suspect they’re both only a couple of lines each and it might not be worth the extra function call.
(As an aside, if you’re really careful with how your random numbers are generated, you don’t even have to store all 60 positions. There are ways of getting random number generators to spit out the same sequence of random numbers given the same starting seed. If you did it this way, you’d only have to store the position of everything 60 frames ago. Look up random number generators and seeds in the random library if you want to dig into this mechanism).
If you’re going to add a replay, you probably are going to want to react to a keypress during the replay to abort it – I hate having to watch the cinematic in a game I’ve played before, I want a “skip” button. :)
Happy coding!
Rok Kužner on Dec. 31, 2021
Thanks for answer! I will shere my code with you when I’m done with coding.
Rok Kužner on Dec. 31, 2021
Do you meybe know how to take pygame window screenshot and how to display it to the screen?
Christopher Trudeau RP Team on Jan. 1, 2022
Hey Rok,
So I haven’t tried this, you’ll have to play a bit, but looking at the documentation, the pygame.display
object has a .get_surface()
method, and the pygame.surface
object can be copied and blitted to other surfaces, so that would be where I started.
Take a look here:
www.pygame.org/docs/ref/display.html
and
www.pygame.org/docs/ref/surface.html
There’s a chance you might have to change some of your methods to draw to the surface and then blit it to the screen, but if you can get the screen’s surface, it likely would just be two lines of code (get_surface()
and copy()
).
Good luck with it.
Rok Kužner on Jan. 1, 2022
Thanks! 😀
froyathehen on April 25, 2022
Christopher Trudeau, not only was it educational but a great deal of amusement! Looking forward to getting through some more content of yours ^^ Thanks!
Christopher Trudeau RP Team on April 26, 2022
Glad to hear you enjoyed it @froyathehen and happy that the jokes aren’t falling completely flat.
AntonB on May 18, 2023
Hi Christopher and team - I really enjoyed this intro to pygame. It was informative and fun, and I’m old enough to get the jokes :) I’m looking forward to exploring more RP content!
Christopher Trudeau RP Team on May 18, 2023
Hi Anton,
Glad you enjoyed the course. You might also like Christopher Bailey’s PyGame course. There is some overlap of the concepts, but the game is different, so you get to see someone else’s coding style for a similar problem:
JCode888 on June 24, 2023
Hi Christopher!
This was a great course! I really enjoyed it!
I decided to try and make mine a more faithful looking clone to the original Asteroids. I created my own ship, asteroid and bullet in Adobe Illustrator and exported them as PNGs to use as my assets. The only slight problem I’ve run into is that the code that breaks the asteroids scales down the outline around them, which makes them look a little broken up in the smaller size asteroids. I tried increasing the “stroke”(outline) around them in Illustrator before exporting, which works somewhat, but I don’t want to make them look too “chunky” as I’m trying to get it to look as close to the original as possible. I believe the original used vector graphics, which probably doesn’t change the look when scaling, unlike scaling a PNG.
I also used my keyboard and Ableton Live to record my own shooting and asteroid explosion sound effects. I was proud of myself for figuring out where to best add the code so that the asteroids always make the explosion noise when they are hit by a bullet. :)
I definitely want to try out some of your suggestions for improvements like fixing the firing after the ship is gone crash, adding more lives and adding a score. Hopefully I can figure all of that out.
Thanks again for a really fun course!
JCode888 on June 25, 2023
I’m trying to implement a score option. I feel like I’m really close, but have tried a ton of things and just can’t seem to get it to display and update on the screen. In utils.py, I added:
def print_score_text(surface, text, font, color=Color("white")):
text_surface = font.render(text, True, color)
rect = text_surface.get_rect()
rect.center = Vector2(40, 40)
surface.blit(text_surface, rect)
basically copying the format of the print_text function. In game.py, I’ve added:
import pygame
from models import Rock, Spaceship
from utils import load_sprite, load_sound, print_text, print_score_text
bullets = []
rocks = []
score = 0
class Asteroids:
def __init__(self):
# Initialize pygame and set the title
pygame.init()
pygame.display.set_caption("Asteroids")
self.clock = pygame.time.Clock()
self.font = pygame.font.Font(None, 64)
self.score_font = pygame.font.Font(None, 24)
self.message = ""
self.score_message = str(score)
importing the print_score_text function, initializing the score variable at 0, setting the size of the font and saying that self.score_message = str(score).
In the game_logic function, I add a global score variable and within the if statement that deals with bullets colliding with the asteroids, I increase the score variable by 20 each time. I know that’s working because I used a print statement to debug and I can see the score increase in the Command Prompt every time I shoot an asteroid.
def _game_logic(self):
global bullets, rocks, score
for obj in self.game_objects:
obj.move(self.screen)
rect = self.screen.get_rect()
for bullet in bullets[:]:
if not rect.collidepoint(bullet.position):
bullets.remove(bullet)
for bullet in bullets[:]:
for rock in rocks[:]:
if rock.collides_with(bullet):
score += 20
print(score)
self.explode.play() #play explosion noise when bullet hits asteroid
rocks.remove(rock)
rock.split()
bullets.remove(bullet)
break
Then in the draw function, I call print_score_text below where the regular print_text function is called.
def _draw(self):
self.screen.blit(self.background, (0, 0))
for obj in self.game_objects:
obj.draw(self.screen)
if self.message:
print_text(self.screen, self.message, self.font)
print_score_text(self.screen, self.score_message, self.score_font)
pygame.display.flip()
self.clock.tick(30)
I’ve tried a hundred things…putting that inside the if statement, outside, outside the draw funtion, making score global in a bunch of different functions etc. Nothing I do seems to make the score update on the screen. It just shows the initial 0 score even though I can see it increase with the print statements in the shell. I don’t know what else to try. Any help is much appreciated!
Christopher Trudeau RP Team on June 25, 2023
Hi JCode888,
First off, it has been almost three years since I wrote this code, so my memory is a little fuzzy.
I don’t immediately see anything wrong with your print_score_text
function, and it definitely should be called inside of the _draw
method, but, I’m hazy on exactly how the coordinate system works (different graphical environments do it differently), so my first inclination is to ask whether you’re sure you’re drawing it on the screen and not off screen.
Second, I see the place where you initialize .score_message
but I don’t see where it gets updated. I’m not sure whether that’s because you’re assuming it gets updated automatically, or because it just isn’t included in the code you showed. If it is the former, you need to update the .score_message
string each and every time you change .score
integer. As you have a special function for printing the score, I might just make the value passed in be the integer itself and convert it into the string inside the function. No need to keep a separate “.score_message” attribute on the object at all.
One way to help debug this kind of thing, is to start with what works. For example, copy the print_text
method exactly, rename it, have the score show up in the middle of the screen like the “end game” message, and see if it updates. It will be in the way of the play, but then you can figure out how to make incremental changes – font size, position, colour, whether the message is updating, etc.
By doing one change at a time, it is easier to track down what is working and what isn’t.
Glad you’re having fun with it. Hopefully you squish this bug soon. Good luck.
JCode888 on June 26, 2023
Thank you so much for your help Christopher!
The score was definitely appearing on screen. In the print_score_text function, I used:
rect.center = Vector2(40, 40)
to place it in the top left corner of the screen. It was appearing, but only as 0, which is what I originally initialized the score variable at.
I used:
self.score_message = str(score)
in my Asteroids class which I thought would update it. But, you mentioning not seeing where it is updated got me thinking that the updating needs to happen inside the game_logic function. (though, I originally thought having score += 20
in the game_logic function was updating it since I already set self.score_message = str(score)
in the Asteroids class.
So now, I just initialize self.score_message = "00"
inside the Asteroids class. Then I use self.score_message = str(score)
inside the game_logic function....and it works!!
One step closer to a fairly close Asteroids clone! Now I just have to figure out how to make multiple lives! Thank you for your help! :)
Christopher Trudeau RP Team on June 27, 2023
Glad that fixed it JCode888.
Become a Member to join the conversation.
mikesult on Oct. 26, 2021
Thank you Christopher and the Real Python team for this course. I really enjoyed it. I especially appreciated the discussion on choosing to use globals and how that changed the code, the tradeoffs and so on. The little bug to find was a good exercise. I ended up using the first method but not before I had created an infinite looping method that I had to use ‘force quit’ to recover infinity->now. Oops. All along the way you discussed some alternate paths and your reason for coding it this way in this context. That discussion is helpful to me to understand the issues that you are considering. Thanks also for all of the resources, sample code for each video segment, etc.
And lastly, maybe most important these days, it was fun.