はじめに
最近,スマホアプリを作りたい欲が出てきたので,Pythonで書けるKivyのチュートリアルをやってました.
この記事は,Kivyの最初のチュートリアル,Pongの一番最後の文章の "few ideas of things you could do" を挑戦状と意訳して,挑戦状に挑んだ結果?をまとめたものです.
以下文章
Where To Go Now?¶
Have some fun
Well, the pong game is pretty much complete. If you understood all of the things that are covered in this tutorial, give yourself a pat on the back and think about how you could improve the game. Here are a few ideas of things you could do:
読める程度に少し整理したのですが,元はコードを書いてる最終のメモ書きなので,読みづらかったら教えてください...
1. いい感じの図形・画像を入れてみよう(Add some nicer graphics)
<PongBall>:
size: 50, 50
canvas:
Ellipse:
source: "./img/ナエトル.png"
pos: self.pos
size: self.size
上みたいな感じで,source
に画像のパスを設定してあげると良い
ナエトルは,写真フォルダに唯一あったいい感じの大きさのやつです.ヒコザルが好きです.
2. 特定の点数になったら,「playerXの勝利!」と表示しよう!
一方のプレイヤーの点数が10点になったら大きく「playerXの勝利!」と表示
<PongGame>:
# もともとのコードに追加する
message: message
Message:
id: message
text: ""
Label:
font_size: 70
center_x: root.width / 2
color:"red"
top: root.top / 2 + 50
text: str(root.message.text)
お気持ちとしては,
-
Message
クラスを新しく作る- この時,
id
を指定して,上で宣言してPythonのコード内で使えるようにする
- この時,
-
Label
を作って,Message
の文字を表示する
from kivy.properties import StringProperty
class Message(Widget):
text = StringProperty()
def show_message(self, player):
self.text = f"player{player} won !!!"
class PongGame(Widget):
# 諸々最初にある
message = ObjectProperty(None) # これ重要!
max_score = 3
# もろもろ
def update(self):
# もろもろ
temp = self.is_game_end()
if temp:
self.message.show_message(temp)
def is_game_end(self):
if self.player1.score == self.max_score:
return "1"
elif self.player2.score == self.max_score:
return "2"
-
Message
クラス内のStringProperty
が結構重要- 普通に
text = ""
って書くと,表示されない
- 普通に
3. 「スタート,停止,リセット」メニューを追加しよう!
- ボタンは普通に追加できる
Click
- KVファイルにon_press: temp()みたいに書いても動かない
class PongGame(Widget):
def callback(self, instance):
instance.text = "some text"
def init_manual(self):
self.reset_btn.bind(on_press=self.callback)
- 下のように
bind
を使ったら動いた(重要だったので2回)
self.reset_btn.bind(on_press=self.reset_game)
リセット
-
PongPaddle
クラスにinit_manual
を追加して,初期状態のy座標を保存し,ボタンが押された際に戻せばできそう
class PongPaddle(Widget):
def init_manual(self):
self.org = [self.score, int(self.center_y)]
def clear(self):
self.score, self.center_y = self.org
- 色々と試したが,できない
- 座標系が原因なのかな?
- https://qiita.com/gotta_dive_into_python/items/abe433ae3b716d4fb8a7
-
体力があればこれをやりたい無かった
停止/スタート
- ボタンが押されたら,表示を変更+ボールの速度を0/前の状態に戻す,をすれば良さそう
- ボタンをテキストを別にしてるが,一緒にしたい...
- (記憶が正しければ,)以前ボタンのみでやっても文字が変わらなかった...
- ボタンをテキストを別にしてるが,一緒にしたい...
- コードを色々と追加する
<PongGame>:
play_control_btn: play_control_btn
control_text: control_text
Button:
id: play_control_btn
size: 100, 100
pos: (root.width - self.width) / 2, (root.height - self.height) / 4 * 3
text: str(root.control_text.text)
ControlText:
id: control_text
text: "Pause"
class ControlText(Widget):
text = StringProperty()
def change_text(self, is_playing):
if is_playing:
self.text = "Play"
else:
self.text = "Pause"
class PongBall(Widget):
pre = [velocity_x, velocity_y]
def init_manual(self):
self.org = [self.velocity, self.pos]
def clear(self):
self.velocity, self.pos = self.org
def stop(self):
self.pre = [self.velocity_x, self.velocity_y]
self.velocity_x, self.velocity_y = 0, 0
def start(self):
self.velocity_x, self.velocity_y = self.pre
class PongGame(Widget):
control_text = ObjectProperty(None)
def control_game(self, instance):
self.control_text.change_text(self.is_playing)
if self.is_playing:
self.ball.stop()
else:
self.ball.start()
self.is_playing ^= True
def init_manual(self):
self.ball.init_manual()
self.is_playing = True
self.play_control_btn.bind(on_press=self.control_game)
class PongApp(App):
def build(self):
game.init_manual()
4. 4人プレイのPongを作ってみよう!
- 上と下のプレイヤークラスを作成する
<PongPaddleVer>:
size: 25, 200
canvas:
Rectangle:
pos: self.pos
size: self.size
<PongPaddleHor>:
size: 200, 25
canvas:
Rectangle:
pos: self.pos
size: self.size
<PongGame>:
player3: player_top
player4: player_bottom
PongPaddleHor:
id: player_top
y: root.height - self.height
center_x: root.center_x
PongPaddleHor:
id: player_bottom
y: root.y
center_x: root.center_x
- Pythonの
PongPaddle
には色々関数が詰まってるから,PongPaddleHor
,PongPaddleVer
にコピペするのは無駄だなぁ~- 2つのクラス,
PongPaddle
を継承すれば良さそう! -
bounce_ball
の処理,上下と左右で違うから,継承する必要は無かったな...
- 2つのクラス,
class PongPaddleVer(PongPaddle):
score = NumericProperty(0)
def move(self, ball):
self.center_y = ball.center_y
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.01
ball.velocity = vel.x, vel.y + offset
class PongPaddleHor(PongPaddle):
score = NumericProperty(0)
def move(self, ball):
self.center_x = ball.center_x
def bounce_ball(self, ball):
if self.collide_widget(ball):
vx, vy = ball.velocity
offset = (ball.center_x - self.center_x) / (self.width / 2)
bounced = Vector(vx, -1 * vy)
vel = bounced * 1.01
ball.velocity = vel.x + offset, vel.y
class PongGame(Widget):
def update(self, dt):
if self.ball.y < self.y:
self.player4.score += 1
self.serve_ball(vel=(4, 0))
if self.ball.top > self.height:
self.player3.score += 1
self.serve_ball(vel=(-4, 0))
- 2人プレイ時は,「PlayerXの勝利!」で良かったが,4人だと「勝ち」ではなく「負け」を表示したほうが良さそうなので,そうした.
5. 跳ね返る処理をもっと現実的にしよう!
- 現状のシンプルのではなく,端っこに当たったら,いい感じに現実っぽくしたい
- めんどくさい...
- また別の機会にやる...
チュートリアルを終えて
Playerを自動的に動かしてみたいよね
(これは,挑戦状をやる前だったので,プレイヤーが二人しかいない感じで書かれてます.)
- 理由
- 今の状態だと,一人で両方動かす必要ある
- しんどい
- やったこと
-
PongPaddle
クラスにmove
関数を追加def move(self, ball): self.center_y = ball.center_y
-
PongGame
クラス内のupdate
関数内にコードを追加self.player2.move(self.ball)
- 過程
-
PongBall
クラスがボールをコントロールしてるから,PongPlayer2
クラスを作って,move
関数でプレイヤーをコントロールすれば良い! - エラーが出て動かない
AttributeError: 'PongPaddle' object has no attribute 'move'
- どうやら,
player2
はPongPaddle
オブジェクトらしい(よく分からず,よりあえずコードを写してた...)
-
move
関数をPongPaddle
クラスに追加したら動いた! - このままだと,手動でPlayer2を動かせるので,以下のコードを消す
if touch.x > self.width - self.width / 3: self.player2.center_y = touch.y
-
-
最終的なコード
Python
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import (
NumericProperty,
ReferenceListProperty,
ObjectProperty,
StringProperty,
)
from kivy.uix.label import Label
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint
class PongPaddle(Widget):
score = NumericProperty(0)
class PongPaddleVer(PongPaddle):
score = NumericProperty(0)
def move(self, ball):
self.center_y = ball.center_y
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.01
ball.velocity = vel.x, vel.y + offset
class PongPaddleHor(PongPaddle):
score = NumericProperty(0)
def move(self, ball):
self.center_x = ball.center_x
def bounce_ball(self, ball):
if self.collide_widget(ball):
vx, vy = ball.velocity
offset = (ball.center_x - self.center_x) / (self.width / 2)
bounced = Vector(vx, -1 * vy)
vel = bounced * 1.01
ball.velocity = vel.x + offset, vel.y
class PongBall(Widget):
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
velocity = ReferenceListProperty(velocity_x, velocity_y)
pre = [velocity_x, velocity_y]
def move(self):
self.pos = Vector(*self.velocity) + self.pos
def stop(self):
self.pre = [self.velocity_x, self.velocity_y]
self.velocity_x, self.velocity_y = 0, 0
def start(self):
self.velocity_x, self.velocity_y = self.pre
class Message(Widget):
text = StringProperty()
def show_message(self, message):
self.text = message
class ControlText(Widget):
text = StringProperty()
def change_text(self, is_playing):
if is_playing:
self.text = "Play"
else:
self.text = "Pause"
class PongGame(Widget):
ball = ObjectProperty(None)
player1 = ObjectProperty(None)
player2 = ObjectProperty(None)
player3 = ObjectProperty(None)
player4 = ObjectProperty(None)
message = ObjectProperty(None)
play_control_btn = ObjectProperty(None)
control_text = ObjectProperty(None)
max_score = 3
def add_temp(self, pos):
label_temp = Label(text="a", pos=pos)
self.add_widget(label_temp)
def serve_ball(self, vel=None):
if vel is None:
vel = (4, randint(-5, 5))
self.ball.center = self.center
self.ball.velocity = vel
def update(self, dt):
self.ball.move()
# Player1以外はボールにオート追従する
# self.player1.move(self.ball)
self.player2.move(self.ball)
self.player3.move(self.ball)
self.player4.move(self.ball)
# bounce of paddles
self.player1.bounce_ball(self.ball)
self.player2.bounce_ball(self.ball)
self.player3.bounce_ball(self.ball)
self.player4.bounce_ball(self.ball)
# went of to a side to score point?
if self.ball.x < self.x:
self.player1.score += 1
self.serve_ball()
if self.ball.right > self.width:
self.player2.score += 1
self.serve_ball()
if self.ball.y < self.y:
self.player4.score += 1
self.serve_ball()
if self.ball.top > self.height:
self.player3.score += 1
self.serve_ball()
temp = self.is_game_end()
if temp:
self.message.show_message(f"player{temp} lose!")
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 is_game_end(self):
players = [self.player1, self.player3, self.player3, self.player4]
for i in range(len(players)):
if players[i].score == self.max_score:
return i + 1
def control_game(self, instance):
self.control_text.change_text(self.is_playing)
if self.is_playing:
self.ball.stop()
else:
self.ball.start()
self.is_playing ^= True
def init_manual(self):
self.is_playing = True
self.play_control_btn.bind(on_press=self.control_game)
class PongApp(App):
def build(self):
game = PongGame()
game.init_manual()
game.serve_ball()
Clock.schedule_interval(game.update, 1.0 / 60.0)
return game
if __name__ == "__main__":
PongApp().run()
kv
#:kivy 2.1.0
<PongBall>:
size: 50, 50
canvas:
Ellipse:
source: "./img/ナエトル.png"
pos: self.pos
size: self.size
<PongPaddleVer>:
size: 25, 200
canvas:
Rectangle:
pos: self.pos
size: self.size
<PongPaddleHor>:
size: 200, 25
canvas:
Rectangle:
pos: self.pos
size: self.size
<PongGame>:
ball: pong_ball
player1: player_left
player2: player_right
player3: player_top
player4: player_bottom
message: message
play_control_btn: play_control_btn
control_text: control_text
canvas:
Rectangle:
pos: self.center_x - 5, 0
size: 10, self.height
Label:
font_size: 70
center_x: root.width / 4
top: root.height / 4
text: "1:" +str(root.player1.score)
Label:
font_size: 70
center_x: root.width * 3 / 4
top: root.height / 5 * 4
text: "2:" + str(root.player2.score)
Label:
font_size: 70
center_x: root.width / 4
top: root.height / 5 * 4
text: "3:" +str(root.player3.score)
Label:
font_size: 70
center_x: root.width * 3 / 4
top: root.height / 4
text: "4:" + str(root.player4.score)
Label:
font_size: 70
center_x: root.width / 2
color:"red"
top: root.top / 2 + 50
text: str(root.message.text)
Message:
id: message
text: ""
Button:
id: play_control_btn
size: 100, 100
pos: (root.width - self.width) / 2, (root.height - self.height) / 4 * 3
text: str(root.control_text.text)
ControlText:
id: control_text
text: "Pause"
PongBall:
id: pong_ball
center: self.parent.center
PongPaddleVer:
id: player_left
x: root.x
center_y: root.center_y
PongPaddleVer:
id: player_right
x: root.width - self.width
center_y: root.center_y
PongPaddleHor:
id: player_top
y: root.height - self.height
center_x: root.center_x
PongPaddleHor:
id: player_bottom
y: root.y
center_x: root.center_x
Exeファイルにしよう!
これはチュートリアルにはなかったが,せっかくだしやる
ここでよく出てくるtouchtracer
はプロジェクトの名前なので,自分のやつに変更する必要がある.
1: python -m PyInstaller --name touchtracer examples-path\demo\touchtracer\main.py
2: .spec
を編集する
-
from kivy_deps import sdl2, glew
を最初の方に追記 - EXE内を下のように編集する
exe = EXE(pyz, Tree('examples-path\\demo\\touchtracer\\'),
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
upx=True
name='touchtracer')
- Analysis内に必要なデータ(画像のパス)を追加する
datas=[ ('learn_kivy/img/ナエトル.png', '.') ],
3: python -m PyInstaller touchtracer.spec
さいごに
kivy, クソめんどくさい.座標もrootとかselfとか親とか子とか色々あるし,pythonファイルとは別にkvファイルにも色々書かないといけないし...
なので,きっともう触らない気がする...
スマホアプリはFlutter/Kotlinが楽なのかな???