Chapter 5

Creating Games

IN THE LAST chapter you built graphical software using a GUI toolkit. This made it really easy to add buttons, text boxes, and all sorts of widgets to our software, but there's another sort of graphical application that doesn't use any of these things: games. We still need to draw things on the screen, but instead of check boxes and menus, we want fireballs, heroes, pits of doom, and all manner of fantastical graphics. Clearly, PySide isn't up to the task, but there is another module that will do exactly what we want, PyGame.

Raspbian comes with PyGame installed, but only for Python 2. Since we're building with Python 3, we'll need to install it. In this case, you'll have to compile the module from scratch, but this is a good chance to learn the process. First you need to install all the packages that PyGame will need, so open LXTerminal, and use apt-get (the package manager) to install the dependencies like so:

sudo apt-get update
sudo apt-get install libsdl-dev libsdl-image1.2-dev
   libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev
   libportmidi-dev libavformat-dev libswscale-dev
   mercurial python3-dev

You'll notice that a lot of this code ends in -dev. These are the development files. You need them when compiling software that uses those libraries.

The next step is to get a copy of the latest version of PyGame. Since we're using Python 3, we need to get the very latest version, so we'll grab it straight from the development platform with the following:

hg clone https://bitbucket.org/pygame/pygame
cd pygame

These two lines will download the current version of PyGame into a new directory called pygame, then move into it. Once it's there, you can build and install the module for Python 3 with:

python3 setup.py build
sudo python3 setup.py install

If everything's gone well, you'll now be able to use PyGame in Python 3. To test that it works, open up the Python Shell in IDLE, and enter:

>>> import pygame

If there are any errors, then something has gone wrong and you'll need to go back and repeat the steps before continuing with the chapter. Whilst you're in the shell, you can try out a few things to see how PyGame works.

>>> pygame.init()
>>> window = pygame.display.set_mode((500, 500))
>>> screen = pygame.display.get_surface()
>>> rect = pygame.Rect(100, 99, 98, 97)
>>> pygame.draw.rect(screen, (100, 100, 100), rect, 0)
>>> pygame.display.update()

You should see this open a new window and draw a grey rectangle. The first line just gets PyGame up and running. The second line opens a new window. The parameter - (500, 500) - is a tuple containing the width and height of the new window. Notice how there's two opening and closing brackets? One set denotes the parameter and the other, the tuple. The third line gets the surface that you can draw on and stores it in the variable screen.

There are two main PyGame classes that you'll be using: Rect and Sprite. The second one we'll look at later, but the first, Rect, is absolutely critical to the way PyGame works. In the fourth line, you create one of these rectangles, which has its top-left corner at coordinates 100, 99, and is 98 pixels wide by 97 tall. The one thing you need to know about PyGame coordinates is that they start from the top-left corner of the screen, so compared to normal graph coordinates, they're upside down.

Rectangles aren't always displayed on the screen (they serve a number of other useful purposes as you'll discover later), but this one will be drawn, and that's done in the next line. The parameter (100, 100, 100) holds the red, green, and blue colours (each one is from 0 to 255), and the 0 is the line thickness (0 means fill, 1 or higher means line thickness). If you were paying attention as you typed, you'd notice that the rectangle doesn't appear in the window. That's because you need to update the screen for any changes to take effect. This we do in the final line.

Try drawing a few other rectangles on the screen to get a feel for how all the different parameters affect the shape.

Building a Game

Those are the very basics of PyGame. Now onto the game you'll build, which is a simple platform game where you control a character who has to run and jump through a level to try and reach a goal. To make it a little tricky, we'll rain down fireballs that she has to dodge as she makes her way there, and if she falls off the platform, there'll be a burning pit of doom waiting to finish her off.

When programming, we don't usually start with the first line. Instead, we make a plan of how we think the program will work. This is like the skeleton of the program that we'll then flesh out. We do this by designing all the classes and their methods, but leaving the implementation blank. As we program, we add flesh to this design until, hopefully, we end up with a finished program. For our game, we have the following design (you can type it out, but it'll be easier to download it from the website as chapter5-pygame-skell.py and add to it as you go through the chapter):

import pygame
import sys
from pygame.locals import *
from random import randint

class Player(pygame.sprite.Sprite):
    '''The class that holds the main player, and controls
    how they jump. nb. The player doesn't move left or right,
    the world moves around them'''

    def _  _init_  _(self, start_x, start_y, width, height):
        pass

    def move_y(self):
        '''this calculates the y-axis movement for the player
        in the current speed'''
        pass

    def jump(self, speed):
        '''This sets the player to jump, but it only can if
        its feet are on the floor'''
        pass

class World():
    '''This will hold the platforms and the goal.
    nb. In this game, the world moves left and right rather
        than the player'''

    def _  _init_  _(self, level, block_size, colour_platform,
                 colour_goals):
        pass

    def move(self, dist):
        '''move the world dist pixels right (a negative dist
        means left)'''
        pass

    def collided_get_y(self, player_rect):
        '''get the y value of the platform the player is
        currently on'''
        pass

    def at_goal(self, player_rect):
        '''return True if the player is currently in contact
        with the goal. False otherwise'''
        pass

    def update(self, screen):
        '''draw all the rectangles onto the screen'''
        pass

class Doom():
    '''this class holds all the things that can kill the player'''
    
    def _  _init_  _(self, fireball_num, pit_depth, colour):
        pass

    def move(self, dist):
        '''move everything right dist pixels (negative dist
        means left)'''
        pass

    def update(self, screen):
        '''move fireballs down, and draw everything on
        the screen'''
        pass

    def collided(self, player_rect):
        '''check if the player is currently in contact with
        any of the doom.
        nb. shrink the rectangle for the fireballs to
        make it fairer'''
        pass

class Fireball(pygame.sprite.Sprite):
    '''this class holds the fireballs that fall from the sky'''
    
    def _  _init_  _(self):
        pass

    def reset(self):
        '''re-generate the fireball a random distance along
        the screen and give them a random speed'''
        pass

    def move_x(self, dist):
        '''move the fireballs dist pixels to the right
        (negative dist means left)'''
        pass

    def move_y(self):
        '''move the fireball the appropriate distance down
        the screen
        nb. fireballs don't accellerate with gravity, but
        have a random speed. if the fireball has reached the
        bottom of the screen, regenerate it'''
        pass

    def update(self, screen, colour):
        '''draw the fireball onto the screen'''
        pass

#options
#initialise pygame.mixer
#initialise pygame
#load level
#initialise variables
finished = False
#setup the background
while not finished:
    pass
    #blank screen
    #check events
    #check which keys are held
    #move the player with gravity
    #render the frame
    #update the display
    #check if the player is dead
    #check if the player has completed the level
    #set the speed

There are only a few pieces of actual code here, but it's enough for us to know what's going on. This is actually a legal Python program, so you can enter it and run it. It won't do anything except spin itself round in a loop until you hit Ctrl+C to stop it, but this gives you a base to work from. As we add pieces, we'll make sure it stays as a running program so you can constantly check how it's playing, and that it's working correctly. Notice the pass statement in every method. This statement does nothing, but Python complains if you have a method with no code in it, so this simple line is required for the code to run.

This is quite a good way to start when you write your own programs. Rather than trying to create the whole thing in one go, you can start by planning how everything will work, and then build up bit-by-bit until you have a fully working program. If needed, you can always change your plan, but it helps to know what you're working towards.

Initialising PyGame

We'll now add a couple of things to get it started. Add the following code to the file where the comments match.

#options
screen_x = 600
screen_y = 400
game_name = Awesome Raspberry Pi Platformer"

#initialise pygame
pygame.init()
window = pygame.display.set_mode((screen_x, screen_y))
pygame.display.set_caption(game_name)
screen = pygame.display.get_surface()

#initialise variables
clock = pygame.time.Clock()

    #check events
    for event in pygame.event.get():
        if event.type == QUIT:
            finished = True

    #set the speed
    clock.tick(20)

You should recognise the section under initialise pygame from earlier. The only change here is that we've taken the screen size out and stored it in two variables. This is so that you can easily change it later without having to try to remember what code does what. All the key options will be stored as global variables in the same place to allow you to tweak the way the game works.

The section under #check events just waits until the user clicks on the cross to close the window, then exits the loop. The two lines for the clock use PyGame's timer to moderate the speed the loop runs at. Since each turn of the loop will correspond to a singe frame of the game, we need to make sure it doesn't run too fast (otherwise, all the action would be over before the user had a chance to do anything). You could just tell Python to sleep for a certain amount of time like you did in the simple turtle game in Chapter 2, but this has the slight problem that you don't know how long the rest of the loop will take. If you run it on a slower computer, the game will run at a different speed to on a fast machine. clock.tick(fps) is the solution to this. It tries to hold the loop at fps loops per second by pausing the loop for the appropriate amount of time, taking into account how long the rest of the loop has taken to run. In the previous code, the final line will calculate how long to wait for so that the loop runs exactly 20 times a second.

Now, let's start building the classes, starting with the Player class. Add the following initialisation method to it:

def _  _init_  _(self, start_x, start_y, width, height):

   pygame.sprite.Sprite._  _init_  _(self)
   self.image = pygame.transform.scale(
      pygame.image.load(player_image), (width, height))
   self.rect = self.image.get_rect()
   self.rect.x = start_x
   self.rect.y = start_y
   self.speed_y = 0
   self.base = pygame.Rect(start_x, start_y + height, width,2)

The key thing about Player is that it inherits from pygame.sprite.Sprite. This allows you to use it to draw an image, but first you have to set up two key variables: self.image and self.rect. Once these are set up, the parts it inherits from pygame.sprite.Sprite will allow you to draw it. Fairly obviously, self.image is the image that you want the sprite to have and self.rect is the rectangle that PyGame will draw it in. Once set up, you can then move and manipulate this rectangle just like any other, and PyGame will move the image round the screen for you. As you'll see later, you move rectangles round by updating their x and y attributes.

The penultimate local variable (speed_y) is used to keep track of the player's up and down speed as she jumps, whilst the final one (base) is a very short rectangle that represents the character's feet. You'll use this to check whether she's standing on a platform.

The player is now ready to draw on the screen, but first you'll need a bit more code. Add the following to the appropriate areas:

#options
player_spawn_x = 50
player_spawn_y = 200
player_image = "lidia.png"

#initialise variables
player = Player(player_spawn_x, player_spawn_y, 20, 30)
player_plain = pygame.sprite.RenderPlain(player)

#render the frame
player_plain.draw(screen)

#update the display
pygame.display.update()

In order to draw a sprite, you need to give it an image to draw. If you're artistic, you may want to create this yourself. However, there's a great collection of images you can use for games at http://opengameart.org. These are all free to download and use in your games, but some of them have licenses that say if you make a game with it, and you distribute that game to other people, you have to let them have the Python code so they can modify it, and build other games with your code if they want to. This concept is known as open source (see note). There are links on every file of http://opengameart.org that tell you exactly what license they're under. The important thing to realise is that you don't have to worry about it unless you distribute your game to other people. Not all of the files there will work well with PyGame. We recommend sticking with .png files for images. After a bit of searching, we like using http://opengameart.org/sites/default/files/styles/medium/public/lidia.png for our game's main character, but feel free to pick a different one (although you'll have to update the player_image variable). As the code is currently, it'll look for the player_image image file in the same directory it's being run from.

To draw a sprite, you also need to add it to a RenderPlain. This just creates the object that you draw on the screen. Here, the player object has its own RenderPlain object called player_plain.

You should now be able to run the code and it'll display the sprite at the coordinates player_spawn_x, player_spawn_y. Technically, it'll be redrawing it 20 times a second, but since it's always in the same place, you can't tell. It should look like Figure 5-1.

Figure 5-1: The simple beginnings of the platform game.

9781118717059-fg0501.tif

With the character drawn, the next task is to make her move. This is just some test code to make sure the animation is working properly. A little later you'll update this to let your character jump. Add the following method to the Player class:

def move_y(self):
    self.rect.y = self.rect.y + 1

This will just move the character slowly down. For this to do anything, though, we'll have to add the following to the appropriate places in our game loop:

#blank screen
screen.fill((0, 0, 0))

#move the player with gravity
player.move_y()

You can now run the code, and you'll see the player move down the window until she disappears off the bottom. We can't do much more with her until we've built a world for her to move round in.

Creating a World

We want to make it as easy as possible to extend this game and make it awesome, so we want it to be really easy to design new levels. We've done this by defining each level as a list of strings. Each string corresponds to a line on the screen, and each character in the string corresponds to a block on that line. A - means that there's a platform there, a G means there's a goal there (the place the player has to reach to finish the level), and anything else means it's blank. To create the basic level, then, add the following under #options:

level=[
 "                              ",
 "                              ",
 "                              ",
 "                              ",
 "                              ",
 "                              ",
 "                              ",
 "          ---                G",
 "     -- --    ---       ------",
 " -- -            -------      "]]
platform_colour = (100, 100, 100)
goal_colour = (0, 0, 255)

If you've entered it correctly, all the lines should be the same length. This is a really simple level, but it'll do for testing. The final two lines also set the colours for the platform and the goal, respectively. As we saw with the rectangle at the start of this chapter, these colours are in RGB values.

Now add the following code to the _ _init_ _ method of the World class to load this:

        def _ _init_ _(self, level, block_size,
               colour_platform,
               colour_goals):
        self.platforms = []
        self.goals = []
        self.posn_y = 0
        self.colour = colour_platform
        self.colour_goals = colour_goals
        self.block_size = block_size

        for line in level:
            self.posn_x = 0
            for block in line:
                if block == "-":
                    self.platforms.append(pygame.Rect(
                        self.posn_x, self.posn_y,
                        block_size, block_size))
                if block == "G":
                    self.goals.append(pygame.Rect(
                        self.posn_x, self.posn_y,
                        block_size, block_size))
                self.posn_x = self.posn_x + block_size
            self.posn_y = self.posn_y + block_size

The code is fairly simple; it just loops through every line, then every character in the line, and builds the appropriate rectangles when it finds the appropriate characters. Before you can use these blocks, you need to also add the update method to the World class, which will draw the blocks onto the screen:

def update(self, screen):
        '''draw all the rectangles onto the screen'''

        for block in self.platforms:
            pygame.draw.rect(screen, self.colour, block, 0)
        for block in self.goals:
            pygame.draw.rect(screen, self.colour_goals, block, 0)

Now you just need to add the following code to create the objects and render them. As always, put them in the right place by looking at the comments.

#initialise variables
world = World(level, 30, platform_colour, goal_colour)

#render the frame
world.update(screen)

You can now run the game, but you still won't find much to play. The world will be drawn, but the character will slowly fall through the level, and continue falling until she disappears off the screen.

Detecting Collisions

Fortunately, it's really easy to get two game elements to interact using PyGame's Rect's colliderect() method. This is incredibly simple, and the format is

rect1.colliderect(rect2)

Where rect1 and rect2 are rectangles. This will return True if the two rectangles overlap, and False otherwise. You can use this to detect when the player is in contact with the world so she doesn't just fall through it. Start with the World class and add:

def collided_get_y(self, player_rect):
        '''get the y value of the platform the player is
        currently on'''
        return_y = -1
        for block in self.platforms:
            if block.colliderect(player_rect):
                return_y = block.y - block.height + 1
        return return_y

This doesn't just check if the player is in contact with any part of the world, but also returns the top of the rectangle that the player is touching, or -1 if the player isn't touching anything. The next step is to update the Player class to move or not, as appropriate

def move_y(self):
    '''this calculates the y-axis movement for the player
    in the current speed'''
    collided_y = world.collided_get_y(self.base)
    if self.speed_y <= 0 or collided_y < 0:
        self.rect.y = self.rect.y + self.speed_y
        self.speed_y = self.speed_y + gravity
    if collided_y > 0 and self.speed_y > 0:
        self.rect.y = collided_y
    self.base.y = self.rect.y+self.rect.height

You'll also need to add one thing to help the player fall realistically:

#options
gravity = 1

You can now run this. The player will now fall until she rests on top of the platform, as shown in Figure 5-2.

Figure 5-2: Our heroine can now stand atop the world we've created.

9781118717059-fg0502.tif

Let's take a look at move_y() to see why this happens. The code deals with two possibilities, and each one has its own if block. The first possibility is that the players are free to move up or down depending on their current speed and gravity. This is the case if they're not touching any part of the world (that is, collided_get_y returns -1). We also want the players to be able to jump up through platforms to get to higher ones, so if the players are currently moving upwards (that is, if self.speed_y <= 0), we treat the character as though she's not touching the world. If either of these conditions is true, then we run:

self.rect.y = self.rect.y + self.speed_y
self.speed_y = self.speed_y + gravity

This moves the character by a distance determined by the current speed, then updates the current speed by gravity (gravity is an acceleration, so this mimics the physics of the real world).

The second possibility (which is checked by the second if block) is that the players are currently in contact with a platform and they're not moving up. If this is the case, the program just adjusts the coordinates of the character so that she's correctly aligned with the world. This is because if the players fall at more than one pixel per frame, it's possible that they could be several pixels below the top of the platform before this is checked. Later on, you may notice that sometimes you can see the character going down then back up slightly as she lands from higher falls, but this mimics people recovering from a landing.

Moving Left and Right

You've almost got something that represents a game now. There's a player and a world, but the players still can't explore it. The next thing to add is movement. There'll be two kinds of movements. Firstly we'll allow the players to jump, but only if they're in contact with the floor, and secondly they'll be able to move left and right.

First add the jump() method to the Player class. Since you already have the character falling, jumping is simple. All you have to do is make sure she's on the ground, then set her moving upwards and let her fall on her own:

def jump(self, speed):
    if world.collided_get_y(self.base)>0:
        self.speed_y = speed

Now to move the player left and right. Actually, it's easier if the players stay still and the world moves left and right behind them. This gives the effect of moving without the problem of the character disappearing off the screen.

All you need to do is loop through every rectangle in the world (that is, both the platforms and the goal), and move them by a given offset. We could just update the rectangle's x and y attributes, but there are also two methods in the Rect class that you and use: move(x-distance, y-distance) and move_ip(x-distance, y-distance). move() returns a new rectangle that is the same but offset by the given distances, whilst move_ip() changes the current rectangle by the distances (ip stands for in place). Since we don't want to keep creating new rectangles every time we move, we'll use move_ip(). The code is

def move(self, dist):
    for block in self.platforms + self.goals:
        block.move_ip(dist, 0)

The only thing left is to add code to the main loop to run the appropriate methods when keys are pressed. There are two ways of doing this in PyGame. The first way involves listening for keypress events and then taking action depending on which keypresses are detected. This is good if you only care about when keys are pressed. The second way is using pygame.key.get_pressed() to return a list with an entry for each key. The value of the item that corresponds to a key will be True if it's held down and False if it isn't. This second method works better if you want users to be able to hold down keys to keep moving. Since we do want users to be able to hold down keys, add the following to the appropriate part of the game loop:

#check which keys are held
    key_state = pygame.key.get_pressed()
    if key_state[K_LEFT]:
        world.move(2)
    elif key_state[K_RIGHT]:
        world.move(-2)
    if key_state[K_SPACE]:
        player.jump(jump_speed)

Note that K_LEFT, K_RIGHT, and K_SPACE are all constants that we imported with pygame.locals. There are also K_a to K_z for the letter keys.

Add the option for the speed of the jump (negative because the PyGame coordinate system is from the top-left):

#options
jump_speed = -10

If you run the code now, you'll have what could be called the basics of a platform game (see Figure 5-3). You can move the character about, and jump over gaps. However, two crucial parts are missing. Firstly, there's no way to complete the level, and secondly, there's nothing trying to stop you.

Figure 5-3: The basics of a platform game running on a Raspberry Pi with less than two hundred lines of Python.

9781118717059-fg0503.tif

Reaching the Goal

Let's deal with the first of these shortcomings first. Partly because it's easier and partly because you'll then have a game you can play-test. Since the code creates, displays, and moves the goal as appropriate, all we have to do is find out if the character's at the goal. This is done in two stages. First, add the following method to the World class:

def at_goal(self, player_rect):
     for block in self.goals:
         if block.colliderect(player_rect):
             return True
     return False

This works in exactly the same way as the collide_get_y() method that we created earlier, except that it only returns True or False. You then need to check this method in the game loop, so add:

#check if the player has completed the level
   if world.at_goal(player.rect):
     print("Winner!")
     finished = True

If you save and run the code now, you'll find that you can run and jump to the goal, then finish the level. You can even fall off the platform and disappear into the abyss never to be seen again, but it still doesn't have much of a challenge to it.

Making a Challenge

To add some game play, there'll be a class called Doom, which holds all the things that can kill the player. In this game, there are two challenges to avoid. Firstly, there's the burning pit of doom that covers the bottom of the screen. This will kill the players if they fall into it. Secondly, and more importantly from the perspective of the game play, there'll be fireballs that drop down from the sky. The players will have to dodge these as they make their way towards the goal.

Firstly, add the burning pit of doom. We'll draw this as a rectangle along the bottom of the screen. Add the following to the Doom class:

def _  _init_  _(self, fireball_num, pit_depth, colour):
    self.base = pygame.Rect(0, screen_y-pit_depth,
                screen_x, pit_depth)
    self.colour = colour

def collided(self, player_rect):
    return self.base.colliderect(player_rect)

def update(self, screen):
    '''move fireballs down, and draw everything on the screen'''
    pygame.draw.rect(screen, self.colour, self.base, 0)

Also add the following to the appropriate parts of the options, variables, and game loop:

#options
doom_colour = (255, 0, 0)
#initialise variables
doom = Doom(0, 10, doom_colour)
    #render the frame
    doom.update(screen)
    #check if the player is dead
    if doom.collided(player.rect):
        print("You Lose!")
        finished = True

This should all be fairly self explanatory. Notice that you don't need to move the burning pit of doom rectangle, as it should always cover the bottom of the screen. The call to the update() method is to move the fireballs, so let's look at them now.

We'll add the whole class in one go here:

class Fireball(pygame.sprite.Sprite):
   '''this class holds the fireballs that fall from the sky'''
   def _  _init_  _(self):
      pygame.sprite.Sprite._  _init_  _(self)
      self.image = pygame.transform.scale(
                   pygame.image.load(fireball_image),
                   (fireball_size, fireball_size))
      self.rect = self.image.get_rect()
      self.reset()

   def reset(self):
      self.y = 0
      self.speed_y = randint(fireball_low_speed,
                     fireball_high_speed)
      self.x = randint(0,screen_x)
      self.rect.topleft = self.x, self.y

   def move_x(self, dist):
      self.rect.move_ip(dist, 0)
      if self.rect.x < -50 or self.rect.x > screen_x:
          self.reset()

   def move_y(self):
      self.rect.move_ip(0, self.speed_y)
      if self.rect.y > screen_y:
         self.reset()

As you can see, this class extends the Sprite class in the same way that the Player class does. The move_x() method works in a similar way to the equivalent method in World, except that here it has to move only a single fireball because we will have one of these fireball objects for each fireball.

To keep up the challenge, the fireballs should constantly fall from the sky. There are a few ways of achieving this, but we've chosen to create a fixed number of fireballs and simply reset them whenever they go off the screen. This reset() method places the fireballs at a random position along the top of the screen and gives them a random velocity.

The randint(a, b) method returns a random integer between a and b, inclusive (that is, including the values of a and b). The screen_x variable makes sure it's on the screen, and two global variables (fireball_low_speed and fireball_high_speed) set the range of speeds a fireball can move at. These numbers are pixels per frame. You don't need a collide method here because you'll deal with that in the Doom class.

Now, update the Doom class to:

class Doom():
  '''this class holds all the things that can kill the player'''
  def _  _init_  _(self, fireball_num, pit_depth, colour):
     self.base = pygame.Rect(0, screen_y-pit_depth,
                 screen_x, pit_depth)
     self.colour = colour
     self.fireballs = []
     for i in range(0,fireball_num):
         self.fireballs.append(Fireball())
     self.fireball_plain = pygame.sprite.RenderPlain(
                           self.fireballs)

  def move(self, dist):
     for fireball in self.fireballs:
         fireball.move_x(dist)

  def update(self, screen):
     for fireball in self.fireballs:
         fireball.move_y()
     self.fireball_plain.draw(screen)
     pygame.draw.rect(screen, self.colour, self.base, 0)

  def collided(self, player_rect):
     for fireball in self.fireballs:
         if fireball.rect.colliderect(player_rect):
             hit_box = fireball.rect.inflate(
                       -int(fireball_size/2),
                       -int(fireball_size/2))
             if hit_box.colliderect(player_rect):
                 return True
     return self.base.colliderect(player_rect)

As you can see, this creates a list of fireballs and adds them to fireball_plain. This is a RenderPlain that works in the same way as player_plain, and allows you to draw the fireballs on the screen. Notice that there are global variables for the number and size of fireballs. Changing these has a dramatic effect on how the game plays, and in many ways, they're the key variables for changing difficulty.

The collided() method is a little different to the previous ones we've done so far. It compares the player to a rectangle half the size of the fireball rectangle. This is because neither the player nor the fireball are perfect rectangles, and the two bounding rectangles can collide even if the actual sprites are some distance apart. This is extremely frustrating for the person playing the game. The method we've used isn't perfect, but it errs towards the player not dying. In other words, it may be possible for the player to skim a fireball and get away with it, but if this collide method returns True then there's definitely a collision.

With these two classes added, you just need the following code to get it all working:

#options
fireball_size = 30
fireball_number = 10
fireball_low_speed = 3
fireball_high_speed = 7
fireball_image = "flame.png"

Change the initialisation of Doom to include fireballs:

doom = Doom(fireball_number, 10, doom_colour)

You'll also need to add lines to the keypress section to make the fireballs move with the background (the lines in bold are the ones you need to add):

#check which keys are held
  key_state = pygame.key.get_pressed()
  if key_state[K_LEFT]:
      world.move(2)
      doom.move(2)
  elif key_state[K_RIGHT]:
      world.move(-2)
      doom.move(-2)

Again, we're using a sprite that we got from http://opengameart.org. In this case it's the one from http://opengameart.org/sites/default/files/flame.png. Feel free to pick another or draw your own. For this to work, the file has to be downloaded and saved in the same directory you're running the game from. Alternatively, you can give the sprites an absolute path. For example, if you're saving everything in the directory /home/pi/my_game/, you could change the line:

fireball_image = "flame.png"

to

fireball_image = "/home/pi/my_game/flame.png"

That way it would work wherever you ran the game from. Now save and run, and the game should look like Figure 5-4.

Figure 5-4: It's a little rough round the edges, but it's a working platform game.

9781118717059-fg0504.tif

Making It Your Own

The mechanics of the game are now in place. Players have to move through the world, dodge the fireballs, and get to the goal. There's still a little polish left to add, but the basics are there. Now's a great time to start making it your own. After all, this isn't a chapter about how to copy code until you have a game; this is a chapter about building your own game. By now you should know enough about what the various bits do to start customising it. The options section is the best place to start.

Depending on your monitor, you may want to change the size of the window. If you think it's a bit too easy, add some more fireballs, or make them larger. Perhaps you want to jump higher, or run faster. All of these should be pretty easy. In fact, you should have learned enough in earlier chapters to now make a simple game menu that you can add to the start of the game. You could make it a simple text-based menu that goes just before #initialise pygame, and lets you set the level of difficulty. At harder levels you could … actually, we'll let you work that out for yourself. If you're feeling ambitious, you could make this menu graphical rather than text-based.

Adding Sound

Hopefully, you now have your own customised version of our game, but don't worry if you don't. The rest of this chapter will still work and you can go back and add your own tweaks later.

Now it's time to add a bit of flare. These are things that don't affect the mechanics of the game, but make it more enjoyable to play. The first is a sound effect, and the second is a background.

Before we can add sounds, we need to initialise the mixer. This basically just gets the sound infrastructure set up and ready to play. It's done with the following code:

#initialise pygame.mixer
pygame.mixer.pre_init(44100, -16, 8, 2048)
pygame.mixer.init()

This allows us to play up to eight sounds at once, although at first, we'll just add a jumping sound effect. Again, we've gone to http://opengameart.org. This time the file is http://opengameart.org/sites/default/files/qubodup-cfork-ccby3-jump.ogg, so again you'll need to download this or a corresponding file. We'll add this file to the options with:

#options
jump_sound = "qubodup-cfork-ccby3-jump.ogg"

Then we need to update the Player class to play the noise at the appropriate time. Add the following to the end of the _  _init_  _() method:

self.sound = pygame.mixer.Sound(jump_sound)

You'll also need to change the jump() method so that it is

def jump(self, speed):
   if world.collided_get_y(self.base)>0:
      self.speed_y = speed
      self.sound.play()

That's all you need to add a bit of sound to the game. You should now find it quite easy to add more effects, like one that plays when players reach the goal, or one that plays when they die. You could also add some background music. However, remember that most music you buy is copyrighted. You can include it in a game you make for yourself without any problems, but if you want to distribute your game, you could get into trouble. Instead, take a look at http://freemusicarchive.org. Like http://opengameart.org, this site contains a wide range of files that you can download and include in your own games. There's a wide range of styles, so you'll almost certainly find something you like. Many of these are also licensed so that if you distribute your game, you also have to distribute the source code.

Adding Scenery

The second nicety we'll add to the game is a background. Of course, you'll need a background image to do this, and we've gone for the file background.png that's in http://opengameart.org/sites/default/files/background.zip. This gives a nice, countryside backdrop, but you could alter the feel of the game by going for something darker and moodier. You'll need to add the following to the options to bring in the file:

#options
background_image = "background.png"

Before, when using images, you extended the Sprite class to create a new class (such as Player and Fireball) to draw them. However, since you don't need to manipulate the rectangle for this, or do any collisions, you can simply load it as an image. This is done with the following code:

#set up the background
background = pygame.transform.scale(pygame.image.load(
             background_image), (screen_x, screen_y)).convert()
bg_1_x = -100
bg_2_x = screen_x - 100

The first line loads the image and scales it to the screen size. It also runs convert() on it. This converts the image from a PNG to a PyGame surface. This makes it render on the screen much faster, which is especially important for an image of this size. You could have done this with the other images you've used, but then you'd lose the transparent sections round the images, making them purely rectangular. The second and third lines set up the variables that hold the x positions of the image. There are two of these because we'll draw the image twice to create a constantly looping background that the players can never move off the end of.

To move the background, update the appropriate section of the game loop to:

#check which keys are held
  key_state = pygame.key.get_pressed()
  if key_state[K_LEFT]:
     world.move(2)
     doom.move(2)
bg_1_x = bg_1_x + 1
     bg_2_x = bg_2_x + 1
     if bg_1_x < screen_x:
         bg_1_x = -screen_x
     if bg_2_x < screen_x:
         bg_2_x = -screen_x
  elif key_state[K_RIGHT]:
     world.move(-2)
     doom.move(-2)
     bg_1_x = bg_1_x - 1
     bg_2_x = bg_2_x - 1
     if bg_1_x > -screen_x:
         bg_1_x = screen_x
     if bg_2_x > -screen_x:
         bg_2_x = screen_x

There's quite a bit going on here. Firstly, did you notice that we move the background by less than we move the world or the doom? This is called parallax scrolling. It creates the appearance of depth by moving objects farther behind at different speeds. It's like when you look out of the window of a moving vehicle and the objects close to you appear to be moving faster than those farther away. It's not exactly advanced 3D graphics, but it helps create a sense of depth. If you want to take things further, you can add layers of backgrounds here. For example, you could draw some trees that move only a bit slower than the platforms, then some hills that move bit slower than the trees, and finally a sun that moves really slowly. As with the sounds, you can take this as far as you want to go.

The second thing that's going on in the code is the if blocks that move the background. Whenever the image moves so it's off one side of the screen, the program moves it back to the other side of the screen. This creates the infinitely scrolling background that constantly loops between the two background images. The only thing left to do is draw the image on the screen:

#render the frame
screen.blit(background,(bg_1_x, 0))
screen.blit(background,(bg_2_x, 0))

These have to be the first lines under #render the frame because otherwise they'll be drawn over the top of the other parts. Figure 5-5 shows the final game.

Figure 5-5: The game with all the elements.

9781118717059-fg0505.tif

Adding the Finishing Touches

This, in essence, is the game fully complete. However, there is one more bit we'll add to make it easier to use. So far, we've been using a level that's hard coded into the game. However, it would be much better if we could set it up so that the users can specify a file to load, and the program would pull the level out of the text in that file. Since the levels are defined by text, this should be quite easy.

Running python3 chapter5-platformer.py (or whatever you've called the file) will run the default level that's inside the main game file, but python3 chapter5-platformer.py mylevel will run the game with the level specified in the file mylevel. To do this, we need to use sys.argv. This is in the sys module, and it's a list containing all the arguments that get passed to Python. sys.argv[0] will be the name of the script we're running, so the argument that contains the filename (if it exists) will be sys.argv[1]. All we have to do, then, is add the following to our program:

#load level
if len(sys.argv) > 1:
    with open(sys.argv[1]) as f:
        level = f.readlines()

The specified file will be read in the same way as the array we've been using up until now. That is, a - is a platform and a G is a goal, and multiple lines define the multiple levels of the game.

Taking the Game to the Next Level

We could go on and on and add more and more to the game. However, we'll end the tutorial here. Not because the game's finished, but because you should now know enough to finish it by yourself. We're not going to dictate to you what the game should have—it's your game, add what you want. However, we will give you some ideas for how to move on:

  • If you haven't already tried tweaking the options, try that now.
  • Creating new levels is a great way to make the game feel like it's your own.
  • The artwork we've used is only a suggestion. See what you can find online, or try making some of your own.
  • Add sprites to the items that are currently just rectangles, like the platform and the burning pit of doom.
  • Build up levels into worlds. Each world could have a different theme, and different artwork to match the theme.
  • Add things that players can collect. These could be coins that count towards the score, or power-ups that allow the players to run faster or jump higher.
  • Add more things that could kill the players. This could be, for example, something that constantly moves right so the players have to keep moving in order to avoid it, something that comes for the players and they have to kill by jumping on, or something that shoots up from below.
  • Score each level on completion. You could do this with timing, or objects that the players collect, or something else.
  • Animate the sprites. Many of the images on http://opengameart.org have a range of poses to allow you to animate objects by constantly scrolling between a set of images.
  • It could speed up if you hold the arrow key down rather than just always moving at a constant speed, or there could be some Run button that enables the players to move faster.

These are just a few ideas to get you started. It's not intended to be a complete list of everything you can do with the game, so get your creative juices flowing and see what you can come up with. If it gets good, you could submit it to the Raspberry Pi store and let other people play it. Just remember the licenses of any images or sounds you've used.

Realistic Game Physics

PyGame is a great module for creating simple games. As you've seen, it's really easy to draw objects on the screen and move them round. However, sometimes you need a bit more power. In the previous example, the character fell as though affected by gravity, but the rest of the physics were a bit off. If you need objects that can interact with each other in more realistic ways, such as bouncing of each other, you'll need to use a physics library.

PyMunk is one such module that allows you to create more life-like games. Using it, you can create a space and add objects, then let PyMunk work out how they'll interact.

You can download PyMunk from http://code.google.com/p/pymunk/downloads/list (you'll need the source release). Once it has downloaded, you can unzip it and move into the new directory with the following (use LXTerminal rather than Python to run these commands):

unzip pymunk-4.0.0.zip
cd pymunk-4.0.0

Unfortunately, there is a slight error in the build file that stops it building correctly on the Raspberry Pi. In order to install it, you need to open setup.py with a text editor (such as LeafPad) and find the lines:

elif arch == 32 and platform.system() == 'Linux':
  compiler_preargs += ['-m32', '-O3']]

Make sure it's the line with arch == 32, not 64. Delete '-m32' from the second line so that it reads:

compiler_preargs += ['-O3']]

Then save the file. Now you'll be able to install PyMunk with:

python3 setup.py build_chipmunk
python3 setup.py build
python3 setup.py install

Again, these both have to be entered in LXTerminal in the PyMunk directory. This may take a while, but once it's complete, you can check that it worked by opening Python and entering the following:

>>> import pymunk

Hopefully, you won't get any errors.

The following example is on the website at chapter5-pymunk.py:

import pygame, pymunk
from pygame.locals import *
from pygame.color import *
from pymunk import Vec2d
import math, sys, random

def to_pygame(position):
    return int(position.x), int(-position.y+screen_y)

def line_to_pygame(line):
    body = line.body
    point_1 = body.position + line.a.rotated(body.angle)
    point_2 = body.position + line.b.rotated(body.angle)
    return to_pygame(point_1), to_pygame(point_2)
    
###options####
screen_x = 600
screen_y = 400
num_balls = 10

pygame.init()
screen = pygame.display.set_mode((screen_x, screen_y))
clock = pygame.time.Clock()
running = True

space = pymunk.Space()
space.gravity = (0.0, -200.0)

#create the base segment
base = pymunk.Segment(pymunk.Body(),(0, 50), (screen_x, 0), 0)
base.elasticity = 0.90
space.add(base)

#create the spinner
spinner_points = [(0, 0), (100, -50), (-100, -50)]
spinner_body = pymunk.Body(100000, 100000)
spinner_body.position = 300, 200
spinner_shape = pymunk.Poly(spinner_body, spinner_points)
spinner_shape.elasticity = 0.5
spinner_joint_body = pymunk.Body()
spinner_joint_body.position = spinner_body.position
joint = pymunk.PinJoint(spinner_body, spinner_joint_body, (0, 0),
                        (0, 0))
space.add(joint, spinner_body, spinner_shape)

#create the balls
balls = []
for i in range(1, num_balls):
    ball_x = int(screen_x/2)
    radius = random.randint(7, 20)
    inertia = pymunk.moment_for_circle(radius, 0, radius, (0, 0))
    body = pymunk.Body(radius, inertia)
    body.position = ball_x, screen_y
    shape = pymunk.Circle(body, radius, (0, 0))
    shape.elasticity = 0.99
    space.add(body, shape)
    balls.append(shape)

while running:
    for event in pygame.event.get():
        if event.type == QUIT:
            running = False
    
     screen.fill((0, 0, 0))
#draw the ball
    for ball in balls:
        pygame.draw.circle(screen,(100, 100, 100), to_pygame(ball.body.position), int(ball.radius), 0)

#draw the spinner
    points = spinner_shape.get_vertices()
    points.append(points[0])
    pygame_points = []
    for point in points:
        x,y = to_pygame(point)
        pygame_points.append((x, y))
    color = THECOLORS["red"]
    pygame.draw.lines(screen, color, False, pygame_points)

#draw the line
    pygame.draw.lines(screen, THECOLORS["lightgray"], False,
                      line_to_pygame(base))
        
    space.step(1.0/50.0)
    pygame.display.flip()
    clock.tick(50)

As you can see, this code uses PyGame to handle the drawing of the graphics and PyMunk to work out how they move.

PyMunk works with spaces like the one set up in the following lines:

space = pymunk.Space()
space.gravity = (0.0, -200.0)

This creates a new object from the Space class and sets its gravity. We can then create objects and add them to the space. This example uses circles, segments (that is, lines), and a polygon that's defined by a series of points. (Note, however, that on the Raspberry Pi, there's a bug that means you can only add a single segment to a space.) There's also a pin joint which, roughly speaking, makes the shape behave as though a single pin has been attached at that position, allowing it to pivot round, but not fall, due to gravity.

In the main loop, we call space.step(1.0/50.0), which tells PyMunk to move all the objects in the space by 1/50th of a second.

The one slightly confusing thing about using PyGame with PyMunk is that they use different coordinate systems. As you saw before, PyGame has the point 0,0 in the top-left corner, but Pymunk has it in the bottom-left. This means that to draw objects in the right place, you need to calculate the new y value. This is the purpose of the function to_pygame(position).

As well as defining the position of the various objects, in PyMunk you can also define the various physical properties they have, such as elasticity and inertia. By tweaking these properties, you can define how your world interacts.

If you run the code, you'll see that PyMunk has done all the difficult tasks of working out the movement, such as handling collisions and calculating how the balls bounce off the floor and each other. The results are in Figure 5-6. However, it comes at a price—this takes much more processing power than our previous game. Whilst the Raspberry Pi can handle simple physics simulations, you need to use them sparingly; otherwise, they'll run too slowly. Using the raspi-config tool, you can overclock your Raspberry Pi, which will help simulations run faster.

Figure 5-6: The PyMunk physics engine takes the hard work out of simulating real-world interactions.

9781118717059-fg0506.tif

We've only touched on the basics here, but hopefully it's enough to get you started. There are some samples in the PyMunk ZIP file that you downloaded earlier. Not all of them run well under Python 3, but they should give you more of a taste of what's going on. There is also some slightly outdated but otherwise good documentation at http://pymunk.googlecode.com/svn/tags/pymunk-2.0.0/docs/api/index.html.

Summary

After reading this chapter, you should know the following:

  • PyGame is a module to help you create games in Python.
  • Classes that extend Pygame.sprite.Sprite can draw images on the screen.
  • Sprites are drawn inside a class's self.rect rectangle.
  • You can also use this rectangle to detect collisions between objects.
  • Parallax scrolling can be used to create a sense of depth with 2D graphics.
  • PyGame can also handle audio.
  • For more realistic physics, you can use a physics engine like PyMunk, but it will slow down the execution.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset