# Simple Flappy Bird Clone using Kivy (with very minimum physics)

This is a very simple clone of Flappy Bird using Kivy, to be used for programming class for teens. The game consists of 3 classes: Bird, Game, and ResetBtn.

Q: How to make it more efficient, clean, and clear for the students? (without using kv file)

Code Explanation:

The Bird class inherits from Image class, a Clock.schedule_interval will call the update method of the bird class (method for the continuous fall). It also has on_touch_down that sets the height of the bird higher than before (discontinuously). There is also a short time image changing effect (Bird facing up after a mouse press, then facing down again after a short time, by the self.fall increment). I was careful to not put equation of physics equation, so I only use increments for the motion of the bird.

flappy_down = "flappy_down.png"
flappy_up = "flappy_up.png"

class Bird(Image):

def __init__(self):
super().__init__(source = flappy_down)
self.height_frac = 0.5
self.fall = 0

def on_touch_down(self, touch):
self.height_frac += 0.15
self.source = flappy_up
self.fall = 0

def update(self, dt):
self.fall += 1
self.height_frac += -1/100
print(self.height_frac)
self.pos_hint = {'x': 0.5, 'y': self.height_frac}
if self.source != flappy_down and self.fall == 15:
self.source = flappy_down


The Game class is a FloatLayout that has 2 children: Bird() and ResetBtn(). The walls/obstacles are made without widgets, they are made using Game.canvas and Rectangle. Collision is counted whenever the center of the bird's image is inside a rectangle. A new wall will be generated every time the previous wall has passed the position at 40% of the window's width, with random height and bottom position. When collision happens, the game will unschedule all scheduled intervals.

class Game(FloatLayout):

def __init__(self):
super().__init__()

self.kiwi = Bird()
self.resetbtn = ResetBtn()
self.label = Label(text = "", halign = 'center', valign = 'center', font_size = 100)

self.kiwi.size_hint = (50/Window.size[0], 50/Window.size[1])
self.kiwi.pos_hint = {'x': 0.5, 'y': 0.5}

self.resetbtn.size_hint = (0.25, 0.1)
self.resetbtn.pos_hint = {'x': 0.05, 'y': 0.85}

self.label.size_hint = (0.5, 0.2)
self.label.pos_hint = {'x': 0.25, 'y': 0.75}

self.walls = []
self.current_wall = None

self.rect = Rectangle(pos=pos, size=size)
self.color = Color(*color)

self.walls.append(self.rect)
self.current_wall = self.rect

height = random.uniform(100, 400)
pos_y = random.uniform(0, Window.size[1]-400)
self.add_wall((745, pos_y), (50, height), (1, 0.3, 0, 1))

def update(self, dt):
for i in self.walls:
i.pos = (i.pos[0]-100*dt, i.pos[1])

self.walls = [i for i in self.walls if i.pos[0] >= -50]

if self.walls[-1].pos[0] <= 0.5*Window.size[0] - 70:
print(len(self.walls))

kiwi_windowpos = [self.kiwi.pos_hint['x']*Window.size[0], \
self.kiwi.pos_hint['y']*Window.size[1]]

print(kiwi_windowpos)

if ((self.current_wall.pos[0] <=(kiwi_windowpos[0] + 25) <= self.current_wall.pos[0] + 50) \
and (self.current_wall.pos[1] <=(kiwi_windowpos[1] + 25) <= self.current_wall.pos[1] + self.current_wall.size[1])) \
or (kiwi_windowpos[1] + 25 <= 0) or (kiwi_windowpos[1] + 25 >= Window.size[1]):
self.label.text = "Game Over"
Clock.unschedule(self.kiwi_update)
Clock.unschedule(self.wall_update)

def reset(self):

self.clear_widgets()
for i in self.walls:
del i
del self.current_wall
self.canvas.clear()
Clock.unschedule(self.kiwi_update)
Clock.unschedule(self.wall_update)

self.kiwi = Bird()
self.resetbtn = ResetBtn()
self.label = Label(text = "", halign = 'center', valign = 'center', font_size = 100)

self.kiwi.size_hint = (50/Window.size[0], 50/Window.size[1])
self.kiwi.pos_hint = {'x': 0.5, 'y': 0.5}

self.resetbtn.size_hint = (0.25, 0.1)
self.resetbtn.pos_hint = {'x': 0.05, 'y': 0.85}

self.label.size_hint = (0.5, 0.2)
self.label.pos_hint = {'x': 0.25, 'y': 0.75}

self.walls = []
self.current_wall = None

self.kiwi_update = Clock.schedule_interval(self.kiwi.update, 0.01)
self.wall_update = Clock.schedule_interval(self.update, 0.01)


Here is the reset button:

class ResetBtn(Button):

def __init__(self):
super().__init__(text = "Reset", halign = "center", valign = "center")

def on_release(self):
super().on_release()
self.parent.reset()


The imports:

import random
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.clock import Clock
from kivy.graphics import Rectangle, Color


Image files:

flappy_up.png:

flappy_down.png:

Implementation:

import random
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.clock import Clock
from kivy.graphics import Rectangle, Color

flappy_down = "/home/asus/Arief_tempo/Tutor_JKT/Python_Tutor/FlappyKivy-master/images/flappy.png"
flappy_up = "/home/asus/Arief_tempo/Tutor_JKT/Python_Tutor/FlappyKivy-master/images/flappyup.png"

class ResetBtn(Button):

def __init__(self):
super().__init__(text = "Reset", halign = "center", valign = "center")

def on_release(self):
super().on_release()
self.parent.reset()

class Bird(Image):

def __init__(self):
super().__init__(source = flappy_down)
self.height_frac = 0.5
self.fall = 0

def on_touch_down(self, touch):
self.height_frac += 0.15
self.source = flappy_up
self.fall = 0

def update(self, dt):
self.fall += 1
self.height_frac += -1/100
print(self.height_frac)
self.pos_hint = {'x': 0.5, 'y': self.height_frac}
if self.source != flappy_down and self.fall == 15:
self.source = flappy_down

class Game(FloatLayout):

def __init__(self):
super().__init__()

self.kiwi = Bird()
self.resetbtn = ResetBtn()
self.label = Label(text = "", halign = 'center', valign = 'center', font_size = 100)

self.kiwi.size_hint = (50/Window.size[0], 50/Window.size[1])
self.kiwi.pos_hint = {'x': 0.5, 'y': 0.5}

self.resetbtn.size_hint = (0.25, 0.1)
self.resetbtn.pos_hint = {'x': 0.05, 'y': 0.85}

self.label.size_hint = (0.5, 0.2)
self.label.pos_hint = {'x': 0.25, 'y': 0.75}

self.walls = []
self.current_wall = None

self.rect = Rectangle(pos=pos, size=size)
self.color = Color(*color)

self.walls.append(self.rect)
self.current_wall = self.rect

height = random.uniform(100, 400)
pos_y = random.uniform(0, Window.size[1]-400)
self.add_wall((745, pos_y), (50, height), (1, 0.3, 0, 1))

def update(self, dt):
for i in self.walls:
i.pos = (i.pos[0]-100*dt, i.pos[1])

self.walls = [i for i in self.walls if i.pos[0] >= -50]

if self.walls[-1].pos[0] <= 0.5*Window.size[0] - 70:
print(len(self.walls))

kiwi_windowpos = [self.kiwi.pos_hint['x']*Window.size[0], \
self.kiwi.pos_hint['y']*Window.size[1]]

print(kiwi_windowpos)

if ((self.current_wall.pos[0] <=(kiwi_windowpos[0] + 25) <= self.current_wall.pos[0] + 50) \
and (self.current_wall.pos[1] <=(kiwi_windowpos[1] + 25) <= self.current_wall.pos[1] + self.current_wall.size[1])) \
or (kiwi_windowpos[1] + 25 <= 0) or (kiwi_windowpos[1] + 25 >= Window.size[1]):
self.label.text = "Game Over"
Clock.unschedule(self.kiwi_update)
Clock.unschedule(self.wall_update)

def reset(self):

self.clear_widgets()
for i in self.walls:
del i
del self.current_wall
self.canvas.clear()
Clock.unschedule(self.kiwi_update)
Clock.unschedule(self.wall_update)

self.kiwi = Bird()
self.resetbtn = ResetBtn()
self.label = Label(text = "", halign = 'center', valign = 'center', font_size = 100)

self.kiwi.size_hint = (50/Window.size[0], 50/Window.size[1])
self.kiwi.pos_hint = {'x': 0.5, 'y': 0.5}

self.resetbtn.size_hint = (0.25, 0.1)
self.resetbtn.pos_hint = {'x': 0.05, 'y': 0.85}

self.label.size_hint = (0.5, 0.2)
self.label.pos_hint = {'x': 0.25, 'y': 0.75}

self.walls = []
self.current_wall = None

self.kiwi_update = Clock.schedule_interval(self.kiwi.update, 0.01)
self.wall_update = Clock.schedule_interval(self.update, 0.01)

class FlApp(App):
def build(self):
g= Game()
g.kiwi_update = Clock.schedule_interval(g.kiwi.update, 0.01)
g.wall_update = Clock.schedule_interval(g.update, 0.01)
return g

if __name__ == "__main__":
FlApp().run()