0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kivyからの挑戦状.Pongを改良しよう!

Last updated at Posted at 2023-04-23

はじめに

最近,スマホアプリを作りたい欲が出てきたので,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に画像のパスを設定してあげると良い
ナエトルは,写真フォルダに唯一あったいい感じの大きさのやつです.ヒコザルが好きです.

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

停止/スタート

  • ボタンが押されたら,表示を変更+ボールの速度を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の処理,上下と左右で違うから,継承する必要は無かったな...
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'
        • どうやら,player2PongPaddleオブジェクトらしい(よく分からず,よりあえずコードを写してた...)
      • 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が楽なのかな???

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?