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()
함수를 호출 하도록 해준거에요. 그리고 PongApp
을 App
클래스로 정의했는데 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.py
의 PongGame
이라는 위젯의 객체로 소속이 된다는 의미입니다.
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.py
에 PongBall
이라는 위젯을 만들어서 그 안에 그래픽을 넣어야합니다. 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.pos
에 self.pos
를 할당해준것도 self.pos
가 기본 속성으로 제공되기 때문입니다.
그러면 이제 만든 탁구공을 게임에 추가시켜볼게요. 기존의 PongGame
위젯 에서 공에 쉽게 접근할수 있도록 ball
이라는 클래스변수를 하나 만들고 거기에다가 PongBall
을 구현해서 객체로 저장해서 사용하도록 할텐데요. 일단 코드에서는 해당 변수에 ObjectProperty
를 할당한뒤 나중에 pong.kv
에서 PongBall
를 할당할거에요.
PongGame
에 ball
을 선언해서 임시로 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
의 객체를 가지던 ball
을 PongBall
위젯으로 대체하게 한거죠.
여기까지 완성된 코드는 다음과 같습니다.
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
공이 움직이려면 공이 어느 방향으로 움직여야할지를 먼저 알아야합니다. 그 정보를 PongBall
의 velocity_x
와 velocity_y
에 각각 저장하여, 다음 스텝에서 x
로는 얼마를 가고, y
로는 얼마를 갈지를 참고하도록 합니다.
- 현재 위치의
x
와y
에 각각 더해줄velocity_x
와velocity_y
의 픽셀값이 바로 진행할 방향과 속도가 됩니다. velocity_x
와velocity_y
의 절대값이 크면 클수록 탁구공의 속도가 빨라지는 것이고,velocity_x
와velocity_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_x
와 velocity_y
를 NumericProperty
로 정의하고, 편의를 위해 이 두개 값을 ReferenceListProperty
로 묶어서 velocity
라는 변수에 다시 저장합니다. 이렇게 함으로써 추후에 velocity
에만 접근하면 velocity_x
와 velocity_y
를 한번에 가져올수 있거든요.
그리고 PongBall
에 move
라는 함수를 하나 선언합니다. 이 함수는 호출될때마다 공을 1스텝 움직이도록 하는 함수입니다. 함수가 호출되면 현재 위치 x
, y
에 이동해야할 위치값 velocity_x
, velocity_y
을 더해서 다시 현재 위치값으로 할당함으로써 탁구공을 1스텝씩 이동하도록 해줍니다.
1스텝은
velocity_x
와velocity_y
에 저장된 만큼이 1스텝입니다.
여기서 주의해야할 점은 pong.kv
에서 정의한 PongBall
의 pos
는 현재위치의 x
와 y
값을 Vector
로 저장하고 있습니다. 그렇기때문에 ReferenceListProperty
로 저장된 velocity
는 Vector
로 변환한 후에 self.pos
와 더하기를 할수 있습니다.
이제 어디선가 move
함수를 호출해줘야 공이 움직이겠죠? PongBall
의 move
함수를 호출해줄 곳은 바로 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
함수가 호출되면 바로 PongBall
의 move
함수를 호출합니다. 일전에 PongBall
은 PongGame
의 ball
안에 객체화 해서 넣어두었었지요? 그래서 PongGame
은 self.ball.move()
로 PongBall
의 move
를 호출할수 있습니다.
그리고 여기에서 공이 정해진 공간을 벗어나지 않도록 코딩을 해주는데요. 공의 현재 위치의 y
좌표가 0
이면 화면의 가장 윗쪽인데 그 픽셀보다 더 위로 올라가 있으면 방향을 반대로 틀어야겠죠? 그건 현재 속도에 -1
을 곱해주면 같은 속도로 반대쪽으로 가도록 할수 있습니다. 마찬가지로 y
가 현재 화면의 높이보다 크거나, x
가 0
보다 작거나, x
가 가로화면 보다 크면 그때마다 -1
을 곱해주어 같은 속도에 방향만 반대로 틀도록 합니다.
이제 앱이 처음 실행되고 PongApp
위젯이 build
될때 1초에 60번씩 PongGame
의 update
함수를 호출하도록 해줄게요.
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))
이 함수에서 처음에 출발할 방향과 속도를 결정하는데 초기값을 x
는 4
, y
는 0
으로 준 Vector
를 랜덤으로 0
도에서 360
도까지 중에서 하나 정해서 회전시키면 진행할 방향과 속도가 정해집니다.
사실 각 변수
x
,y
의 수치가 즉 속도이며 그 수치의 차이가 방향을 결정하는 것이지요.
예를 들어 x
, y
를 4
와 0
으로 각각 설정한 상태에서 랜덤으로 회전을 시켰는데 0
도가 나왔다면 돌아가지 않고 그대로 x
의 속도는 4
, y
의 속도는 0
이 될것 입니다. 그러면 움직임은 x
로만 진행되고 y
는 0
이기 때문에 아무리 더해도 0
이됩니다. 그 말은 공이 중간에서 시작했다면 가로로 왔다 갔다 하는데 시작하는 x
의 값이 양의 값이니까 x
의 값이 큰쪽이 진행 방향이 되어 공은 처음에 오른쪽으로 갔다가 벽에 부딪히면 -
가 되어 반대편으로 움직이겠지만 y
의 값은 계속 0
인 상태로 남아 세로로는 움직이지 않게 될것입니다. 만약 랜덤각도가 180
도가 나왔다면 x
의 값은 -4
가 되고 y
는 여전히 0
인 상태로 남아 마찬가지로 가로 움직임을 하겠지만 시작방향이 x
가 적은 숫자인 왼쪽 방향으로 먼저 움직인뒤 벽에 부딪히면 +
로 전환되어 오른쪽으로 움직이지만 여전히 세로 움직임은 없겠죠. 그러면 그와 반대로 x
, y
를 마찬가지로 4
와 0
으로 설정했는데 랜덤으로 회전를 시켰더니 90
도가 나왔다면 이때는 x
가 0
이 되고 y
가 4
가 되어 공은 y
값이 큰쪽인 위로 먼저 움직이는데 이때 x
값은 0
이므로 0
에 0
을 아무리 더해도 여전히 0
이니 이때는 가로 움직임은 없고 세로로만 왔다 갔다 할뿐입니다. 그런데 이때 회전하는 각도가 45
도가 나왔다면 어떨까요? 이때는 x=3
, y=3
으로 초기화가 되어 우측 상단으로 x
와 y
가 동일하게 증가하여 비례그래프 처럼 사선으로 움직이게 됩니다. 그러다가 공이 천장에 부딪히면 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
, palyer2
를 PongGame
에 선언해주었습니다.
그리고 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
의 중간값을 중앙에 위치시켜 초기화를 합니다.
이때 플레이어의 id
를 player_left
, player_right
으로 주고, 코드에서 선언한 PongGame
의 플레이어 변수 player1
, palyer2
에 각각 할당을 해줍니다.
여기까지 두명의 플레이어의 탁구채를 생성하고 필요한 위치에 위치하는 것까지 완료 했습니다. 이제 사용자가 이 탁구채를 움직일 수 있어야겠죠? 여기서 집중하여야할 것이 바로 Widget
입니다. PongPaddle
은 Widget
을 확장하여 만든 클래스인데 Kivy에서 제공하는 이 Widget
은 on_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
이 함수에서 전달받는 인자값은 self
와 touch
인데, touch
는 현재 위치의 x
, y
값을 가지고 있어 사용자가 어디에서 드래그 하고 있는지 알수 있습니다. 여기에서 우리가 해야할 것은 사용자입력이 왼쪽 플레이어의 진영에서 이루어진것인지 오른쪽 플레이어의 진영에서 이루어진것인지를 알아야할 필요가 있습니다. touch.x
의 위치가 화면을 3개로 나눈 1/3영역에서 이루어진 것이라면 왼쪽 플레이어로 간주하고 touch.y
의 값을 player1.center_y
에 할당하여 왼쪽 탁구채의 세로 중간값을 사용자가 입력한 값으로 대체해줍니다. 반대로 touch.x
의 터치가 화면을 3개로 나눈 3/3영역에서 이루어진 것이라면 오른쪽 플레이어로 간주하고 touch.y
의 값을 player2.center_y
에 할당하여 오른쪽 탁구채의 세로 중간값을 입력한 값으로 대체해줍니다.
여기까지 사용자의 입력을 받고, 탁구채를 사용자가 입력한 대로 움직이도록 했습니다. 그렇다면 이제 움직이는 공을 탁구채로 쳐냈을때 어떻게 해야하는지 생각해봅시다. PongGame
이 update
될때마다 탁구공이 탁구채와 만났는지 확인을 해줄 필요가 있습니다. 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%
올리는 것으로 합니다. 이는 velocity
의 x
, y
값에 1.1
을 곱해줌으로써 해결이 됩니다. 그리고 추가로 공이 탁구채의 어느위치에 맞냐에 따라서 각도를 조금씩 다르게 줍니다.
예를 들어 탁구공이 탁구채의 정중앙에 맞는다면 별다른 변화가 없겠지만 탁구공이 탁구채의 윗쪽에 맞을 수록 velocity.y
의 수치를 조금더 높여주는 겁니다. velocity.y
의 수치를 높여준다는 말은 탁구공의 방향이 조금더 윗쪽을 향하도록 틀어준다는 뜻입니다. 이는 탁구채의 윗쪽을 맞을 수록 더욱 수치가 높아지며, 반대로 탁구채의 아랫쪽을 맞으면 velocity.y
의 값을 줄여줍니다. 아래에 맞으면 맞을수록 공을 더욱 아래쪽으로 기울이겠다는 의미입니다. 이때 공이 탁구채에 맞는 위치에 대한 변화는 velocity.x
의 값에는 영향을 미치지 않습니다.
이제 PongGame
이 update
될때마다 위에 정의한 내용을 반영하도록 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
함수를 호출하는 부분은 그대로 둡니다. 그 밑에 player1
과 player2
의 bounce_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를 이용한 모바일앱 만들기 탁구게임편이었습니다. 시청해주셔서 감사합니다.