Page 1 of 1

Walk-Through a "Platform Game" made with Pygame, Part #1

#1 DK3250  Icon User is offline

  • Pythonian
  • member icon

Reputation: 291
  • View blog
  • Posts: 921
  • Joined: 27-December 13

Posted 24 January 2017 - 10:17 AM

This tutorial is made with Python 3.4 and Pygame 1.9

The code used in this tutorial requires 4 image files. Those 4 files are attached; they must be downloaded and saved in the same directory as the game file.

Introduction

To give you an idea of what we will achieve in this tutorial, I present here a screen-dump from the game.

Attached Image

Here you can identify the main components (objects) that we will need for the game:
  • The Player (the Mario figure)
  • The Enemy (the black bomb-with-legs figure)
  • The Platforms (blue blocks – and the green 'bottom')
  • The Coin (yellow
circle in bottom left corner)

Actually, I will present two versions of the game.
In this Part #1 of the tutorial I'll focus mostly on Pygame issues and basic OOP (Object Oriented Programming) but keep a very simple game loop and allow some open ends in the code.

In Part #2 (separate tutorial) I'll handle some more advanced game control issues. This requires the introduction of a fifth object:

  • The Game (an abstract object, handling important aspects of the game)

While the first 4 objects are intuitively easy to understand, the Game object requires a little more explanation, thus a separate section.

Handling of Graphics in Pygame

Before turning to the game code, I want to introduce how graphics is handled in Pygame.
A graphic element normally has two parts: A 'Surface' and a 'Rect'.
Throughout this tutorial I'll use the names 'Surface' and 'Rect' when talking about the abstract objects. In real code any valid name can be assigned to the 'Surface' and 'Rect' variables.

The ‘Surface’ handles all about size and appearance including colorization, transparency and many specialized task (not relevant to this tutorial).
The ‘Rect’ handles all about position and collisions (and also many other tasks). The Rect has a number of position handlers like: Rect.center, Rect.midbottom, Rect.topright.
From a ‘Surface’, a ‘Rect’ can be defined using

Rect = Surface.get_rect()
Rect.center = (my_x, my_y)
or:
Rect = Surface.get_rect(center=(my_x, my_y))


Movement of the sprites are handled by assigning to one of the Rect handlers:
Rect.centerx += 10 # moves the sprite 10 pixels to the right

After such movement, all Rect handlers are automatically updated.
When blitting one surface onto another, the blit() function is used:
screen.blit(Surface, Rect) # blits 'Surface' onto 'screen' according to the 'Rect' position


Collisions

One of many things you can do with Rect is to check for overlap (collision) with another Rect or a list of other Rects.

To check for collision between two single Rects, you use the colliderect() method:
if rect_1.colliderect(rect_2):
    # collision
else:
    # no collision


The statement ”rect_1.colliderect(rect_2)” returns either True or False.
We will use it to check for collision between player, enemy and coin.

To check for collision between a single Rect and a group of Rects, you use the collidelist() method:
result = Rect.collidelist(Rect_list)

The variable result will now have the value -1 if no collisions happened or a value indicating which Rect from the list that collided with the single one.
This is used in the on_ground() method of Player and Enemy.

The Platform Game

OK, - time for some game coding.

We start with the basic initializations; most is probably well known:
import pygame, sys, random
pygame.init()

X = 900
Y = 600

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 155, 0)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Pygame Presentation by DK3250")
clock = pygame.time.Clock()
pygame.time.set_timer(pygame.USEREVENT + 1, 10000)

acc = 2
As usual (in my tutorials) I use 'X' and 'Y' for the display dimensions, and the name 'screen' for the actual display.
The last line defines a variable 'acc', this is the parameter for acceleration and used to make jumps look natural. Like the color constants this parameter will be globally available.

pygame.time.set_timer(pygame.USEREVENT + 1, 10000)

This line defines a timer that will appear in the event queue every 10.000 milliseconds (every 10 seconds).
You have probably seen eventID like pygame.KEYDOWN or pygame.MOUSEBUTTONUP, but may be unfamiliar with pygame.USEREVENT.
The eventID is really just small integer numbers, normally in the range from 0 to 24, but as no-one can remember which number is KEYDOWN and which is MOUSEBUTTONUP we use the named versions of the numbers. The last standard eventID is pygame.USEREVENT (== 24 on my system), and the next ones (up to 36) is free for you to use.
So, “pygame.USEREVENT + 1” simply evaluates to 25 – but DON’T use this value directly, it will make maintenance of the code a nightmare.
Here we use “pygame.USEREVENT + 1” (equal to eventID = 25) to get a signal from a timer, every 10 seconds.


Now, to the first of the four classes:
class Platform():
    def __init__(self, sizex, sizey, posx, posy, color):
        self.surf = pygame.surface.Surface((sizex, sizey))
        self.rect = self.surf.get_rect(midbottom=(posx, posy))
        self.surf.fill(color)

    def draw(self):
        screen.blit(self.surf, self.rect)
This simple class describes the Platform objects.
Each platform instance has a surface and a rect; the surface is filled with a monochrome color.
The draw() method blits the object to the screen.

The individual Platform instances will be created with all other objects later in the code.


The Player class is by far the most complicated in this code:
class Player():
    def __init__(self):
        self.jump = False
        self.left = False
        self.right = False
        self.lives = 5
        self.heart = pygame.image.load('heart.png').convert()
        self.surf = pygame.image.load('player.jpg').convert()
        self.rect = self.surf.get_rect(midbottom=(X//2, Y - 100))
        self.y_speed = 0

    def event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and self.on_ground():
                    self.jump = True
            elif event.type == pygame.USEREVENT + 1:
                enemy.timer = True

        self.left = False
        self.right = False
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.left = True
        if keys[pygame.K_RIGHT]:
            self.right = True

    def move(self):
        if self.jump:
            self.y_speed = -18
            self.jump = False
        self.rect.bottom += self.y_speed

        if self.left and self.rect.left > 0:
            self.rect.centerx -= 5
        if self.right and self.rect.right < X:
            self.rect.centerx += 5

        if self.on_ground():
            if self.y_speed >= 0:
                self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
                self.y_speed = 0
            else:
                self.rect.top = p_rects[self.rect.collidelist(p_rects)].bottom
                self.y_speed = 2
        else:
            self.y_speed += acc

    def on_ground(self):
        collision = self.rect.collidelist(p_rects)
        if collision > -1:
            return True
        else:
            return False

    def draw(self):
        screen.blit(self.surf, self.rect)
        for i in range(self.lives):
            screen.blit(self.heart, [i*20 + 20, 20])

In the __init__() method a number of attributes is initialized. The jump, left and right are Booleans used to control the movement of the player. The lives attribute indicates the players health – in the game this value will decrease if 'player' is hit by, or collides with, 'enemy'. The y-speed is used in jump and falls, see later.

The event() method handles all userevents and the timer mentioned earlier.
if event.key == pygame.K_SPACE and self.on_ground():
    self.jump = True

We want the player to jump whenever the Space Bar is hit; but to prevent 'air-jumps' we also need to check whether the player is actually on a platform – to this end we call a specialized method, on_ground()

elif event.type == pygame.USEREVENT + 1:
    enemy.timer = True

Now that we check the event queue, we also check if the timer has 'gone off'. This part of the event loop does not affect the player instance, only the enemy instance. It is therefore not a best OOP practice to have this functionality within the Player class. We will deal with this in the next version of the game.


When moving the player, we need to hold down the left/right arrow on the keyboard. We need to register this constant keypress. To this, we cannot use the event.type as it will only register the change (from up to down or vice versa) not the status of the key. Instead we can use

keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    self.left = True
if keys[pygame.K_RIGHT]:
    self.right = True

Each key on the keyboard has a ”pygame number” from 0 to about 250. The pygame.key.get_pressed() returns a list of same length as the number of keys. This list consists of only 0s and 1s. '0' means that the corresponding key is not pressed, while '1' indicates a pressed key.
We don't need to know the ”pygame number” for the key, as we can just use the easy-to-remember format as shown.


Now we have updated status of jump, left and right; and we are ready to move the player. The first part of move() account for the simple movements: If jump is activated, the y_speed is set to -18 (value set by me to make the jump reasonable high – you can change it). The next four lines accounts for the left/right movement within the screen dimensions.


Next we need to land on a platform or bounce if a platform is hit from below:
if self.on_ground():
    if self.y_speed >= 0:
        self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
        self.y_speed = 0
    else:
        self.rect.top = p_rects[self.rect.collidelist(p_rects)].bottom
        self.y_speed = 2
else:
    self.y_speed += acc
ok, this is tricky – please re-read the section about collisions above, and maybe even the next section explaining the on_ground() method first, and then come back here.
In the first part of the code (the major if- part) we know there is an overlap between the player and a platform.
If y_speed > 0, the player is actually landing from above on the platform.
We want to place the bottom of the player on the top of the relevant platform – but with one pixel of overlap; otherwise we are not able to detect that player is on ground. All of this we do in line 3 above.
Finally, we want the decent to stop, so: y_speed = 0
If y_speed == 0, the player is already on the platform, and the conditions are maintained.
If y_speed < 0, player hit the platform from below.
Now we need the player.rect.top to align with platform.bottom and change y_speed to downward.
If player is not on ground (the last else-clause), the velocity in y-direction is increased by the acceleration.


The on_ground() method:
collision = self.rect.collidelist(p_rects)
if collision > -1:
    return True
else:
    return False

Here the single player.rect is checked for collision against the list of platform-rects.
If any collision is detected (collision > -1), True is returned; otherwise, False.

Finally, in the draw() method, you see how a Surface without a Rect can be blitted by direct assignment of position – this position is where the Surface's topleft corner will be placed.


Let's have a break

Pew, this was dry. Let's make a break and see it in action. We only need to include the object instantiation and a simple game-loop (The list comprehension in line 105 will be mentioned later):
import pygame, sys, random
pygame.init()

X = 900
Y = 600

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 155, 0)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Pygame Presentation by DK3250")
clock = pygame.time.Clock()
pygame.time.set_timer(pygame.USEREVENT + 1, 10000)

acc = 2


class Platform():
    def __init__(self, sizex, sizey, posx, posy, color):
        self.surf = pygame.surface.Surface((sizex, sizey))
        self.rect = self.surf.get_rect(midbottom=(posx, posy))
        self.surf.fill(color)

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


class Player():
    def __init__(self):
        self.jump = False
        self.left = False
        self.right = False
        self.lives = 5
        self.heart = pygame.image.load('heart.png').convert()
        self.surf = pygame.image.load('player.jpg').convert()
        self.rect = self.surf.get_rect(midbottom=(X//2, Y - 100))
        self.y_speed = 0

    def event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and self.on_ground():
                    self.jump = True
            elif event.type == pygame.USEREVENT + 1:
                #enemy.timer = True
                pass  # modified until enemy is enabled

        self.left = False
        self.right = False
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.left = True
        if keys[pygame.K_RIGHT]:
            self.right = True

    def move(self):
        if self.jump:
            self.y_speed = -18
            self.jump = False
        self.rect.bottom += self.y_speed
        
        if self.left and self.rect.left > 0:
            self.rect.centerx -= 5
        if self.right and self.rect.right < X:
            self.rect.centerx += 5

        if self.on_ground():
            if self.y_speed >= 0:
                self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
                self.y_speed = 0
            else:
                self.rect.top = p_rects[self.rect.collidelist(p_rects)].bottom
                self.y_speed = 2
        else:
            self.y_speed += acc

    def on_ground(self):
        collision = self.rect.collidelist(p_rects)
        if collision > -1:
            return True
        else:
            return False

    def draw(self):
        screen.blit(self.surf, self.rect)
        for i in range(self.lives):
            screen.blit(self.heart, [i*20 + 20, 20])


platforms = []
platforms.append(Platform(X, 100, X//2, Y, GREEN))
platforms.append(Platform(200, 15, 500, Y-180, BLUE))
platforms.append(Platform(300, 15, 200, 340, BLUE))
platforms.append(Platform(250, 15, 480, 260, BLUE))
platforms.append(Platform(300, 15, 150, 180, BLUE))
platforms.append(Platform(300, 15, 500, 100, BLUE))
platforms.append(Platform(80, 15, 830, 260, BLUE))
platforms.append(Platform(80, 15, 800, 340, BLUE))
p_rects = [p.rect for p in platforms]

player = Player()

while True:
    clock.tick(30)
    screen.fill(WHITE)
    
    player.event()
    player.move()
    
    player.draw()
    for p in platforms:
        p.draw()

    pygame.display.flip()


Running this code will show a number of platforms (ground is simply a very big platform) and a player. You can move the player around using the left/right arrows and make him jump using the space bar. Try a jump landing on a platform. Enjoy.


Back to Coding

This will soon become boring, we need an enemy and we need a collectable coin – this will make the game much more exciting. So two more objects:
class Enemy():
    def __init__(self):
        self.surf = pygame.image.load('enemy.jpg').convert()
        self.rect = self.surf.get_rect(midtop=(X//2, 0))
        self.x_speed = random.randint(3, 7)
        self.y_speed = 0
        self.timer = False

    def move(self):
        self.rect.centerx += self.x_speed
        if self.rect.left <= 0 or self.rect.right >= X:
            self.x_speed *= -1

        if self.on_ground():
            self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
            self.y_speed = 0
        else:
            self.y_speed += acc
        self.rect.bottom += self.y_speed

        self.hit()

        if self.timer:
            self.timer = False
            self.rect.midtop = (X//2, 0)
            self.x_speed = random.randint(3, 7) * ((self.x_speed > 0) - (self.x_speed < 0))

    def on_ground(self):
        collision = self.rect.collidelist(p_rects)
        if collision > -1:
            return True
        else:
            return False

    def hit(self):
        if player.rect.colliderect(self.rect):
            player.lives -= 1
            player.rect.midbottom = (X//2, Y - 100)
        # if lives == 0:
            # do something..

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

The __init__() part should mostly be recognizable; we import a picture and make a corresponding Rect locating the enemy at the top of the screen. We create a random x-speed and no fall speed.
Finally we initialize a 'self.timer' to False. You might remember from the players event() method, that this variable is set to True every 10 second – it will then restart the enemy, see next paragraph.


The move() method first handles the bounce from the left and right walls; whenever a wall is hit, the x_speed parameter is negated, moving the enemy in the opposite direction.
Then the landing on platforms is handled, this is exactly as the player.
The call to hit() is covered below.
Finally we run the timer part:
if self.timer:
    self.timer = False
    self.rect.midtop = (X//2, 0)
    self.x_speed = random.randint(3, 7) * ((self.x_speed > 0) - (self.x_speed < 0))

If the timer has gone off, we start by resetting the 'timer' variable to False – Otherwise the code will act as if the timer is activated all the time. Then the position is reset, and so is the speed. The strange expression multiplied to the nominal speed maintains the left/right direction:
if speed>0 we multiply with (1-0) = 1
if speed<0 we get (0-1) = -1

The on_ground() method is the same as for player.

The hit() method test for collision between enemyRect and playerRect – remember, when only two Rects are checked, the output is True/False. If they hit, player’s life is decreased by one and player is reset to the starting position on ground level.

Clearly, is the number of lives falls to zero, something should happen. In this version of the game I'll leave this to you; in the next version, I'll show an example.


The Coin object looks like this:
class Coin():
    def __init__(self):
        self.positions = [(600, 245), (250, 325), (40, 500), (850, 500), (830, 245), (800, 325)]
        self.surf = pygame.image.load('coin.png').convert()
        self.rect = self.surf.get_rect(midbottom=random.choice(self.positions))
        self.count = 0
        self.small_surf = pygame.transform.scale(self.surf, (20, 20))

    def hit(self):
        if player.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)
            self.count += 1
        elif enemy.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)
        # if self.count > value:
            # do something

    def draw(self):
        screen.blit(self.surf, self.rect)
        for i in range(self.count):
            screen.blit(self.small_surf, [850 - i*20, 20])


In the __init__() all possible positions for the coin is given in a list. In a real game this list should probably be a little longer. If the coin was allowed to spawn completely random on the screen, there is a risk of ending up in positions out of reach for the player.
The 'count ' keeps track of how many coins the player has collected.
This attribute could also be placed with the player (as a player.coins attribute), but for now I've placed it here.
The pygame.transform.scale() takes a surface and changes its size and then outputs the result in a new surface. Here we make a mini-icon of the coin, for later use in the draw() method.

The hit() method check if player hits the coin or the enemy hits the coin; in both situations the coin will spawn at random again.

Also here I have an open end: If the number of coins collected surpass a certain threshold, something should happen. In the next version I'll give an example of this.

This almost finalises the first version of the game. Before showing the full game code, I only need to comment on line 182:
p_rects = [p.rect for p in platforms]

Here a list is generated by list comprehension. List Comprehension is outside the scope of this tutorial. In this case the rect attribute is extracted from all instances of Platform, and placed in a new list, 'p_rects'.

The full code of this first version of the game is here (with only few comments or docstrings; comments are covered by this tutorial):
"""
Demonstration Program by DK3250, January 2017

The intention is to demonstrate some basic Pygame functionality while
also using Python class objects.

The code is meant to be part of a tutorial in Dream-In-Code; the code is
explained in detail in the tutorial, thus the in-code doc-strings are
short or omitted entirely.
"""
import pygame, sys, random
pygame.init()

X = 900
Y = 600

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 155, 0)

screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("Pygame Presentation by DK3250")
clock = pygame.time.Clock()
pygame.time.set_timer(pygame.USEREVENT + 1, 10000)

acc = 2


class Platform():
    def __init__(self, sizex, sizey, posx, posy, color):
        self.surf = pygame.surface.Surface((sizex, sizey))
        self.rect = self.surf.get_rect(midbottom=(posx, posy))
        self.surf.fill(color)

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


class Player():
    def __init__(self):
        self.jump = False
        self.left = False
        self.right = False
        self.lives = 5
        self.heart = pygame.image.load('heart.png').convert()
        self.surf = pygame.image.load('player.jpg').convert()
        self.rect = self.surf.get_rect(midbottom=(X//2, Y - 100))
        self.y_speed = 0

    def event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and self.on_ground():
                    self.jump = True
            elif event.type == pygame.USEREVENT + 1:
                enemy.timer = True

        self.left = False
        self.right = False
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.left = True
        if keys[pygame.K_RIGHT]:
            self.right = True

    def move(self):
        if self.jump:
            self.y_speed = -18
            self.jump = False
        self.rect.bottom += self.y_speed

        if self.left and self.rect.left > 0:
            self.rect.centerx -= 5
        if self.right and self.rect.right < X:
            self.rect.centerx += 5

        if self.on_ground():
            if self.y_speed >= 0:
                self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
                self.y_speed = 0
            else:
                self.rect.top = p_rects[self.rect.collidelist(p_rects)].bottom
                self.y_speed = 2
        else:
            self.y_speed += acc

    def on_ground(self):
        collision = self.rect.collidelist(p_rects)
        if collision > -1:
            return True
        else:
            return False

    def draw(self):
        screen.blit(self.surf, self.rect)
        for i in range(self.lives):
            screen.blit(self.heart, [i*20 + 20, 20])


class Enemy():
    def __init__(self):
        self.surf = pygame.image.load('enemy.jpg').convert()
        self.rect = self.surf.get_rect(midtop=(X//2, 0))
        self.x_speed = random.randint(3, 7)
        self.y_speed = 0
        self.timer = False

    def move(self):
        self.rect.centerx += self.x_speed
        if self.rect.left <= 0 or self.rect.right >= X:
            self.x_speed *= -1

        if self.on_ground():
            self.rect.bottom = p_rects[self.rect.collidelist(p_rects)].top + 1
            self.y_speed = 0
        else:
            self.y_speed += acc
        self.rect.bottom += self.y_speed

        self.hit()

        if self.timer:
            self.timer = False
            self.rect.midtop = (X//2, 0)
            self.x_speed = random.randint(3, 7) * ((self.x_speed > 0) - (self.x_speed < 0))

    def on_ground(self):
        collision = self.rect.collidelist(p_rects)
        if collision > -1:
            return True
        else:
            return False

    def hit(self):
        if player.rect.colliderect(self.rect):
            player.lives -= 1
            player.rect.midbottom = (X//2, Y - 100)
        # if lives == 0:
            # do something..

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


class Coin():
    def __init__(self):
        self.positions = [(600, 245), (250, 325), (40, 500), (850, 500), (830, 245), (800, 325)]
        self.surf = pygame.image.load('coin.png').convert()
        self.rect = self.surf.get_rect(midbottom=random.choice(self.positions))
        self.count = 0
        self.small_surf = pygame.transform.scale(self.surf, (20, 20))

    def hit(self):
        if player.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)
            self.count += 1
        elif enemy.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)
        # if self.count > value:
            # do something

    def draw(self):
        screen.blit(self.surf, self.rect)
        for i in range(self.count):
            screen.blit(self.small_surf, [850 - i*20, 20])


platforms = []
platforms.append(Platform(X, 100, X//2, Y, GREEN))
platforms.append(Platform(200, 15, 500, Y-180, BLUE))
platforms.append(Platform(300, 15, 200, 340, BLUE))
platforms.append(Platform(250, 15, 480, 260, BLUE))
platforms.append(Platform(300, 15, 150, 180, BLUE))
platforms.append(Platform(300, 15, 500, 100, BLUE))
platforms.append(Platform(80, 15, 830, 260, BLUE))
platforms.append(Platform(80, 15, 800, 340, BLUE))
p_rects = [p.rect for p in platforms]

player = Player()
enemy = Enemy()
coin = Coin()

while True:
    clock.tick(30)
    screen.fill(WHITE)

    player.event()
    player.move()
    enemy.move()
    coin.hit()

    player.draw()
    enemy.draw()
    coin.draw()
    for p in platforms:
        p.draw()

    pygame.display.flip()


End of Part #1

I hope you enjoyed and appreciated this detailed walk-through.
Do you want more? Is the walk-through too detailed? Or does it need more details?
Any comments or questions are welcome.

I'll soon submit a Part #2 of this tutorial in which a more advanced game-loop is established; enabling the game core to be wrapped in opening page, closing page, game levels and more.

Attached image(s)

  • Attached Image
  • Attached Image
  • Attached Image
  • Attached Image


Is This A Good Question/Topic? 0
  • +

Page 1 of 1