Baby's First Android App

12/12/16

Categories: Projects Tags: Android

~/contents

Let’s Count: Baby’s First Android App

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.

Version 1: How Do

Screenshot

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() 

Version 2: Crude Yet Effective

Screenshot

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() 

Version 3: I’ve Got 99 Problems and GUI is All of Them

Screenshot

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} 

Version 4: Ren’Py to the Rescue

Screenshot

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.

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.

Version 5: Flappy Fruit

Screenshot

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 kiddo 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 

Version 6: Flappy Foo

It turned out Flappy Fruit was a little too simple. Kiddo wasn’t as engaged with it. Marlin 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.

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 

Final Version

The final version of the game, Count the Fruit, was created in Godot and is available on itch.io as an .apk and can also be played in the browser.