Building a Flappy Bird Game with the Kivy Library in Python

Flappy Bird Game is a classic game where a player controls a bird to avoid pipes by tapping on the screen to make the bird “flap.” In this article, we’ll go step-by-step to build the game using Kivy, a popular Python library for developing multitouch applications.

We’ll cover setting up the Kivy library, creating the game environment, and adding basic game mechanics like gravity and collision detection.

Prerequisites

  • Python 3.x installed on your system.
  • Familiarity with Python and basic game concepts.
  • Installation of Kivy (if not already installed) via:
pip install kivy

Step 1: Load essential modules

import kivy
kivy.require('1.7.2')
 
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.core.window import Window
from kivy.properties import NumericProperty
from kivy.clock import Clock
from kivy.graphics import Rectangle
from random import

Step 2: Configure the graphics

from kivy.config import Config
Config.set('graphics','resizable',0) #don't make the app re-sizeable
#Graphics fix
 #this fixes drawing issues on some phones
Window.clearcolor = (0,0,0,1.) 

The game includes three main components: the spaceship, the asteroids, and the main application. We’ll create a dedicated class for each, as well as a separate class for buttons. Since multiple game objects will need to handle graphical elements, it’s helpful to have a general-purpose class to set up Kivy widgets that can be displayed and moved around the screen. This approach streamlines the graphics setup and makes it easier to manage object movement on the screen.

class WidgetDrawer(Widget):
    #This widget is used to draw all of the objects on the screen
    #it handles the following:
    # widget movement, size, positioning
    #whever a WidgetDrawer object is created, an image string needs to be specified
    #example:    wid - WidgetDrawer('./image.png')
 
    #objects of this class must be initiated with an image string
#;You can use **kwargs to let your functions take an arbitrary number of keyword arguments
#kwargs ; keyword arguments
    def __init__(self, imageStr, **kwargs): 
        super(WidgetDrawer, self).__init__(**kwargs) #this is part of the **kwargs notation
#if you haven't seen with before, here's a link http://effbot.org/zone/python-with-statement.html     
        with self.canvas: 
#setup a default size for the object
            self.size = (Window.width*.002*25,Window.width*.002*25) 
#this line creates a rectangle with the image drawn on top
            self.rect_bg=Rectangle(source=imageStr,pos=self.pos,size = self.size) 
#this line calls the update_graphics_pos function every time the position variable is modified
            self.bind(pos=self.update_graphics_pos) 
            self.x = self.center_x
            self.y = self.center_y
#center the widget 
            self.pos = (self.x,self.y) 
#center the rectangle on the widget
            self.rect_bg.pos = self.pos 
 
    def update_graphics_pos(self, instance, value):
#if the widgets position moves, the rectangle that contains the image is also moved
        self.rect_bg.pos = value  
#use this function to change widget size        
    def setSize(self,width, height): 
        self.size = (width, height)
 #use this function to change widget position    
    def setPos(xpos,ypos):
        self.x = xpos
        self.y = ypos

The class above is responsible for rendering graphics, making images movable, and automatically updating their position whenever the position property changes. This allows changes in the widget’s x position to directly affect the x position of its image. By having each widget with graphical elements inherit from this class, we can streamline the code and improve efficiency.

Next, we’ll create two classes—one for the ship and one for the asteroids—that will utilize the WidgetDrawer class to handle image rendering.

class Asteroid(WidgetDrawer):
    #Asteroid class. The flappy ship will dodge these
    velocity_x = NumericProperty(0) #initialize velocity_x and velocity_y
    velocity_y = NumericProperty(0) #declaring variables is not necessary in python
 #update the position using the velocity defined here. every time move is called we change the position by velocity_x  
    def move(self):                    
        self.x = self.x + self.velocity_x 
        self.y = self.y + self.velocity_y 
    def update(self): 
#the update function moves the astreoid. Other things could happen here as well (speed changes for example)       
        self.move()

The next class is for the ship. It looks similar to the asteroid class.

class Ship(WidgetDrawer):
    #Ship class. This is for the main ship object. 
    #velocity of ship on x/y axis
 
    impulse = 3 #this variable will be used to move the ship up
    grav = -0.1 #this variable will be used to pull the ship down
 
    velocity_x = NumericProperty(0) #we wont actually use x movement
    velocity_y = NumericProperty(0) 
 
    def move(self):                    
        self.x = self.x + self.velocity_x 
        self.y = self.y + self.velocity_y 
 
        #don't let the ship go too far
        if self.y  Window.height*0.95: #don't let the ship go up too high
            self.impulse = -3
 
    def determineVelocity(self):
        #move the ship up and down
        #we need to take into account our acceleration
        #also want to look at gravity
        self.grav = self.grav*1.05  #the gravitational velocity should increase
        #set a grav limit
        if self.grav < -4: #set a maximum falling down speed (terminal velocity)
            self.grav = -4
        #the ship has a propety called self.impulse which is updated
        #whenever the player touches, pushing the ship up
        #use this impulse to determine the ship velocity
        #also decrease the magnitude of the impulse each time its used
 
        self.velocity_y = self.impulse + self.grav
        self.impulse = 0.95*self.impulse #make the upward velocity decay
 
    def update(self):
        self.determineVelocity() #first figure out the new velocity
        self.move()              #now move the ship

Next we will create the main widget for the screen. This widget will have the ship and asteroids drawn on top of it.

class GUI(Widget):
    #this is the main widget that contains the game. 
    asteroidList =[] #use this to keep track of asteroids
    minProb = 1700 #this variable used in spawning asteroids
    def __init__(self, **kwargs):
        super(GUI, self).__init__(**kwargs)
        l = Label(text='Flappy Ship') #give the game a title
        l.x = Window.width/2 - l.width/2
        l.y = Window.height*0.8
        self.add_widget(l) #add the label to the screen
 
        #now we create a ship object
 #notice how we specify the ship image
        self.ship = Ship(imageStr = './ship.png')
        self.ship.x = Window.width/4
        self.ship.y = Window.height/2
        self.add_widget(self.ship)
 
    def addAsteroid(self):
        #add an asteroid to the screen 
        #self.asteroid 
        imageNumber = randint(1,4)
        imageStr = './sandstone_'+str(imageNumber)+'.png'     
        tmpAsteroid = Asteroid(imageStr)
        tmpAsteroid.x = Window.width*0.99
 
        #randomize y position
        ypos = randint(1,16)
 
        ypos = ypos*Window.height*.0625
 
        tmpAsteroid.y = ypos
        tmpAsteroid.velocity_y = 0
        vel = 10
        tmpAsteroid.velocity_x = -0.1*vel
 
        self.asteroidList.append(tmpAsteroid)
        self.add_widget(tmpAsteroid)
 
    #handle input events
    #kivy has a great event handler. the on_touch_down function is already recognized 
    #and doesn't need t obe setup. Every time the screen is touched, the on_touch_down function is called
    def on_touch_down(self, touch):
        self.ship.impulse = 3 #give the ship an impulse
        self.ship.grav = -0.1 #reset the gravitational velocity
 
    def gameOver(self): #this function is called when the game ends
        #add a restart button
        restartButton = MyButton(text='Restart')
 
        #restartButton.background_color = (.5,.5,1,.2)
        def restart_button(obj):
        #this function will be called whenever the reset button is pushed
            print 'restart button pushed'
            #reset game
            for k in self.asteroidList:
                self.remove_widget(k)
 
                self.ship.xpos = Window.width*0.25
                self.ship.ypos = Window.height*0.5
                self.minProb = 1700
            self.asteroidList = []
 
            self.parent.remove_widget(restartButton)
 #stop the game clock in case it hasn't already been stopped
            Clock.unschedule(self.update)
 #start the game clock           
            Clock.schedule_interval(self.update, 1.0/60.0) 
        restartButton.size = (Window.width*.3,Window.width*.1)
        restartButton.pos = Window.width*0.5-restartButton.width/2, Window.height*0.5
        #bind the button using the built-in on_release event
        #whenever the button is released, the restart_button function is called       
        restartButton.bind(on_release=restart_button) 
 
        #*** It's important that the parent get the button so you can click on it
        #otherwise you can't click through the main game's canvas
        self.parent.add_widget(restartButton)
 
    def update(self,dt):
                #This update function is the main update function for the game
                #All of the game logic has its origin here 
                #events are setup here as well
        #update game objects
        #update ship
        self.ship.update()
        #update asteroids
        #randomly add an asteroid
        tmpCount = randint(1,1800)
        if tmpCount > self.minProb:            
            self.addAsteroid()
            if self.minProb < 1300:
                self.minProb = 1300
            self.minProb = self.minProb -1
 
        for k in self.asteroidList:
            #check for collision with ship
            if k.collide_widget(self.ship):
                print 'death'
                #game over routine
                self.gameOver()
                Clock.unschedule(self.update)
                #add reset button
            k.update()

Full Source Code:

import kivy
kivy.require('1.7.2')

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.core.window import Window
from kivy.properties import NumericProperty
from kivy.clock import Clock
from kivy.graphics import Rectangle
from kivy.config import Config
from random import randint

# Set the app to non-resizable and fix drawing issues on some devices
Config.set('graphics', 'resizable', False)
Window.clearcolor = (0, 0, 0, 1)  # Set background color to black

class WidgetDrawer(Widget):
    """
    WidgetDrawer class handles drawing images and updating positions.
    Initialize with an image path for the object to be displayed.
    """
    def __init__(self, imageStr, **kwargs):
        super(WidgetDrawer, self).__init__(**kwargs)
        with self.canvas:
            self.size = (Window.width * 0.05, Window.width * 0.05)
            self.rect_bg = Rectangle(source=imageStr, pos=self.pos, size=self.size)
            self.bind(pos=self.update_graphics_pos)
            self.center = (Window.width / 2, Window.height / 2)
            self.rect_bg.pos = self.pos

    def update_graphics_pos(self, instance, value):
        # Update rectangle position when widget position changes
        self.rect_bg.pos = value

    def setSize(self, width, height):
        # Set the size of the widget
        self.size = (width, height)

    def setPos(self, xpos, ypos):
        # Set the position of the widget
        self.pos = (xpos, ypos)


class Ship(WidgetDrawer):
    """
    Ship class, defining movement and properties specific to the player's ship.
    """
    impulse = NumericProperty(0)
    grav = NumericProperty(-0.1)

    def update(self):
        # Update the ship's position based on impulse and gravity
        self.y += self.impulse
        self.impulse += self.grav
        # Ensure the ship stays within the window
        if self.y < 0:
            self.y = 0
            self.impulse = 0


class Asteroid(WidgetDrawer):
    """
    Asteroid class, defines movement for asteroids.
    """
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)

    def move(self):
        self.x += self.velocity_x
        self.y += self.velocity_y

    def update(self):
        self.move()


class GUI(Widget):
    """
    Main game widget containing the ship, asteroids, and game logic.
    """
    asteroidList = []
    minProb = 1700  # Used to control asteroid spawning probability

    def __init__(self, **kwargs):
        super(GUI, self).__init__(**kwargs)
        
        # Title Label
        title = Label(text='Flappy Ship')
        title.center_x = Window.width / 2
        title.y = Window.height * 0.8
        self.add_widget(title)

        # Create and position the ship
        self.ship = Ship(imageStr='./ship.png')
        self.ship.pos = (Window.width / 4, Window.height / 2)
        self.add_widget(self.ship)

    def addAsteroid(self):
        # Add an asteroid with random image and position
        imageStr = f'./sandstone_{randint(1, 4)}.png'
        tmpAsteroid = Asteroid(imageStr)
        tmpAsteroid.x = Window.width * 0.99
        tmpAsteroid.y = randint(1, 16) * Window.height * 0.0625
        tmpAsteroid.velocity_x = -1
        tmpAsteroid.velocity_y = 0
        self.asteroidList.append(tmpAsteroid)
        self.add_widget(tmpAsteroid)

    def on_touch_down(self, touch):
        # Apply impulse and reset gravity on touch
        self.ship.impulse = 3
        self.ship.grav = -0.1

    def gameOver(self):
        # Function to handle game over event
        restartButton = Button(text='Restart')

        def restart_button(instance):
            print("Restart button pushed")
            for asteroid in self.asteroidList:
                self.remove_widget(asteroid)
            self.ship.pos = (Window.width * 0.25, Window.height * 0.5)
            self.minProb = 1700
            self.asteroidList = []
            self.parent.remove_widget(restartButton)
            Clock.unschedule(self.update)
            Clock.schedule_interval(self.update, 1.0 / 60.0)

        restartButton.size = (Window.width * 0.3, Window.width * 0.1)
        restartButton.pos = (Window.width / 2 - restartButton.width / 2, Window.height / 2)
        restartButton.bind(on_release=restart_button)
        self.parent.add_widget(restartButton)

    def update(self, dt):
        # Main game update function, runs every frame
        self.ship.update()
        if randint(1, 1800) > self.minProb:
            self.addAsteroid()
            self.minProb = max(1300, self.minProb - 1)

        for asteroid in self.asteroidList:
            if asteroid.collide_widget(self.ship):
                print("Game Over: Ship collided with an asteroid")
                self.gameOver()
                Clock.unschedule(self.update)
            asteroid.update()


class ClientApp(App):
    def build(self):
        parent = Widget()  # Main container for widgets
        game = GUI()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        parent.add_widget(game)
        return parent


if __name__ == '__main__':
    ClientApp().run()

You can download the images used in the game form below

Final Output:

Conclusion

Building a Flappy Bird game with the Kivy library in Python provides a rewarding experience that combines creativity, logical thinking, and hands-on programming. This project allows you to apply foundational programming concepts such as object-oriented design, event handling, and animation, all while leveraging Kivy’s powerful UI and graphics capabilities. Through the development process, you learn to manage game elements like gravity, collision detection, and scoring, giving you insight into how classic 2D games are constructed.

Kivy’s flexibility makes it ideal for such applications, offering intuitive handling of touch events, easy manipulation of widgets, and a wide range of customizations for interactive graphics. This project serves as a strong stepping stone for anyone interested in game development, mobile app development, or simply creating engaging visual applications in Python. With this foundation, you can expand your game further by adding features such as increasing difficulty levels, scoring systems, or even creating unique obstacles, making it an excellent way to deepen your skills and understanding of game mechanics and Python-based UI development.

Author

Sona Avatar

Written by

Leave a Reply

Trending

CodeMagnet

Your Magnetic Resource, For Coding Brilliance

Programming Languages

Web Development

Data Science and Visualization

Career Section

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4205364944170772"
     crossorigin="anonymous"></script>