Kivy is a Python library with a supplemental high-level language (kivy language/kv) that is touted as a toolkit for rapid app development. I'd read good things about it, and it seemed like a good engine to try for my first tablet game. It will be a fruit counting game for my kid. No ads, no pay-to-play, no crap, just good old-fashioned fruit and numbers, the way God intended.
Concept: Each round the player is given a number of fruit and clicks them all to progress for 10 rounds.
The documentation is thorough and there is a Pong game tutorial, but I struggled to initially, primarily because I kept conflating kv and Python. Looking at other people's code is always the best way for me to learn and reviewing Kivy projects on github was very helpful, particularly this tic-tac-toe game.
Since I found everyone else's project code so useful, I decided to post the source for each version of my app as I go along.
The first version of the game is a grid of 10 apples. Each time an apple is clicked, it has a bite taken out of it, until all 10 are clicked, then it resets.
import kivy
kivy.require('1.9.1')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.popup import Popup
from kivy.uix.button import Button
class MainApp(App):
def build(self):
return FruitBoard()
class ShowPopup(Popup):
def __init__(self, message, **kwargs):
self.size=(300,300)
self.title=message
self.content=Button(text="Close")
self.content.bind(on_press=self.dismiss)
super(ShowPopup, self).__init__(**kwargs)
self.open()
class FruitBoard(GridLayout):
def __init__(self, **kwargs):
self.cols = 5
self.rows = 2
self.selected = 0
self.num = 10
super(FruitBoard, self).__init__(**kwargs)
self.unclicked_bg = "images/apple.png"
self.clicked_bg = "images/apple_clicked.png"
for _ in xrange(self.num):
self.add_widget(Button(font_size=100,text=str(_+1),background_normal=self.unclicked_bg, on_press=self.button_pressed))
def reset(self):
for child in self.children:
child.background_normal = self.unclicked_bg
self.selected = 0
def button_pressed(self, b):
self.selected +=1
b.background_normal=self.clicked_bg
if self.selected == self.num:
self.counted_all()
def counted_all(self):
ShowPopup("You counted them all!")
self.reset()
if __name__ == '__main__':
MainApp().run()
The second version is the working prototype. It incorporates screens and different types of fruit. After each round the board is reset. After 10 rounds the game ends.
At this stage I began to have issues with some of the baked-in widgets, particularly GridLayout, which is inflexible. I had trouble both with the layout itself and the fact that grid contents kept getting stretched.
import kivy
kivy.require('1.9.1')
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.uix.button import Button
from random import choice,shuffle
FRUITS = ["apple","banana","watermelon","orange"]
ROUNDS = 0
MAX_ROUNDS = 10
STARTING_NUM = 1
GRID_ORDER = [(1,1),(1,2),(2,2),(2,2),(3,2),(3,2),(3,3),(3,3),(3,3),(4,3)]
class MainApp(App):
def build(self):
app_screenmanager = ScreenManager()
screen1 = MainMenu(name='main_menu')
screen2 = GameScreen(name='game_screen')
app_screenmanager.add_widget(screen1)
app_screenmanager.add_widget(screen2)
return app_screenmanager
class MenuButton(Button):
def __init__(self, **kwargs):
self.font_size = 80
self.size_hint=(.35,.25)
super (MenuButton, self).__init__(**kwargs)
class MainMenu(Screen):
def __init__ (self,**kwargs):
super (MainMenu, self).__init__(**kwargs)
mm = AnchorLayout(anchor_x='center',anchor_y='top')
mm_label = Label(text="Let's Count!",size_hint=(1,.3),font_size=150)
mm.add_widget(mm_label)
start_box = AnchorLayout(anchor_x='center',anchor_y='center')
mm_start = MenuButton(text="Start",size=self.size)
mm_start.bind(on_press=self.switch)
start_box.add_widget(mm_start)
quit_box = AnchorLayout(anchor_x='center',anchor_y='bottom')
mm_quit = MenuButton(text="Quit")
mm_quit.bind(on_press=self.quit)
quit_box.add_widget(mm_quit)
mm_box = BoxLayout(orientation='vertical',padding=100)
mm_box.add_widget(mm)
mm_box.add_widget(start_box)
mm_box.add_widget(quit_box)
self.add_widget(mm_box)
def switch(self, *args):
self.manager.current = "game_screen"
def quit(self, *args):
MainApp.get_running_app().stop()
class GameScreen(Screen):
def __init__(self,**kwargs):
super (GameScreen,self).__init__(**kwargs)
return_box = AnchorLayout(anchor_x='center',anchor_y='bottom')
game_return = Button(text="Return",size_hint=(.25,.1))
game_return.bind(on_press=self.switch)
return_box.add_widget(game_return)
board_box = AnchorLayout(anchor_x='center',anchor_y='top')
gameboard = FruitBoard(fruit=choice(FRUITS),num=STARTING_NUM,size_hint=(1,.9),cols=1,rows=1)
board_box.add_widget(gameboard)
game_box = FloatLayout()
game_box.add_widget(board_box)
game_box.add_widget(return_box)
self.add_widget(game_box)
def switch(self,*args):
self.manager.current = 'main_menu'
class FruitBoard(GridLayout):
def __init__(self, **kwargs):
self.num = kwargs["num"]
self.fruit = kwargs["fruit"]
self.spacing = 50
self.padding = 50
self.cols = kwargs["cols"]
self.rows = kwargs["rows"]
self.selected = 0
super(FruitBoard, self).__init__(**kwargs)
self.build_board()
def reset(self):
self.clear_widgets()
self.selected = 0
def build_board(self):
self.rows, self.cols = GRID_ORDER[self.num-1]
for f in xrange(self.num):
self.add_widget(Button(font_size=100,text="",background_normal=("images/%s.png" % self.fruit),background_disabled_normal=("images/%s_clicked.png" % self.fruit), on_press=self.button_pressed,border=(0,0,0,0),size=(150,150),size_hint=(.25,.25)))
def restart(self):
global ROUNDS, FRUITS
ROUNDS +=1
if ROUNDS < MAX_ROUNDS:
self.reset()
self.num+=1
self.fruit = choice(FRUITS)
self.build_board()
else:
ShowPopup("You counted all the fruit!")
MainApp.get_running_app().stop()
def button_pressed(self, b):
if not b.text:
self.selected +=1
b.disabled = True
b.text=str(self.selected)
if self.selected == self.num:
self.counted_all()
def counted_all(self):
ShowPopup("You counted " + str(self.num) +"!")
self.restart()
class ShowPopup(Popup):
def __init__(self, message, **kwargs):
self.size=(300,300)
self.title=message
self.content=Button(text="Close")
self.content.bind(on_press=self.dismiss)
super(ShowPopup, self).__init__(**kwargs)
self.open()
if __name__ == '__main__':
MainApp().run()
This version of the game is fully skinned. It has 12 fruits, randomly-selected and non-repeating, and it displays the number counted and the final number. The code is refactored. Wrangling with the layouts taught me a lot, but here are the pertinents:
Buttons always scale their backgrounds, so to maintain a square apect ratio for the images on the fruit grid I had to make a custom Image class that had button behavior.
Font outlines are listed in the documentation, but they're in 1.9.2 and this public release is 1.9.1.
I ended up converting some of the widgets to kivylanguage. kv does make it easier to read and organize, so it was well worth figuring out the kv syntax and how it interacts with the Python side of the script.
Last, but not least, ModalView is your friend.
import kivy
kivy.require('1.9.1')
from kivy.app import App
from kivy.graphics import Rectangle
from kivy.uix.gridlayout import GridLayout
from kivy.uix.modalview import ModalView
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.core.audio import SoundLoader
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.uix.button import Button
from kivy.uix.behaviors import ButtonBehavior
from functools import partial
import random
FRUIT_LIST = ["apple","banana","watermelon","orange","lemon","pomegranate","pear","mango","kiwi","pineapple","coconut","peach"]
NUMBER_IMAGES = ["one","two","three","four","five","six","seven","eight","nine","ten"]
class MainApp(App):
def build(self):
app_screenmanager = ScreenManager()
screen1 = MainMenu(name='main_menu')
screen2 = GameScreen(name='game_screen')
app_screenmanager.add_widget(screen1)
app_screenmanager.add_widget(screen2)
upbeat = SoundLoader.load('sound/upbeat.mp3')
upbeat.play()
return app_screenmanager
class MenuButton(Button):
def __init__(self, **kwargs):
self.size_hint=(.25,.15)
self.border=(0,0,0,0)
self.text=""
self.valign='middle'
self.halign='center'
self.background_normal = "images/button.png"
self.font_name="HirukoBlackAlternate.ttf"
self.font_size=40
self.padding=(0.15,.15)
self.color=(0,0,0,1)
super (MenuButton, self).__init__(**kwargs)
class MainMenu(Screen):
def switch(self, *args):
self.manager.current = "game_screen"
def quit(self, *args):
MainApp.get_running_app().stop()
class GameScreen(Screen):
def switch(self,*args):
self.manager.current = 'main_menu'
class FruitBoard(GridLayout):
def __init__(self, **kwargs):
self.selected = 0
self.rounds = 0
self.num = 1
super (FruitBoard, self).__init__(**kwargs)
self.build_board()
def build_board(self):
GRID_ORDER = [(1,1),(1,2),(1,3),(2,2),(3,2),(3,2),(3,3),(3,3),(3,3),(4,3)]
self.fruit_list = FRUIT_LIST[:]
self.fruit = self.next_fruit()
self.rows, self.cols = GRID_ORDER[self.num-1]
for f in xrange(self.num):
b = FruitButton(fruit=self.fruit)
self.add_widget(b)
def next_fruit(self):
i = random.randint(0, len(self.fruit_list)-1)
return self.fruit_list.pop(i)
def reset(self):
self.clear_widgets()
self.selected = 0
self.build_board()
def restart(self):
self.rounds = 0
self.num = 1
self.reset()
def start_next_round(self, pop, but):
pop.dismiss()
self.rounds +=1
if self.rounds < 10:
self.num+=1
self.fruit = self.next_fruit()
self.reset()
else:
self.restart()
self.parent.parent.switch()
class FruitButton(ButtonBehavior, Image):
def __init__(self, **kwargs):
self.source = "images/%s.png" % kwargs["fruit"]
self.fruit = kwargs["fruit"]
self.border = (0,0,0,0)
self.selected = False
super (FruitButton, self).__init__(**kwargs)
def on_press(self):
if not self.selected:
self.source = "images/%s_clicked.png" % self.fruit
self.selected = True
mama = self.parent
mama.selected +=1
if mama.rounds > 1:
self.parent.parent.parent.ids.number_label.text = str(mama.selected) #probably indicates an organizational problem
if mama.selected == mama.num:
self.parent.parent.parent.ids.number_label.text = ""
if mama.num > 1:
if mama.fruit == "peach" or mama.fruit == "mango":
fruit = mama.fruit.title() + "es!"
else:
fruit = mama.fruit.title() + "s!"
else:
fruit = mama.fruit.title() + "!"
num_img = "images/%s.png" % NUMBER_IMAGES[mama.num-1]
roundmodal = ModalView(size_hint=(0.75, 0.75),auto_dismiss=False,background=num_img)
message = FloatLayout()
b = Button(border=(0,0,0,0),background_color=(1,1,1,0))
b.bind(on_release=partial(mama.start_next_round,roundmodal))
l = RoundLabel(text=fruit)
message.add_widget(b)
message.add_widget(l)
roundmodal.add_widget(message)
roundmodal.open()
class RoundLabel(Label):
pass
if __name__ == '__main__':
MainApp().run()
# main.kv
<MainMenu>:
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: "images/leaf.png"
FloatLayout:
Image:
source: "images/title.png"
pos_hint: {'center_x':0.5,'center_y':.8}
size_hint: .9,.5
MenuButton:
text: "Play"
on_press: root.switch()
pos_hint: {'center_x':0.5,'y':0.3}
MenuButton:
text: "Quit"
on_press: root.quit()
pos_hint: {'center_x':0.5,'y':0}
<GameScreen>:
number_label: number_label
canvas.before:
Rectangle:
pos: self.pos
size: self.size
source: "images/leaf.png"
FloatLayout:
id: flayout
RoundLabel:
id: number_label
text: ""
size_hint: .25,.25
pos_hint: {'center_x':0.5,'y':0}
FruitBoard:
cols: 1
rows: 1
spacing: 20
padding_top: 100
size_hint: 1,.75
pos_hint: {'x':0,'y':.2}
Button:
size_hint: .1, .1
pos_hint: {'x':.01,'y':.025}
background_normal: "images/return_button.png"
background_down: "images/return_button.png"
border: 0,0,0,0
on_press: root.switch()
<RoundLabel>:
border: 0,0,0,0
background_color: 0,0,0,0
text_size: self.width - 50, self.height - 50
halign: 'center'
valign: 'bottom'
font_name: "HirukoBlackAlternate.ttf"
font_size: 80
color: 1,1,1,1
pos_hint: {'center_x':0.5,'y':-0.1}
After extensive difficulties building a Kivy distribution I decided to port Let’s Count! to Ren/Py. Ren’Py is a visual novel engine, however, it has a really excellent system for screens that makes designing button-based mini-games a snap. I’d never used the Ren’Py Android Packaging Tool (RAPT) before so I thought this would be a good starting application.
I made some minor adjustments to screens.rpy and gui.rpy, but this essentially the source. This version incorporates voice audio and basic animations. A monkey randomly pops up during the game and says, “Yum yum yum!”
init python:
from random import shuffle
BASE_FRUIT_LIST = ["apple","banana","watermelon","orange","lemon","pomegranate","pear","mango","kiwi","pineapple","coconut","peach"]
NUMBERS = ["one","two","three","four","five","six","seven","eight","nine","ten"]
ROUNDS = 10
renpy.music.register_channel("yum", mixer="sfx",loop=False, tight=True, file_prefix='sfx/', file_suffix='.mp3')
renpy.music.register_channel("numbers",mixer="sfx",loop=False, tight=True, file_prefix="sfx/")
def clicked(selected,num,fruit):
total = len(selected)
number_sfx = NUMBERS[total-1] + ".mp3"
if renpy.get_screen("counter"):
renpy.hide_screen("counter")
if total == num:
if num > 1:
if fruit == "mango" or fruit == "peach":
plural = "es"
else:
plural = "s"
else:
plural = ""
final_fruit = fruit.title() + plural
fruit_sfx = final_fruit+".mp3"
if total == ROUNDS:
renpy.music.play([number_sfx,fruit_sfx,"fanfare.wav"],channel='numbers')
renpy.show_screen("monkey","laugh")
else:
renpy.music.play([number_sfx,fruit_sfx,"yeah.mp3"],channel='numbers')
renpy.show_screen("number_display",num,total,final_fruit)
else:
renpy.show_screen("counter",total)
if COMPLETED_ROUNDS > 2 and COMPLETED_ROUNDS < ROUNDS-1:
if not MONKEY_SHOWN:
monkey_chance = renpy.random.randint(0,4)
if not monkey_chance:
sfx = renpy.random.choice(["yumyum1","yumyum2","yumyum3"])
renpy.show_screen("monkey",sfx)
def reset_board(num):
global COMPLETED_ROUNDS, MONKEY_SHOWN
MONKEY_SHOWN = False
COMPLETED_ROUNDS +=1
renpy.hide_screen("fruitboard")
if COMPLETED_ROUNDS < ROUNDS:
fruit = random_fruit()
renpy.show_screen("fruitboard",fruit=fruit,num=num+1)
else:
start_game()
def start_game():
global FRUIT_LIST, COMPLETED_ROUNDS, MONKEY_SHOWN
COMPLETED_ROUNDS = 0
MONKEY_SHOWN = False
FRUIT_LIST = BASE_FRUIT_LIST[:]
next_fruit = random_fruit()
renpy.show_screen("fruitboard",next_fruit, 1)
def random_fruit():
global FRUIT_LIST
if not FRUIT_LIST:
FRUIT_LIST = BASE_FRUIT_LIST[:]
shuffle(FRUIT_LIST)
i = renpy.random.randint(0, len(FRUIT_LIST)-1)
return FRUIT_LIST.pop(i)
label start:
play music "music/upbeat.mp3" fadeout 1.0 fadein 1.0
$ start_game()
label gameloop:
pause
jump gameloop
transform button_zoom(x):
zoom x
transform waggle:
on show:
zoom 0.0
linear 0.25 zoom 1.25
linear 0.5 zoom 1.0
on hide:
linear 0.5 alpha 0.0
transform custom_fade(st,ht):
on show:
alpha 0.0
linear st alpha 1.0
on hide:
linear ht alpha 0.0
transform monkey_dance:
choice:
parallel:
choice:
xalign 0.0
choice:
xalign 0.5
choice:
xalign 1.0
parallel:
choice:
yalign 1.8
linear 0.5 yalign 1.0
pause 1.0
linear 0.5 yalign 1.8
choice:
yzoom -1.0 yalign -0.8
linear 0.5 yalign 0.0
pause 1.0
linear 0.5 yalign -0.8
choice:
parallel:
choice:
yalign 0.0
choice:
yalign 0.5
choice:
yalign 1.0
parallel:
choice:
rotate 90 xpos -500
linear 0.5 xpos -175
pause 1.0
linear 0.5 xpos -500
choice:
rotate -90 xpos 1200
linear 0.5 xpos 800
pause 1.0
linear 0.5 xpos 1200
style mystyle_text:
xalign 0.5
size 100
color "#fff"
outlines [(10,"#000",0,0)]
screen fruitboard(fruit, num):
default selected = set()
$ GRID_ORDER = [(1,1,500),(2,1,400),(3,1,300),(2,2,300),(3,2,300),(3,2,300),(4,2,300),(4,2,300),(5,2,250),(5,2,250)]
$ cols, rows, x = GRID_ORDER[num-1]
frame:
xalign 0.5 yalign 0.5
background "leaf"
vbox:
xfill True yfill True
at custom_fade(0.25,0.25)
grid cols rows:
xalign 0.5 yalign 0.5 spacing 20
for item in range(0,num):
imagebutton:
idle ProportionalScale("images/"+fruit+".png",x,x)
insensitive ProportionalScale("images/"+fruit+"_clicked.png",x,x)
action [Play("audio","sfx/pop.ogg"),AddToSet(selected,item),Function(clicked,selected,num,fruit)]
for i in range(num, rows*cols):
add Null()
imagebutton:
idle "return_button" at button_zoom(0.25)
action [Play("audio","sfx/chime.ogg"),MainMenu(confirm=False)]
xalign 0.02 yalign 0.98
screen number_display(num, total, fruit):
modal True
frame:
xalign 0.5 ypos 50 background None
at custom_fade(1.0,0.5)
vbox:
imagebutton:
idle NUMBERS[total-1] at button_zoom(0.75)
action [Function(reset_board,num), Hide("number_display")]
if num==total:
text fruit style_group "mystyle" ypos -100
screen counter(total):
vbox:
xalign 0.98 yalign 0.98 at waggle
add NUMBERS[total-1] at button_zoom(0.25)
screen monkey(sfx):
add "monkey" at monkey_dance
on "show" action [Play("yum",sfx),ToggleVariable("MONKEY_SHOWN")]
timer 4.0 action [Hide("monkey")]
## handy scaling function from Cironian https://lemmasoft.renai.us/forums/viewtopic.php?f=32&t=8011
init python:
class ProportionalScale(im.ImageBase):
def __init__(self, imgname, maxwidth, maxheight, bilinear=True, **properties):
img = im.image(imgname)
super(ProportionalScale, self).__init__(img, maxwidth, maxheight, bilinear, **properties)
self.imgname = imgname
self.image = img
self.maxwidth = int(maxwidth)
self.maxheight = int(maxheight)
self.bilinear = bilinear
def load(self):
child = im.cache.get(self.image)
currentwidth, currentheight = child.get_size()
xscale = 1.0
yscale = 1.0
if (currentwidth > self.maxwidth):
xscale = float(self.maxwidth) / float(currentwidth)
if (currentheight > self.maxheight):
yscale = float(self.maxheight) / float(currentheight)
if (xscale < yscale):
minscale = xscale
else:
minscale = yscale
newwidth = currentwidth * minscale
newheight = currentheight * minscale
#Debug code to see when the loading really happens
#renpy.log("Loading image %s from %f x %f to %f x %f" % (self.imgname, currentwidth , currentheight , newwidth, newheight))
if self.bilinear:
try:
renpy.display.render.blit_lock.acquire()
rv = renpy.display.scale.smoothscale(child, (newwidth, newheight))
finally:
renpy.display.render.blit_lock.release()
else:
try:
renpy.display.render.blit_lock.acquire()
rv = renpy.display.pgrender.transform_scale(child, (newwidth, newheight))
finally:
renpy.display.render.blit_lock.release()
return rv
def predict_files(self):
return self.image.predict_files()
I was pleased to find using RAPT is user-friendly.
Download and install the Java Development Environment.
Set the JAVA_HOME environment variable so RAPT can find it.
Locate the path (mine was C:\Program Files (86x)\Java\jdk1.8.0_112)
My Computer > Properties > Advanced System Settings > Envronment Variables > System Variables
Create or modify JAVA_HOME with the above path
Open Ren’Py as Administrator. Go to the Android menu and select the emulation type.
Click Install SDK. Ren’Py will automatically download and install the Android SDK if you have JDE properly installed and referenced. Once a key is generated and the app is configured, you can build and install.
The build process took a few minutes and the app works perfectly on my tablet. I didn’t have any issues with button transparencies, which was a limitation mentioned in the documentation.
So there you have it: baby’s first app.
I decided to add a new level called Flappy Fruit. Flappy birds fly back and forth with fruit. The player counts to 10 by touching the birds. The code for this level is simpler than the previous one, even though it has moving parts.
Ren’Py’s “use” screen action allows me to make four flappy bird screens that utilize a base birdie screen. Ren’Py allows the same image to be shown multiple times at once using the “as” command, but screens don’t work that way. You can only show a screen with a specific name once. If you want to reuse a screen you’ll need use or transclude.
Kids handle touch input differently than adults. I noticed Bookworm would always double or triple-tap and generally doesn’t wait for popups. So if you’re making a game for kids you may need to include mechanisms account for buttons being clicked in rapid succession, or clicks earlier than you would generally anticipate.
A future version of Flappy Fruit should allow counting up to 20. If you’ve looked at my code, you already know
NUMBERS = ["one","two","three","four","five","six","seven","eight","nine","ten"]
mama done a bad thing. Yes, I did that, even knowing I would probably expand the counting range. A better naming scheme for the number images would have been no1, no2, no3… So when I need 11, I can easily split a string and know I need two no1 side-by-side.
init python:
BASE_FRUIT_LIST = ["apple","banana","watermelon","orange","lemon","pomegranate","pear","mango","kiwi","pineapple","coconut","peach"]
NUMBERS = ["one","two","three","four","five","six","seven","eight","nine","ten"]
ROUNDS = 10
SPEED = 5.0
COMPLETED_ROUNDS = 0
YLIST = [0.0,0.5,1.0]
BIRDS = set(["yellowbirdie","pinkbirdie","greenbirdie","chicky"])
renpy.music.register_channel("numbers",mixer="sfx",loop=False, tight=True, file_prefix="sfx/")
def random_fruit():
global FRUIT_LIST
if not FRUIT_LIST:
FRUIT_LIST = BASE_FRUIT_LIST[:]
shuffle(FRUIT_LIST)
i = renpy.random.randint(0, len(FRUIT_LIST)-1)
return FRUIT_LIST.pop(i)
def start_flappy():
global FRUIT_LIST, COMPLETED_ROUNDS, AVAILABLE_BIRDS, YS
COMPLETED_ROUNDS = 0
FRUIT_LIST = BASE_FRUIT_LIST[:]
AVAILABLE_BIRDS = set(BIRDS)
YS = set(YLIST)
if not renpy.get_screen("sky"):
renpy.show_screen("sky")
new_flappy()
def new_flappy():
global AVAILABLE_BIRDS, YS
if COMPLETED_ROUNDS == ROUNDS:
reset_flappy()
else:
if AVAILABLE_BIRDS:
fruit = random_fruit()
bird = AVAILABLE_BIRDS.pop()
if not YS:
YS = set(YLIST)
y = YS.pop()
renpy.show_screen(bird, fruit=fruit,y=y)
def reset_flappy():
renpy.music.play("fanfare.wav",channel='numbers')
start_flappy()
label flappy:
play music "music/upbeat.mp3" fadeout 1.0 fadein 1.0
$ start_flappy()
label gameloop:
pause
jump gameloop
image yellowbird:
"images/yellowbird1.png"
0.25
"images/yellowbird2.png"
0.25
repeat
image pinkbird:
"images/pinkbird1.png"
0.25
"images/pinkbird2.png"
0.25
repeat
image greenbird:
"images/greenbird1.png"
0.25
"images/greenbird2.png"
0.25
repeat
image chicken:
"images/chicken1.png"
0.15
"images/chicken2.png"
0.15
"images/chicken3.png"
0.15
"images/chicken4.png"
0.15
repeat
transform flying(s,y):
on show:
parallel:
yalign y
parallel:
choice:
xpos -500
linear s xpos 1780
repeat
choice:
xzoom -1.0
xpos 1780
linear s xpos -700
repeat
on hide:
linear 1.0 yalign 2.5
transform bird_number:
zoom 0.20
transform bird_dissolve:
on hide:
alpha 1.0
linear 0.5 alpha 0.0
screen sky():
modal True
frame:
background Fixed(Solid("#0FF"),"cloud_scene")
xfill True yfill True
vbox:
style_group "sky"
textbutton "Fast" action SetVariable("SPEED",3.0)
textbutton "Medium" action SetVariable("SPEED",5.0)
textbutton "Slow" action SetVariable("SPEED",7.0)
use returnbutton
timer 3.0 action Function(new_flappy) repeat True
if COMPLETED_ROUNDS == 10:
timer 0.5 action Return()
style sky_button_text:
size 30
color "#0FF"
selected_color "#000"
style sky_button:
xalign 1.0
style sky_vbox:
xalign 0.95 yalign 0.95 spacing 5
screen chicky(fruit,y):
zorder 2
use birdie("chicken",fruit,"chicky",y)
screen greenbirdie(fruit,y):
zorder 2
use birdie("greenbird",fruit,"greenbirdie",y)
screen yellowbirdie(fruit,y):
zorder 2
use birdie("yellowbird",fruit,"yellowbirdie",y)
screen pinkbirdie(fruit,y):
zorder 2
use birdie("pinkbird",fruit,"pinkbirdie",y)
screen birdie(bird,fruit,parent,y):
default selected = False
$ hit = bird + "ouch"
frame:
background None xmaximum 400 ymaximum 800
at flying(SPEED,y)
if COMPLETED_ROUNDS < ROUNDS:
imagebutton:
at button_zoom(0.5)
idle Fixed(Transform(bird, ypos=0),Transform(fruit,ypos=240))
insensitive Fixed(Transform(hit, ypos=0),Transform(fruit,ypos=240))
action [Hide("flappy_number"),SetScreenVariable("selected",True),SetVariable("COMPLETED_ROUNDS",COMPLETED_ROUNDS+1),AddToSet(AVAILABLE_BIRDS,parent),Play("audio","sfx/coin.ogg"), Show("flappy_number",num=COMPLETED_ROUNDS),Hide(parent)]
else:
## this keeps the birds from being triggered before game is reset
add Fixed(Transform(bird, ypos=0),Transform(fruit,ypos=240)) at button_zoom(0.5)
timer 0.25 action Hide(parent,transition=dissolve)
screen flappy_number(num):
zorder 1
if num <= 10:
$ num_sfx = NUMBERS[num] + ".mp3"
vbox:
xalign 0.5 yalign 0.5 at waggle
add NUMBERS[num] at button_zoom(0.75)
timer 2.0 action Hide("flappy_number")
on "show" action Play("numbers", num_sfx)
transform button_zoom(x):
zoom x
transform waggle:
on show:
zoom 0.0
linear 0.25 zoom 1.25
linear 0.5 zoom 1.0
on hide:
linear 0.5 alpha 0.0
screen returnbutton():
imagebutton:
idle "return_button" at button_zoom(0.5)
action [Play("audio","sfx/chime.ogg"),MainMenu(confirm=False)]
xalign 0.02 yalign 0.98
It turned out Flappy Fruit was a little too simple. Bookworm wasn’t as engaged with it. Bard suggested it be structured more like Let’s Count, with x fruits needing to be tapped per round. He wanted the fruit to remain visible at the bottom of the screen until the round was over.
So, I settled on a button that dropped when clicked, with the fruit half remaining at the bottom of the screen and the bird half flying away. The simplest solution I could think of was to use a Python show statements to show a new fruit and bird images on a custom layer over the button before it is hidden, then animate the bird to fly away. We know the button’s xpos, so we pass that down and use image_size to get the fruit’s dimensions and figure out where the bird should be.
At this stage I don’t think Flappy Fruit adds real value to the base game, it isn’t engaging enough. I wanted to add more variety to the birds’ flight patterns, for instance have horizontal as well as vertical movement, but the challenge is figuring out the button’s xpos when I haven’t explicitly set it.
There is no real documentation on renpy.get_widget_properties, which seems like the only way to get a screen widget’s xpos after the fact, and I could only ever get it to return empty tuples. renpy.get_placement also returns empty tuples. Short of creating the screen in Python, I’m not sure what to do. I thought I could use renpy.get_mouse_pos, but there was some issue with timing so I was not getting the coordinates from the moment of click. I think it would work better with touch input, but at this point I suspect I need to take a break from this project and completely rethink how the buttons are being handled. A proper solution shouldn’t be so messy.
## FLAPPY
init:
define config.layers = [ 'master', 'transient', 'screens', 'fruits', 'overlay' ]
init python:
from random import shuffle
BASE_FRUIT_LIST = ["apple","banana","watermelon","orange","lemon","pomegranate","pear","mango","kiwi","pineapple","coconut","peach"]
NUMBERS = ["no"+str(n) for n in range(1,11)]
BIRDS = set(["yellowbirdie","pinkbirdie","greenbirdie","chicky"])
ROUNDS = 10
SPEED = 3.0
COMPLETED_ROUNDS = 0
FLAPPY_COUNT = 0
def start_flappy():
global CURRENT_ROUND, FRUIT_LIST, FLAPPY_COUNT, AVAILABLE_BIRDS, CURRENT_FRUIT, XS
CURRENT_ROUND = 1
FLAPPY_COUNT = 0
FRUIT_LIST = BASE_FRUIT_LIST[:]
AVAILABLE_BIRDS = set(BIRDS)
CURRENT_FRUIT = random_fruit()
XS = [n*96 for n in range(2,12)]
renpy.show_screen("sky")
create_flappy()
def create_flappy():
global AVAILABLE_BIRDS, XS
if not AVAILABLE_BIRDS:
AVAILABLE_BIRDS = set(BIRDS)
bird = renpy.random.choice(list(AVAILABLE_BIRDS))
AVAILABLE_BIRDS.remove(bird)
shuffle(XS)
x = XS.pop()
renpy.show_screen(bird, fruit=CURRENT_FRUIT,x=x)
def flappy_click(x,bird):
global FLAPPY_COUNT, CURRENT_ROUND, CURRENT_FRUIT
FLAPPY_COUNT +=1
prev_fruits = ["f"+str(n) for n in range(1,FLAPPY_COUNT)]
renpy.show(CURRENT_FRUIT+"_clicked",at_list=[fruit_drop(x),button_zoom(0.5)],layer='fruits',tag=("f"+str(FLAPPY_COUNT)),behind=prev_fruits)
stat = renpy.image_size(CURRENT_FRUIT+".png")
renpy.show(bird,at_list=[flit(x,stat[0]),button_zoom(0.5)],layer="screens",tag=("b"+str(FLAPPY_COUNT)))
if FLAPPY_COUNT == CURRENT_ROUND:
if CURRENT_ROUND == ROUNDS:
renpy.music.play("yeah.mp3",channel='numbers')
renpy.full_restart()
else:
CURRENT_ROUND+=1
else:
create_flappy()
def flappy_reset():
global FLAPPY_COUNT, AVAILABLE_BIRDS,CURRENT_FRUIT, XS
FLAPPY_COUNT = 0
XS = [n*96 for n in range(2,12)]
for s in BIRDS:
if renpy.get_screen(s):
renpy.hide(s)
renpy.scene("fruits")
CURRENT_FRUIT = random_fruit()
create_flappy()
transform flying(s,x):
on show:
yanchor 1.0
ypos 0 xpos x
linear s ypos 720
block:
linear s ypos 0
linear s ypos 720
repeat
on hide:
linear 1.0 ypos 720
transform flit(x,y):
ypos 720-y xpos x
linear 1.0 ypos -400
transform fruit_drop(x):
yanchor 1.0 ypos 720 xpos x
transform drop:
linear 1.0 yanchor 1.0 ypos 720
screen sky():
modal True
frame:
background Fixed(Solid("#ADD8E6"),"cloud_scene")
xfill True yfill True
vbox:
style_group "sky"
textbutton "Fast" action SetVariable("SPEED",3.0)
textbutton "Medium" action SetVariable("SPEED",5.0)
textbutton "Slow" action SetVariable("SPEED",7.0)
use returnbutton
screen flappy_number(x,bird):
default num = FLAPPY_COUNT
default num_sfx = NUMBERS[num] + ".mp3"
zorder 1
if FLAPPY_COUNT <= 10:
if FLAPPY_COUNT == CURRENT_ROUND-1:
vbox:
xalign 0.5 yalign 0.5 at waggle
imagebutton:
idle NUMBERS[num] at button_zoom(0.75)
action [Hide("flappy_number"),Function(flappy_reset)]
timer 2.0 action [Function(flappy_click,x,bird)]
else:
vbox:
xalign 0.5 yalign 0.25 at waggle
add NUMBERS[num] at button_zoom(0.25)
timer 2.0 action [Hide("flappy_number"), Function(flappy_click,x,bird)]
on "show" action Play("numbers", num_sfx)
screen var(s):
text str(s)
screen birdie(bird,fruit,parent,x):
default selected = False
vbox:
at flying(SPEED,x)
if FLAPPY_COUNT == CURRENT_ROUND - 1:
imagebutton:
at button_zoom(0.5)
idle VBox(Transform(bird,ypos=100),fruit,order_reverse=True)
insensitive VBox(Transform(bird+"ouch",ypos=100),fruit+"_clicked",order_reverse=True)
action [SetScreenVariable("selected",True),AddToSet(AVAILABLE_BIRDS,parent),Play("audio","sfx/fanfare.wav"),Show("flappy_number",x=x,bird=bird),Hide(parent)]
else:
imagebutton:
at button_zoom(0.5)
idle VBox(Transform(bird,ypos=100),fruit,order_reverse=True)
insensitive VBox(Transform(bird+"ouch",ypos=100),fruit+"_clicked",order_reverse=True)
action [SetScreenVariable("selected",True),AddToSet(AVAILABLE_BIRDS,parent),Play("audio","sfx/coin.ogg"),Show("flappy_number",x=x,bird=bird),Hide(parent)]
screen chicky(fruit,x):
zorder 2
use birdie("chicken",fruit,"chicky",x)
screen greenbirdie(fruit,x):
zorder 2
use birdie("greenbird",fruit,"greenbirdie",x)
screen yellowbirdie(fruit,x):
zorder 2
use birdie("yellowbird",fruit,"yellowbirdie",x)
screen pinkbirdie(fruit,x):
zorder 2
use birdie("pinkbird",fruit,"pinkbirdie",x)
image yellowbird:
"images/yellowbird1.png"
0.25
"images/yellowbird2.png"
0.25
repeat
image pinkbird:
"images/pinkbird1.png"
0.25
"images/pinkbird2.png"
0.25
repeat
image greenbird:
"images/greenbird1.png"
0.25
"images/greenbird2.png"
0.25
repeat
image chicken:
"images/chicken1.png"
0.15
"images/chicken2.png"
0.15
"images/chicken3.png"
0.15
"images/chicken4.png"
0.15
repeat
style sky_button_text:
size 30
color "#0FF"
selected_color "#000"
style sky_button:
xalign 1.0
style sky_vbox:
xalign 0.95 yalign 0.05 spacing 5