Playing the Whole Game

Getting started with an unfamiliar library can be a daunting task - especially with one as large as pygame. So let's start by using the first principle of David Perkins' Seven Principles of Teaching: Play the Whole Game. Try out the game below, then come back to this page. All of the elements you see in the game are broken down here.

Try This Out!

Here is a simple pygame example that uses the concepts below:

Why Pygame?

Pygame is a tool for developing video games using python. The library allows you to do a few things that are not otherwise easy to do:

  • Open a graphical user interface (GUI) window
  • Draw objects, shapes, and text in the window
  • Get player input without stopping the game (which is what would happen if input() was used)

Pygame is best for simple games, and is good for learning the logic behind an event loop.

Installing Pygame

For installing on a variety of devices, see the official Getting Started wiki here.

For Windows, enter the following in a command prompt:

python -m pip install pygame

To test that it works correctly, try:

python -m pygame.examples.aliens

If pygame is installed properly, it should open an aliens game that looks like this:

Pygame Aliens

To test that a python file will run correctly, create a .py file and run:

import pygame

print(pygame.ver)
pygame 2.4.0 (SDL 2.26.4, Python 3.9.7)
Hello from the pygame community. https://www.pygame.org/contribute.html
2.4.0

The Basic Framework

All pygame applications need this basic structure to run.

The code below has been modified from Python Simplified's video Create a Simple Video Game with Pygame - Step by Step Tutorial for Python Beginners!.

# Running this code will open an empty pygame window. Clicking the "quit" button on the window closes the application.

# 1. Import libraries
import pygame
from pygame.locals import *

# 2. Initialize all imported pygame modules - this will raise exceptions if it fails
pygame.init()

# 3. Global variable used with the event listener in step 7
running = True

# 4. Clock used for controlling frame rate
clock = pygame.time.Clock()

# 5. Set display window size - the window is pygame's built-in GUI
screen = pygame.display.set_mode((700,500))

# 6. Event loop - all motions and events that happen as the game runs are inside this loop
while running:

    # 7. Event listener for quitting the game - when the player clicks the "quit" button on the window, "running" is set to False and the application closes
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
    
# 8. Set the frame rate to 60 FPS (frames per second) - a constant frame rate helps the game run smoother
    clock.tick(60)

# 9. Close the application
pygame.quit()
pygame 2.4.0 (SDL 2.26.4, Python 3.9.7)
Hello from the pygame community. https://www.pygame.org/contribute.html

The empty window looks like this:

Pygame Aliens

Coordinates in Pygame

Pygame coordinates work a little differently than a typical graph: (0,0) is the top left corner of the window and positive values are counted right and down from this point. A point (50,100) would look like this:

Normal Coordinates vs. Pygame Coordinates

Setting the Window Title and Icon

Changing the title of the game window can be done using:

pygame.display.set_caption("My Game")

The default pygame icon can be changed into any image using a surface (see the section on Surfaces below):

# a pygame.Surface object that stores an image
surface = pygame.Surface()
# change the display icon
pygame.display.set_icon(surface)

Event Listeners

Event listeners wait to perform an action until a certain type of event has occurred, usually player input. For example, when the player presses the left arrow key, move a character left. The general format is:

Check that a certain type of event has occurred --> Do something.

In the basic framework, we used an event listener to quit the application when the player closes the window.

running = True                          # flag to control whether or not the game will run
pygame.init()                           # initialize the window
while running:
    for event in pygame.event.get():    # All events of the game are stored in the events module. We need to "get" them to look at them.
        if event.type == QUIT:          # Checking if a "QUIT" type of event has occured, a.k.a. the player clicking the "quit" button
            running = False
pygame.quit()                           # close the window

Surfaces

A pygame.Surface object is used for representing images in the game. For example, a surface can be an icon that the player controls, or the background of the game. Surfaces can only be moved using the coordinates of its top left corner.

There are two types of surfaces:

  • Display Surface: the window of the game. It is always visible, and there is only one. The pygame application will not run without one.
# create a display surface with width=700 and height=500
screen = pygame.display.set_mode((700,500))
  • Regular Surface: a surface that may or may not be visible on the screen. There can be an unlimited number of them.
# create a surface of width=5, height=10
regular_surf = pygame.surface((5, 10))

Image Imports

Images imported using pyame.image.load() are automatically stored as (regular) surfaces.

# create a surface from an image import
img_surf = pygame.image.load('intro_to_pygame_images/image.png')

Converting to Pygame-Friendly Format

Surface.convert() and Surface.convert_alpha() are used to turn an image into a format that is easier for pygame to handle. Note that convert() makes transparent pixels appear black or white when the surface is added to the screen. To keep transparent pixels, use convert_alpha().

The pygame modules and display screen must be initialized before these methods are called.

# initialize modules and screen
pygame.init()
screen = pygame.display.set_mode((700,500))

# create a surface from an image import and use convert()
img_surf = pygame.image.load('intro_to_pygame_images/image.png').convert()

# OR create surface and use convert_alpha()
img_surf = pygame.image.load('intro_to_pygame_images/image.png').convert_alpha()

# close application
pygame.quit()

Surface Dimensions

To get the dimensions of a surface, use Surface.get_width() and Surface.get_height(). The pygame modules and display screen must be initialized first.

# get dimensions of image.png

# initialize modules and screen
pygame.init()
screen = pygame.display.set_mode((700,500))

# create a surface from an image import
img_surf = pygame.image.load('intro_to_pygame_images/image.png')

# get dimensions
width = img_surf.get_width()
height = img_surf.get_height()
print(width, height)

# close application
pygame.quit()
120 120

Resizing Surfaces

To resize a surface, use pygame.transform.scale(Surface, (width, height)).

# resize img_surf to width=60, height=60
img_surf_small = pygame.transform.scale(img_surf, (60,60))
# check that the resizing worked - get new dimensions of img_surf_small

# initialize modules and screen
pygame.init()

# get dimensions
width = img_surf_small.get_width()
height = img_surf_small.get_height()
print(width, height)

# close application
pygame.quit()
60 60

Rectangles

A pygame.Rect object is used to store rectangular coordinates (location data). Unlike surfaces, which can only be moved using the top left corner, rectangles can be moved using a variety of attributes:

x,y
top, left, bottom, right
topleft, bottomleft, topright, bottomright
midtop, midleft, midbottom, midright
center, centerx, centery
size, width, height
w,h

To create a rectangle, use pygame.Rect(left, top, width, height).

# create a rectangle with the top left corner located at x=100, y=200, and with width=10 and height=20
rect = pygame.Rect(100, 200, 10, 20)
# use a keyword argument to create a rectangle with its center located at x=100, y=200, and with width=10 and height=20
rect = pygame.Rect(center=(100,200), width=10, height=20)

Drawing a Rectangle on the Screen

Use pygame.draw.rect(Surface, (R,G,B), Rect) to draw a rectangle on the display surface. The pygame modules and display screen must be initialized before creating the rectangle, and pygame.display.update() must be called after draw() in order for the rectangle to appear on the screen.

# draw a red rectangle on the screen

# initialize modules, global variable, clock and screen
pygame.init()
running = True
clock = pygame.time.Clock()
screen = pygame.display.set_mode((700,500))

# create a rectangle with the top left corner located at x=100, y=200, and with width=50 and height=60
rect = pygame.Rect(100, 200, 50, 60)

# event loop
while running:
    # event listener for quitting the game
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
    
    # draw rectangle on screen in red (RED=255, GREEN=0, BLUE=0)
    pygame.draw.rect(screen, (255, 0, 0), rect)
    
    # update the display - required for the rectangle to appear
    pygame.display.update()

    # set frame rate
    clock.tick(60)

# close application
pygame.quit()

The result:

Pygame window with red rectangle

Storing Location Data of a Surface

The get_rect() method is used on surfaces to return a Rect object with the same size as the surface. By default, the position of the Rect is (0,0).

# create a surface from an image import - Surface stores image data
img_surf = pygame.image.load('intro_to_pygame_images/image.png')
# create a rectangle from the surface - Rect stores location data
img_rect = img_surf.get_rect()
# create a rectangle from a surface, located 200 pixels from the left and 400 pixels from the top of the screen
img_rect = img_surf.get_rect((200,400))

Adding an Image to the Screen

The Surface.blit() method places an image onto the screen of the pygame application. It requires:

  • A display surface
  • An image surface
  • A destination, either a rectangle or coordinates

This method must be done inside of the event loop.

The order of Surface.blit() calls is important - images will be placed on the screen in the order that they are called.

# place an image on the screen at the location of its rectangle
screen.blit(img_surf, img_rect)
# place an image on the screen so that its top left corner is at x=100, y=200
screen.blit(img_surf, (100,200))

Movement

Here we combine event listeners, surfaces and rectangles to move a player's icon around a screen.

Let's use the arrow keys to control the icon. We need an event listener for each arrow key, and a change in x and y as the result. Remember that pygame coordinates work differently than normal cartesian coordinates.

Note that pygame.display.update() must be called in the event loop to show the player's new position on the screen.

See the example below.

import pygame
from pygame.locals import *

# initialize modules and window
pygame.init()
running = True
clock = pygame.time.Clock()
screen = pygame.display.set_mode((500,500))

# player's character - a bear icon
bear_surf = pygame.image.load('intro_to_pygame_images/brown_bear.png').convert_alpha()
bear_loc = bear_surf.get_rect()

# event loop
while running:
    # event listeners
    for event in pygame.event.get():

        # user quitting game
        if event.type == QUIT:
            running = False

        # movements for player icon - uses arrow keys and movement occurs every key press
        # KEYDOWN is the downward press of any key on the keyboard
        # move() specifies the change in x and y when the key is pressed
        if event.type == KEYDOWN:
            if event.key == K_LEFT:
                bear_loc = bear_loc.move([-10, 0])
            if event.key == K_RIGHT:
                bear_loc = bear_loc.move([10, 0])
            if event.key == K_UP:
                bear_loc = bear_loc.move([0, -10])
            if event.key == K_DOWN:
                bear_loc = bear_loc.move([0, 10])

    # add bear image to screen
    screen.blit(bear_surf, bear_loc)

    # update the display surface
    pygame.display.update()

    clock.tick(60)

pygame.quit()

The result:

Pygame Window with Repeating Icon

There are three problems with the result:

Problem Solution
1. When the icon is moved around the screen, old images are still visible. In the event loop, use pygame.draw.rect() to draw a background on the screen before the icon is added. For every iteration of the event loop, the background will cover all previous icons. Simply changing the colour of the display screen using fill() would not have this effect.
2. The icon only moves when a key is pressed, but not when keys are held down. Use pygame.key.get_pressed() to retrieve the current state of all keys, so that the icon continues to move when arrow keys are held down.
3. The icon can move beyond the visible window. Use clamp_ip() to confine the icon's Rect object (bear_loc) to the screen's Rect object. Note that screen is a Surface object, so we need to call get_rect() on it first.
import pygame
from pygame.locals import *

# initialize modules and window
pygame.init()
running = True
clock = pygame.time.Clock()
screen = pygame.display.set_mode((500,500))

# player's character - a bear icon
bear_surf = pygame.image.load('intro_to_pygame_images/brown_bear.png').convert_alpha()
bear_loc = bear_surf.get_rect()

# event loop
while running:
    # event listeners
    for event in pygame.event.get():

        # user quitting game
        if event.type == QUIT:
            running = False

    # movements for player icon
    # if a key is held, the movement continues without stopping
    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        bear_loc = bear_loc.move([-10, 0])
    if keys[pygame.K_RIGHT]:
        bear_loc = bear_loc.move([10, 0])
    if keys[pygame.K_UP]:
        bear_loc = bear_loc.move([0, -10])
    if keys[pygame.K_DOWN]:
        bear_loc = bear_loc.move([0, 10])

    # add background - using a rectangle instead of using screen.fill() ensures old images aren't left behind as an object moves
    pygame.draw.rect(screen, (191,180,214), pygame.Rect(0,0,800,800))   # surface, colour, Rectangle(left coord, top coord, width, height)

    # get rectangle of display screen
    screen_rect = screen.get_rect()
    
    # confine the player's icon to the rectangle of the display screen
    bear_loc.clamp_ip(screen_rect)

    # add bear image to screen
    screen.blit(bear_surf, bear_loc)

    # update the display surface
    pygame.display.update()

    clock.tick(60)

pygame.quit()

The result:

Pygame Window with Corrected Player Movement

Obstacles

Let's make some obstacles that the player cannot cross, such as a wall. Do this using a pygame.Rect object. Collisions will be handled using the sides of the player rectangle and the wall rectangle.

In the event loop, use event listeners to check if a movement will cause a collision. If not, allow the movement to occur.

Pygame.Rect Objects

Create player's character and wall:

# player's character - bear icon
bear_surf = pygame.image.load('intro_to_pygame_images/brown_bear.png').convert_alpha()
bear_loc = bear_surf.get_rect()

# obstacle - wall
wall = pygame.Rect((200,200),(10,100))

In the event loop, check that movements are allowed:

keys = pygame.key.get_pressed()

if keys[pygame.K_LEFT]:

    # Left movement occurs UNLESS...
    # player is within y-range of wall...........................and player is left of wall.....and player will collide with wall
    if bear_loc.bottom > wall.top and bear_loc.top < wall.bottom and bear_loc.left >= wall.right and bear_loc.left - 10 < wall.right: pass
    # otherwise, move left
    else: bear_loc = bear_loc.move([-10, 0])

if keys[pygame.K_RIGHT]:
    
    # Right movement occurs UNLESS...
    # player is within y-range of wall...........................and player is right of wall.....and player will collide with wall
    if bear_loc.bottom > wall.top and bear_loc.top < wall.bottom and bear_loc.right <= wall.left and bear_loc.right + 10 > wall.left: pass
    # otherwise, move right
    else: bear_loc = bear_loc.move([10, 0])

if keys[pygame.K_UP]:

    # Upward movement occurs UNLESS...
    # player is within x-range of wall...........................and player is below wall........and player will collide with wall
    if bear_loc.right > wall.left and bear_loc.left < wall.right and bear_loc.top >= wall.bottom and bear_loc.top - 10 < wall.bottom: pass
    # otherwise, move up
    else: bear_loc = bear_loc.move([0, -10])

if keys[pygame.K_DOWN]:

    # Downward movement occurs UNLESS...
    # player is within x-range of wall...........................and player is above wall........and player will collide with wall
    if bear_loc.right > wall.left and bear_loc.left < wall.right and bear_loc.bottom <= wall.top and bear_loc.bottom + 10 > wall.top: pass
    # otherwise, move down
    else: bear_loc = bear_loc.move([0, 10])

After the event listeners, the wall must also be drawn:

# draw wall
pygame.draw.rect(screen, (154, 146, 173), wall)

Interactive Objects

Let's make an object that writes some text when the player collides with it.

Create an object to interact with (a tree):

# surface and rectangle for interactive object
tree_surf = pygame.image.load('intro_to_pygame_images/tree.png').convert_alpha()
tree_loc = tree_surf.get_rect(center = (400,400))

Create flags (boolean variables) that check the status of the interaction. This will prevent repetition of text later on:

# flag for whether the tree has been interacted with - If True, the text will appear when the player collides with the tree
play_collision_text = True

# flag for whether the user has responded yet - If True, the user has not responded yet
accept_response = True

In the event loop, create a flag that is True when the player collides with the tree:

# flag for collision with tree - True if a collision is occurring
collide_tree = pygame.Rect.colliderect(bear_loc, tree_loc)

Print some text when the player collides with the tree. Set play_collision_text to False so that the message will only print once:

# collision with tree
if collide_tree and play_collision_text:
    # prevent text from repeating
    play_collision_text = False
    print("There is a tall tree... with something at the top!")
    print("Climb tree? Yes [y] or No [n]: ")

Use an event listener to accept the player's responses.

The player has the option to climb the tree ("y" key) or not ("n" key). If the player chooses not to climb the tree, they should be able to change their decision later. Therefore, we should accept the player's responses until the "Yes" option has been fulfilled. When this happens, accept_reponse will be set to False.

The event listener should only work if:

  • the tree has been interacted with: play_collision_text = False
  • the player has not yet climbed the tree (clicked the "y" key): accept_response = True
# once the tree has been interacted with, accept responses and prevent interaction from repeating
if event.type == KEYDOWN and not play_collision_text and accept_response:
    if event.key == K_y:
        print("y")
        print("You climb to the top and find a KEY.")
        accept_response = False
    elif event.key == K_n:
        print("n")
        print("It looks too tall to climb anyway.")

At the end of the event loop, the tree should be added to the screen:

# add tree image to screen
screen.blit(tree_surf, tree_loc)

Text Handling

Let's make text appear on the screen instead of printing it to the terminal. This is done by rendering a string into text using a Font object and Font.render().

# get list of available system fonts
fonts = pygame.font.get_fonts()
for f in fonts:
   print(f)

arial
arialblack
bahnschrift
calibri
cambria
cambriamath
candara
comicsansms
consolas
constantia
corbel
couriernew
ebrima
franklingothicmedium
gabriola
gadugi
georgia
impact
inkfree
javanesetext
leelawadeeui
leelawadeeuisemilight
lucidaconsole
lucidasans
malgungothic
malgungothicsemilight
microsofthimalaya
microsoftjhenghei
microsoftjhengheiui
microsoftnewtailue
microsoftphagspa
microsoftsansserif
microsofttaile
microsoftyahei
microsoftyaheiui
microsoftyibaiti
mingliuextb
pmingliuextb
mingliuhkscsextb
mongolianbaiti
msgothic
msuigothic
mspgothic
mvboli
myanmartext
nirmalaui
nirmalauisemilight
palatinolinotype
sansserifcollection
segoefluenticons
segoemdl2assets
segoeprint
segoescript
segoeui
segoeuiblack
segoeuiemoji
segoeuihistoric
segoeuisemibold
segoeuisemilight
segoeuisymbol
segoeuivariable
simsun
nsimsun
simsunextb
sitkatext
sylfaen
symbol
tahoma
timesnewroman
trebuchetms
verdana
webdings
wingdings
yugothic
yugothicuisemibold
yugothicui
yugothicmedium
yugothicuiregular
yugothicregular
yugothicuisemilight
holomdl2assets
agencyfb
algerian
bookantiqua
arialrounded
baskervilleoldface
bauhaus93
bell
bernardcondensed
bodoni
bodoniblack
bodonicondensed
bodonipostercompressed
bookmanoldstyle
bradleyhanditc
britannic
berlinsansfb
berlinsansfbdemi
broadway
brushscript
bookshelfsymbol7
californianfb
calisto
castellar
centuryschoolbook
centaur
century
chiller
colonna
cooperblack
copperplategothic
curlz
dubai
dubaimedium
dubairegular
elephant
engravers
erasitc
erasdemiitc
erasmediumitc
felixtitling
forte
franklingothicbook
franklingothicdemi
franklingothicdemicond
franklingothicheavy
franklingothicmediumcond
freestylescript
frenchscript
footlight
garamond
gigi
gillsans
gillsanscondensed
gillsansultracondensed
gillsansultra
gloucesterextracondensed
gillsansextcondensed
centurygothic
goudyoldstyle
goudystout
harlowsolid
harrington
haettenschweiler
hightowertext
imprintshadow
informalroman
blackadderitc
edwardianscriptitc
kristenitc
jokerman
juiceitc
kunstlerscript
widelatin
lucidabright
lucidacalligraphy
lucidafaxregular
lucidafax
lucidahandwriting
lucidasansregular
lucidasansroman
lucidasanstypewriterregular
lucidasanstypewriter
lucidasanstypewriteroblique
magneto
maiandragd
maturascriptcapitals
mistral
modernno20
monotypecorsiva
extra
niagaraengraved
niagarasolid
ocraextended
oldenglishtext
onyx
msoutlook
palacescript
papyrus
parchment
perpetua
perpetuatitling
playbill
poorrichard
pristina
rage
ravie
msreferencesansserif
msreferencespecialty
rockwellcondensed
rockwell
rockwellextra
script
showcardgothic
snapitc
stencil
twcen
twcencondensed
twcencondensedextra
tempussansitc
vinerhanditc
vivaldi
vladimirscript
wingdings2
wingdings3
hpsimplified
hpsimplifiedbdit
hpsimplifiedit
hpsimplifiedlt
hpsimplifiedltit
hpsimplifiedrg
hpsimplifiedjpanlight
hpsimplifiedjpanregular
hpsimplifiedhanslight
hpsimplifiedhansregular
artifaktelementtruetype
artifaktelementregulartruetype
bankgothic
bankgothicmedium
cityblueprint
commercialpi
commercialscript
countryblueprint
dutch801roman
dutch801
dutch801extra
euroromanoblique
euroroman
isocpeur
isocteur
monospace821
panroman
romantic
romans
sansserifboldoblique
sansserif
sansserifoblique
stylus
superfrench
swiss721
swiss721outline
swiss721condensed
swiss721condensedoutline
swiss721blackcondensed
swiss721extended
swiss721blackextended
swiss721black
swiss721blackoutline
technicbold
techniclite
technic
universalmath1
vineta
acaderef
aigdt
amdtsymbols
amgdt
geniso
complex
gdt
gothice
gothicg
gothici
greekc
greeks
isocp2
isocp3
isocp
isoct2
isoct3
isoct
italicc
italict

monotxt
proxy1
proxy2
proxy3
proxy4
proxy5
proxy6
proxy7
proxy8
proxy9
romanc
romand
romant
scriptc
scripts
simplex
syastro
symap
symath
symeteo
symusic
txt
cmb10
cmex10
cmmi10
cmr10
cmss10
cmsy10
cmtt10
dejavusans
dejavusansoblique
dejavusansdisplay
dejavusansmono
dejavusansmonooblique
dejavuserif
dejavuserifdisplay
stixgeneralregular
stixgeneralbold
stixgeneralbolditalic
stixgeneralitalic
stixnonunicoderegular
stixnonunicodebold
stixnonunicodebolditalic
stixnonunicodeitalic
stixsizefivesymregular
stixsizefoursymbold
stixsizefoursymregular
stixsizeonesymbold
stixsizeonesymregular
stixsizethreesymbold
stixsizethreesymregular
stixsizetwosymbold
stixsizetwosymregular

Create a Font object with a font and size:

# font = "lucidaconsole", size = 14-point
font = pygame.font.SysFont("lucidaconsole", 14)

Inside the event loop, draw the text onto a new Surface using Font.render(text, antialias, color, background). The antialias argument smoothes out the letters if True, so if a pixelated font is desired, set antialias=False. Note that this function can only render one line of text at a time.

# apply the text to a surface; make the text smooth and in red on a white background
text = "Have a nice day"
text_surf = font.render(text, True, "red", "white")

In the same way an image can be added to the screen, use Surface.blit() to make the text appear in the game window:

# make the text appear on the screen with the top left corner located at x=100, y=200
screen.blit(text_surf, (100,200))

Improving Efficiency

There is no built-in way to handle multiple lines of on-screen text in pygame. If multiple lines are needed, use a function like the one below:

def write_lines(display_surface: pygame.Surface, lines: list[str], x: int, y: int, increment: int, colour: str="black"):
    """
    Draws lines of text on the display surface. The top left corner of the first line of text
    begins at position (x,y) and the y-value increases by the increment for each line.
    
    Parameters:
    - display_surface (pygame.Surface): surface to display text on
    - lines (list[str]): text to add to screen
    - x (int): x-value of the top left corner of the first line of text
    - y (int): y-value of the top left corner of the first line of text
    - increment (int): number of pixels the y-value will increase by for each consecutive line
    - colour (str): colour of text

    Returns: None
    """

    for line in lines:
        text_surf = font.render(line, True, colour)
        display_surface.blit(text_surf, (x,y))
        y += increment

Note that the increment should always be larger than the height of the font to prevent lines of text from overlapping.

Going Forward

Feeling confident in your pygame skills? Why not add characters into the game that can talk to the player through the power of AI! Check out the Pygame with AI tutorial here.

References and Companion Files

References:

Companion Files: