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 4 - Make a “flapping” flappy bird
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 thanscreen_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 aDISTANCE
from the right side (hence a new pipe is expected). Therefore, if we wantDISTANCE
to be smaller thanscreen_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 exactlyscreen_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 thanscreen_width/2 - pipe_width/2
(while still smaller thanscreen_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 thanDISTANCE
.
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:
- 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 iswidth + pipe_width
, which makes they gradually moves into the screen). - 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 reacheswidth - DISTANCE
. - When 2) is satisfied and a new pipe changes
visible
toTRUE
, 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 isactive
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: