A More Compact Pygame Tutorial


There's strictly no warranty for the correctness of this text. You use any of the information provided here at your own risk.


Contents:

  1. Introduction
  2. A Minimal Example
  3. Moving To Object Orientation
  4. Keyboard- and Joystick Input
  5. Sprite Movement and Speed Control
  6. Skeleton Example
  7. The Methods "update()" and "draw()


1. Introduction

Pygame is a library , that makes it possible to write computer games in Python. Mainly 2D, Arcade-style games. Think of C64- or Amiga-games. On modern PCs, Pygame would be fast enough to recreate such games, that were originally written in Assembly language, in Python.

If you wanted to write 3D-games, Pygame wouldn't be the preferred choice. It is possible to combine it with the "PyOpenGL" library to create 3D graphics, or to write the 3D code yourself, but that's all relatively difficult and kind of deprecated. It is much easier to use the game engine "Panda3D". Then you can create elaborate 3D objects in the powerful software "Blender", and import them into your Panda3D-game. The game logic of such a game can also be written in Python. The results are quite impressive, when you take a look at this beginner tutorial for example.
Many of today's commercial game developers may have moved on to a 3D game engine called "Unity", but "Panda3D" (originally developed by Disney) has the advantage of being open source software.

But back to Pygame. So Pygame is mainly used to create 2D games.

This is my second and newer webpage about Pygame. The older page is still here. This page is more streamlined, as I think, I'm understanding Pygame a little better now. The old page is more detailed though, so instead of replacing it, I upload this as an addition.


2. A Minimal Example

A minimal Pygame-script would for example look like this. This script just displays a red rectangle, and you can press "q" to quit:

#!/usr/bin/python3
# coding: utf-8
import pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption('Sprite Example')
clock   = pygame.time.Clock()
surface = pygame.Surface((20, 20))
surface = surface.convert()
surface.fill((200, 200, 0))
rect = surface.get_rect()
rect.topleft = (400, 300)
while True:
    clocktick = clock.tick(50)
    screen.fill((0, 0, 0))
    screen.blit(surface, rect)
    pygame.event.pump()
    pressed = pygame.key.get_pressed()
    if pressed[pygame.K_q]:
        break
    pygame.display.flip()
pygame.quit()

Explanation:

So there are quite a number of things to consider, and at first this kind of code may look rather unfamiliar. But you have to admit, it makes sense, if you want to write a game.


3. Moving To Object Orientation

Let's write the example script above in a more object oriented way, as we would, when working on a real game project:

#!/usr/bin/python3
# coding: utf-8

import pygame

class YellowGameObject(pygame.sprite.Sprite):

    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.x = x
        self.y = y
        self.createImage()

    def createImage(self):
        # We're changing the name "surface" to the name "image" here:
        self.image = pygame.Surface((20, 20))
        self.image = self.image.convert()
        self.image.fill((200, 200, 0))
        self.rect = self.image.get_rect()
        self.rect.topleft = (self.x, self.y)

    def draw(self, screen):
        screen.blit(self.image, self.rect)

class RectanglesGroup(pygame.sprite.Group):

    def __init__(self):
        pygame.sprite.Group.__init__(self)

    def draw(self, screen):
        for s in self.sprites():
            s.draw(screen)

class Main:

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption('Sprite Example')
        self.clock  = pygame.time.Clock()
        self.initSprites()
        while True:
            self.clock.tick(50)
            self.screen.fill((0, 0, 0))
            self.rectanglesgroup.draw(self.screen)
            pygame.event.pump()
            pressed = pygame.key.get_pressed()
            if pressed[pygame.K_q]:
                break
            pygame.display.flip()
        pygame.quit()

    def initSprites(self):
        self.yellowgameobject = YellowGameObject(400, 300)    
        self.rectanglesgroup = RectanglesGroup()
        self.rectanglesgroup.add(self.yellowgameobject)

Main()

So we would write a class for each game object (the "YellowGameObject"-object in this case). And we would let it inherit from "pygame.sprite.Sprite". That wouldn't actually give the class much sprite functionality, it just would make it compatible to the usage of Pygame groups.
Inside the game object's class, we define "self.image" as its surface. And "self.rect" as the surface's rect. This is also expected for the group-functionality.
We can then define a group of such objects and add the game object to it.
In the main-loop, the group's "draw"-method is called, in order to blit all game objects in the group to the screen with a single call.


4. Keyboard- and Joystick Input

In a game, you probably want to move your character with the PC's cursor keys and use the left "Control"-key for "fire". And if you have a joystick, you probably want to use it in a similar way (up, down, left, right, fire).
Using the Pygame input routines (which themselves are a bit more "low-level"), I wrote a class "InputHandler" which provides just that behaviour (with non-blocking keyboard input). It can be used like this:

import pygame
from inputhandler import InputHandler
...
class Main:
    def ....       
        self.ih = InputHandler(hasjoystick = True)
        ...
        while self.running:
            self.clocktick = self.clock.tick(FPS)
            ....
            if self.checkInput() == "quit":
                print("Bye.")
                self.running = False
                continue
            pygame.display.flip()
        pygame.quit()

    def checkInput(self):
        # "action" is a dictionary with the keys "left right up down fire quit":
        action = self.ih.getKeyboardAndJoystickAction()
        for i in action.keys():
            if i == "quit" and action[i]:
                return "quit"
            if action[i]:
                # Do something (move or fire).
                pass
        return 0

So in the method ".checkInput()", you get a dictionary called "action" from the input handler with the keys "left", "right", "up", "down", "fire" and "quit". And if one or several of the keyboard-keys mentioned above have been pressed, the corresponding values in the dictionary have turned from "False" to "True".
In my opinion that makes it rather easy to handle user input.
If you want different keyboard behaviour, you'd have to edit or rewrite the input handler. Editing it to define other keyboard keys is actually not that difficult.


5. Sprite Movement and Speed Control

In general, sprites are moved by setting a new value for the rect's "topleft" attribute, and then blitting the surface at the rect's new location to the screen. (Rect-objects also have the attributes "x" and "y", which represent the elements of "topleft".)
So if you get for example "left" from the input handler, you'd have a method ".move()" in the sprite's class, that changes the rect accordingly.
But there's another twist you have to keep in mind: If you just move the sprite by a few pixels, you'll get into trouble with controlling the speed, especially if several sprites are supposed to move with different speeds.
Instead it is done like this: The main loop also passes the return value of the "clock.tick()" function to the sprite's ".move"-method. This value represents the time (in milliseconds) that has passed for one frame. And the sprite's class also has an attribute "speed".
Then the sprite's rect is moved by the amount:

self.speed * clocktick

That way most of the problems with controlling the speed of sprites are gone.

So it's:

class YellowGameObject:
    def __init__(self, x, y, speed):
        ...
        self.speed = speed
        ...

    def move(self, direction, clocktick):
        if direction == "left":
            self.rect.x -= self.speed * clocktick
        ...

class Main:
    def __init__(self):
        ...
        self.initSprites()
        ...
        while self.running:
            self.clocktick = self.clock.tick(FPS)
            ....
            if self.checkInput() == "quit":
                print("Bye.")
                self.running = False
                continue
            pygame.display.flip()
        pygame.quit()

    def initSprites(self):
        self.yellowthing = YellowGameObject(400, 300, speed = 0.1)
        ...

    def checkInput(self):
        ...
        for i in action.keys():
            ...
            if action[i]:
                self.yellowthing.move(i, self.clocktick)
        return 0

When running at 50 frames per second, "self.clocktick" should be at around 20. So "self.speed" is typically smaller than 1.


There may be situations, when a sprite shouldn't move faster than one unit per frame. For example, if a collision with a thin line is to be detected, the sprite may have passed beyond that line in a single frame when moving too fast.
So here's still the method, I found earlier, to slow movement down.
The function "pygame.time.get_ticks()" returns the number of milliseconds that have passed, since the application started. With this, you can set up a delay time in a certain function like this:

#!/usr/bin/python
import pygame
...
SPEEDSETTING = 50
...
self.starship = Spaceship(delaytime = SPEEDSETTING * 2)

# Main loop:
while True:
    self.timer = pygame.time.get_ticks()
    # Mainloop's timer is passed to the parameter "currenttime" of the class' update method:
    self.starship.move(currenttime = self.timer)
    self.starship.update(self.screen)
    pygame.display.flip()

class Spaceship(pygame.sprite.Sprite):

    def __init__(self, delaytime):
        pygame.sprite.Sprite.__init__(self)
        # The class has its own timer, that is different from main loop's timer:
        self.timer        = pygame.time.get_ticks()
        self.delaytime    = delaytime
        self.surface      = pygame.Surface((100, 100))
        self.rect         = self.surface.get_rect()
        self.rect.topleft = (100, 100)

    def move(self, currenttime):

        if currenttime - self.timer ≤ self.delaytime:
            return

        # Doing something slowed down:
        self.rect.x += 1
        ...
        # This line is necessary at the end to make it work:
        # Originally it was : self.timer += self.delaytime
        # but I think the following is correct:
        self.timer = currenttime

    def update(self, surface):
        surface.blit(self.surface, self.rect)


6. Skeleton Example

Now we should be ready to write a kind of skeleton script, that could be used as a template for game projects.

As explained above, the input handler has been outsourced and has to be imported as well.

#!/usr/bin/python3
# coding: utf-8

import pygame
import os
from inputhandler import InputHandler

FPS = 50

COLORS = {"black"       : (0, 0, 0),
          "red"         : (192, 0, 0),
          "transparent" : (0, 0, 0, 0) }

class MySprite(pygame.sprite.Sprite):

    def __init__(self, speed):
        pygame.sprite.Sprite.__init__(self)
        self.speed = speed

    def getPosition(self):
        return self.rect.topleft

    def setPosition(self, x, y):
        self.rect.topleft = (x, y)

    def draw(self, screen):
        screen.blit(self.image, self.rect)

class Ball(MySprite):

    def __init__(self, speed):
        MySprite.__init__(self, speed)
        self.createImage()

    def move(self, direction, screensize, clocktick):
        if direction == "left":
            self.rect.x -= self.speed * clocktick
            if self.rect.x < 0:
                self.setPosition(screensize[0], self.rect.y)
        if direction == "right":
            self.rect.x += self.speed * clocktick
            if self.rect.x > screensize[0]:
                self.setPosition(0, self.rect.y)
        if direction == "up":
            self.rect.y -= self.speed * clocktick
            if self.rect.y < 0:
                self.setPosition(self.rect.x, screensize[1])
        if direction == "down":
            self.rect.y += self.speed * clocktick
            if self.rect.y > screensize[1]:
                self.setPosition(self.rect.x, 0 )

    def createImage(self):
        self.image = pygame.Surface((30, 30))
        self.image = self.image.convert_alpha()
        self.rect  = self.image.get_rect()
        self.image.fill(COLORS["transparent"])
        pygame.draw.circle(self.image, COLORS["red"], (self.rect.width // 2, self.rect.height // 2), self.rect.height // 2)

class BallGroup(pygame.sprite.Group):

    def __init__(self, *args):
        pygame.sprite.Group.__init__(self, *args)

    def draw(self, surface):
        for s in self.sprites():
            s.draw(surface) 

class Main:

    def __init__(self):
        os.environ['SDL_VIDEO_WINDOW_POS'] = "185, 30"
        pygame.init()
        self.screensize = (800, 600)
        self.screen = pygame.display.set_mode(self.screensize)
        pygame.display.set_caption("Hello Ball")
        self.ih = InputHandler(True)
        self.clock = pygame.time.Clock()
        self.initSprites()
        self.running = True
        while self.running:
            self.clocktick = self.clock.tick(FPS)
            self.screen.fill(COLORS["black"])
            self.ballgroup.draw(self.screen)
            if self.checkInput() == "quit":
                print("Bye.")
                self.running = False
                continue
            pygame.display.flip()
        pygame.quit()

    def initSprites(self):
        self.ball = Ball(speed = 0.3)
        self.ball.setPosition(400, 300)
        self.ballgroup = BallGroup()
        self.ballgroup.add(self.ball)

    def checkInput(self):
        # "action" is a dictionary with the keys "left right up down fire quit":
        action = self.ih.getKeyboardAndJoystickAction()
        for i in action.keys():
            if i == "quit" and action[i]:
                return "quit"
            if action[i]:
                self.ball.move(i, self.screensize, self.clocktick)
        return 0

Main()

In the meantime, I watched the 12-part Pygame tutorial of Caleb Shaw, and split my code into several files accordingly.
I also made use of the methods "update()" and "draw()", that are inherited from "pygame.sprite.Group", as shown in the tutorial.


7. The Methods "update()" and "draw()

The Methods "update()" and "draw() are supposed to be inherited from "pygame.sprite.Sprite" and "pygame.sprite.Group".
The sprites in your program should inherit from "pygame.sprite.Sprite". Then the sprite can be added to groups.
There should be a group "all_sprites", that contains all the sprites in the program.
The main-loop then calls:

self.all_sprites.update()
self.all_sprites.draw(self.screen)

The "update()"-method is used for managing the changes to the sprite before it is drawn. The changes may especially regard the sprite's position or his look (animation).
If for example the sprite moves a pixel to the right this frame, the "update()"-method is supposed to take care of that.
So "update()" has to be implemented in the sprite's class.

"draw()" on the other hand, can just be inherited.
It takes the surface as an argument, to which "self.image" of the sprite is blitted.
So to make this work, the attributes "self.image" and "self.rect" have to be defined in the sprite's class.
Although I haven't looked into the implementation of draw(), it basically does this:

def draw(self, surface):
    surface.blit(self.image, self.rect)

So most of the time, there won't be a special "draw()"-method in the classes of sprites, because it is inherited, when the sprite inherits from "pygame.sprite.Sprite". Here's an example again:

#!/usr/bin/python3
# coding: utf-8

import pygame

class YellowGameObject(pygame.sprite.Sprite):

    def __init__(self, game, x, y, speed):
        pygame.sprite.Sprite.__init__(self)
        self.game  = game
        self.x     = x
        self.y     = y
        self.speed = speed
        self.createImage()

    def createImage(self):
        self.image = pygame.Surface((20, 20))
        self.image = self.image.convert()
        self.image.fill((200, 200, 0))
        self.rect = self.image.get_rect()
        self.rect.topleft = (self.x, self.y)

    def update(self):
        # Prepares the sprite's position and look.
        self.moveRight()

    def moveRight(self):
        change = self.speed * self.game.clocktick
        self.rect.x += change
        if self.rect.x > 800:
            self.rect.x = 0

class Main:

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        pygame.display.set_caption('Sprite Example')
        self.clock  = pygame.time.Clock()
        self.initSprites()
        while True:
            self.clocktick = self.clock.tick(50)
            self.screen.fill((0, 0, 0))

            self.all_sprites.update()
            self.all_sprites.draw(self.screen)

            pygame.event.pump()
            pressed = pygame.key.get_pressed()
            if pressed[pygame.K_q]:
                break
            pygame.display.flip()
        pygame.quit()

    def initSprites(self):
        self.yellowgameobject = YellowGameObject(self, 400, 300, 0.5)
        self.all_sprites = pygame.sprite.Group(self.yellowgameobject)

Main()


Back to the main page


Author: hlubenow2 {at-symbol} gmx.net