Create a Flappy Bird Clone With Python P6

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

At the end of part 5, we successfully generated a pipe pair (with one toppipe and one botpipe), of which we can provide an initial longitude and the height of toppipe to decide how and where the pipe is drawn. We were also able to “move” the pipes from right to left, which creates an effect of the bird flying from left to right. We also made the pipe reset to its original longitude once it reaches the left side of the screen, so that it looks like there are several pipes.

Here is how it looks right now.

However, what if we want the distance between to pipes to be smaller than a screen width? i.e. what if at some points we want more than one pipe visible on the screen?

This problem is solvable by having two pipes (in here, a “pipe” is understood as one toppipe - botpipe system that we built up last time).

Let’s stop a moment and think about it: what will be different if we have two pipes?

  • The distance between two pipes (which we denote DISTANCE) shouldn’t be smaller than screen_width/2 - pipe_width/2, otherwise the first pipe won’t have left the screen (hence cannot be brought back to the original position) when the second pipe has traveled a DISTANCE from the right side (hence a new pipe is expected). Therefore, if we want DISTANCE to be smaller than screen_width/2, we will need a third pipe. Let’s just stick with two pipes for now, if we can make it work, then three pipes will be easy.
  • If DISTANCE is exactly screen_width/2 - pipe_width/2, then at the exact moment the first pipe is out of screen, the second pipe will be at the exact middle of the screen, hence the first pipe can reset it’s location and start right up. This will be the perfect scenario.
  • However, if the DISTANCE is larger than screen_width/2 - pipe_width/2 (while still smaller than screen_width), the first pipe will not be able to pop right out after it’s reset to the original longitude, since the second pipe might have only traveled less than DISTANCE.

We can also make an experiment. Let’s create another pipe with original longitude of DISTANCE from the original longitude of the first pipe

DISTANCE = 150
...
pipe1 = Pipe(100, width + pipe_width)
pipe2 = Pipe(50, width + pipe_width + DISTANCE)
while 1:
    ...
    pipe1.draw()
    pipe2.draw()

And as expected, the pipes move in a very unexpected way (see what I did there, lol).

This problem can be solved by creating a sophisticated mathematics synchronization, so that the pipes’ original positions, the distance between them, and their velocity work perfectly together.

Or it can be solved by creating a real synchronizing logic, which allows a pipe to be “released” only when the previous pipe has traveled DISTANCE.

I don’t know about you, but I like the second option better, it will still work if we decide that we would like to change any of the parameters, even the screen size.

Before we do that, did you notice that in the previous image, we have different gaps for the two pipes? Seems like we have mistakes in our logic to draw the pipes: for some reason, shorter toppipe leads to shorter botpipe, while it should be longer botpipe instead.

And sure enough, when we check the draw() function, it can be seen quite clear that we set the latitude of the botpipe to be pipe_height-self.top_pipe_height + GAP, which leads to larger latitude when the top_pipe_height is smaller (remember, the coordiates is counted from top left, so larger latitude means the image starts from further down, which in this case creates shorter botpipe). What we want is actually to start drawing the botpipe at top_pipe_height + GAP.

Now as we’re at it, I can also see that the area was wrong, too. Originally, I said that we want to not draw anything outside of the visible screen, that’s why we use the optional area. However, the botpipe’s area now is (0, 0, pipe_width, pipe_height), which essentially means the whole of botpipe image. (I’m embarrassed, OMG)

Let’s fix that too. The pipe_width should be ok, but what about the height. Since we starts drawing the botpipe at top_pipe_height + GAP, and stops drawing at screen_height, the bot_pipe_height should be screen_height subtracts top_pipe_height + GAP.

Here is the draw() function now:

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

Now that we fixed drawing logic, the gap turned out to be too small. Let’s increase it to 150.

OK. Back to the problem we have about pipe’s moving condition. As a pipe is now moving all the time, and being sent back to its original position every time it reaches the end of the road, we can’t really control when it appears. Let’s change that by adding a condition:

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

    def draw(self):
        ...

    def move(self):
        if self.visible:
            self.draw()
            self.longitute = self.longitute - VELOCITY
        if self.longitute < -pipe_width:
            self.longitute = self.initial_longitute
            self.visible = False

What we have here is a visible parameter, which acts like a lock. It allows the pipe to be drawn and moved only when it’s TRUE, which gives us better controlling of when the pipe is allowed to move. Its original value is FALSE, and is turned to FALSE again once the pipe is out of the screen, so if we want to make it move, we need to explicitly set it to TRUE, i.e. it will move only when we allow, which is exactly what we want.

Now instead of having to tinker the original position and velocity to control how the pipes appear, we can explicitly set its visible state. But how do we provide the logic? Let’s think about it for a moment:

  1. Since the pipes don’t move all the time, we don’t need to set their original positions DISTANCE apart. Instead, they all can start from the same position (which is width + pipe_width, which makes they gradually moves into the screen).
  2. The next pipe in line should be allowed to move into the screen when and only when the current pipe has moved DISTANCE, or its latitude reaches width - DISTANCE.
  3. When 2) is satisfied and a new pipe changes visible to TRUE, then that new pipe should be the current pipe, i.e. we should observe its position (instead of the previous one) until 2) happens again.

Since it looks like we will have to manage both the pipes (or all pipes, just in case we want more than two) together using some logic, why don’t we create a class for it?

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

    def move(self):
        for pipe in self.pipes:
            pipe.move()
        if self.pipes[self.active_pipe].longitute < (width - DISTANCE):
            self.active_pipe = 1 - self.active_pipe
            self.pipes[self.active_pipe].visible = True

The logic is just as we just explained: we have a list of the pipes we want to manage, and the first one (index 0) on the list is set to be the active_pipe, and this pipe is also set to be visible, which means it will be the first one to move (remember, by default visible is FALSE, so we need to explicitly change it to TRUE)

The move() function of this new class just simply calls move() function of every pipe. Since the visible state of the pipes are different, some pipes will move and some won’t. The PipeSystem doesn’t care about that. It, however, knows for sure that the active pipe is visible, and actively monitor that pipe’s longitude in every move. Once the pipe has moved DISTANCE, the active flag is passed to the next pipe, and the new active pipe will be set to visible, and will be monitored from that moment on, until it, in turn, has moved DISTANCE.

You may have questioned: what about those pipes that are no longer the active one? We just stop monitoring them and just let them run, what if they act strangely?

Well, we don’t have to care too much about that, once a pipe is out of the screen, its visible state is reset, so it won’t do anything until it’s called again. Here, since we have only two pipes, it won’t take long until the pipe is active again. When we have more pipes, those pipes will just line up in queue

We now can replace the pipe1 and pipe2 in the main program with just a pipeSystem:

pipe_system = PipeSystem()

while 1:
    ...
    pipe_system.move()

Here is what it looks like now: our little bird seems like it is “phasing” through all the pipes, kinda like The Flash, or Uchiha Obito

However, our bird might want to be able to jump and fall, rather than flying on a straight line. Let’s give him that ability next time.

Before we depart, here is our full code at this point. See you in the next part.

import sys, pygame
pygame.init()

GAP = 150
VELOCITY = 2
DISTANCE = 150

class Pipe:
    def __init__(self, top_pipe_height, longitute):
        self.top_pipe_height = top_pipe_height
        self.longitute = longitute
        self.initial_longitute = longitute
        self.visible = False

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

    def move(self):
        if self.visible:
            self.draw()
            self.longitute = self.longitute - VELOCITY
        if self.longitute < -pipe_width:
            self.longitute = self.initial_longitute
            self.visible = False

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

    def move(self):
        for pipe in self.pipes:
            pipe.move()
        if self.pipes[self.active_pipe].longitute < (width - DISTANCE):
            self.active_pipe = 1 - self.active_pipe
            self.pipes[self.active_pipe].visible = True

# 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")

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

bird_images = [bird_upflap, bird_midflap, bird_downflap]

size = width, height = background.get_size()
pipe_size = pipe_width, pipe_height = toppipe.get_size()

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

bird_idx = 0
increment = 1
pipe_system = PipeSystem()

while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: sys.exit()

    # Determine the current bird
    bird = bird_images[bird_idx]
    bird_idx += increment

    # Change increment direction if necessary
    if bird_idx >= 2 or bird_idx <= 0:
        increment = -increment

    screen.blit(background, (0, 0))
    screen.blit(bird, (0, bird_y_pos))
    pipe_system.move()
    pygame.display.flip()

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