Build Flappy Bird Using Pygame

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

Introduction

In this project, we will break down the code for creating a simple Flappy Bird game using the Pygame library into manageable steps. By following these steps, you will learn how to build the game gradually. Each step will include a brief explanation, code blocks, and comments to help you understand and implement the game. Let's get started!

👀 Preview

Alt text

ðŸŽŊ Tasks

In this project, you will learn:

  • How to set up the project files for the Flappy Bird game
  • How to show the welcome animation for the game
  • How to implement the main game logic for Flappy Bird
  • How to display the game over screen when the player loses
  • How to define helper functions for the game

🏆 Achievements

After completing this project, you will be able to:

  • Use the Pygame library to create games
  • Understand game development concepts such as game loops, collisions, and animation

Create the Project Files

First, we'll create the project files for the Flappy Bird game.

cd ~/project
touch flappy.py
sudo pip install pygame

In this step, we'll set up the basic project structure and import necessary libraries. We'll also define some constants and load initial game assets.

from itertools import cycle
import random
import sys
import pygame
from pygame.locals import *

FPS = 30
SCREENWIDTH = 288
SCREENHEIGHT = 512
PIPEGAPSIZE = 100  ## gap between upper and lower part of pipe
BASEY = SCREENHEIGHT * 0.79
## image and hitmask  dicts
IMAGES, HITMASKS = {}, {}

## list of all possible players (tuple of 3 positions of flap)
PLAYERS_LIST = (
    ## red bird
    (
        "data/sprites/redbird-upflap.png",
        "data/sprites/redbird-midflap.png",
        "data/sprites/redbird-downflap.png",
    ),
    ## blue bird
    (
        "data/sprites/bluebird-upflap.png",
        "data/sprites/bluebird-midflap.png",
        "data/sprites/bluebird-downflap.png",
    ),
    ## yellow bird
    (
        "data/sprites/yellowbird-upflap.png",
        "data/sprites/yellowbird-midflap.png",
        "data/sprites/yellowbird-downflap.png",
    ),
)

## list of backgrounds
BACKGROUNDS_LIST = (
    "data/sprites/background-day.png",
    "data/sprites/background-night.png",
)

## list of pipes
PIPES_LIST = (
    "data/sprites/pipe-green.png",
    "data/sprites/pipe-red.png",
)
  • We import the necessary libraries for the game, including pygame for creating the game, random for generating random elements, sys for system-related functions, and pygame.locals for key constants.
  • We initialize Pygame with pygame.init().
  • Constants like FPS, SCREENWIDTH, SCREENHEIGHT, PIPEGAPSIZE, and BASEY are defined to set up the game's dimensions and speed.
  • We create empty dictionaries (IMAGES and HITMASKS) to store game assets.
  • Lists of player, background, and pipe assets are defined using file paths.
âœĻ Check Solution and Practice

Show the Welcome Animation

In this step, we'll create the welcome screen animation of the Flappy Bird game.

def showWelcomeAnimation():
    """Shows welcome screen animation of flappy bird"""
    ## index of player to blit on screen
    playerIndex = 0
    playerIndexGen = cycle([0, 1, 2, 1])
    ## iterator used to change playerIndex after every 5th iteration
    loopIter = 0

    playerx = int(SCREENWIDTH * 0.2)
    playery = int((SCREENHEIGHT - IMAGES["player"][0].get_height()) / 2)

    messagex = int((SCREENWIDTH - IMAGES["message"].get_width()) / 2)
    messagey = int(SCREENHEIGHT * 0.12)

    basex = 0
    ## amount by which base can maximum shift to left
    baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()

    ## player shm for up-down motion on welcome screen
    playerShmVals = {"val": 0, "dir": 1}

    while True:
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
                ## make first flap sound and return values for mainGame
                return {
                    "playery": playery + playerShmVals["val"],
                    "basex": basex,
                    "playerIndexGen": playerIndexGen,
                }

        ## adjust playery, playerIndex, basex
        if (loopIter + 1) % 5 == 0:
            playerIndex = next(playerIndexGen)
        loopIter = (loopIter + 1) % 30
        basex = -((-basex + 4) % baseShift)
        playerShm(playerShmVals)

        ## draw sprites
        SCREEN.blit(IMAGES["background"], (0, 0))
        SCREEN.blit(
            IMAGES["player"][playerIndex], (playerx, playery + playerShmVals["val"])
        )
        SCREEN.blit(IMAGES["message"], (messagex, messagey))
        SCREEN.blit(IMAGES["base"], (basex, BASEY))

        pygame.display.update()
        FPSCLOCK.tick(FPS)
  • We define the showWelcomeAnimation function responsible for displaying the welcome screen animation.
  • The function sets up variables for animation and handles user input to start the game.
  • It uses a loop to update the animation frames and check for user input to begin the game.
  • The animation includes the bird flapping and a message displayed on the screen.
  • The pygame.display.update() function is used to update the display, and FPSCLOCK.tick(FPS) controls the frame rate.
âœĻ Check Solution and Practice

Main Game Logic

In this step, we'll implement the main game logic for Flappy Bird.

def mainGame(movementInfo):
    score = playerIndex = loopIter = 0
    playerIndexGen = movementInfo["playerIndexGen"]
    playerx, playery = int(SCREENWIDTH * 0.2), movementInfo["playery"]

    basex = movementInfo["basex"]
    baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()

    ## get 2 new pipes to add to upperPipes lowerPipes list
    newPipe1 = getRandomPipe()
    newPipe2 = getRandomPipe()

    ## list of upper pipes
    upperPipes = [
        {"x": SCREENWIDTH + 200, "y": newPipe1[0]["y"]},
        {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[0]["y"]},
    ]

    ## list of lowerpipe
    lowerPipes = [
        {"x": SCREENWIDTH + 200, "y": newPipe1[1]["y"]},
        {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[1]["y"]},
    ]

    dt = FPSCLOCK.tick(FPS) / 1000
    pipeVelX = -128 * dt

    ## player velocity, max velocity, downward acceleration, acceleration on flap
    playerVelY = -9  ## player's velocity along Y, default same as playerFlapped
    playerMaxVelY = 10  ## max vel along Y, max descend speed
    playerMinVelY = -8  ## min vel along Y, max ascend speed
    playerAccY = 1  ## players downward acceleration
    playerRot = 45  ## player's rotation
    playerVelRot = 3  ## angular speed
    playerRotThr = 20  ## rotation threshold
    playerFlapAcc = -9  ## players speed on flapping
    playerFlapped = False  ## True when player flaps

    while True:
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
                if playery > -2 * IMAGES["player"][0].get_height():
                    playerVelY = playerFlapAcc
                    playerFlapped = True

        ## check for crash here
        crashTest = checkCrash(
            {"x": playerx, "y": playery, "index": playerIndex}, upperPipes, lowerPipes
        )
        if crashTest[0]:
            return {
                "y": playery,
                "groundCrash": crashTest[1],
                "basex": basex,
                "upperPipes": upperPipes,
                "lowerPipes": lowerPipes,
                "score": score,
                "playerVelY": playerVelY,
                "playerRot": playerRot,
            }

        ## check for score
        playerMidPos = playerx + IMAGES["player"][0].get_width() / 2
        for pipe in upperPipes:
            pipeMidPos = pipe["x"] + IMAGES["pipe"][0].get_width() / 2
            if pipeMidPos <= playerMidPos < pipeMidPos + 4:
                score += 1

        ## playerIndex basex change
        if (loopIter + 1) % 3 == 0:
            playerIndex = next(playerIndexGen)
        loopIter = (loopIter + 1) % 30
        basex = -((-basex + 100) % baseShift)

        ## rotate the player
        if playerRot > -90:
            playerRot -= playerVelRot

        ## player's movement
        if playerVelY < playerMaxVelY and not playerFlapped:
            playerVelY += playerAccY
        if playerFlapped:
            playerFlapped = False

            ## more rotation to cover the threshold (calculated in visible rotation)
            playerRot = 45

        playerHeight = IMAGES["player"][playerIndex].get_height()
        playery += min(playerVelY, BASEY - playery - playerHeight)

        ## move pipes to left
        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            uPipe["x"] += pipeVelX
            lPipe["x"] += pipeVelX

        ## add new pipe when first pipe is about to touch left of screen
        if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]["x"] < 5:
            newPipe = getRandomPipe()
            upperPipes.append(newPipe[0])
            lowerPipes.append(newPipe[1])

        ## remove first pipe if its out of the screen
        if len(upperPipes) > 0 and upperPipes[0]["x"] < -IMAGES["pipe"][0].get_width():
            upperPipes.pop(0)
            lowerPipes.pop(0)

        ## draw sprites
        SCREEN.blit(IMAGES["background"], (0, 0))

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
            SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))

        SCREEN.blit(IMAGES["base"], (basex, BASEY))
        ## print score so player overlaps the score
        showScore(score)

        ## Player rotation has a threshold
        visibleRot = playerRotThr
        if playerRot <= playerRotThr:
            visibleRot = playerRot

        playerSurface = pygame.transform.rotate(
            IMAGES["player"][playerIndex], visibleRot
        )
        SCREEN.blit(playerSurface, (playerx, playery))

        pygame.display.update()
        FPSCLOCK.tick(FPS)
  • We define the mainGame function, which contains the core logic for the Flappy Bird game.
  • The function handles user input, updates the game state, checks for collisions, and keeps track of the score.
  • The game loop runs continuously, updating the game's display and logic.
  • Player controls are handled through key events (space or up arrow).
  • The function also checks for collisions with pipes and the ground, updates the score, and manages the animation of the bird.
  • The game loop continues until the player crashes or exits the game.
âœĻ Check Solution and Practice

Display the Game Over Screen

In this step, we'll create the game over screen that appears when the player loses.

def showGameOverScreen(crashInfo):
    """crashes the player down and shows gameover image"""
    score = crashInfo["score"]
    playerx = SCREENWIDTH * 0.2
    playery = crashInfo["y"]
    playerHeight = IMAGES["player"][0].get_height()
    playerVelY = crashInfo["playerVelY"]
    playerAccY = 2
    playerRot = crashInfo["playerRot"]
    playerVelRot = 7

    basex = crashInfo["basex"]

    upperPipes, lowerPipes = crashInfo["upperPipes"], crashInfo["lowerPipes"]

    while True:
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
                if playery + playerHeight >= BASEY - 1:
                    return

        ## player y shift
        if playery + playerHeight < BASEY - 1:
            playery += min(playerVelY, BASEY - playery - playerHeight)

        ## player velocity change
        if playerVelY < 15:
            playerVelY += playerAccY

        ## rotate only when it's a pipe crash
        if not crashInfo["groundCrash"]:
            if playerRot > -90:
                playerRot -= playerVelRot

        ## draw sprites
        SCREEN.blit(IMAGES["background"], (0, 0))

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
            SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))

        SCREEN.blit(IMAGES["base"], (basex, BASEY))
        showScore(score)

        playerSurface = pygame.transform.rotate(IMAGES["player"][1], playerRot)
        SCREEN.blit(playerSurface, (playerx, playery))
        SCREEN.blit(IMAGES["gameover"], (50, 180))

        FPSCLOCK.tick(FPS)
        pygame.display.update()
  • The showGameOverScreen function displays the game over screen when the player loses.
  • It shows the player's final score, plays sound effects, and waits for the player to press space or up arrow to restart the game.
  • The animation includes the bird falling to the ground and the game over message displayed on the screen.
âœĻ Check Solution and Practice

Define Helper Functions

In this step, we define helper functions that are used in the game.

def playerShm(playerShm):
    """oscillates the value of playerShm['val'] between 8 and -8"""
    if abs(playerShm["val"]) == 8:
        playerShm["dir"] *= -1

    if playerShm["dir"] == 1:
        playerShm["val"] += 1
    else:
        playerShm["val"] -= 1


def getRandomPipe():
    """returns a randomly generated pipe"""
    ## y of gap between upper and lower pipe
    gapY = random.randrange(0, int(BASEY * 0.6 - PIPEGAPSIZE))
    gapY += int(BASEY * 0.2)
    pipeHeight = IMAGES["pipe"][0].get_height()
    pipeX = SCREENWIDTH + 10

    return [
        {"x": pipeX, "y": gapY - pipeHeight},  ## upper pipe
        {"x": pipeX, "y": gapY + PIPEGAPSIZE},  ## lower pipe
    ]


def showScore(score):
    """displays score in center of screen"""
    scoreDigits = [int(x) for x in list(str(score))]
    totalWidth = 0  ## total width of all numbers to be printed

    for digit in scoreDigits:
        totalWidth += IMAGES["numbers"][digit].get_width()

    Xoffset = (SCREENWIDTH - totalWidth) / 2

    for digit in scoreDigits:
        SCREEN.blit(IMAGES["numbers"][digit], (Xoffset, SCREENHEIGHT * 0.1))
        Xoffset += IMAGES["numbers"][digit].get_width()


def checkCrash(player, upperPipes, lowerPipes):
    """returns True if player collides with base or pipes."""
    pi = player["index"]
    player["w"] = IMAGES["player"][0].get_width()
    player["h"] = IMAGES["player"][0].get_height()

    ## if player crashes into ground
    if player["y"] + player["h"] >= BASEY - 1:
        return [True, True]
    else:
        playerRect = pygame.Rect(player["x"], player["y"], player["w"], player["h"])
        pipeW = IMAGES["pipe"][0].get_width()
        pipeH = IMAGES["pipe"][0].get_height()

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            ## upper and lower pipe rects
            uPipeRect = pygame.Rect(uPipe["x"], uPipe["y"], pipeW, pipeH)
            lPipeRect = pygame.Rect(lPipe["x"], lPipe["y"], pipeW, pipeH)

            ## player and upper/lower pipe hitmasks
            pHitMask = HITMASKS["player"][pi]
            uHitmask = HITMASKS["pipe"][0]
            lHitmask = HITMASKS["pipe"][1]

            ## if bird collided with upipe or lpipe
            uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask)
            lCollide = pixelCollision(playerRect, lPipeRect, pHitMask, lHitmask)

            if uCollide or lCollide:
                return [True, False]

    return [False, False]


def pixelCollision(rect1, rect2, hitmask1, hitmask2):
    """Checks if two objects collide and not just their rects"""
    rect = rect1.clip(rect2)

    if rect.width == 0 or rect.height == 0:
        return False

    x1, y1 = rect.x - rect1.x, rect.y - rect1.y
    x2, y2 = rect.x - rect2.x, rect.y - rect2.y

    for x in range(rect.width):
        for y in range(rect.height):
            if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]:
                return True
    return False


def getHitmask(image):
    """returns a hitmask using an image's alpha."""
    mask = []
    for x in range(image.get_width()):
        mask.append([])
        for y in range(image.get_height()):
            mask[x].append(bool(image.get_at((x, y))[3]))
    return mask
  • The playerShm function oscillates the value of playerShm["val"] between 8 and -8. This is used to move the bird up and down on the welcome screen.
  • The getRandomPipe function returns a randomly generated pipe. It generates a random gap between the upper and lower pipes.
  • The showScore function displays the score in the center of the screen. It uses the IMAGES["numbers"] list to display the score.
  • The checkCrash function returns True if the player collides with the ground or pipes. It uses the pixelCollision function to check for collisions.
  • The pixelCollision function checks if two objects collide and not just their rects. It uses the getHitmask function to get the hitmask for the player and pipes.
  • The getHitmask function returns a hitmask using an image's alpha. It uses the image.get_at function to get the alpha value of each pixel in the image.
âœĻ Check Solution and Practice

The Main Function

In this step, we define the main function that initializes the game and starts the game loop.

def main():
    global SCREEN, FPSCLOCK
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    SCREEN = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
    pygame.display.set_caption("Flappy Bird")

    ## numbers sprites for score display
    IMAGES["numbers"] = (
        pygame.image.load("data/sprites/0.png").convert_alpha(),
        pygame.image.load("data/sprites/1.png").convert_alpha(),
        pygame.image.load("data/sprites/2.png").convert_alpha(),
        pygame.image.load("data/sprites/3.png").convert_alpha(),
        pygame.image.load("data/sprites/4.png").convert_alpha(),
        pygame.image.load("data/sprites/5.png").convert_alpha(),
        pygame.image.load("data/sprites/6.png").convert_alpha(),
        pygame.image.load("data/sprites/7.png").convert_alpha(),
        pygame.image.load("data/sprites/8.png").convert_alpha(),
        pygame.image.load("data/sprites/9.png").convert_alpha(),
    )

    ## game over sprite
    IMAGES["gameover"] = pygame.image.load("data/sprites/gameover.png").convert_alpha()
    ## message sprite for welcome screen
    IMAGES["message"] = pygame.image.load("data/sprites/message.png").convert_alpha()
    ## base (ground) sprite
    IMAGES["base"] = pygame.image.load("data/sprites/base.png").convert_alpha()

    while True:
        ## select random background sprites
        randBg = random.randint(0, len(BACKGROUNDS_LIST) - 1)
        IMAGES["background"] = pygame.image.load(BACKGROUNDS_LIST[randBg]).convert()

        ## select random player sprites
        randPlayer = random.randint(0, len(PLAYERS_LIST) - 1)
        IMAGES["player"] = (
            pygame.image.load(PLAYERS_LIST[randPlayer][0]).convert_alpha(),
            pygame.image.load(PLAYERS_LIST[randPlayer][1]).convert_alpha(),
            pygame.image.load(PLAYERS_LIST[randPlayer][2]).convert_alpha(),
        )

        ## select random pipe sprites
        pipeindex = random.randint(0, len(PIPES_LIST) - 1)
        IMAGES["pipe"] = (
            pygame.transform.flip(
                pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), False, True
            ),
            pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(),
        )

        ## hitmask for pipes
        HITMASKS["pipe"] = (
            getHitmask(IMAGES["pipe"][0]),
            getHitmask(IMAGES["pipe"][1]),
        )

        ## hitmask for player
        HITMASKS["player"] = (
            getHitmask(IMAGES["player"][0]),
            getHitmask(IMAGES["player"][1]),
            getHitmask(IMAGES["player"][2]),
        )

        movementInfo = showWelcomeAnimation()
        crashInfo = mainGame(movementInfo)
        showGameOverScreen(crashInfo)
  • The main function initializes the game, sets up the display, and starts the game loop.
  • It loads game assets, including images, and randomly selects background, player, and pipe sprites.
  • The game loop handles the welcome animation, the main game, and the game over screen.
  • The game continues to loop until the player exits the game or closes the window.
âœĻ Check Solution and Practice

Run the Game

In this step, we'll run the Flappy Bird game.

if __name__ == "__main__":
    main()
  • The __name__ == "__main__" condition checks if the current module is being run by itself or imported by another module.
  • If the current module is being run by itself, the main function is called to start the game.

Once you've completed all the steps, you can run the Flappy Bird game using the following command:

cd ~/project
python flappy.py
Alt text
âœĻ Check Solution and Practice

Summary

In this project, we've split the Flappy Bird game code into multiple steps and provided explanations for each step. You've learned how to create a basic game structure, handle player input, update game state, check for collisions, and display game screens. You can now run and play the Flappy Bird game using the provided code. Enjoy your game development journey!

Other Python Tutorials you may like