2021年2月8日月曜日

Python+Kivy 備忘録 kivy の描画関数は描画関数じゃない?編

 実はまだ作っています MO スケルトン。予想通り緊急事態宣言が延長されましたが、別に休みが増えている訳でも無く、タイムリミットが迫っています。
当初サーバー側を作ろうかと思っていましたが、もう少しクライアント側を作らないと、サーバーが失敗しているか?の判断も付かない為、クライアント側を重視で制作中。
…その間に見つかったちょっと残念な kivy の仕様を書いてみます。(割と今更な内容らしいですけど、日本語でコードを載せている方が見当たらなかったので)

さて、kivy は強力なUIを持つライブラリですし、UIとゲーム画面の混在するアプリを作りたいものです。
こんな画面を作る場合、
ゲーム画面(描画領域)は空の BoxLayout を置いてココに色々描きこむ事になります。
…まあ、それは良いのですが、 kivy のゲーム系サンプルコードを読むと描画の開始時に
self.canvas.clear()
を行いなさいというコードを良くみかけるのですが…。
21年2月現在でUI混ざりのアプリで self.canvas.clear() を行うと全てのUIが消えてしまいます。
しかも、表示されないボタンの当たり判定だけ残っていてクリックできるという状態になります。
…多分、この怪し過ぎる挙動は改善されると思うのですが。

で、次善策として BoxLayout の描画領域だけ Rectangle で塗りつぶして描画するコードを書いてみたのがこちら

コード

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.graphics import Color, Ellipse, Line, Rectangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.clock import Clock

class BtRev(Button):
    def on_press(self):
        self.parent.Rev()

class BoxMain(BoxLayout):
    orientation='vertical'
    def __init__(self,**kwargs):
        super(BoxMain,self).__init__(**kwargs)
        self.speed = 0.1
        self.ball_x = 0.0
        self.boxDraw = BoxLayout(size_hint_y=4)
        self.add_widget(self.boxDraw)
        self.b1 = BtRev(text='Rev',size_hint_y=1)
        self.add_widget(self.b1)

    def  Rev(self):
        self.speed = self.speed * -1.0

    def update(self,dt):
        with self.canvas.before:
            w = self.boxDraw.size[0]
            h = self.boxDraw.size[1]

            x = self.ball_x * w - w/20.0
            y = self.boxDraw.top-h/2.0-h/20.0

            self.ball_x = self.ball_x + self.speed
            if self.ball_x > 1.0:
                self.ball_x = 0.0
            elif self.ball_x < 0.0:
                self.ball_x = 1.0

            Color(0,0,0,1)
            Rectangle(pos=(self.boxDraw.right-w,self.boxDraw.top-h),size=(w,h))

            Color(1,1,1,1)
            Ellipse(pos=(x,y),size=(w/10.0,h/10.0))

class BoundBall_miss(App):
    def build(self):
        bxmain = BoxMain()
        Clock.schedule_interval(bxmain.update, 1.0/12.0)
        return bxmain

if __name__ == "__main__":
    BoundBall_miss().run()


…あらかじめ書いておきますが、コレは失敗コードです。
実行すると始めのうちは良いのですが、だんだん処理が重くなっていきます。
原因を探る為「kivy Ellipse 遅くなる」等で検索すると…出るわ出るわ、同様の事例が山ほど出ます。

結論から書いてしまえば、kivy の Rectangle や Ellipse 関数はただの描画関数ではありません。
試しに返り値を print で出力してみると一目瞭然。
オブジェクトを作ってらっしゃる!?
つまりコレは四角や丸を描画するオブジェクトを生成する関数であり、生成されたオブジェクトはフレーム毎にそれぞれが描画を行っています。
こんな事をしていればどんどん処理が重くなっていくのは当然です。
ちなみに、何処に積み重なっているのかと言えば、描画先の self.canvas や self.canvas.before 等であり、割り当てられた描画オブジェクトを全開放する関数が前述の self.canvas.clear() 等のようです。
私の古い常識からすれば Ellipse 関数は CreateEllipseObject とでも名乗るべきで、返り値でハンドラを返してユーザーに開放させるべきですが(というかこの関数自体、オブジェクトの実体参照先を返してくる)今の時代の感性と合わないのかもしれません。
又、python はメモリ管理をガーベジコレクションで行っており、コードを書く人間はメモリ開放を明記しない事が多いのでこういう書き方になったのかもしれないのですが…逆に python しか言語を知らない人は大ハマりするのでは?

で、改善版コードはこちら

from kivy.app import App
from kivy.uix.widget import Widget

from kivy.graphics import Color, Ellipse, Line, Rectangle
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.clock import Clock

class BtRev(Button):
    def on_press(self):
        self.parent.Rev()

class BoxMain(BoxLayout):
    orientation='vertical'
    def __init__(self,**kwargs):
        super(BoxMain,self).__init__(**kwargs)
        self.speed = 0.1
        self.ball_x = 0.0
        self.list_el = []

        self.boxDraw = BoxLayout(size_hint_y=4)
        self.add_widget(self.boxDraw)

        self.b1 = BtRev(text='Rev',size_hint_y=1)
        self.add_widget(self.b1)

    def  Rev(self):
        self.speed = self.speed * -1.0

    def update(self,dt):
        with self.canvas.before:
            w = self.boxDraw.size[0]
            h = self.boxDraw.size[1]

            x = self.ball_x * w - w/20.0
            y = self.boxDraw.top-h/2.0-h/20.0

            self.ball_x = self.ball_x + self.speed
            if self.ball_x > 1.0:
                self.ball_x = 0.0
            elif self.ball_x < 0.0:
                self.ball_x = 1.0
            # remove old ball
            for old in self.list_el:
                self.canvas.before.children.remove(old)
                self.list_el.remove(old)

            Color(1,1,1,1)
            self.list_el.append(Ellipse(pos=(x,y),size=(w/10.0,h/10.0)))

class BoundBall(App):
    def build(self):
        bxmain = BoxMain()
        Clock.schedule_interval(bxmain.update, 1.0/12.0)

        return bxmain

if __name__ == "__main__":
    BoundBall().run()


※21年2/12 更新。以前は self.canvas.before.remove としていましたが、self.canvas.before.children.remove に変更しました。
自分で生成した Ellipse オブジェクトをリスト化しておいて、描画の開始時に古いオブジェクトを開放しています。
うーん kivy ライブラリってなんというか、妙な所で設計が雑な気がするのは気のせいでしょうか?

0 件のコメント:

コメントを投稿