Page 1 of 1

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

#1 DK3250  Icon User is offline

  • Pythonian
  • member icon

Reputation: 305
  • View blog
  • Posts: 975
  • Joined: 27-December 13

Posted 27 January 2017 - 07:51 AM

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

In this section a Game object is introduced, handling various aspects of the code flow.

For an introduction to this type of Game objects and game loops, please read the second half of this tutorial: http://www.dreaminco...and-game-loops/
The link explains in detail how the Game object works; here I will show it in real code, and only comment on aspects that are new.

Compared to Part #1, the existing class objects are only moderately modified. The most important change is the relocation of the event handler from the Player object to the new Game object.

I'll start presenting the full code. Then the Game object will be explained in detail and finally the modification of the other objects will be briefly mentioned.
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)
GREY = (220, 220, 220)

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)


class Platform():
    rects = []
    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)
        Platform.rects.append(self.rect)

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

    def event(self):
        pass


class Player():
    def __init__(self):
        self.jump = False
        self.left = False
        self.right = False
        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):
        if self.jump:
            self.y_speed = -18
            self.jump = False

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

        self.rect.bottom += self.y_speed

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

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

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


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

    def event(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 = Platform.rects[self.rect.collidelist(Platform.rects)].top + 1
            self.y_speed = 0
        else:
            self.y_speed += game.acc

        self.rect.bottom += self.y_speed
        self.hit()

        if game.timer:
            game.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(Platform.rects)
        if collision > -1:
            return True
        else:
            return False

    def hit(self):
        if (game.player.rect.colliderect(self.rect) and
            game.player.rect.midbottom != (X//2, Y - 99)):
            game.lives -= 1
            game.player.rect.midbottom = (X//2, Y - 99)

    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

    def event(self):
        if game.player.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)
            game.coin_count += 1
        elif game.enemy.rect.colliderect(self.rect):
            self.rect.midbottom = random.choice(self.positions)

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


class Game():
    def __init__(self):
        self.levels = [self.level_1, self.level_2, self.level_3]
        self.heart_surf = pygame.image.load('heart.png').convert()
        self.coin_surf = pygame.transform.scale(pygame.image.load('coin.png').convert(), (20, 20))
        self.acc = 2
        self.timer = False
        self.level = 0
        self.state = self.startpage

    def init(self):
        """
    A game state function.
    Called at the start of a new level.
"""
        self.lives = 5
        self.coin_count = 0
        self.sprites = [self]
        self.levels[self.level]()
        self.state = self.loop

    def level_1(self):
        self.player = Player()
        self.sprites.append(self.player)
        self.enemy = Enemy()
        self.sprites.append(self.enemy)
        self.coin = Coin()
        self.sprites.append(self.coin)

        self.sprites.append(Platform(X, 100, X//2, Y, GREEN))
        self.sprites.append(Platform(200, 15, 500, Y-180, BLUE))
        self.sprites.append(Platform(300, 15, 200, 340, BLUE))
        self.sprites.append(Platform(250, 15, 480, 260, BLUE))
        self.sprites.append(Platform(300, 15, 150, 180, BLUE))
        self.sprites.append(Platform(300, 15, 500, 100, BLUE))
        self.sprites.append(Platform(80, 15, 830, 260, BLUE))
        self.sprites.append(Platform(80, 15, 800, 340, BLUE))

    def level_2(self):
        print("LEVEL 2")
        self.level_1()  # level 1 used as dummy

    def level_3(self):
        print("LEVEL 3")
        self.level_1()  # level 1 used as dummy

    def event(self):
        " a game state function "
        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.player.on_ground():
                    self.player.jump = True
            elif event.type == pygame.USEREVENT + 1:
                self.timer = True

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

    def draw(self):
        for i in range(self.lives):
            screen.blit(self.heart_surf, [i*20 + 20, 20])
        for i in range(self.coin_count):
            screen.blit(self.coin_surf, [850 - i*20, 20])

    def loop(self):
        " a game state function "
        clock.tick(30)
        screen.fill(WHITE)

        for s in self.sprites:
            s.event()
        for s in self.sprites:
            s.draw()
        pygame.display.flip()

        if self.lives == 0:
            self.state = self.play_again
        if self.coin_count == 10:
            self.level += 1
            self.state = self.init

    def play_again(self):
        " a game state function "
        play = button("Play Again", GREEN, ((X-200)//3, Y//8*5))
        stop = button("Stop", RED, ((X-200)//3*2+100, Y//8*5))
        pygame.display.flip()
        self.buttons = [(play, self.init), (stop, self.endpage)]
        self.state = self.mouse_click

    def mouse_click(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.state = self.end
                    return
                elif event.type == pygame.MOUSEBUTTONDOWN and pygame.mouse.get_pressed() == (1, 0, 0):
                    pos = pygame.mouse.get_pos()
                    for btn, action in self.buttons:
                        if btn.collidepoint(pos):
                            self.state = action
                            return
            pygame.time.wait(20)

    def end(self):
        pygame.quit()
        sys.exit()

    def startpage(self):
        " a game state function "
        screen.fill(WHITE)
        font = pygame.font.SysFont("Segoe Print", 30)
        txt_surf = font.render("Platform Game by DK3250", 1, BLUE)
        txt_rect = txt_surf.get_rect(center=(X//2, Y//2))
        screen.blit(txt_surf, txt_rect)
        
        play = button("Play Now", BLACK, ((X-100)//2, Y//8*5))
        self.buttons = [(play, self.init)]
        pygame.display.flip()
        
        self.state = self.mouse_click

    def endpage(self):
        " a game state function "
        screen.fill(WHITE)
        font = pygame.font.SysFont("Segoe Print", 30)
        txt_surf = font.render("Thank you for playing", 1, RED)
        txt_rect = txt_surf.get_rect(center=(X//2, Y//2))
        screen.blit(txt_surf, txt_rect)
        
        stop = button("Exit", BLACK, ((X-100)//2, Y//8*5))
        self.buttons = [(stop, self.end)]
        pygame.display.flip()
        
        self.state = self.mouse_click


def button(txt, color, pos):
    button_font = pygame.font.SysFont("Segoe Print", 16)
    btn_surf = pygame.Surface((100, 40))
    btn_rect = btn_surf.get_rect(topleft=(pos))
    btn_surf.fill(GREY)
    pygame.draw.rect(btn_surf, BLACK, (0, 0, 100, 40), 1)
    txt_surf = button_font.render(txt, 1, color)
    txt_rect = txt_surf.get_rect(center=(50,20))
    btn_surf.blit(txt_surf, txt_rect)
    screen.blit(btn_surf, btn_rect)
    return btn_rect


game = Game()
while True:
    game.state()


I recommend that you copy the code to your editor, and scroll to line 138 where the Game description starts. Follow the code as you read through the comments below.

  • During instantiation of the Game object (in line 295), we initialize the following:
  • levels; a list of all possible game levels (here, only three)
  • heart_surf; the health icon
  • coin_surf; the icon indicating progress towards next level
  • acc; the formerly (in Part #1) global constant is now a Game constant
  • timer; formerly an enemy attribute
  • level; the actual game level
  • state; the Game state upon leaving the __init__() method


The init() method is called whenever a new level is initiated:
  • lives are reset to 5
  • coin_count is reset to 0
  • self.sprites = [self] The term 'sprite' refers to a two dimensional graphical element integrated into a larger surface. Clearly 'player', 'enemy', etc. are sprites, but as the Game instance holds heart_surf and coin_surf, game itself has sprite attributes. This is simply the start of a list of all sprites, the rest is append'ed in the level_#() methods.
  • self.levels[self.level]() executes the 'current level' function; example: At start self.level == 0, the expression becomes: self.levels[0] == self.level_1 – which is then executed.
  • state; the Game state upon leaving the init() method


The level_1() method makes instances of Player, Enemy, Coin, and the Platforms used in this level. All these sprite elements are added to the self.sprites list.
The reason to have all sprites (including self) in one single list will become clear in the loop() method.
level_2() and level_3() would in a real game represent new lay-out of platforms, coin-positions and maybe even new features (new types of objects) like moving platforms or potions that can stun the enemy or whatever... Here, I have included the two levels for demonstration and let them point to level_1(); the print statement is to prove that next level has been achieved.

The event() method is essentially the same as in Part #1.
Here, I consider event() as belonging to the Game object. Most of the events regards the player, but one event (the timer) regards the enemy. In general I find it most logic to place the event() in the Game part and then 'distribute' the actual events to the relevant part of the game.

The draw() method takes care of the two types of sprite that may come in multiple numbers. As the number vary throughout the game, they are not named individually in the sprite list; instead they are covered by the leading 'self' element in the sprite list.

In the loop() method the reason for the sprite list is found: It makes it possible to update and draw all sprites in two simple for loops:
for s in self.sprites:
    s.event()
for s in self.sprites:
    s.draw()


This requires, however, that all sprite objects has an event() and a draw() method. The move() and hit() methods from Part #1 is simply renamed, and to the Platform class an empty method is added.

It is always a good idea to do all the movements/events first and then update the screen; if we did:
for s in self.sprites:
    s.event()
    s.draw()


we would encounter quite bad imperfections when objects collide (try it..).

If we have used all the players lives, the player 'dies' and we change the game state to 'play_again'.
If we manage to collect 10 coins, we go to the next game level.

The play_again() method establishes two buttons generated using a global function, button().
The button() function could be integrated under the Game umbrella but I leave it in the main in order to also show this possibility. More about this in the final comments.
The Button() function returns a rect element that we can use to detect a mouse click in the buttons.
In play_again() the rect element and the game state activated by the button is saved as a tuple in a 'buttons' list.

In mouse_click() a small event loop is established waiting for a mouse click on one of the buttons.
One line in this method needs a comment:
elif event.type == pygame.MOUSEBUTTONDOWN and pygame.mouse.get_pressed() == (1, 0, 0):

The enevtID 'pygame.MOUSEBUTTONDOWN' also reacts to the scroll wheel on the mouse; to make sure that only a left-click is accepted as valid input, the second condition, ”pygame.mouse.get_pressed() == (1, 0, 0)” is added.
On mouse click on a button, the button action is activated by a change in game.state

The startpage() and endpage() methods are here very rudimentary; included only as a demonstration of the principle. I a real game you should include an introduction to the game in the startpage and maybe a highscore on the endpage; for highscore, see: http://www.dreaminco...ule-for-pygame/

This concludes the explanation of the Game object; now, let's review the other objects.

The Platform class has not changed except for the addition of an empty event() method. This is necessary in order to run the sprite list in the Game object as explained.

The Player class is simplified as the event-loop is moved to Game. The method formerly named move() is now event().

The Enemy class is slightly modified, accounting for the movement of ‘timer' to the Game class. The method formerly named move() is now event().

The Coin class is modified accounting for the coin_count located in the Game class. The method formerly named hit() is now event()

Comments

The code is not perfect, it was never meant to be. The code is built to demonstrate how a game can be programmed, not to constitute a finalized game.
I have observed that a new coin can spawn in the same position as the existing, leading to multiple coin_count – this should be rectified; I'll leave this to you.
If you play the game to beyond level_3, an error will occur as the level counter grows larger than the number of levels defined. Also this, I'll leave to you to handle.
You can expand the game to include some kind of score or time; this can then be handled in a highscore module: http://www.dreaminco...ule-for-pygame/

The button() function is modified from a much more general Button class. I plan to issue a tutorial about Pygame buttons and Pygame sliders in the near future.

I'll appreciate any feedback, and I'll be happy to answer any questions.

Is This A Good Question/Topic? 0
  • +

Page 1 of 1