Welcome!

This is the fifth post about mobile development with Python. In this post, we’ll code our first mobile game with Kivy! Android and iOS game development with Python?? Wow! Really nice, hum? So, let’s check this out!

I – About the tutorials

(If you had read another tutorial, you can go to the topic III, because I and II is the same in all tutorials)

All my next posts will be about this tutorial series. I’ll show you how to use each mobile API, like compass, camera, sensors, sound, and others. After the API, we will start to test some Python libs, as PyGame and OpenCV. I’m using Android with buildozer, but some libs are compatible with iOS too(I’ll tell you when it’s compatible), so if you test it, send a feedback if possible.

In each post, I’ll show a sample app using on of them. When I finish all the tutorial, you will be able to develop almost any app with Python. In the principle, I’m not going to show the design stuff, but I recommend you to check Kivy and PySide. After finishing theses tutorials, maybe I can start a tutorial series about Kivy, for the design, but it’s easier to find at Google 🙂

You can check the Github repo here, and if you like it, please, star it 🙂

To be able to read all the tutorials, please follow my blog or subscribe your email to get updates. Another important thing if your feedback. I’m Brazilian and trying to write in English, so if you have any problem, please, just let me know. To make the tutorials better ,try to comment always that is possible, showing your experiences, tips for a better tutorial, and this kind of stuff.

Before start, you can check this links to configure all the prerequisites:

II – Index

You will be able to get the links for all tutorials here, I’ll be updating when a new tutorial is published.

Tutorial #1 – Accelerometer

Tutorial #2 – Camera

Tutorial #3 – Kivy Designer

Tutorial #4 – Debug

Tutorial #5 – Android and iOS game with Python

Tutorial #6 – Reading and saving user data

Tutorial #7 – Use Android classes with PyJNIus

III – About this tutorial

This is a big post, to work in this game, we are going to learn and use a lot of Kivy. But don’t worry, I’ll be writing around more five posts related to this one, explaining more about each component used in this game, like how to save scores, how to monetize your Kivy app, how to work with multiple scenes and more.

You can download and test our app here:

You can get all the tutorial sources here.

This is a simple game. You’ll see a grid in the screen, and some buttons will blink some times. Then it’ll stop to blink and you need to remember what was the last block to blink. Very nice, hum? It’s a simple and nice example to help you learn more about Kivy. So, let’s start right now!

IV – UI – opengameart.org

Just I’d like to thanks http://opengameart.org/ , I’m now designer, and in this kind of project, this website can be very useful :).

I used these links to this game:

I edited these files with Gimp and Inkscape, if you want to edit something, open the design folder and get the files.

V – Create your project

Go to a folder and add two files, main.py and main.kv.

Now, run:

buildozer init

to configure buildozer.

VI – Game interface

The game is divided in two screens, the menu and the game screen.

Menu:

You can check the id of each widget with the yellow color. The game interface was created with kv. In your main.kv file, add the following code:

<MenuScreen>: Image: id: background source: "res/background.png" allow_stretch: True #image in fullscreen keep_ratio: False #image in fullscreen Image: id: title source: "res/title.png" pos_hint: {'top': 1.3} Label: id: label_best text: 'Best score: 1' font_size: '27sp' pos_hint: {'x': 0, 'y':0.15} ImageButton: id: new_game source: "res/new_game.png" on_press: root.new_game() size_hint: 0.6, 0.15 pos_hint: {'x': .2, 'y': .4} allow_stretch: True #image full the button keep_ratio: False #image full the button ImageButton: id: share source: "res/share.png" on_press: root.share() size_hint: 0.6, 0.15 pos_hint: {'x': .2, 'y': .25} allow_stretch: True #image full the button keep_ratio: False #image full the button ImageButton: id: exit source: "res/exit.png" on_press: root.exit() size_hint: 0.6, 0.15 pos_hint: {'x': .2, 'y': .1} allow_stretch: True #image full the button keep_ratio: False #image full the button

In this source, we used allow_stretch: True and keep_ratio: False so the image can fit the parent widget. The background fits sthe layout and the ImageButton fits the Button.

We use pos_hint: {‘x’: value, ‘y’:value} to place our widget in the screen.

The size_hint: 0.6, 0.15 resize the width to 60% of the parent and height to 15%.

Game:

You can check the id of each widget with the yellow color. The game interface was created with kv. In your main.kv file, add the following code:

<GameScreen>: Image: source: "res/background.png" allow_stretch: True #image in fullscreen keep_ratio: False #image in fullscreen Label: id: label_level font_size: '25sp' pos_hint: {'x': 0, 'y':0.45} Label: id: label_last_block text: '' font_size: '20sp' pos_hint: {'x': 0, 'y':0.35} Label: id: label_best font_size: '20sp' pos_hint: {'x': 0, 'y':-0.4} ImageButton: id: back_menu pos_hint: {'x': .75, 'y': .9} source: "res/menu.png" size_hint: .23, .08 allow_stretch: True #image in fullscreen keep_ratio: False #image in fullscreen on_press: root.go_menu() ImageButton: id: restart source: "res/restart.png" size_hint: .23, .08 allow_stretch: True #image in fullscreen keep_ratio: False #image in fullscreen on_press: root.restart() GridLayout: id: game_grid cols: 2 rows: 2 size_hint: 0.9, root.width*0.9/root.height pos_hint: {'x': 0.05, 'y': (1 - root.width*0.9/root.height) / 2}

Now, the game grid is empty. We’ll fill it with the Python code.

VII – Game code

Now, we can code it. As I told you, this is a big post and we have a lot of important thing in this code. So I’ll be releasing more posts in the next week to cover everything used in this game. I’ll post about sounds, files, how to monetize the app and how to manage multiple scenes.

From now, I commented everything possible in this code, so I hope that is easy to understand. If you have any question, feel free to ask here 🙂

# Some resource that helped a lot 🙂 # http://opengameart.org/content/fantasy-ui-button # http://opengameart.org/content/ui-button # http://opengameart.org/content/brick-wall # http://opengameart.org/content/menu-loop from kivy.utils import platform from kivy.app import App from kivy.uix.floatlayout import FloatLayout from kivy.core.audio import SoundLoader from kivy.uix.behaviors import ButtonBehavior from kivy.uix.image import Image from kivy.properties import ObjectProperty from kivy.lang import Builder from kivy.clock import Clock #clock to schedule the game update from kivy.storage.jsonstore import JsonStore from os.path import join import random if platform() == 'android': from jnius import cast from jnius import autoclass from revmob import RevMob as revmob #here we create a custom class called ImageButton. It's a image, however with # some button properties. We use this class to use the on_press event class ImageButton(ButtonBehavior, Image): pass # This is the App screen, all game scenes(menu and the game) extends this object class AppScreen(FloatLayout): # MainApp referece, so we can call some functions provided by this object app = ObjectProperty(None) # This is our menu screen. class MenuScreen(AppScreen): def __init__(self, app): #init the object, receiving the MainApp instance super(MenuScreen, self).__init__() self.app = app # get the MainApp reference # switch to the GameScreen def new_game(self): self.app.open_screen('game') # just close our application def exit_game(self): exit(0) # App share with Android # It will open an intent to share a message def share(self): if platform() == 'android': #check if the app is on Android # read more here: http://developer.android.com/training/sharing/send.html PythonActivity = autoclass('org.renpy.android.PythonActivity') #request the activity instance Intent = autoclass('android.content.Intent') # get the Android Intend class String = autoclass('java.lang.String') # get the Java object intent = Intent() # create a new Android Intent intent.setAction(Intent.ACTION_SEND) #set the action # to send a message, it need to be a Java char array. So we use the cast to convert and Java String to a Java Char array intent.putExtra(Intent.EXTRA_SUBJECT, cast('java.lang.CharSequence', String('Fast Perception'))) intent.putExtra(Intent.EXTRA_TEXT, cast('java.lang.CharSequence', String('Wow, I just scored %d on Fast Perception. Check this game: https://play.google.com/store/apps/details?id=com.aronbordin.fastperception' % (self.best_score)))) intent.setType('text/plain') #text message currentActivity = cast('android.app.Activity', PythonActivity.mActivity) currentActivity.startActivity(intent) # show the intent in the game activity # will be called when our screen be displayed def run(self): self.best_score = self.app.store.get('score')['best'] # get the best score saved self.ids.label_best.text = 'Best score: ' + str(self.best_score) #show it if platform() == 'android': # if we are using android, we can show an ADs if(random.randint(0, 5) == 1): # get a random and show a ads in the menu revmob.show_popup() #Our game screen, where everything happens 🙂 class GameScreen(AppScreen): def __init__(self, app): #init the object, receiving the MainApp instance super(GameScreen, self).__init__() self.app = app self.init() #initialize defaults values to start a new game def init(self): self.level = 1 # the current level self.best_level = self.app.store.get('score')['best'] # best level self.game_grid = self.ids.game_grid # the game grid widget instance self.label_level = self.ids.label_level # the label widget to show the current level self.grid_size = 2 #we are using a grid layout to show our blocks, the fist level has a 2x2 grid self.last_id = None #last btn blinked self.remain_interaction = 4 # number of blinks before turn off self.can_click = False # if the user can choose a block. Only true while not blinking # called when the game scene be displayed def run(self): self.start_game() # start a new game round def start_game(self, dt = None): self.can_click = False #while playing, user cannot choose a block self.ids.label_last_block.text = '' # clear the id of last blinked block self.check_best() #check if user has a new highscore self.ids.label_best.text = 'Best level: ' + str(self.best_level) # show the best score in the screen self.ids.restart.pos_hint = {'x': -1, 'y': 0.025} #hide the restart button Clock.unschedule(self.update) # stop updating the game screen self.label_level.text = 'Level: ' + str(self.level) #show the current level self.remain_interaction = random.randint(2 + self.level/2, self.level*2) #generate a random number of interactions self.draw_screen() #show the blocks in the screen # here we set the speed of the animation if self.level < 10: interval = 1 - self.level*0.1 else: interval = 0.1 - self.level*0.001 #update the screen using the interval calculated above Clock.schedule_interval(self.update, interval) #each update a different block will blink def update(self, dt): # when last_id is not none there is a green block in the screen if(self.last_id is not None): self.game_grid.ids.get(self.last_id).source = 'res/block.png' #turn off the block if self.remain_interaction == 0: #if executed all interactions Clock.unschedule(self.update) #stop updating the screen self.ids.label_last_block.text = 'Click in the last block that blinked' #ask user to click in a block self.can_click = True # allow the click return #if we can still blinking: id = 'btn_' + str(random.randint(0, self.grid_size*self.grid_size-1)) #generate a random int to blink a random block self.game_grid.ids.get(id).source= 'res/block_blink.png' #make this block border green self.last_id = id # save the id of the green block self.remain_interaction -= 1 # draw the blocks in the screen def draw_screen(self): self.game_grid.clear_widgets() # this command remove all children(blocks) from the game_grid # according to the level, we can change the size of the grid if(self.level < 3): self.game_grid.cols = 2 self.game_grid.rows = 2 self.grid_size = 2 elif self.level < 5: self.game_grid.cols = 3 self.game_grid.rows = 3 self.grid_size = 3 elif self.level < 10: self.game_grid.cols = 4 self.game_grid.rows = 4 self.grid_size = 4 elif self.level < 15: self.game_grid.cols = 5 self.game_grid.rows = 5 self.grid_size = 5 elif self.level < 20: self.game_grid.cols = 6 self.game_grid.rows = 6 self.grid_size = 6 # this is a block in KV lang. btn_str = ''' ImageButton: source: 'res/block.png' allow_stretch: True keep_ratio: False''' # generate a grid_sizeXgrid_size grid for i in range(0, self.grid_size*self.grid_size): btn = Builder.load_string(btn_str) # create a ImageButton from the string id = 'btn_' + str(i) btn.id = id # set the button ID self.game_grid.ids[id] = btn #add this button to the list of children of the GameGrid btn.bind(on_press=self.on_btn_press) # bind the press event self.game_grid.add_widget(btn) # and show this button on the grid # grid button event def on_btn_press(self, btn): if(self.can_click is True): # check if the user can click self.can_click = False self.game_grid.ids.get(self.last_id).source = 'res/block_blink.png' # blink the clicked button if(btn.id == self.last_id): # if the clicked button was the last one to blink self.ids.label_last_block.text = 'Right!' # the user was right self.level += 1 #user can go to the next level Clock.schedule_once(self.start_game, 1) #wait a second and start the next level else: self.ids.label_last_block.text = 'Wrong :(' #user was wrong self.ids.restart.pos_hint = {'x': 0.75, 'y': 0.025} #the restart button is visible now # compare the current level with the highscore def check_best(self): if self.level &amp;amp;gt; self.best_level: self.best_level = self.level self.app.store.put('score', best=self.level) #save the best score. We are saving it in the key 'score', in the child 'best' # open the menu screen def go_menu(self): Clock.unschedule(self.update) self.init() self.app.open_screen('menu') # restart the first level def restart(self): Clock.unschedule(self.update) self.init() self.start_game() # This is the main app # This object create our application and manage all game screens class MainApp(App): #create the application screens def build(self): data_dir = getattr(self, 'user_data_dir') #get a writable path to save our score self.store = JsonStore(join(data_dir, 'score.json')) # create a JsonScore file in the available location if(not self.store.exists('score')): # if there is no file, we need to save the best score as 1 self.store.put('score', best=1) if platform() == 'android': # if we are on Android, we can initialize the ADs service revmob.start_session('54c247f420e1fb71091ad44a') self.screens = {} # list of app screens self.screens['menu'] = MenuScreen(self) #self the MainApp instance, so others objects can change the screen self.screens['game'] = GameScreen(self) self.root = FloatLayout() self.open_screen('menu') self.sound = SoundLoader.load('res/background.mp3') # open the background music # kivy support music loop, but it was not working on Android. I coded in a different way to fix it # but if fixed, we can just set the loop to True and call the play(), so it'll auto repeat # self.sound.loop = True It # was not working on android, so I wrote the following code: self.sound.play() # play the sound Clock.schedule_interval(self.check_sound, 1) #every second force the music to be playing return self.root # play the sound def check_sound(self, dt = None): self.sound.play() # when the app is minimized on Android def on_pause(self): self.sound.stop() # the stop the sound Clock.unschedule(self.check_sound) if platform() == 'android': #if on android, we load an ADs and show it revmob.show_popup() return True # when the app is resumed def on_resume(self): self.sound.play() # we start the music again Clock.schedule_interval(self.check_sound, 1) # show a new screen. def open_screen(self, name): self.root.clear_widgets() #remove the current screen self.root.add_widget(self.screens[name]) # add a new one self.screens[name].run() # call the run method from the desired screen if __name__ == '__main__': MainApp().run()

To help you understand the game logic. The user always start on the first level. Then we draw some blocks in the screen and blink them calculated times. After it, the user must to click in one of the blocks. So we check if the button clicked is the same that we expect, if so, we just move to the next scene.

To manage multiple scenes, we load each of them in a variable. So we just add the desired screen to the MainApp widget and show it.

To monetize this app, we are going to use http://revmobmobileadnetwork.com/

They officially support Kivy apps, so it’s easier to work with it in this post. Just download this file: http://sdk.revmobmobileadnetwork.com/kivy.html#download , move the libs and revmob folders to your project root and go to the next step.

VIII – Configuration

This app will need some special configurations, because we are using the revmob to monetize it. Open your buildozer.spec file and add the following permissions:

android.permissions = INTERNET,ACCESS_WIFI_STATE,READ_PHONE_STATE,ACCESS_NETWORK_STATE

Now, to add the revmob API, add the following line:

android.add_jars = %(source.dir)s/libs/*.jar

Add Pyjnius

requirements = kivy, pyjnius

And, as we are using a .mp3 file, add .mp3 in this line:

source.include_exts = py,png,jpg,kv,atlas,mp3

And finally, set your app version with

version = 1.0.0

You can read my full buildozer.spec here:

[app] # (str) Title of your application title = Fast Perception # (str) Package name package.name = fastperception # (str) Package domain (needed for android/ios packaging) package.domain = com.aronbordin # (str) Source code where the main.py live source.dir = . # (list) Source files to include (let empty to include all the files) source.include_exts = py,png,jpg,kv,atlas,mp3 # (list) Source files to exclude (let empty to not exclude anything) #source.exclude_exts = spec # (list) List of directory to exclude (let empty to not exclude anything) #source.exclude_dirs = tests, bin # (list) List of exclusions using pattern matching #source.exclude_patterns = license,images/*/*.jpg # (str) Application versioning (method 1) # version.regex = __version__ = ['&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;](.*)'['&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;] # version.filename = %(source.dir)s/main.py # (str) Application versioning (method 2) version = 1.0.0 # (list) Application requirements requirements = kivy, pyjnius # (list) Garden requirements #garden_requirements = # (str) Presplash of the application presplash.filename = %(source.dir)s/res/bytedebugger.png # (str) Icon of the application icon.filename = %(source.dir)s/res/icon.png # (str) Supported orientation (one of landscape, portrait or all) orientation = portrait # (bool) Indicate if the application should be fullscreen or not fullscreen = 1 # # Android specific # # (list) Permissions android.permissions = INTERNET,ACCESS_WIFI_STATE,READ_PHONE_STATE,ACCESS_NETWORK_STATE # (int) Android API to use android.api = 14 # (int) Minimum API required (8 = Android 2.2 devices) android.minapi = 8 # (int) Android SDK version to use android.sdk = 14 # (str) Android NDK version to use android.ndk = 9c # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) #android.ndk_path = # (str) Android SDK directory (if empty, it will be automatically downloaded.) #android.sdk_path = # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) #android.p4a_dir = # (list) python-for-android whitelist #android.p4a_whitelist = # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity # (list) List of Java .jar files to add to the libs so that pyjnius can access # their classes. Don't add jars that you do not need, since extra jars can slow # down the build process. Allows wildcards matching, for example: # OUYA-ODK/libs/*.jar #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar android.add_jars = %(source.dir)s/libs/*.jar # (list) List of Java files to add to the android project (can be java or a # directory containing the files) #android.add_src = # (str) python-for-android branch to use, if not master, useful to try # not yet merged features. #android.branch = master # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled #android.ouya.category = GAME # (str) Filename of OUYA Console icon. It must be a 732x412 png image. #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png # (str) XML file to include as an intent filters in &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;activity&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; tag #android.manifest.intent_filters = # (list) Android additionnal libraries to copy into libs/armeabi #android.add_libs_armeabi = libs/android/*.so #android.add_libs_armeabi_v7a = libs/android-v7/*.so #android.add_libs_x86 = libs/android-x86/*.so #android.add_libs_mips = libs/android-mips/*.so # (bool) Indicate whether the screen should stay on # Don't forget to add the WAKE_LOCK permission if you set this to True #android.wakelock = False # (list) Android application meta-data to set (key=value format) #android.meta_data = # (list) Android library project to add (will be added in the # project.properties automatically.) #android.library_references = # # iOS specific # # (str) Name of the certificate to use for signing the debug version # Get a list of available identities: buildozer ios list_identities #ios.codesign.debug = &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;iPhone Developer: &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;lastname&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;firstname&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; (&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;lt;hexstring&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;)&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; # (str) Name of the certificate to use for signing the release version #ios.codesign.release = %(ios.codesign.debug)s [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) log_level = 1 # ----------------------------------------------------------------------------- # List as sections # # You can define all the &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;list&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; as [section:key]. # Each line will be considered as a option to the list. # Let's take [app] / source.exclude_patterns. # Instead of doing: # # [app] # source.exclude_patterns = license,data/audio/*.wav,data/images/original/* # # This can be translated into: # # [app:source.exclude_patterns] # license # data/audio/*.wav # data/images/original/* # # ----------------------------------------------------------------------------- # Profiles # # You can extend section / key with a profile # For example, you want to deploy a demo version of your application without # HD content. You could first change the title to add &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;(demo)&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; in the name # and extend the excluded directories to remove the HD content. # # [app@demo] # title = My Application (demo) # # [app:source.exclude_patterns@demo] # images/hd/* # # Then, invoke the command line with the &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot;demo&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;quot; profile: # # buildozer --profile demo android debug

IX – Running our Python game on Android and iOS

Now, we can run it and check if anything is working. It’s a really big post, so if you have any problem in any step, feel free to comment here. I tested only on Android, but it’s probably going to work with iOS too.

To run this game on Android, use the following command:

buildozer --verbose android debug deploy run

To run this game on iOS, use the following command:

buildozer --verbose ios debug deploy run

Or, just type python main.py to run it on your PC.

That’s it. Thank’s for reading!

Aron Bordin

Hey!! I’m available for freelances!! If you need anything, just contact me!