~/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
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
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
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
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.
Version 5: Flappy Fruit
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.