Create a Flappy Bird Clone With Python P8

Posted in tutorials python -

This is one part of a multi-part tutorial. To see other posts in the same series, please click below:

Part 1 - Setup virtualenv

Part 2 - Setup Pygame

Part 3 - Start making game

Part 4 - Make a “flapping” flappy bird

Part 5 - Make the bird fly

Part 6 - Pipe System

Part 7 - Kill the Bird

Part 8 - Add game logic

Part 9 - Finalize the game

Game logic

Now that the whole thing looks (kinda) like a game, it’s time to give it some rules. Here are some ideas:

  • The bird should start dropping and the pipes start moving after the user press the first key.
  • There should be a grading system.
  • When the bird touches a pipe, a GAMEOVER mode is triggered.
  • In GAMEOVER mode:
    • A “gameover” sign will appear.
    • The pipe shouldn’t move.
    • The bird should drop to the ground, and stay there.
    • The user can’t control anything else.

In this part, we’re gonna finish up the game by providing those functionalities.

Kill the bird

First thing first, there should be a logic the determine the bird’s death. We will implement things that should happen after the bird dies later, so for now, let’s just print out the screen that “Bird dies” every time the bird touches the pipe, or the ground.

The bird should die if any part of it touches the pipe, which means that part lies in the same position with any part of the pipes, which means that part of the bird’s longitude is larger or equal to pipe’s longitude, and smaller than equal than the pipe’s longitude plus pipe’s width, and its latitude lies outside the gap

In a pipe pair (of toppipe and botpipe), we know their longitude (i.e. their x position), the length of the toppipe, and the GAP size. We also know the width of the pipes, the size of the bird, and the fact that the bird’s longitude is always fixed (0 in our case). Therefore: - a pipe pair can only touch the bird when it’s longitude is smaller or equal than bird_width, and larger or equal than - pipe_width. - When the pipe pair is on the “zone” (which means it can touch the bird), it will touch the bird if and only if the bird’s latitude (y position) is either smaller-or-equal than top_bot_height or larger-or-equal than top_bot_height + GAP

With that logic, we can make a function that check whether a bird (an instance of the Bird class) touches a pipe (an instance of the Pipe class):

def bird_touches_pipe(bird, pipe):
    if pipe.longitude <= bird.longitude + bird_width and pipe.longitude + pipe_width >= bird.longitude:
        if bird.latitude <= pipe.top_pipe_height or bird.latitude + bird_height >= pipe.top_pipe_height + GAP:
            return True
    return False

We can test this function by adding this to the loop:

    if bird_touches_pipe(bird, pipe_system.pipes[0]):
        print("Bird touches first pipe")
    else:
        bird.move()
        pipe_system.move()

When we test it out, the pipes and the bird will stop moving when bird touches the first pipe (it doesn’t work for the second pipe yet).

To make the “die” logic work with both the pipes (or all pipes, in case we have more than two), one can simply perform the bird_touches_pipe function with all the pipes. However, that maybe computationally wasteful (it’s not a heavy function, but remember we repeat it for every iteration). Since only the pipe pair that is inside the “zone” can touch the bird, no matter how many pipe pairs the PipeSystem has, we only have to check one of them. Once that pipe is out of “zone”, the “check” flag is passed to the next one (similar to when we check the “active” pipe’s position for releasing the next one).

class PipeSystem:
    def __init__(self, bird):
        pipe1 = Pipe(100, width + pipe_width)
        pipe2 = Pipe(50, width + pipe_width)
        self.pipes = [pipe1, pipe2]
        self.releasing_pipe_idx = 0
        self.near_bird_pipe_idx = 0
        self.pipes[self.releasing_pipe_idx].visible = True
        self.bird = bird

    def move(self):
        for pipe in self.pipes:
            pipe.move()
        if self.pipes[self.releasing_pipe_idx].longitute < (width - DISTANCE):
            self.releasing_pipe_idx = 1 - self.releasing_pipe_idx
            self.pipes[self.releasing_pipe_idx].visible = True
        if self.pipes[self.near_bird_pipe_idx].longitude + pipe_width < bird.longitude:
            self.near_bird_pipe_idx = 1 - self.near_bird_pipe_idx

In this code we did two things: - First, we rename the active_pipe to releasing_pipe_idx, which reflects the nature of the logic better - Second, we add the near_bird_pipe_idx, which is the index of the pipe that we should check with the bird. We passed this index to the next pipe when we detect that the current pipe has passed the bird, which means the bird can no longer touch it. That’s why we need to add the bird’s instance here.

You can configure that the bird also dies if it touches the ground, but I won’t implement that. Either way, if the bird touches the ground, it’s likely to touch the next pipe and die.

Now that we can kill the bird, let’s figure out what to do in that case.

GameOver mode

Here are some points that we should have when the game’s ended:

  • In the images folder, we’ve already had a “gameover.png”. One of the things we should do when “Game Over” is to display this image.
  • Secondly, the pipes should stop moving, but they shouldn’t disappear from the screen, like what we currently have.
  • The user should no longer be able to control the bird.
  • Also, it’d be nice to have the bird dropping from wherever it is to the ground, as it is now “dead”.

The first point should be easy, we just need to blit “gameover.png” on top of everything. For the second point, we just need to call pipe_system.draw() explicitly. We can also easily prevent user’s interaction by simply give a condition for the interaction.

The last point, the bird dropping dead, while is just optional, maybe a bit challenging. However, we’ve already made the bird dropping, so it shouldn’t be a problem. Also, as we know the pygame.transform.rotate, we can rotate the bird image to make it look more like it’s dead.

Previously, I have made a flappybird game with an entirely different image called “bird_dead”. However, I think rotating “bird_downflap.png” will work fine.

    def die(self):
        self.drop()
        self.draw(self.die_image)

You might have guessed what the self.die_imageis. You are right, it is the rotated version of the bird_downflap image that we stated earlier.

As we have everything we need for the GAMEOVER mode, let’s create a function for it:

def game_over():
    bird.die()
    pipe_system.draw()
    screen.blit(gameover_sign, gameover_location)

Question: Could you guess what the gameover_location is?

And since we have game_over(), let’s also have a game_on()

def game_on():
    bird.move()
    pipe_system.move()

Our loop now looks a bit less complex:

# Game state
gameon = True
while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: sys.exit()
        if event.type == pygame.KEYDOWN:
            if gameon:
                bird.jump()
            else:
                reset()

    screen.blit(background, (0, 0))
    if bird_touches_pipe_system(bird, pipe_system):
        gameon = False
    if gameon:
        game_on()
    else:
        game_over()
    pygame.display.flip()
    clock.tick(30)

By the way, I added a reset() function, which resets the game once it’s over and the user keeps pressing a button. I’ll leave you with figuring out how it works (If you get difficulties, the function will be in the source code).

Grading system

There are two things we need to take care when we implement the scoring system:

  1. How to keep score
  2. How to print out the score

The first point is easy enough: Every time the bird “flies” pass a Pipe instance, we can increase the point. We’ve had the logic of checking if the bird “flied” pass a pipe in class PipeSystem, so we just need to add the scoring there.

Since we graded the bird for its performance, I will implement this scoring system inside the Bird class, but please feel free to do it globally if you prefer.

The second problem is a bit more complex: how do we put the score to the screen? Sure, we have images for 0, 1, 2, 3,…, 9, so we can just blit them, but what about 10? Or any number with more than one digit?

First we have to “translate” the multi-digit number into the elemental digits, and then translate those digits into equivalent images. Then, we need to blit all of the images into the screen in a sequence, so that together, they look like the number that we begin with

Let’s get started with importing all the number images. Since we’re likely to have to convert the score to string and pick the image for every digit, let’s import these images into a dictionary whose keys are the characters of the digits.

digits = {str(num): pygame.image.load(f"images/{num}.png") for num in range(10)}

The merging of all needed images can be done by creating a new surface whose width is the sum of all images’ widths. We then blit all needed image onto this new surface, and then finally blit this new surface onto the screen.

Wait! Why don’t we just blit everything onto the screen? That will work, but the problem with it is that it will be difficult to place the whole number in the middle of the screen. (I’m not saying it can’t be done, but the code will be a bit ugly). Also by generating a new surface, we can isolate the logic and don’t have to manage all the separated images at once inside the loop. You are welcome to try out that method, anyway

def get_score_surface(bird):
    score_str = str(bird.points)
    required_images = [digits[digit] for digit in score_str]
    all_widths = [image.get_width() for image in required_images]
    combined_surface = pygame.Surface((sum(all_widths), required_images[0].get_height()))
    current_pos = 0
    for position, image in enumerate(required_images):
        combined_surface.blit(image, (current_pos, 0))
        current_pos += all_widths[position]

    return combined_surface

I hope that the code is understandable. Basically, we convert the bird’s score to string, and store the images that needed to render that string into a list. With that list, we can get all the widths of the images and generate a new Surface that has same height as all images, and has width equals sum of all widths. From that, we can blit each image onto the new Surface, with the position based on the total width of all images before it. Finally, we blit this surface onto the screen on every iteration.

This is how the game looks like right now. (You can simply move the blit for the combined_surface after every other blit for the score to appear on top, I like it better this way).

So yes, basically our little game is functional, but our job does not end here. See you next time, and as usual, here is our code until this point:

import sys, pygame

GAP = 150
VELOCITY = 2
DISTANCE = 150
GRAVITY = 1
BIRD_JUMP_ACC = 20


class Pipe:
    def __init__(self, top_pipe_height, longitude):
        self.top_pipe_height = top_pipe_height
        self.longitude = longitude
        self.initial_longitude = longitude
        self.visible = False

    def draw(self):
        bot_pipe_top = self.top_pipe_height + GAP
        screen.blit(toppipe, (self.longitude, 0), (0, pipe_height-self.top_pipe_height, pipe_width, self.top_pipe_height))
        screen.blit(botpipe, (self.longitude, bot_pipe_top), (0, 0, pipe_width, height - bot_pipe_top))

    def move(self):
        if self.visible:
            self.draw()
            self.longitude = self.longitude - VELOCITY
        if self.longitude < -pipe_width:
            self.reset()

    def reset(self):
        self.longitude = self.initial_longitude
        self.visible = False


class PipeSystem:
    def __init__(self, bird):
        pipe1 = Pipe(100, width + pipe_width)
        pipe2 = Pipe(50, width + pipe_width)
        self.pipes = [pipe1, pipe2]
        self.releasing_pipe_idx = 0
        self.near_bird_pipe_idx = 0
        self.pipes[self.releasing_pipe_idx].visible = True
        self.bird = bird

    def move(self):
        for pipe in self.pipes:
            pipe.move()
        if self.pipes[self.releasing_pipe_idx].longitude < (width - DISTANCE):
            self.releasing_pipe_idx = 1 - self.releasing_pipe_idx
            self.pipes[self.releasing_pipe_idx].visible = True
        if self.pipes[self.near_bird_pipe_idx].longitude + pipe_width < bird.longitude \
                or not self.pipes[self.near_bird_pipe_idx].visible:
            self.near_bird_pipe_idx = 1 - self.near_bird_pipe_idx
            self.bird.points += 1

    def draw(self):
        for pipe in self.pipes:
            if pipe.visible:
                pipe.draw()

    def reset(self):
        for pipe in self.pipes:
            pipe.reset()
        self.releasing_pipe_idx = 0
        self.near_bird_pipe_idx = 0
        self.pipes[self.releasing_pipe_idx].visible = True


class Bird:
    def __init__(self, initial_latitude):
        self.longitude = 0
        self.latitude = initial_latitude
        self.initial_latitude = initial_latitude
        self.velocity = 0
        self.images = [bird_upflap, bird_midflap, bird_downflap]
        self.die_image = bird_die
        self.image_idx = 0
        self.idx_increment = 1
        self.points = 0

    def die(self):
        self.drop()
        self.draw(self.die_image)

    def draw(self, bird):
        screen.blit(bird, (self.longitude, self.latitude))
        self.drop()

    def drop(self):
        self.latitude += self.velocity
        if self.latitude >= height - bird_height:
            self.latitue = height - bird_height
            self.velocity = 0
        else:
            self.velocity += GRAVITY

    def move(self):
        # Determine the current bird image
        bird = self.images[self.image_idx]
        self.image_idx += self.idx_increment
        self.draw(bird)

        # Change increment direction if necessary
        if self.image_idx >= 2 or self.image_idx <= 0:
            self.idx_increment = -self.idx_increment

    def jump(self):
        self.velocity -= BIRD_JUMP_ACC
        if self.latitude <= 0:
            self.latitude = 0
            self.velocity = 0

    def reset(self):
        self.latitude = self.initial_latitude
        self.velocity = 0


def bird_touches_pipe(bird, pipe):
    if pipe.longitude <= bird.longitude + bird_width and \
            pipe.longitude + pipe_width >= bird.longitude:
        if bird.latitude <= pipe.top_pipe_height or \
                bird.latitude + bird_height >= pipe.top_pipe_height + GAP:
            return True
    return False

def bird_touches_pipe_system(bird, pipe_system):
    return bird_touches_pipe(bird, pipe_system.pipes[pipe_system.near_bird_pipe_idx])


def get_score_surface(bird):
    score_str = str(bird.points)
    required_images = [digits[digit] for digit in score_str]
    all_widths = [image.get_width() for image in required_images]
    combined_surface = pygame.Surface((sum(all_widths), required_images[0].get_height()))
    current_pos = 0
    for position, image in enumerate(required_images):
        combined_surface.blit(image, (current_pos, 0))
        current_pos += all_widths[position]

    return combined_surface

pygame.init()

# Load images
background = pygame.image.load("images/background-day.png")
bird_upflap = pygame.image.load("images/redbird-upflap.png")
bird_midflap = pygame.image.load("images/redbird-midflap.png")
bird_downflap = pygame.image.load("images/redbird-downflap.png")
bird_die = pygame.transform.rotate(bird_downflap, -90)
gameover_sign = pygame.image.load("images/gameover.png")

botpipe = pygame.image.load("images/pipe-green.png")
toppipe = pygame.transform.rotate(botpipe, 180)

digits = {str(num): pygame.image.load(f"images/{num}.png") for num in range(10)}

# Glbal sizes of objects
size = width, height = background.get_size()
pipe_size = pipe_width, pipe_height = toppipe.get_size()

screen = pygame.display.set_mode(size)
bird_width, bird_height = bird_upflap.get_size()
bird_y_pos = int(height/2 - bird_height/2)

g_width, g_height = gameover_sign.get_size()
gameover_location = (int(width/2 - g_width/2), int(height/2 - g_height/2))

# Global objects
bird = Bird(bird_y_pos)
pipe_system = PipeSystem(bird)
clock = pygame.time.Clock()

# Game state.
gameon = True

def game_over():
    bird.die()
    pipe_system.draw()
    screen.blit(gameover_sign, gameover_location)

def game_on():
    bird.move()
    pipe_system.move()
    # print(bird.points)

def reset():
    global gameon
    gameon = True
    bird.reset()
    pipe_system.reset()


while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: sys.exit()
        if event.type == pygame.KEYDOWN:
            if gameon:
                bird.jump()
            else:
                reset()

    screen.blit(background, (0, 0))
    score_surface = get_score_surface(bird)
    screen.blit(score_surface, (int(width/2 - score_surface.get_width()/2), int(height/3)))
    if bird_touches_pipe_system(bird, pipe_system):
        gameon = False
    if gameon:
        game_on()
    else:
        game_over()
    pygame.display.flip()
    clock.tick(30)

To see other posts in the same series, please click below:

Part 1 - Setup virtualenv

Part 2 - Setup Pygame

Part 3 - Start making game

Part 4 - Make a “flapping” flappy bird

Part 5 - Make the bird fly

Part 6 - Pipe System

Part 7 - Kill the Bird

Part 8 - Add game logic

Part 9 - Finalize the game

Written by Huy Mai