Python Kivy – Pong Game Tutorial

Simple Graphic

이번시간에는 Kivy를 가지고 탁구게임을 한번 만들어 보도록 할게요. 앱이름은 pong이라고 할거에요. 일단 소스코드를 보관할 pong이라는 폴더를 만드시고요, 그 안에 main.py파일을 생성해서 아래 코드를 입력해주세요.

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()

위의 코드에서 한거라곤 이름 정한거 밖에 없어요. App의 이름은 Pong이구요. 그래서 맨밑에 실행하는 파일명이 main이면 PongApp()을 객체로 구현한뒤에 run()함수를 호출 하도록 해준거에요. 그리고 PongAppApp클래스로 정의했는데 App 클래스가 호출이 되면 build()를 가장먼저 실행하는데 여기서 PongGame위젯을 객체화하여 반환하도록 했습니다. PongGame이 선언된 클래스를 보시면 아무것도 없이 pass만 넣어두었습니다.

이렇게 PongApp을 만들건데 실행되면 PongGame을 실행하라고 일단 윤곽만 만든거에요. 참고로 PongGame은 Kivy에서 지원하는 Widget클래스를 확장해서 만들었습니다. Widget은 화면에 보여주는 각종 속성들과 사용자 입력을 받아내는 이벤트들이 기본적으로 장착이 되어 있어서 화면에 보이는 모든 객체들은 전부 Kivy가 제공하는 Widget을 확장하여 정의해야합니다.

이제 화면구성을 해볼텐데요. Kivy가 좋은점이 몇가지 설정만으로 화면을 구성할수 있는 kv설정파일을 지원한다는 겁니다. main.py와 같은 폴더에 pong.kv라는 파일을 생성해주세요. 이곳에 화면그래픽에 필요한 설정을 해줄겁니다.

그래픽 설정 파일명이 pong.kv인 것은 그냥 우연이 아니라 main.py에서 지어준 앱의 이름이 PongApp이기 때문입니다. App빼고 Pong이 이 앱의 실제 이름인데 소문자로 파일명을 만들어 주면 해당 앱이 로딩될때 자동으로 해당 파일도 로딩이 되어 별도의 설정이나 코딩없이 바로 그래픽이 적용이 됩니다.

main.py과 같은 폴더에 생성해 준 pong.kv라는 파일에 아래 코드를 입력합니다.

#: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"

pong.kv파일의 가장 첫번째 줄을 보면 #:kivy 1.0.9라고 주석이 달려있습니다. 이는 Kivy버젼을 명시하는 것인데, 버젼에 따라 설정파일의 형식이 달라지므로 kv파일의 가장 첫 줄에는 무조건 #:kivy로 시작되는 버젼명을 명시하도록 하는것이 필수사항입니다.

그 밑으로 <PongGame>이라고 되어있는데 kv파일에서 <>로 감싸여 있는 것은 바로 소속 위젯을 뜻합니다. 즉슨, kv파일의 <PongGame>에 나열된 그래픽이 main.pyPongGame이라는 위젯의 객체로 소속이 된다는 의미입니다.

pong.kv안의 코드를 보면 <PongGame>아래에 처음 설정된 키값이 바로 canvas입니다. canvas로 설정된 값에는 그림이나 도형이 들어간다는 것을 뜻합니다. canvas밑에 보면 Rectangle이 키로 설정되어있는데 바로 화면에 사각형을 하나 그리겠다는 의미입니다. 포지션은 x축은 가운데에서 왼쪽으로 5픽셀 간곳, 그리고 세로로는 가장 윗쪽에 위치시킨다고 설정이 되어있습니다. 그리고 사각형의 가로는 10픽셀, 그리고 세로는 PongGame에 할당된 모든 세로길이를 전부 세로길이로 갖습니다. 세로로는 끝까지 화면을 꽉채우라는 의미지요.

그리고 라벨이 두개 있는데, 폰트사이즈는 70픽셀, 가로위치는 하나는 왼쪽 1/4위치에 하나는 오른쪽 3/4위치에 놓으라고 설정되어있습니다. 그리고 그 안에 문자열 “0”을 각 라벨에 넣으라고도 되어있습니다.

여기까지 놓고 실행을 한번 해볼까요?

python pong/main.py

설명드린대로 가운데 얇은 사각형이 가로 10픽셀, 세로 화면 끝까지 그려져 있고, 각각 화면의 1/4, 3/4지점에 문자열 “0”이 라벨로 올라가 있습니다.

여기까지 보면 우리편 진영과 상대편 진영을 나누고 점수를 보여줄 점수판까지 만들어진 상태입니다.

Making a Ball

탁구게임에서 가장 중요한 “탁구공”을 만들어 보도록 하겠습니다. 공을 만들려면요. 물론 그래픽이 있어야겠지만 그래픽을 어디에 설정할거냐는 바로 코드에서 정해지는 것이기 때문에 main.pyPongBall이라는 위젯을 만들어서 그 안에 그래픽을 넣어야합니다. main.py에 새로운 위젯클래스 PongBall을 하나 추가합니다. 아직 안에 들어갈 내용은 없으니 그냥 pass만 넣을게요. 다른 위젯에서 불러쓸 위젯이니까 제일 위에 정의하는게 좋겠죠? 참고로 마찬가지로 Widget을 확장해서 선언했습니다.

class PongBall(Widget):
    pass

그리고 그 위젯 안에 그래픽을 넣을거에요. pong.kv를 열어서 PongBall에 들어갈 그래픽을 정의합니다.

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

kv파일의 <PongBall>은 코드의 PongBall위젯과 같은 의미입니다. 그래서 kv에서 정의된 그래픽들은 해당 위젯에 포함이 됩니다. PongBall에서 사용할 그래픽의 크기는 가로세로 50픽셀입니다. 그리고 그 안에 canvas를 정의하여 Ellipse로 동그라미를 그려줍니다. 그리고 동그라미의 위치는 현재 PongBall의 위치로, 그리고 동그라미의 크기는 현재 PongBall의 크기로 설정해줍니다. Widget에는 각종 기본 속성 들이 들어가있는데 self.pos를 따로 선언해준 적이 없는데 Ellipse.posself.pos를 할당해준것도 self.pos가 기본 속성으로 제공되기 때문입니다.

그러면 이제 만든 탁구공을 게임에 추가시켜볼게요. 기존의 PongGame위젯 에서 공에 쉽게 접근할수 있도록 ball이라는 클래스변수를 하나 만들고 거기에다가 PongBall을 구현해서 객체로 저장해서 사용하도록 할텐데요. 일단 코드에서는 해당 변수에 ObjectProperty를 할당한뒤 나중에 pong.kv에서 PongBall를 할당할거에요.

PongGameball을 선언해서 임시로 ObjectProperty객체를 할당합니다.

from kivy.properties import ObjectProperty

class PongGame(Widget):
    ball = ObjectProperty(None)

이제 임시로 넣어준 객체를 실제 공으로 바꿀 차례인데요. 그 액션은 pong.kv설정파일에서 합니다.

pong.kv파일을 열어서 가장 먼저 <PongGame>아래에 PongBall라는 속성을 추가해서 해당 객체를 구현하도록 합니다. 그리고 나서 해당 객체에 pong_ball이라는 이름을 준뒤에 그 이름을 ball변수에 할당함으로써 PongBall의 객체를 ball에 넣습니다.

<PongGame>:
    ball: pong_ball
...
    PongBall:
        id: pong_ball
        center: self.parent.center

종합해서 정리를 해드리자면, PongBall을 코드에서 위젯으로 생성하고 설정파일에서 그래픽을 추가합니다. 그 뒤에 PongGame위젯에 ball이라는 변수를 하나 생성하고 설정파일에 들어가서 <PongGame>아래에 PongBall을 차일드로 추가한뒤 그 객체에 pong_ball라는 이름을 주어 PongGame위젯에서 선언한 ball에 할당함으로써 초기값으로 ObjectProperty의 객체를 가지던 ballPongBall위젯으로 대체하게 한거죠.

여기까지 완성된 코드는 다음과 같습니다.

main.py

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

class PongBall(Widget):
    pass

class PongGame(Widget):
    ball = ObjectProperty(None)

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

자 그러면 여기까지 한번 실행을 해볼까요?

코드에 입력한 대로 정 가운데 동그란 공이 하나 생겼습니다.

Adding Ball Animation

공이 움직이려면 공이 어느 방향으로 움직여야할지를 먼저 알아야합니다. 그 정보를 PongBallvelocity_xvelocity_y에 각각 저장하여, 다음 스텝에서 x로는 얼마를 가고, y로는 얼마를 갈지를 참고하도록 합니다.

  • 현재 위치의 xy에 각각 더해줄 velocity_xvelocity_y의 픽셀값이 바로 진행할 방향과 속도가 됩니다.
  • velocity_xvelocity_y의 절대값이 크면 클수록 탁구공의 속도가 빨라지는 것이고,
  • velocity_xvelocity_y의 값이 차이가 많이 나면 날수록 비례 곡선에서 벗어나는 방향이 되는 것입니다
  • velocity_x의 절대값이 클수록 가로 움직임이 커지고, velocity_y의 절대값이 클수록 세로 움직임이 커집니다.

현재 공의 위치는 pong.kv<PongBall>아래에 pos으로 저장되어 있으니까 공의 방향을 저장할 변수를 아래와 같이 정의합니다.

from kivy.properties import (
    NumericProperty, ReferenceListProperty, ObjectProperty
)
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

PongBall에 공의 방향과 속도를 저장할 velocity_xvelocity_yNumericProperty로 정의하고, 편의를 위해 이 두개 값을 ReferenceListProperty로 묶어서 velocity라는 변수에 다시 저장합니다. 이렇게 함으로써 추후에 velocity에만 접근하면 velocity_xvelocity_y를 한번에 가져올수 있거든요.

그리고 PongBallmove라는 함수를 하나 선언합니다. 이 함수는 호출될때마다 공을 1스텝 움직이도록 하는 함수입니다. 함수가 호출되면 현재 위치 x, y에 이동해야할 위치값 velocity_x, velocity_y을 더해서 다시 현재 위치값으로 할당함으로써 탁구공을 1스텝씩 이동하도록 해줍니다.

1스텝은 velocity_xvelocity_y에 저장된 만큼이 1스텝입니다.

여기서 주의해야할 점은 pong.kv에서 정의한 PongBallpos는 현재위치의 xy값을 Vector로 저장하고 있습니다. 그렇기때문에 ReferenceListProperty로 저장된 velocityVector로 변환한 후에 self.pos와 더하기를 할수 있습니다.

이제 어디선가 move 함수를 호출해줘야 공이 움직이겠죠? PongBallmove함수를 호출해줄 곳은 바로 PongGame입니다. 현재 PongGame위젯에는 ball이라는 변수 하나만 선언되어 있는데요. 그 밑에 update라는 함수를 하나 선언해서 그 안에서 move를 호출해주는 로직을 만들어 보도록 할게요.

class PongGame(Widget):
    ball = ObjectProperty(None)

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

        if (self.ball.y < 0) or (self.ball.top > self.height):
            self.ball.velocity_y *= -1

        if (self.ball.x < 0) or (self.ball.right > self.width):
            self.ball.velocity_x *= -1

일단 위의 update함수가 호출되면 바로 PongBallmove함수를 호출합니다. 일전에 PongBallPongGameball안에 객체화 해서 넣어두었었지요? 그래서 PongGameself.ball.move()PongBallmove를 호출할수 있습니다.

그리고 여기에서 공이 정해진 공간을 벗어나지 않도록 코딩을 해주는데요. 공의 현재 위치의 y좌표가 0이면 화면의 가장 윗쪽인데 그 픽셀보다 더 위로 올라가 있으면 방향을 반대로 틀어야겠죠? 그건 현재 속도에 -1을 곱해주면 같은 속도로 반대쪽으로 가도록 할수 있습니다. 마찬가지로 y가 현재 화면의 높이보다 크거나, x0보다 작거나, x가 가로화면 보다 크면 그때마다 -1을 곱해주어 같은 속도에 방향만 반대로 틀도록 합니다.

이제 앱이 처음 실행되고 PongApp위젯이 build될때 1초에 60번씩 PongGameupdate함수를 호출하도록 해줄게요.

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

Kivy에서 제공하는 Clock함수를 이용하여 매 1/60초마다 PongGame.update를 호출하도록 스케줄했습니다. 그리고 바로 위에 game.serve_ball()을 호출하도록 했는데요. serve_ball은 처음에 공을 던질때나 게임을 다시 시작할때 호출되는 함수입니다. serve_ball을 호출할때마다 공이 가운데로 와서 다시 시작하는데 이때 진행할 방향과 속도도 여기에서 정해줍니다.

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

이 함수에서 처음에 출발할 방향과 속도를 결정하는데 초기값을 x4, y0으로 준 Vector를 랜덤으로 0도에서 360도까지 중에서 하나 정해서 회전시키면 진행할 방향과 속도가 정해집니다.

사실 각 변수 x, y의 수치가 즉 속도이며 그 수치의 차이가 방향을 결정하는 것이지요.

예를 들어 x, y40으로 각각 설정한 상태에서 랜덤으로 회전을 시켰는데 0도가 나왔다면 돌아가지 않고 그대로 x의 속도는 4, y의 속도는 0이 될것 입니다. 그러면 움직임은 x로만 진행되고 y0이기 때문에 아무리 더해도 0이됩니다. 그 말은 공이 중간에서 시작했다면 가로로 왔다 갔다 하는데 시작하는 x의 값이 양의 값이니까 x의 값이 큰쪽이 진행 방향이 되어 공은 처음에 오른쪽으로 갔다가 벽에 부딪히면 -가 되어 반대편으로 움직이겠지만 y의 값은 계속 0인 상태로 남아 세로로는 움직이지 않게 될것입니다. 만약 랜덤각도가 180도가 나왔다면 x의 값은 -4가 되고 y는 여전히 0인 상태로 남아 마찬가지로 가로 움직임을 하겠지만 시작방향이 x가 적은 숫자인 왼쪽 방향으로 먼저 움직인뒤 벽에 부딪히면 +로 전환되어 오른쪽으로 움직이지만 여전히 세로 움직임은 없겠죠. 그러면 그와 반대로 x, y를 마찬가지로 40으로 설정했는데 랜덤으로 회전를 시켰더니 90도가 나왔다면 이때는 x0이 되고 y4가 되어 공은 y값이 큰쪽인 위로 먼저 움직이는데 이때 x값은 0이므로 00을 아무리 더해도 여전히 0이니 이때는 가로 움직임은 없고 세로로만 왔다 갔다 할뿐입니다. 그런데 이때 회전하는 각도가 45도가 나왔다면 어떨까요? 이때는 x=3, y=3으로 초기화가 되어 우측 상단으로 xy가 동일하게 증가하여 비례그래프 처럼 사선으로 움직이게 됩니다. 그러다가 공이 천장에 부딪히면 y-3으로 변경되어 x로는 오른쪽 y로는 아래쪽으로 움직이다가 오른쪽 벽에 부딪히면 이때 x도 마저 -3으로 기호가 바뀌어 좌표그래프의 0쪽을 향하는 방향으로 탁구공이 사선으로 움직이게 됩니다.

여기까지의 전체 코드는 다음과 같습니다.

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()

        if (self.ball.y < 0) or (self.ball.top > self.height):
            self.ball.velocity_y *= -1

        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

여기까지의 코드를 실행해보면 탁구공이 혼자 이리저리 돌아다니면서 벽에 부딪히면 방향을 바꿔 가는 것을 보실수 있으실거에요.

Connect Input Events

이제 이 공을 사용자의 입력을 받아 움직이게 할 차례입니다. 사용자가 내리칠 탁구채 만드는거랑 이겼을때 점수판에 점수만 올라가면 될거 같아요. 우선 탁구채를 만들게요. 마찬가지로 Kivy에서 제공하는 Widget을 기본으로 클래스를 선언합니다.

class PongPaddle(Widget):
    score = NumericProperty(0)

class PongGame(Widget):
    ball = ObjectProperty(None)
    player1 = ObjectProperty(None)
    player2 = ObjectProperty(None)

PongPaddle이라는 위젯을 만들고 탁구채를 사용자라고 생각하고 점수를 그 안에다 저장하도록 할게요. 그리고 해당 플레이어들을 저장할 변수 player1, palyer2PongGame에 선언해주었습니다.

그리고 pong.kv에서 탁구채 모양을 만들어 볼게요.

<PongPaddle>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
...
<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right
...
    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

PongPaddle위젯에 가로는 25로 얇게 세로는 200으로 긴 사각형을 하나 추가할게요. 그리고 만든 탁구채를 <PongGame>에 추가 합니다. 탁구채의 위치는 왼쪽의 플레이어가 x값을 0을 갖게 하고, 해당 탁구채의 y 중간값은 화면의 중앙에 위치시킵니다. 오른쪽 플레이어는 화면의 가로크기에서 탁구채의 두께를 뺀 만큼으로 오른쪽 끝에 붙이고 마찬가지로 탁구채의 y의 중간값을 중앙에 위치시켜 초기화를 합니다.

이때 플레이어의 idplayer_left, player_right으로 주고, 코드에서 선언한 PongGame의 플레이어 변수 player1, palyer2에 각각 할당을 해줍니다.

여기까지 두명의 플레이어의 탁구채를 생성하고 필요한 위치에 위치하는 것까지 완료 했습니다. 이제 사용자가 이 탁구채를 움직일 수 있어야겠죠? 여기서 집중하여야할 것이 바로 Widget입니다. PongPaddleWidget을 확장하여 만든 클래스인데 Kivy에서 제공하는 이 Widgeton_touch_down, on_touch_move, 또는 on_touch_up과 같은 사용자 입력을 기본적으로 제공합니다. 우리는 사용자가 탁구채를 잡고 옮기는 액션을 캐치해야하므로 PongGame위젯 안에 on_touch_move함수를 재선언합니다. on_touch_move는 물체를 잡고 드래그하는 동안 계속적으로 현재 위치를 읽어주게 되어 있습니다.

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

이 함수에서 전달받는 인자값은 selftouch인데, touch는 현재 위치의 x, y값을 가지고 있어 사용자가 어디에서 드래그 하고 있는지 알수 있습니다. 여기에서 우리가 해야할 것은 사용자입력이 왼쪽 플레이어의 진영에서 이루어진것인지 오른쪽 플레이어의 진영에서 이루어진것인지를 알아야할 필요가 있습니다. touch.x의 위치가 화면을 3개로 나눈 1/3영역에서 이루어진 것이라면 왼쪽 플레이어로 간주하고 touch.y의 값을 player1.center_y에 할당하여 왼쪽 탁구채의 세로 중간값을 사용자가 입력한 값으로 대체해줍니다. 반대로 touch.x의 터치가 화면을 3개로 나눈 3/3영역에서 이루어진 것이라면 오른쪽 플레이어로 간주하고 touch.y의 값을 player2.center_y에 할당하여 오른쪽 탁구채의 세로 중간값을 입력한 값으로 대체해줍니다.

여기까지 사용자의 입력을 받고, 탁구채를 사용자가 입력한 대로 움직이도록 했습니다. 그렇다면 이제 움직이는 공을 탁구채로 쳐냈을때 어떻게 해야하는지 생각해봅시다. PongGameupdate될때마다 탁구공이 탁구채와 만났는지 확인을 해줄 필요가 있습니다. PongPaddle위젯에 bounce_ball라는 함수를 선언하여, 탁구채 위젯이 탁구공과 접촉을 했는지 collide_widget함수를 통해 확인을 합니다. 서로 만났다면 collide_widget함수가 True를 반환하여 필요한 처리를 해줄수 있게 됩니다.

class PongPaddle(Widget):

    score = NumericProperty(0)

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            bounced = Vector(-1 * vx, vy)
            vel = bounced * 1.1
            offset = (ball.center_y - self.center_y) / (self.height / 2)
            ball.velocity = vel.x, vel.y + offset

일단 탁구채에 맞으면 x의 방향을 반대로 만들어 주어야합니다. 그건 기존 velocity.x의 값에 -1을 곱함으로써 해결이 됩니다. 그리고 탁구채에 한번 맞을 때마다 속도를 10% 올리는 것으로 합니다. 이는 velocityx, y값에 1.1을 곱해줌으로써 해결이 됩니다. 그리고 추가로 공이 탁구채의 어느위치에 맞냐에 따라서 각도를 조금씩 다르게 줍니다.

예를 들어 탁구공이 탁구채의 정중앙에 맞는다면 별다른 변화가 없겠지만 탁구공이 탁구채의 윗쪽에 맞을 수록 velocity.y의 수치를 조금더 높여주는 겁니다. velocity.y의 수치를 높여준다는 말은 탁구공의 방향이 조금더 윗쪽을 향하도록 틀어준다는 뜻입니다. 이는 탁구채의 윗쪽을 맞을 수록 더욱 수치가 높아지며, 반대로 탁구채의 아랫쪽을 맞으면 velocity.y의 값을 줄여줍니다. 아래에 맞으면 맞을수록 공을 더욱 아래쪽으로 기울이겠다는 의미입니다. 이때 공이 탁구채에 맞는 위치에 대한 변화는 velocity.x의 값에는 영향을 미치지 않습니다.

이제 PongGameupdate될때마다 위에 정의한 내용을 반영하도록 update함수를 수정합니다.

class PongGame(Widget):
...
    def update(self, dt):
        self.ball.move()

        self.player1.bounce_ball(self.ball)
        self.player2.bounce_ball(self.ball)

        if (self.ball.y < self.y) or (self.ball.top > self.top):
            self.ball.velocity_y *= -1

        if self.ball.x < self.x:
            self.player2.score += 1
            self.serve_ball(vel=(4, 0))
        if self.ball.right > self.width:
            self.player1.score += 1
            self.serve_ball(vel=(-4, 0))

기존에 move함수를 호출하는 부분은 그대로 둡니다. 그 밑에 player1player2bounce_ball을 호출하여 공이 탁구채에 접촉했는지 우선 확인하고 둘이 닿았다면 bounce_ball에서 방향을 틀고 속도를 올리는 등 필요한 처리를 해주게 됩니다.

공이 천장이나 바닥에 부딪혔을때는 원래 대로 y의 방향만 반대로 틀어 계속 진행하도록 하되, 단 왼쪽 벽이나 오른쪽 벽에 부딪혔을때는 다르게 처리해주어야합니다. 만약 공이 왼쪽 벽에 부딪혔다면, 왼쪽 탁구채에 맞지 않았다는 이야기 입니다. 탁구채에 맞았다면 벽에 닿기전에 공이 방향을 틀었을테니까요. 그런데 탁구공의 위치가 왼쪽벽을 뚫고 들어갔다면 이것은 왼쪽 플레이어가 게임에 진것입니다. 이때는 오른쪽 사용자의 점수를 올려주고 오른쪽 사용자가 먼저 공을 칠수 있도록 공을 오른쪽으로 서브해줍니다. 반대로 공의 오른쪽 끝이 현재 화면의 가로크기보다 더 넘어설때 왼쪽 사용자에게 1점 추가하고 왼쪽 플레이어에게 서브를 해줍니다.

여기까지 전체코드는 다음과 같습니다.

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 PongPaddle(Widget):
    score = NumericProperty(0)

    def bounce_ball(self, ball):
        if self.collide_widget(ball):
            vx, vy = ball.velocity
            bounced = Vector(-1 * vx, vy)
            vel = bounced * 1.1
            offset = (ball.center_y - self.center_y) / (self.height / 2)
            ball.velocity = vel.x, vel.y


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 = Vector(vel).rotate(randint(0, 360))

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

        self.player1.bounce_ball(self.ball)
        self.player2.bounce_ball(self.ball)

        if (self.ball.y < self.y) or (self.ball.top > self.top):
            self.ball.velocity_y *= -1

        if self.ball.x < self.x:
            self.player2.score += 1
            self.serve_ball(vel=(4, 0))
        if self.ball.right > 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

실행을 해보면 탁구채를 잡아서 공을 쳐낼수 있고 공을 못쳤을때 상대편 점수가 올라갑니다.

여기까지 따라오시느라 수고 많으셨습니다. 이상 Python 모듈 Kivy를 이용한 모바일앱 만들기 탁구게임편이었습니다. 시청해주셔서 감사합니다.

References