Python for Android Tutorial #5 – Our first Mobile Game with Kivy!!

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!

Python Mobile course

I - About the Tutorials

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 major part are compatible with iOS as well.

Must read:

Source code

Get updates Follow @aron-bordin
Star it: Star
Contribute: Fork
Download: Download

II - Index

Available tutorials:

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:

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.

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 not a 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 Widget Ids

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 the 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 create more posts to explain each component. 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 > 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: https://github.com/aron-bordin/Kivy-Tutorials/blob/master/5_Perception/Perception/buildozer.spec

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

Aron Bordin
Computer Science Student and AI researcher. Always coding something fun :)

[Tutorial] Developing Android Background Services

### Welcome!In this post, I'll show you how to develop background services on Android Studio. We'll see two type of services: `Service` a...… Continue reading