Kivyとは
Python のGUIライブラリーKivyは、PythonのGUIライブラリーの中では最も勢いのあるライブラリーと言われており、海外ではpyQt、pySideなどのQt系のあるライブラリーよりも勢いがあります。
特徴を簡単に述べると以下の通りです。
- PythonでGUIを作成するツール
- Python2/3対応
- マルチプラットフォーム(win,Mac,Linux,RaspberryPi.Android,ios)
- Kv LanguageというUIを記述するメタ言語がある
- MITライセンス
今回は公式チュートリアルの「A Simple Paint App」」と「pon game」を拡張したものを作ります。
※今回の記事は 2018/04/22に行われた技術書典4で出店した本からの抜粋になります
A Simple Paint Appの拡張
概要
Kivyの公式チュートリアルに紹介している、A Simple Paint App( https://kivy.org/docs/tutorials/firstwidget.html )を拡張します。
日本語の翻訳記事は以下( https://pyky.github.io/kivy-doc-ja/tutorials/firstwidget.html )になります。
もとの内容の説明
kivy の公式チュートリアルは以下の通りです
機能としては
- 画面をクリック(ドラッグ)するたびに始点が〇で描かれて線が引かれる
- 線の色は毎回ランダムな色
- 「clear」ボタンを押すと、線が削除されて真っ黒な画面が表示される
改造して最終的に完成するアプリ
最終的に完成するのは以下のアプリです。
機能としては以下の通りです
- 画面をクリックしながらドラッグすることで色をぬる
- Clearボタンで画面を削除する
- 各色のボタンを押すことでぬる色が変化する
- Line width(スライダー)を変更することで線の太さを変更する
- saveボタンをクリックすることで画面を画像保存する
ファイルについて
Githubに配置しています
ファイルの構造
android.txt ・・・Kivy Lancher で実行時の設定
color_button_down.png ・・・カラーボタンのクリック時の画像ファイル
color_button_normal.png ・・・カラーボタンのクリックしていないときの画像ファイル
icon.png ・・・ Kivy Lancher で実行時のアイコン
main.py ・・・ 実際のロジックが書かれているファイル
mypaint.kv ・・・ レイアウトが書かれたkv ファイル
最終的に完成するコード
# -*- coding: utf-8 -*-
from random import random
from kivy.app import App
from kivy.config import Config
# 起動時の解像度の設定
Config.set('graphics', 'width', '1024')
Config.set('graphics', 'height', '768') # 16:9
Config.set('graphics', 'resizable', False) # ウインドウリサイズ禁止
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.graphics import Color, Ellipse, Line
from kivy.properties import ObjectProperty
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.togglebutton import ToggleButton
from kivy.utils import get_color_from_hex # 色の16進数表示を可能にする
from kivy.core.window import Window
class MyPaintWidget(Widget):
#pass
last_color = '' # 画面クリアを押された場合の最後の色
line_width = 3 # 線の太さ
def on_touch_down(self, touch):
if Widget.on_touch_down(self, touch):
return
color = (random(), 1, 1)
with self.canvas:
#Color(*color, mode='hsv')
#d = 30.
#Ellipse(pos=(touch.x - d / 2, touch.y - d / 2), size=(d, d))
touch.ud['line'] = Line(points=(touch.x, touch.y), width=self.line_width)
def set_line_width(self, line_width=3):
self.line_width = line_width
def on_touch_move(self, touch):
if touch.ud: # スライダーを動かす際のエラーを解除するため
touch.ud['line'].points += [touch.x, touch.y]
def set_color(self, new_color):
''' 塗る色を変更する '''
self.last_color = new_color
self.canvas.add(Color(*new_color))
class MyCanvasWidget(Widget):
def clear_canvas(self):
MyPaintWidget.clear_canvas(self)
class MyPaintApp(App):
#paint_id = ObjectProperty(None)
#self.painter.test # これでClearボタンにアクセス可能
def __init__(self, **kwargs):
super(MyPaintApp, self).__init__(**kwargs)
self.title = '画像表示'
def build(self):
parent = Widget()
self.painter = MyCanvasWidget()
# 起動時の色の設定を行う
self.painter.ids['paint_area'].set_color(
get_color_from_hex('#000000')) #黒色を設定
#clearbtn = Button(text='Clear')
#clearbtn.bind(on_release=self.clear_canvas)
#parent.add_widget(self.painter)
#parent.add_widget(clearbtn)
#return parent
return self.painter
#def clear_canvas(self, obj):
def clear_canvas(self):
'''
画面をきれいにする。行うことは以下の2点
1:画面をクリアーにする
2:最後にセットしていた色をセットしなおす
'''
self.painter.ids['paint_area'].canvas.clear()
self.painter.ids['paint_area'].set_color(self.painter.ids['paint_area'].last_color)
def save_canvas(self):
# 時間があるときに一時的にcanvas.beforeに背景を塗り潰す処理を加えるの
# https://kivy.org/docs/api-kivy.core.window.html?highlight=screenshot#kivy.core.window.WindowBase.screenshot
Window.screenshot(); # スクリーンショットを保存する
#self.painter.export_to_png('a.png') # 画像を保存する ただしこのやり方だとウィンドウカラーが適用されないので描いていない部分が透明になる
class ColorButton(ToggleButton):
def _do_press(self):
'''
何も押されていない状態で設定が解除されるのを防ぐためToggleButtonの関数を継承して変更する
Source code for kivy.uix.behaviors.button
https://kivy.org/docs/_modules/kivy/uix/behaviors/button.html
'''
if self.state == 'normal':
# ボタンを押されてない場合は状態を変更する
ToggleButtonBehavior._do_press(self)
if __name__ == '__main__':
Window.clearcolor = get_color_from_hex('#ffffff') # ウィンドウの色を白色に変更する
MyPaintApp().run()
kvファイル
#:import hex_color kivy.utils.get_color_from_hex
<ColorButton>:
background_normal: 'color_button_normal.png'
background_down: 'color_button_down.png'
group: 'color'
border: (5, 5, 5, 5)
on_release: app.painter.ids['paint_area'].set_color(self.background_color)
#on_release: app.painter.paint_id.set_color(self.background_color) # この方法でもset_corlorにアクセス可能
<MyCanvasWidget>:
paint_id:paint_area
id: canvas_area
test:button1
Button:
text: 'save'
color: 1, 1, 1 , 1
font_size: 20
on_release: app.save_canvas()
border: (2, 2, 2, 2)
x: 0
top: root.top
width: 80
height: 40
BoxLayout:
orientation: 'vertical'
height: root.height
width: root.width
MyPaintWidget:
id: paint_area
size_hint_y: 0.8
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
Label:
size_hint_x: 0.1
text: 'Line width %s' % int(s1.value) if s1.value else 'Line width not set'
color: 0,0,0,1
Slider:
id: s1
size_hint_x: 0.9
value: 3
range: (1,100)
step: 1
on_touch_down:app.painter.ids['paint_area'].set_line_width(self.value)
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
clear_btn:button1
Button:
id: button1
text: "Clear"
ont_size: 30
on_release: app.clear_canvas()
ColorButton:
text: "white "
color: 0, 0, 0 , 1
background_color: hex_color('#ffffff')
ColorButton:
text: "black "
state: 'down'
background_color: hex_color('#000000')
ColorButton:
text: "red "
background_color: hex_color('#ff0000')
ColorButton:
text: "biue "
background_color: hex_color('#0000ff')
ColorButton:
text: "green "
background_color: hex_color('#008000')
ColorButton:
text: "orange"
background_color: hex_color('#ff4500')
ColorButton:
text: "purple"
background_color: hex_color('#800080')
ファイル形式について
main.pyはUTF-8形式で保存してください。kvファイルはshift-jis形式で保存してください。
レイアウト全体
レイアウトは大きく縦に3つに分かれています
①描画部分
②線の太さを設定するスライダー部分
③削除ボタンと線の描画する色を設定する
mypaint kv のMyCanvasWidget
(Widgetクラス)が今回のレイアウトを行っているクラスです。この中でBoxLayoutを用いて縦3つのクラスを形成しています。以下はkvファイルで該当部分の抜粋になります。
<MyCanvasWidget>:
BoxLayout:
orientation: 'vertical'
height: root.height
width: root.width
MyPaintWidget:
id: paint_area
size_hint_y: 0.8
BoxLayout:
size_hint_y: 0.1
BoxLayout:
size_hint_y: 0.1
BoxLayout
は縦、または横にwidgetを並べることができます。
今回はプロパティorientation
を行うことで縦に3つ並べています。
MyPaintWidget
widgetはチュートリアルのころからあったクラスでこれが①描画部分になります。
次に上から2つ目のBoxLayout
ですが、ここが「②線の太さを設定するスライダー部分」になります。
最後の3つ目のBoxLayout
ですがここが「③削除ボタンと線の描画する色を設定する」となっています。
画面全体を作るBoxLayout
の設定ですが以下の部分を注目してください。
BoxLayout:
orientation: 'vertical'
height: root.height
width: root.width
高さの「height」に「root.height」、幅のプロパティ「width」に「root.width」を設定しています。ここでrootはKv Languageの予約語で対象のWidgetの大元のインスタンスを指します。ここではMyCanvasWidget
を指しています。
つまりここでは BoxLayoutの幅と高さを大元のMyCanvasWidget
の幅と高さに設定 しています。
なぜ、このようなことをしているかというと、ここでの内容はPythonファイルではadd_widget(BoxLayout())
と同じです。すなわちwidetクラスのBoxLayoutを足しています。ここで注意したいのはwidetクラスの「width」、「height」はともに100(単位:ピクセル)が初期値だからです。
これはよくkivyを始めたばかりの人が良く誤解する一つで、widgetを追加しただけだと初期値のサイズ(100×100)が適用されてしまい画面サイズに対し小さいwidgetが表示されてしまいます。
公式チュートリアルのPaint Appではこのことを利用して、サイズを指定せずにparent.add_widget(clearbtn)
でクリアボタンを追加しています。
しかし初期のサイズについて説明がないので、実際にKivyで一からレイアウトを設定した場合にうまくいかずに 「部品サイズの初期値も意味不明」 ということで挫折される方が一定数いるようです。
Kivyの公式チュートリアルとプログラミングガイドを翻訳した感想ですと、両者には記載がないですが知っておくとよいことがいくつかあり、この内容もその1つです。
縦のサイズですが各widgetのsize_hint_y
で設定しており、0.8,0.1,0.1がそれぞれの比率です。この内容を合計した値の比率を全体とした場合の比率になります。
今回は、0.8+0.1+0.1 = 1なので全体を1とした場合のそれぞれの割合で高さを設定しています。
削除ボタンと線の描画する色を設定する
kvファイルで削除ボタンを分離する
もとのチュートリアルではpythonファイルに削除ボタンを記載していましたが、
今回はKv Languageファイルに記載しています。
Button:
id: button1
text: "Clear"
ont_size: 30
on_release: app.clear_canvas()
ボタンをクリックすると、appはKv Languageの予約語でAppクラスを指します。ここではPytthonファイルのMyPaintApp()
クラスを指します。その中のclear_canvas()
を使用して画面を削除します。
参考:Kivy Language(翻訳済み) ( https://pyky.github.io/kivy-doc-ja/api-kivy.lang.html )
class MyPaintApp(App):
def clear_canvas(self):
'''
画面をきれいにする。行うことは以下の2点
1:画面をクリアーにする
2:最後にセットしていた色をセットしなおす
'''
self.painter.ids['paint_area'].canvas.clear() ・・・①
self.painter.ids['paint_area'].set_color(self.painter.ids['paint_area'].last_color)・・・②
①はidsを使用してMyPaintWidget()
のcanvas
(描画部分を)クリアしています。チュートリアルでは、root
を使用して描画部分をクリアーしていましたが、チュートリアルでは同じ描画していたcanvas
と同じwidget内にいたのでcanavas
をクリアしていましたが今回は、描画するwidgetとボタンを配置するwidgetが違うので、Appクラスからkvファイルないで設定したid(paint_area)でアクセスしてcanvas
を削除しています。
②に関してはコードの説明は後でしますが、canvas
は画面の表示をクリア(初期状態)に戻すだけでなく描画する色も初期値(黒)に戻します。そのため最後に記録していた色をセットしなおします。
トグルボタンで削除ボタン、色を変更するボタンを追加
描画する色を選択するのに今回はToggle button(トグルボタン) ( https://kivy.org/docs/api-kivy.uix.togglebutton.html )を使用しています。
関係するコードは以下の通りです。
mypaint.kv
#:import hex_color kivy.utils.get_color_from_hex
<ColorButton>:
background_normal: 'color_button_normal.png'
background_down: 'color_button_down.png'
group: 'color'
border: (5, 5, 5, 5)
on_release: app.painter.ids['paint_area'].set_color(self.background_color)
#on_release: app.painter.paint_id.set_color(self.background_color) # この方法でもset_corlorにアクセス可能
<MyCanvasWidget>:
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
clear_btn:button1
ColorButton:
text: "white "
color: 0, 0, 0 , 1
background_color: hex_color('#ffffff')
ColorButton:
text: "black "
state: 'down'
background_color: hex_color('#000000')
ColorButton:
text: "red "
background_color: hex_color('#ff0000')
ColorButton:
text: "biue "
background_color: hex_color('#0000ff')
ColorButton:
text: "green "
background_color: hex_color('#008000')
ColorButton:
text: "orange"
background_color: hex_color('#ff4500')
ColorButton:
text: "purple"
background_color: hex_color('#800080')
main.py
class ColorButton(ToggleButton):
def _do_press(self):
'''
何も押されていない状態で設定が解除されるのを防ぐためToggleButtonの関数を継承して変更する
Source code for kivy.uix.behaviors.button
https://kivy.org/docs/_modules/kivy/uix/behaviors/button.html
'''
if self.state == 'normal':
# ボタンを押されてない場合は状態を変更する
ToggleButtonBehavior._do_press(self)
トグルボタンですが、ToggleButton
を継承してColorButton
を作成しております。機共通の内容についてはkvファイル内のColorButton
で定義しています。個別の機能については<MyCanvasWidget>
ないで配置する際に設定しています。
またここでは2つの事を行っています。
①ボタンのデザインを行う
②トグルボタンのgroupとstateを用いてボタンの制御を行う
③ボタンを押すと各色に描画色を設定する
①ボタンのデザインを行う
トグルボタン自体のレイアウトは公式でも用意されていますが、色ごとに用意したかったので自前で設定をすることにしています。
<ColorButton>:
background_normal: 'color_button_normal.png'
background_down: 'color_button_down.png'
border: (5, 5, 5, 5)
background_normal
で通常のボタン時の画像を、background_down
でクリック時の画像を設定しています。
設定している「color_button_normal.png」と「color_button_down.png」ですが、ファイルは幅と高さが10pxと白い画像ファイルとしています。
またborderを設定していることで角丸のデザインを設定しています。
各色に関しては、MyCanvasWidget
内でそれぞれの色を設定しています。
#:import hex_color kivy.utils.get_color_from_hex
<MyCanvasWidget>:
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
clear_btn:button1
ColorButton:
text: "white "
color: 0, 0, 0 , 1
background_color: hex_color('#ffffff')
ColorButton:
text: "black "
state: 'down'
background_color: hex_color('#000000')
ColorButton:
text: "red "
background_color: hex_color('#ff0000')
ColorButton:
text: "biue "
background_color: hex_color('#0000ff')
ColorButton:
text: "green "
background_color: hex_color('#008000')
ColorButton:
text: "orange"
background_color: hex_color('#ff4500')
ColorButton:
text: "purple"
background_color: hex_color('#800080')
#:import
を用いることでkvファイル内でPythonのライブラリをimportできます。今回は hex_color kivy.utils.get_color()
を用いることで16進数カラーに設定できるようにしています。あとはMyCanvasWidget
内のbackground_color
でボタンの背景色を変更しています。
②トグルボタンのgroupとstateを用いてボタンの制御を行う
ボタンをクリックして色を設定していますが、この時に条件としては以下の2つがあります
1.ある色のボタンを押した場合に、ある色のボタンがONになり、それまで設定していた他の色のボタンがoffになる。(複数のボタンの状態を連動させる)
2.onになっているボタンをクリックしてもoffにさせない
1.に関してはkivyのトグルボタンにはgroup
という仕組みがあり、group
の値が同一のトグルボタンは状態が連動されるという仕組みがあります。今回はgroup
にcolor
という値を設定することで、色を設定するボタンの仕組みを連動しています。
2.に関してはToggleButton
のstate
がボタンの状態を設定しています。
以下のコードのようにPythonファイル内でボタンの状態(state)がOFF(nomarl)の場合のみ状態が変更するようにしています。
class ColorButton(ToggleButton):
def _do_press(self):
if self.state == 'normal':
# ボタンを押されてない場合は状態を変更する
ToggleButtonBehavior._do_press(self)
③ボタンを押すと各色に描画色を設定する
pythonないで各色に変更しています。
コードとしては描画色に設定するさいにlast_color
にセットする描画色
を格納します。
こうすることでcanvasをクリアした際に描画する色をセットすることができます。
class MyPaintWidget(Widget):
last_color = '' # 画面クリアを押された場合の最後の色
def set_color(self, new_color):
''' 塗る色を変更する '''
self.last_color = new_color
self.canvas.add(Color(*new_color))
スライダーで線の太さを変更する
kvファイル
<MyCanvasWidget>:
BoxLayout:
orientation: 'horizontal'
size_hint_y: 0.1
Label:
size_hint_x: 0.1
text: 'Line width %s' % int(s1.value) if s1.value else 'Line width not set'
color: 0,0,0,1
Slider:
id: s1
size_hint_x: 0.9
value: 3
range: (1,100)
step: 1
on_touch_down:app.painter.ids['paint_area'].set_line_width(self.value)
pythonファイル
class MyPaintWidget(Widget):
line_width = 3 # 線の太さ
def set_line_width(self, line_width=3):
self.line_width = line_width
スライダーの値と線の太さを連動しています。
画面の色について
Window.clearcolor = get_color_from_hex('#ffffff') # ウィンドウの色を白色に変更
画面の色に関してはmain.py内でウィンドウの色を白色に変更しています。
保存ボタンでスクリーンショットをとる
class MyPaintApp(App):
def save_canvas(self):
# 時間があるときに一時的にcanvas.beforeに背景を塗り潰す処理を加える
# https://kivy.org/docs/api-kivy.core.window.html?highlight=screenshot#kivy.core.window.WindowBase.screenshot
Window.screenshot(); # スクリーンショットを保存する
#self.painter.export_to_png('a.png') # 画像を保存する ただしこのやり方だとウィンドウカラーが適用されないので描いていない部分が透明になる
saveボタンでウィンドウのスクリーンショットを取得しています。
export_to_png()
では描かれていない部分が透明になるため今回は使用していないです。
export_to_png()
の実行前で一時的に色を塗りつぶす処理を追加するか、起動時にwidget全体を背景色で塗りつぶす処理を加えると状況は改善すると思いますが今回は試していません。
Kivy Lancherでアンドロイド端末で動かす
Google PlayからKivy Lancher ( https://play.google.com/store/apps/details?id=org.kivy.pygame&hl=ja )をインストールします。
android.txt に以下を記載して アンドロイド端末内にファイルを転送します
title=Paint app
author=<auther name>
orientation=landscape
詳しくは以下の使い方を参考にしてください。
Python Kivyの使い方④ ~Androidでの実行~(
https://qiita.com/dario_okazaki/items/4f6373051afb70b794d9 )
まとめ
今回の内容で、ある程度ペイントソフトとしての機能が実装できたかと思います。
機能として追加するとしたら以下のようになるかと思います。
- 背景を画像に読み込む
- カスタマイズした色を加える
- アンドウ、リドゥの機能を加える
etc
Kivyでどの程度のペイントソフトができるかは以下のアプリがGoogle Play Storeに以下のアプリがありお勧めです。
Spring Paint( https://play.google.com/store/apps/details?id=ek.myspaint )
また今回のコードですが、公式のチュートリアルにかなり手を加えたのでコードでしたらほぼ別物に近いです。
公式のPaint Appですがpythonコードのみで作成されており、少ないコードでマルチタッチのペイントソフトができるということを売りにしているみたいですがそれにしても機能が少なすぎます。またkivyのcanvasはAPIリファレンスの完成度が低いのもあり個人的には公式のチュートリアルとしてはあまり出来が良くないと思っています。
参考:
APIリファレンス Canvas( https://kivy.org/docs/api-kivy.graphics.instructions.html )
(翻訳,要約) Kivyのcanvasに関して知っておくべき10の事 (https://qiita.com/gotta_dive_into_python/items/edf5574f83f422de657c)
Pong Gameの拡張
概要
Kivyの公式チュートリアルに紹介している、Pong Game( https://kivy.org/docs/tutorials/pong.html )を拡張します。
日本語の翻訳記事は以下( https://pyky.github.io/kivy-doc-ja/tutorials/pong.html )になります。
もとの内容の説明
もとのアプリは起動するとすぐに、ゲームが開始されます。
ボールが動きだして左右のラケットを上下に動かしてボールを打ち返します。
打ち返しに失敗して左右の画面端にボールが行くと点数が加算されて表示されます。
ゲームオーバなどはありません。ひたすら左右のラケットを自分で動かしてボールが打ち返すだけです。
改造して最終的に完成するアプリ
改造して、完成するアプリは以下の通りです
起動直後にスタート画面が表示されます。STARTボタンをクリックするとゲーム画面が表示されます。ENDボタンをクリックするとゲームを終了します。
「START GAME」ボタンをクリックするとゲームが開始します。
ゲームが進めます。
どちらかが5点先取すると勝利画面に自動で遷移します
追加した内容は以下になります。
① ゲーム画面に画像を追加する
②ゲーム画面にスタートボタン/リセットボタンを追加する
③ゲーム画面にスタート画面を追加する
④勝利画面を追加して5点とったら自動で移動する
⑥config.iniを使用して起動直後の画面サイズを設定する
ファイルについて
Githubに配置しています
ファイルの構造
最終的に完成するコード
│ config.ini
│ main.py
│ pong.kv
│
└─image
background.jpg
ball01_01.png
paddle1.png
paddle2.png
player1_win.jpg
player2_win.jpg
start_background.jpg
main.py
import kivy
kivy.require('1.1.1')
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import NumericProperty, ReferenceListProperty,\
ObjectProperty
from kivy.vector import Vector
from kivy.clock import Clock
from kivy.config import Config
Config.read("config.ini")
from kivy.uix.screenmanager import ScreenManager, Screen
screen_manager = ScreenManager()
class StartMenu(Screen):
''' スタートメニュー '''
pass
class PonGameWindow(Screen):
''' ゲーム画面 '''
pass
class Player1Win(Screen):
pass
class Player2Win(Screen):
pass
class PongPaddle(Widget):
score = NumericProperty(0)
def bounce_ball(self, ball):
if self.collide_widget(ball):
vx, vy = ball.velocity
offset = (ball.center_y - self.center_y) / (self.height / 2)
bounced = Vector(-1 * vx, vy)
vel = bounced * 1.1
ball.velocity = vel.x, vel.y + offset
class PongBall(Widget):
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
velocity = ReferenceListProperty(velocity_x, velocity_y)
def move(self):
self.pos = Vector(*self.velocity) + self.pos
class PongGame(Widget):
def __init__(self, screen_manager=None):
super(PongGame, self).__init__()
self.screen_manager = screen_manager
ball = ObjectProperty(None)
player1 = ObjectProperty(None)
player2 = ObjectProperty(None)
def serve_ball(self, vel=(4, 0)):
self.ball.center = self.center
self.ball.velocity = vel
def update(self, dt):
self.ball.move()
# bounce ball off paddles
self.player1.bounce_ball(self.ball)
self.player2.bounce_ball(self.ball)
# bounce ball off bottom or top
if (self.ball.y < self.y) or (self.ball.top > self.top):
self.ball.velocity_y *= -1
# went off a side to score point?
if self.ball.x < self.x:
self.player2.score += 1
self.serve_ball(vel=(4, 0))
if self.ball.x > self.width:
self.player1.score += 1
self.serve_ball(vel=(-4, 0))
# 得点チェックして5点を超えたらゲーム終了
pong_score = 5
if self.player1.score >= pong_score:
self.serve_ball(vel=(0, 0))
self.player1.score = 0
self.player2.score = 0
self.screen_manager.current = "winner_player_1"
return
elif self.player2.score >= pong_score:
self.serve_ball(vel=(0, 0))
self.player1.score = 0
self.player2.score = 0
self.screen_manager.current = "winner_player_1"
return
def on_touch_move(self, touch):
if touch.x < self.width / 3:
self.player1.center_y = touch.y
if touch.x > self.width - self.width / 3:
self.player2.center_y = touch.y
def start_pong(self):
#self.remove_widget(self.ids['start_button'])
self.remove_widget(self.ids.start_button)
self.ids.reset_button.center_y = 20
self.serve_ball()
Clock.schedule_interval(self.update, 1.0 / 60.0)
def reset_pong(self):
self.serve_ball()
self.player1.score = 0
self.player2.score = 0
class PongApp(App):
def build(self):
#game = PongGame()
#game.serve_ball()
#Clock.schedule_interval(game.update, 1.0 / 60.0)
pong = PongGame(screen_manager=screen_manager)
pon_game = PonGameWindow(name="pon")
pon_game.add_widget(pong)
screen_manager.add_widget(StartMenu(name='start'))
screen_manager.add_widget(pon_game)
screen_manager.add_widget(Player1Win(name='winner_player_1'))
screen_manager.add_widget(Player2Win(name='winner_player_2'))
return screen_manager
# return game
def reset_game(self):
print("reset")
if __name__ == '__main__':
PongApp().run()
pong.kv
#:kivy 1.0.9
<PongBall>:
size: 50, 50
canvas:
Ellipse:
source: 'image/ball01_01.png'
pos: self.pos
size: self.size
<PongPaddle>:
size: 25, 200
canvas:
Rectangle:
source: 'image/paddle1.png'
pos:self.pos
size:self.size
<PongGame>: #ゲーム画面
ball: pong_ball
player1: player_left
player2: player_right
canvas:
Rectangle:
source: 'image/background.jpg' #背景を変更
pos: 0, 0
size: self.width, self.height
Label:
font_size: 20
center_x: root.width / 4
top: root.top
text: "Player1"
Label:
font_size: 70
center_x: root.width / 4
top: root.top - 50
text: str(root.player1.score)
Label:
font_size: 20
center_x: root.width * 3 / 4
top: root.top
text: "Player2"
Label:
font_size: 70
center_x: root.width * 3 / 4
top: root.top - 50
text: str(root.player2.score)
PongBall:
id: pong_ball
center: self.parent.center
PongPaddle:
id: player_left
x: root.x
center_y: root.center_y
PongPaddle:
id: player_right
canvas:
Rectangle:
source: 'image/paddle2.png'
pos:self.pos
size:self.size
x: root.width-self.width
center_y: root.center_y
Button:
id: start_button
size: 200, 50
#background_normal: 'img/iniciar-btn-normal.png'
#background_down: 'img/iniciar-btn-pressed.png'
text: 'START GAME'
center_x: root.width/2
center_y: root.height/2
on_press:
#root.remove_btn(self)
root.start_pong()
#reinicia_jogo_btn.center_x = 1 * root.width/4
#menu_btn.center_x = 3 * root.width/4
Button:
id: reset_button
size: 200, 50
text: 'RESET'
center_x: root.width/2
center_y: root.height + 50
on_press:root.reset_pong()
size_hint_y: None
<StartMenu>: # メニュー(スタート画面)
canvas:
Rectangle:
source: 'image/start_background.jpg' # スタート画面を出す
size: self.width, self.height # 画面サイズに合わす
Button: #スタートボタン
size_hint: 0.2, 0.1
center_x: root.center_x - self.width # 真ん中 -画像の幅
center_y: root.center_y # 画面真ん中
text: 'START'
on_release:
root.manager.transition.direction = 'left'
root.manager.current = 'pon'
Button: # 終了ボタン
size_hint: 0.2, 0.1
center_x: root.center_x + self.width
center_y: root.center_y
text: 'END'
on_press: app.stop() # アプリ終了
<Player1Win>:
canvas:
Rectangle:
source: 'image/player1_win.jpg'
size: self.width, self.height
Button:
size_hint: 0.2, 0.1
center_x: root.center_x
center_y: root.center_y -root.height/10 * 4
text: 'GO START'
on_press:
root.manager.current = "start"
<Player2Win>:
canvas:
Rectangle:
source: 'image/player2_win.jpg'
size: self.width, self.height
Button:
size_hint: 0.2, 0.1
center_x: root.center_x
center_y: root.center_y -root.height/10 * 4
text: 'GO START'
on_press:
root.manager.current = "start"
config.ini
[graphics]
resizable = 0
width = 1024
height = 768
ゲーム画面に画像を追加する
今回は、ボール、ラケット、配置します。
基本的な方法は、どれも同じで、canvasで各widgetと同じ大きさの矩形(ボールの場合は円)を描き、その矩形のに画像を配置します。
ラケットを配置する例は以下の通りです。
<PongPaddle>:
size: 25, 200
canvas:
Rectangle: ・・・矩形を描きます
source: 'image/paddle1.png' ・・・画像を配置します。
pos:self.pos ・・・描画の開始位置をwidgetと同じにします
size:self.size ・・・描画する図形の大きさをwidgetと同じにします
ゲーム画面にスタートボタン/リセットボタンを追加する
該当のコードは以下の通りです
<PongGame>: #ゲーム画面
ball: pong_ball
player1: player_left
player2: player_right
Button: スタートボタン
id: start_button
size: 200, 50
text: 'START GAME'
center_x: root.width/2
center_y: root.height/2
on_press:
root.start_pong()
Button: リセットボタン
id: reset_button
size: 200, 50
text: 'RESET'
center_x: root.width/2
center_y: root.height + 50 ・・・画面外にボタンを配置する
on_press:root.reset_pong()
class PongGame(Widget):
def start_pong(self):
#self.remove_widget(self.ids['start_button'])
self.remove_widget(self.ids.start_button) ・・・ スタートボタンを画面から削除する
self.ids.reset_button.center_y = 20 ・・・リセットボタンを画面内に移動する
self.serve_ball()
Clock.schedule_interval(self.update, 1.0 / 60.0) ・・・画面を更新する
def reset_pong(self):
self.serve_ball()
self.player1.score = 0
self.player2.score = 0
ここで注目するべきは2点あります。
①リセットボタンの位置について
②schedule_interval()
をstart_pong()
内に記述する
①リセットボタンの位置について
リセットボタンの座標ですがcenter_y
にroot.height + 50
を設定しています。root.height
は画面の高さです。これに50を足して画面外にボタンを設定して画面内に表示されないようにしています。スタートボタンを押してstart_pong()
を実行したした際に、remove_widget()
でスタートボタンを削除して、リセットボタンの座標ですが center_y
に20を設定して画面右下に表示しています。
なぜこのような動作にしたかというとkivyにはadd_widget()
を使用してwidetを使用できますが add_widget()を使うとボタンなどの挙動が必要なものは同時にイベントをバインドしないと動作しないのですが、その手間を省きたかったためです。
画面外にボタンを表示せずに画面外に逃がして、必要な場合だけボタンを画面に表示するようにしました。
②「schedule_interval()」をstart_pong()内に記述する
公式のチュートリアルだと画面を更新するschedule_interval()
をPongApp()
クラスのbuild()
関数に記述しています。この内容ですとアプリが起動した直後にゲームが始まってしまいます。そのために今回はstart_pong()
内に記述してゲーム画面に遷移した直後はゲームが開始されていない状態を作っています。
ゲーム画面にスタート画面を追加する
Kivyのscreen manegerを使用して画面を複数作成しています。
該当のコードは以下の通りです
<PongGame>: #ゲーム画面
<StartMenu>: # メニュー(スタート画面)
Button: #スタートボタン
size_hint: 0.2, 0.1
center_x: root.center_x - self.width # 真ん中 -画像の幅
center_y: root.center_y # 画面真ん中
text: 'START'
on_release:
root.manager.transition.direction = 'left' ・・・移動時に左に移動するように設定する。
root.manager.current = 'pon' ・・・ponの画面に移動する
<Player1Win>:
<Player2Win>:
from kivy.uix.screenmanager import ScreenManager, Screen
screen_manager = ScreenManager()
class StartMenu(Screen):
''' スタートメニュー '''
pass
class PonGameWindow(Screen):
''' ゲーム画面 '''
pass
class Player1Win(Screen):
pass
class Player2Win(Screen):
pass
class PongGame(Widget):
def __init__(self, screen_manager=None):
super(PongGame, self).__init__()
self.screen_manager = screen_manager
class PongApp(App):
def build(self):
pong = PongGame(screen_manager=screen_manager)
pon_game = PonGameWindow(name="pon") ・・・nameを設定する
pon_game.add_widget(pong)・・PonGameWindowにpon gameを設定します。
screen_manager.add_widget(StartMenu(name='start'))
screen_manager.add_widget(pon_game)
screen_manager.add_widget(Player1Win(name='winner_player_1'))
screen_manager.add_widget(Player2Win(name='winner_player_2'))
screen manegerですが、複数の画面(スライド)を作れますが。追加できるのはscreenクラスのみとなります。そのため、PongGame
クラスをscrrenクラスの子クラスであるpon_game()
クラスにadd_widget()
で追加しています。
また、
screen_manager.add_widget(StartMenu(name='start'))
StartMenuで起動時のスライドを指定できます。今回はstart=StartMenu()を指定できます。
参考:screen manager
(公式リファレンス) https://kivy.org/docs/api-kivy.uix.screenmanager.html
(翻訳(途中)) https://pyky.github.io/kivy-doc-ja/api-kivy.uix.screenmanager.html
勝利画面を追加して5点とったら自動で移動する
該当のコードは以下の通りです
class PongGame(Widget):
def update(self, dt):
# 得点チェックして5点を超えたらゲーム終了
pong_score = 5
if self.player1.score >= pong_score:
self.serve_ball(vel=(0, 0))
self.player1.score = 0
self.player2.score = 0
self.screen_manager.current = "winner_player_1"
return
elif self.player2.score >= pong_score:
self.serve_ball(vel=(0, 0))
self.player1.score = 0
self.player2.score = 0
self.screen_manager.current = "winner_player_1"
画面を更新するごとに、player1とplayer2の得点を判定して5点以上になった場合は以下の処理を実行します。
ボールの位置を画面真ん中に戻します
player1とplayer2のscore(点数)を0に設定します。
勝利画面(winner_player_1 or winner_player_2)に遷移します。
config.iniを使用して起動直後の画面サイズを設定する
該当のコードは以下の通りです。
from kivy.config import Config
Config.read("config.ini")
[graphics]
resizable = 0
width = 1024
height = 768
詳しくは参考のリンク先を見ていただきたいですが、Kivyは設定を行う画面の解像度など初期値を設定設定をconfig.iniに
記載して管理できます。本来はインストール先( "C:\Users<ユーザ名.kivy *" )にありますが、そこにあるファイルの内容を変えてしまうと、次回起動時の設定のすべてがデフォルトと変わってしまうので、今回用のconfig.iniを作成して、それを読み込むことで実行を行っています。なおconfig.iniの読み込みは極力コードの最初の方で行ってください。
if __name__ == '__main__':
の中で行ったりすると既に設定値がデフォルトのconfig.ini
を読み込んでいるので反映されません。
参考:Python Kivyの使い方① ~Kv Languageの基本~ 15.起動時の解像度を変更およびパラメータの使いまわしについて: https://qiita.com/dario_okazaki/items/7892b24fcfa787faface#15%E8%B5%B7%E5%8B%95%E6%99%82%E3%81%AE%E8%A7%A3%E5%83%8F%E5%BA%A6%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%8A%E3%82%88%E3%81%B3%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%81%AE%E4%BD%BF%E3%81%84%E3%81%BE%E3%82%8F%E3%81%97%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6
まとめ
今回の内容で、見た目に関しては通常のゲームに近いものができたかと思います。
もう少し改良して通常のゲームのようにするには以下の機能の追加が必要かと思います。
- 音楽を鳴らす
- ラケットを打ち返したさい、壁にボールが当たった際に効果音を追加する
- 打ち返したさいに加速度をつける
- ラケット1のみユーザーが操作してラケット2は自動で制御して打ち返す
また、今回直していないですが、「勝利画面に行った後にスタート画面にもどってスタートボタンを押したときにラケットの位置が元に戻っていないなど完全なスタート画面に戻っていない」
という内容があります。
完全なゲームを作るのには細かい機能追加とバグの改修が必要かと思いますが、今回の内容の延長にある内容なので今回の内容が理解できればさほど苦労しないかと思います。
また今回のコードはチュートリアルのコードに基本は追加という形になっています。Pong Gameはチュートリアルとしては拡張性のあるよいコードだと個人的には思いますが、もう少し踏み込んで今回の内容あたりまでを紹介すればと思っています。