Introduction To Pygame
Pygame is a set of Python modules designed for writing video games and complex multimedia applications. It handles game development tasks such as rendering graphics, managing events, playing sounds, and handling user input.
Pygame’s architecture makes it particularly appealing for rapid prototyping and experimentation, as its straightforward API allows for quick implementation of game mechanics and interactive features. The library operates on top of SDL (Simple DirectMedia Layer), ensuring that Pygame applications can run on a wide variety of platforms without modification.
Resources
-
Official Pygame documentation exists and is decent.
-
This video is nearly 4 hours long but goes from zero to full game using Pygame and is very helpful in getting the gist of what it’s all about. You can find the code on its GH repo.
-
Pygame is really a polite Python wrapper around SDL (Simple Direct Media Layer). Often true insight is found reading the SDL documentation directly.
Pygame Architecture
Pygame is divided into several key modules, each responsible for a distinct
aspect of game development. These include pygame.display
which manages the
game window and screen, pygame.image
to handle image loading and rendering,
and pygame.mixer
for sounds and music.
Pygame Performance
If you look at examples you often see the following cryptic code.
clock= pygame.time.Clock() while game_loop_running: clock.tick(60) # limits FPS to 60
That clock tick comment suggests (as found in the most basic official example) that it is to limit the frame rate. But why would you want to do this? The real reason this clock checking is critical is that if your computer can run your code very quickly it will jump to 100% CPU continuously trying to look for any change in status as quickly as possible. But if you add this tick setting, it will realize that it’s well ahead of schedule and shut down until it needs to wake up and attend to more details at the next 17ms (in the case of 60fps) scheduled interval. So if your loop is taking .5ms to run it will just relax for 16ms or so and your CPU usage will be much more sensible.
Event Handling
There are two main ways to approach event handling, the Event Queue and querying Key States more proactively.
Event Queue
Event handling in Pygame is managed through pygame.event
, which
tracks and processes user inputs and other game events. Pygame can detect a
wide array of input events, including key presses, mouse movements, and
joystick inputs.
Pygame manages an event queue, which stores all events detected during the
game loop. Developers interact with this queue via the pygame.event.get()
method, which returns a list of all pending events. By iterating over this
list, the game can respond to each event, such as moving a character when a key
is pressed or quitting the game when the close button is clicked.
Event Key States
Calling pygame.key.get_pressed()
will check for keyboard key events
and pygame.mouse.get_pressed()
checks mouse buttons. This method
returns the state of all keys/buttons at the moment it’s called,
allowing you to check if a specific key or button is pressed without
needing an event to be generated and placed in the queue.
The pygame.ket.get_pressed()
method returns the state of all
keyboard keys in a single call. This function generates a sequence
(typically a tuple or list) where each element corresponds to a key on
the keyboard, and the boolean value at each position indicates whether
that key is currently being pressed. The sequence index corresponds to
a key’s integer ID (which can be found in the Pygame key constants,
like pygame.K_a for the "A" key, pygame.K_SPACE for the spacebar,
etc.). You can use these constants to index into the sequence and
check if a particular key is pressed. It’s essentially like accessing
a dictionary where the keys are the integer IDs of the keyboard keys.
Surface Objects
In Pygame, the Surface object represents an image or any drawable area.
It is the interface to the underlying
SDL Surface data structure.
Essentially, it’s a canvas where graphical elements can be drawn,
manipulated, or transformed. Each Surface is associated with a
specific width and height, and it can hold either a still image loaded
from a file or dynamically generated graphics. Surfaces play a crucial
role in rendering, as they can be blitted (BLock Image Transfer,
basically just an efficient copy) onto other Surfaces, including the
main display surface obtained via pygame.display.set_mode()
. This
blitting process is central to Pygame’s rendering system, allowing for
the layering of images and efficient screen updates. Surface objects
can be scaled and rotated as well as blitted with other surface
objects.
The main game window that is visible is the Display Surface. It is
initialized with pygame.display.set_mode()
. When you draw on this
surface, it gets rendered to the screen. You can also have off-screen
surfaces that can help compose things before being swapped out to the
display surface.
Besides explicitly using the pygame.Surface()
class directly,
loading images will also create a Surface to contain them.
racetrack= Surface( (300,200) )
player= pygame.image.load('graphics/player.png').convert_alpha()
Surfaces support transparency to help in composing complex scenes out of multiple Surface objects. Surfaces can be controlled to the pixel level. If a rectangular collision box is not fine enough, you may need to look into the pixels of the surfaces involved.
Rect Objects
Rect objects represent rectangular areas and are used for collision detection, positioning, and geometry. They are the interface to the underlying SDL_Rect data structure. In Pygame a Rect can be created from a Surface object, defining the area it covers, or manually by specifying the position and size. They are instrumental in managing game objects' positions, as they allow for easy manipulation of coordinates. Rect objects support a variety of operations, such as moving, resizing, and testing intersections with other Rects, making them important for both layout management and gameplay mechanics, such as detecting when a player character intersects with an enemy or a collectible.
Here is an extremely simple working example that loads an image file
into a Surface
object. This image surface generates a Rect
object
for positioning. The rect location is set and it is eventually blitted
onto the screen surface.
#!/usr/bin/python import pygame pygame.init() screen= pygame.display.set_mode((800,600)) mygraphic_image= pygame.image.load('myimg.png') # Load an image into a surface. mygraphic_rect= mygraphic_image.get_rect() # A positioning rect for the surface. mygraphic_rect.topleft= (100, 100) # Position rect on screen. # Main game loop running= True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running= False screen.fill((0, 0, 0)) # Blank the screen. screen.blit(mygraphic_image,mygraphic_rect) # Blit image to rect. pygame.display.flip() # Update the display. pygame.quit()
Rect Attribute Assignment
The Rect object has attributes which can be used to move and align the Rect. Here are some examples.
rect1.right= X
rect2.bottomleft= (X,Y)
rect3.center= (X,Y)
rect4.midtop= X
rect5.midright= Y
You can also resize the Rect by assigning attributes like this.
rect1.width= X
rect2.h= Y
rect2.size= (X,Y)
And this could be a handy feature.
x,y,w,h= myrect
Rect Collision Detection
Pygame provides some support for basic but useful collision detection
in the Rect
class. The most basic form is to have two Rect
objects
and call the colliderect()
method on one passing in the other. If it
returns True
they overlap. Here’s a basic example.
player_rect= pygame.Rect(50,50,32,32) enemy_rect= pygame.Rect(100,100,32,32) if player_rect.colliderect(enemy_rect): print("Collision detected!")
There is also a collidepoint()
method that will check to see if the
Rect
object has overlapped some specific point. Here’s a good
example of that.
mouse_pos= pygame.mouse.get_pos() if player_rect.collidepoint(mouse_pos): print("Player rectangle clicked!")
There’s also a collidelist()
method which checks to see if a certain
rect has collided with any of a list of other objects. Here is an
illustrative example of that.
enemy_list= [enemy1_rect,enemy2_rect,enemy3_rect] if player_rect.collidelist(enemy_list) != -1: print("Player collided with an enemy!")
Sprite Objects And Sprite Groups
Sprites are objects that represent non-static entities in your game, such as
characters or items. The pygame.sprite.Sprite
class is used to create
sprites, which should be extended to include image and rect attributes for
graphical representation and positioning, respectively. For managing and
updating groups of sprites, use pygame.sprite.Group
objects. Groups enable
efficient rendering and updates of contained sprites with mygroup.draw(screen)
and mygroup.update()
, facilitating collision detection and sprite management.
Here’s a complete example that creates a sprite group of a bunch of
stars and then spins them around the center of the screen. You can see
the Star class extends the pygame.sprite.Sprite
class and that’s
what allows this entity to be rendered with no further worry. This
leaves the Star class to mostly just worry about what your game needs
the stars to do (in this example, rotate).
#!/usr/bin/python import pygame from random import randint from math import sin,cos pygame.init() width,height= 800,600 screen= pygame.display.set_mode((width, height)) pygame.display.set_caption('Rotating Starfield') clock= pygame.time.Clock() class Star(pygame.sprite.Sprite): def __init__(self,x,y): super().__init__() ss= randint(1,8) self.image= pygame.Surface((ss,ss)) # Set random star size.. self.image.fill((255,255,255)) # White stars. self.rect= self.image.get_rect(center=(x,y)) self.cenX= x - width//2 self.cenY= y - height//2 def update(self,ang): # Rotate the star around center. rotated_x= self.cenX*cos(ang) - self.cenY*sin(ang) rotated_y= self.cenX*sin(ang) + self.cenY*cos(ang) self.rect.x= rotated_x + width//2 self.rect.y= rotated_y + height//2 stars= pygame.sprite.Group() for _ in range(100): # Create stars. x,y= randint(0,width),randint(0,height) stars.add(Star(x,y)) angle= 0 running= True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running= False screen.fill((0, 0, 0)) # Clear screen with black. stars.update(angle) # Update stars. stars.draw(screen) # Draw stars. angle += 0.007 # Increment radians for rotation. pygame.display.flip() clock.tick(60) # Maintain 60 FPS. pygame.quit()
Use pygame.sprite.GroupSingle
for sprite management that could
not sensibly benefit from managing multiple sprites at the same time.
For example, the player’s sprite is probably not part of a collection
of similar identical sprites. Really, I can’t understand why there
would be a GroupSingle which sounds like an oxymoron. I think one
trick that could make it useful is if you add additional sprites to
the single-style "group", they will displace others instead of adding
to them.
Sprite groups can be used as kind of an attribute system for sprites. It really seems like more of a way to organize things where assignment to various groups is relatively high performance than tracking some other kind of stats, for example, monsters_agro, monsters_visible, monsters_active, monsters_spawned could all be sprite groups containing the correct entities.
There are several render groups that are derived from sprite group objects.
-
RenderPlain
- A group that just renders the sprites. -
RenderClear
- Clears the old ones and renders the group. -
RenderUpdates
- Clears and renders just the rects that need it.
The general approach for what inherits what to set things up looks a bit like this (incomplete) code.
class Player(pygame.sprite.Sprite): def __init__(self): super().__init__() self.image= pygame.image.load('/player.png').convert_alpha() class Mob(pygame.sprite.Sprite): def __init__(self,type): super().__init__() if type == 'creeper': # Do creepy stuff... if type == 'skelly': # Do skeletal stuff... def update(self): # Gets called on all members upon mob_group.update() self.check_health() self.animation_stuff() player= pygame.sprite.GroupSingle() # Actually a kind of group. player.add(Player()) # Add the player class to the "group". mob_group= pygame.sprite.Group() mob_group.add(Obstacle('creeper')) mob_group.add(Obstacle('skelly')) mob_group.update()
Sounds And Music
The primary Pygame mechanism for sound effects is
pygame.mixer.Sound
. Here’s a basic idea of what that looks like.
self.jump_sound= pygame.mixer.Sound('audio/jump.mp3') self.jump_sound.set_volume(0.5) ... self.jump_sound.play() # When ready to make this sound.
This Sound
class can also be used for full looped music tracks as in
the following example.
bg_music= pygame.mixer.Sound('audio/music.wav') bg_music.set_volume(0.1) bg_music.play(loops=-1)
There does also appear to be a pygame.mixer.music
class. (Why is it
not capitalized like the Sound
class? Don’t know.) The difference
between the music playback and regular Sound playback is that the
music is streamed, and never actually loaded all at once. The mixer
system only supports a single music stream at once. Apparently MP3
files work but OGG files work better.
Fonts And Text
Pygame makes it pretty easy to put text on the screen. You can use True Type or OpenType fonts quite easily. Here’s an example that draws "Game Over" on the screen and then scales it up for a kind of zoom effect.
#!/usr/bin/python import pygame import sys pygame.init() screen= pygame.display.set_mode((640, 480)) pygame.display.set_caption('Font Size Example') fontsize= 10 running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False screen.fill((0,0,0)) # Clear screen. font= pygame.font.Font("myfont.ttf",fontsize) text_surface= font.render('Game Over',True,(255,255,255)) # Middle arg is antialias. text_rect= text_surface.get_rect(center=(320,240)) screen.blit(text_surface, text_rect) pygame.display.flip() # Update display. fontsize += 1 # Increase the font size. pygame.time.Clock().tick(60) if fontsize > 300: # Stop increasing the font size at some point. running = False pygame.quit() sys.exit()
Here’s another font demonstration that prints out all of the Unicode symbols involved in music notation. It then nicely draws a grid and a helpful reference X so you know where the anchor for the symbol is. It then labels the symbol with the hex value using the default system font.
#!/usr/bin/python # Display all of the glyphs for music symbols. # This is in the Unicode range 0x1D100 to 0x1D1FF. # The font used is available here: # github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoMusic/NotoMusic-Regular.ttf import pygame pygame.init() clock= pygame.time.Clock() WIDTH,HEIGHT= 1200,1200 ROWS,COLS= 16,16 grid_width,grid_height= WIDTH//COLS,HEIGHT//ROWS screen= pygame.display.set_mode((WIDTH,HEIGHT)) screen.fill((0,0,0)) Mfont= pygame.freetype.Font("NotoMusic-Regular.ttf",32) Tfont= pygame.freetype.SysFont(None,13) # Default text font. def draw_text(F,surface,text,x,y,color): F.render_to(surface,(x,y),text,color) for i in range(0x1D100, 0x1D1FF + 1): # Unicode music symbols range. col= (i-0x1D100)%COLS row= (i-0x1D100)//COLS x,y= col*grid_width,row*grid_height tx,ty=x+(grid_width//2.5),y+5 # Draw the glyph on the screen draw_text(Mfont,screen, chr(i),tx,ty,(255,200,255)) draw_text(Tfont,screen,f"0x{i:X}",tx-20,ty+55,(255,200,255)) Xs= 5 # Center point X marker size. pygame.draw.line(screen,(255,0,55),(tx-Xs,ty-Xs),(tx+Xs,ty+Xs)) pygame.draw.line(screen,(255,0,55),(tx+Xs,ty-Xs),(tx-Xs,ty+Xs)) # Draw grid. pygame.draw.line(screen,(155,155,255),(x,0),(x,HEIGHT)) pygame.draw.line(screen,(155,155,255),(0,y),(WIDTH,y)) pygame.display.update() # == Game Loop running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False keys= pygame.key.get_pressed() if keys[pygame.K_q] or keys[pygame.K_ESCAPE]: running= False clock.tick(60) # 60 FPS and not 100% CPU. pygame.quit()