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 4 - Make a “flapping” flappy bird
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_image
is. 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:
- How to keep score
- 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: