22 Ağustos 2015 Cumartesi

Kivy ile Python GUI Örneği

Orjinali http://kivy.org/docs/tutorials/pong.html

Merhaba,

Bu sefer yine Python programlama dilini kullanarak bişeyler yapacağız. Kivy hızlı uygulama geliştirmek için açık kaynak bir Python kütüphanesi.

Bu çalışmada Kivy kütüphanesini kullanarak basit Pong oyunu yapacağız.



Kivy kütüphanesi sitesi www.kivy.org adresinden zip olarak indirilip kullanılabiliyor. Kullanmak için de yazmış olduğunuz python programı indirdiğiniz klasör içindeki kivy.bat dosyası ile çalıştırınca gerisi otomatik hallediliyor.

İlk önce uygulamamız için bir klasör oluşturup içine main.py adında bir Python script dosyası üretiyoruz.
from kivy.app import App
from kivy.uix.widget import Widget

class PongGame(Widget):
    pass

class PongApp(App):
    def build(self):
        return PongGame()


if __name__ == '__main__':
    PongApp().run()
Dosyayı kaydedip uygulamayı çalıştırırsak sadece siyah bir pencere elde etmiş oluruz. Çok basit bir Kivy uygulaması yazdık. Burada Widget sınıfından bir PongGame sınıfı klonlayıp App sınıfından klonladığımız PongApp uygulama sınıfı içinde yerleştirip en sonunda uygulamamızı çalıştırıyoruz.

Şimdi bu siyah arkaplan üzerine yayılmaya başlayalım. PongGame sınıfının boş olan görüntüsüne birşeyler ekleyebilmek için önce .kv uzantılı bir görsel dosyası oluşturacağız. Bu amaçla adı pong.kv olan bir yeni dosya üretiyoruz.
#:kivy 1.0.9

<PongGame>:    
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
            
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"
Uygulamamızın sınıfı PongApp olduğu için dosya adı pong.kv olmalı. İlk satır her kv dosyasında olması gereken bir satır. Burada hangi Kivy versiyonunu kullandığımız yazıyor. Arkasından gelen satırlar sanki bir Python script gibi di mi?. Programı çalıştırıp bakın ekrana neler geldi. Bu kod içinde ekradaki 2 tane "0" sayısını ve tam ortadaki dikey çizgiyi tahmin edebildiniz mi?

<PongGame>:
 satırı uygulamamızdaki PongGame sınıfının görselini tanımlama bloğuna başladığımızı bildiriyor.

canvas birçok programlama dilinde çizim yapılacak alan tanımlarken kullanılır burada da ekranın ortasındaki kalın çizgiyi (pardon dikdörtgeni) çizeceğiz. canvas içerisinde Rectangle bloğu kullanarak sadece bir tane dikdörtgen çiziliyor. Dikdörtgenin pos özelliği sol üst köşesinin koordinatlarını belirler. Koordinatlar aynı şekil pencerenin sol üst köşesi x=0, y=0 dan başlar sağa ve aşağıya doğru büyür. Burada yatay olarak pencere ortasından 5 piksel sola, dikey olarak da en üste ayarlanıyor. Niye 5 piksel sola çünkü 10 piksel genişliğinde bir dikdörtgen çizeceğiz ve tam ortada olmasını istiyoruz. Dikdörtgenin size özelliği sırasıyla genişlik ve yüksekliği belirler. Burada genişlik 10 piksel yükseklik ise pencere yüksekliği kadar ayarlanmış.

Gelelim Label  bloklarına , önce yazı boyutu font_size özelliği ile 70 piksel yapılıyor. Yazının yatay olarak ortası center_x özelliği ile pencerenin yatay olarak 1/4 ve 3/4 ünde iki adet Label var. Yazıların üst koordinatları top özelliği ile pencere üst çizgiden 50 piksel aşağıda olacak. Yazılarda ne yazacağı da text özelliği ile 0 yapılıyor.

Uygulamayı çalıştırıp pencere boyutunu değiştirince görüyoruzki yerleştirdiğimiz elemanlar da otomatik olarak pozisyon ve boyut değiştiriyor. Genel olarak sabit duran ekran elemanları hazır, topumuza geçelim.

Şimdi bir PongBall sınıfı tanımlayacağız ve bunun görseli ve hareketlerini oluşturacağız. PongBall sınıfının Python kodu şöyle:
class PongBall(Widget):

    # x ve y ekseninde topun hızı
    velocity_x = NumericProperty(0)
    velocity_y = NumericProperty(0)

    # referencelist property ile ball.velocity özelliği ya da
    # kısaca örneğin w.pos , w.x ve w.y kullanabiliriz
    velocity = ReferenceListProperty(velocity_x, velocity_y)

    # ``move`` fonksiyonu topumuzu bir adım sonrasına götürür
    #  bunu belli sıklıkta çağırmak topa animasyon verir
    def move(self):
        self.pos = Vector(*self.velocity) + self.pos
Tabi topumuzun görseli için de bir kv kuralı tanımlıyoruz:
<PongBall>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size
50x50 piksel boyutlarında bir yuvarlak çiziyoruz. Yaptığımız kodların çalışabilmesi için Properties ve Vector kütüphanelerini de programa dahil edeceğiz. Kodumuzun son hali ve kv dosyamız şu hale geldi:

main.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import NumericProperty, ReferenceListProperty
from kivy.vector import Vector


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):
    pass


class PongApp(App):
    def build(self):
        return PongGame()


if __name__ == '__main__':
    PongApp().run()

pong.kv
#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size          

<PongGame>:
    canvas:
        Rectangle:
            pos: self.center_x-5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"
    
    PongBall:
        center: self.parent.center

Dikkat ettiyseniz sadece PongBall görselini tanımlamadık , tanımladığımız topu gidip PongGame görselinde ekranın ortasına koyduk.

Evet sevgili top ekranın ortasına geldi , tanımında move fonksiyonu da var ama hareket etmiyor neden? Çünkü bu move fonksiyonunu belirli aralıklarla çağırmamız gerekir. Şu anda kimse çağırmıyor. Bu belirli aralıkta çağırma işlemlerinde Kivy kütüphanesinin Clock sınıfı kullanılır.
Clock.schedule_interval(game.update, 1.0/60.0)
Bu kod game sınıfının update fonksiyonunu saniyede 60 kere çağırır.

Bu noktada biraz duralım. Biz topumuzu tanımladık ve onun move fonksiyonunu çağırmak için uygulamamıza kod ilave ediyoruz. Ancak daha topumuzun ana kod içersinde herhangi bir tanımlaması yok. Top sadece bir yönde gitmeyecek duvarlara ve raketlere vurunca geri sekecek falan. O zaman biz tüm bu hareketler için update adında bir fonksiyon tanımlıyalım ve move yerine onu çağıralım:
class PongGame(Widget):

    def update(self, dt):
        # ball.move ve diğer işlemler burada
        pass

class PongApp(App):

    def build(self):
        game = PongGame()
        Clock.schedule_interval(game.update, 1.0/60.0)
        return game
Bir de kv dosyası içerisine PongBall görselimize ana programda ulaşabilmek için bir isimlendirme ekleyeceğiz. Bunu sağlamak için PongGame sınıfımıza bir ObjectProperty ekleyeceğiz ve kv kuralında tanımladığımız nesneye bağlayacağız.
class PongGame(Widget):
    ball = ObjectProperty(None)

    def update(self, dt):
        self.ball.move()

        # üst ve alt kenardan sekmeler
        if (self.ball.y < 0) or (self.ball.top > self.height):
            self.ball.velocity_y *= -1

        # sol ve sağ kenardan sekmeler
        if (self.ball.x < 0) or (self.ball.right > self.width):
            self.ball.velocity_x *= -1
kv dosyasındaki PongBall nesnesine bir id vermeyi unutmayalım:
<PongGame>:
    ball: pong_ball

    # ... (canvas ve Label'lar)

    PongBall:
        id: pong_ball
        center: self.parent.center
Tüm bu değişiklikleri yapsak da top hala hareket etmiyor. Nedeni hızının sıfır olması. Topa ilk başlangıçta bir hız vermek için serve_ball adlı bir fonksiyon yazalım. Son gelinen programlar şöyle:

main.py
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 random import randint


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):
    ball = ObjectProperty(None)

    def serve_ball(self):
        self.ball.center = self.center
        self.ball.velocity = Vector(4, 0).rotate(randint(0, 360))

    def update(self, dt):
        self.ball.move()

        #üst ve alt kenarda zıplama
        if (self.ball.y < 0) or (self.ball.top > self.height):
            self.ball.velocity_y *= -1

        #sol ve sağ kenarda zıplama
        if (self.ball.x < 0) or (self.ball.right > self.width):
            self.ball.velocity_x *= -1


class PongApp(App):
    def build(self):
        game = PongGame()
        game.serve_ball()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        return game


if __name__ == '__main__':
    PongApp().run()

pong.kv
#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size          

<PongGame>:
    ball: pong_ball
    
    canvas:
        Rectangle:
            pos: self.center_x-5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"
    
    PongBall:
        id: pong_ball
        center: self.parent.center

Sonunda topumuz dolaşmaya başladı, kenarlardan da sekiyor. Sıra geldi hareket eden raketler ve skor takibine. Sınıf eklenmesi ve kv kuralı eklemelerinin ayrıntılarına daha önce anlattığımız için girmeyeceğiz. Bunun yerine Player görselinin bizim komutlarımıza göre nasıl hareket edeceğine odaklanalım. PongPaddle sınıfı için yazılan kodu ve kv kuralının tamamını yazı sonunda bulabilirsiniz.

Kivy'de kullanıcı olaylarına on_touch_down , on_touch_move ve on_touch_up metodlarıyla erişilir. Default olarak olay oluştuğunda bu metodlar ana pencere kodundan itibaren pencere içindeki ve onların içindeki görsellerde sırasıyla aranır, ilk bulunan metod çalıştırılır. Bizim raketler pek de matah bir hareket yapmıyor sadece yukarı aşağı hareket ediyor. Bu yüzden sadece ana pencereye ekleyeceğimiz tek bir metodla ikisini de kontrol edebiliriz.
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
Burada ekranın sağında ya da solunda tıklama yapılmasına göre soldaki ya da sağdaki oyuncunun raketi hareket ettiriliyor.

Her oyuncu için skoru bir NumericProperty içinde saklayacağız. Skor etiketlerini oyuncuların score NumericProperty'si ile eşleştireceğiz. Bu durumda oyuncunun score özelliğini değiştirince otomatik olarak Skor tabela yazıları değişecektir.

Top kenardan dışarı çıktığında skoru yenileyip ortadan oyuna yeni bir top sokacağız. Bu amaçla update metodunu değiştireceğiz. PongPaddle sınıfına ayrıca bounce_ball metodu ekleyerek topun raketin değişik noktalarından değişik açılarla sekmesini sağlayacağız. İşte bu PongPaddle sınıfı:
class PongPaddle(Widget):

    score = NumericProperty(0)

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            speedup  = 1.1
            offset = 0.02 * Vector(0, ball.center_y-self.center_y)
            ball.velocity =  speedup * (offset - ball.velocity)
Not: Bu kodda özellikle raket kenarlardan top değişik sekiyor isterseniz kendiniz iyileştirme yapabilirsiniz.

Ve kodumuzun son hali:

main.py
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


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):
    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 of 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 of to 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))

    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


class PongApp(App):
    def build(self):
        game = PongGame()
        game.serve_ball()
        Clock.schedule_interval(game.update, 1.0 / 60.0)
        return game


if __name__ == '__main__':
    PongApp().run()

pong.kv
#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size          

<PongPaddle>:
    size: 25, 200
    canvas:
        Rectangle:
            pos:self.pos
            size:self.size

<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right
    
    canvas:
        Rectangle:
            pos: self.center_x-5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: str(root.player1.score)
        
    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
        x: root.width-self.width
        center_y: root.center_y

Hepsi bu kadar , yeni yazılarda görüşmek üzere...