패들(Paddle)과 테니스공(TennisBall) 객체 먼저 살펴보자. 두 객체 모두 이전에 만들어 반사판(Bar), 공(Ball) 객체과 크게 다르지 않아 이해에 큰 어려움은 없을 것 같다. 필자의 것과 크게 다른 부분은 패들객체와 볼객체에서 각자에게서 충돌에 대한 처리를 하는 부분이 없고, 메인객체인 게임객체(Game)에서 그 부분을 처리하게 된다.
class Paddle(Rect):
"""
Paddle represents one player on the screen.
It is drawn like a long rectangle and positioned either left or
right on the screen.
Two helper methods move the paddle up or down.
"""
def __init__(self, start_x, start_y):
super().__init__(start_x, start_y, PADDLE_WIDTH, PADDLE_HEIGHT)
def up(self):
if self.y - 5 > 40:
self.y -= 5
def down(self):
if self.y + self.height + 5 < HEIGHT - 40:
self.y += 5
def draw(self):
screen.draw.filled_rect(self, MAIN_COLOR)
class TennisBall():
"""
Represents a tennis ball on the screen
"""
def __init__(self, start_pos, dt):
"""
Initialize the tennis ball position and set the movement rate
"""
self.x, self.y = start_pos
self.dx = self.dy = dt
@property
def pos(self):
return (self.x, self.y)
def move(self):
self.x += self.dx
self.y += self.dy
def draw(self):
screen.draw.filled_circle(self.pos, TENNIS_BALL_RADIUS, MAIN_COLOR)
그 밖에 차이점으로는 테니스공 객체는 Rect를 상속받지 않았고, 그렇기 때문에 객체 스스로 자신의 위치정보를 관리해야 할 필요성이 있으며, 그를 위해 38~40라인에서 보는 바와 같이 pos 라는 자신의 현재 위치를 나타내는 속성값을 별도로 갖고 있는 것을 알 수 있다. 또한 property라는 데코레이터(decorator) 를 사용한 파이썬 문법은 처음 등장했기 때문에 낫설기도 할텐데, 간단히 설명하면 특정 데코레이터를 함수명 위에다 @와 함께 붙히면기존 함수를 수정하지 않고 그 기능을 확장하는 방법을 제공하는 문법으로여기서는 pos가 더이상 객체의 메소드 함수가 아니라 객체의 속성(property)처럼 간주되길 원한다는 목적에서 붙혔다고 간단히 생각하자. 다시말해 해당 코드(38~40라인)는 아래 5번 라인과 100% 동일하다.
그럼, 저렇게 코딩하면 될 것을 왜 구지 데코레이터를 붙혀서 따로 분리해서 코딩했는가 라고 했을 때, 게임을 만든 저자가 pos 라는 속성값이 중요하기 때문에 강조해서 눈의 띄게 하려는 의미정도로 이해하면 될 것 같다. 이제 다음으로 게임객체를 살펴볼텐데, 거기서 이 property 데코레이터가 한번 더 사용되고, 그제서야 이 데코레이터의 진정한 목적을 알 수 있을 것이다. 그럼, 본격적으로 가장 핵심이면서 덩치도 크고 무거운 게임객체를 살펴보도록 하자.
게임객체는 자신에게 필요한 구성품과 같은 각각의 객체를 직접 생성하고 소멸시키는 것까지 제어한다. 13~14라인에 패들객체를 직접 생성하여 내부 속성값으로 할당하고 있다. 그리고, 연이은 16번 라인의 set_ball 멤버함수를 통해 테니스공 객체를 생성하고 있다.
여기서 set ball 함수의 인자값으로 ball_pos 속성값을 넘기고 있는 것을 알 수 있다. 18~23번 라인의 ball_pos함수에 위에서 언급했던 그 데코레이터가 다시 등장하고 있는데, property 데코레이터를 사용해 본질상 함수인 것을 변수(속성값)처럼 활용할 수 있게 변경했다. 그래서, ball_pos가 더이상 함수가 아닌, self.ball_pos 로 호출하면서 마치 변수처럼 사용하고 있다. ball_pos 함수를 살펴보면 먼저와 테니스공의 pos와 같이 단순히 현재 위치값을 반환하는게 아니라, 반환 전에 공의 위치를 현재 패들의 위치에 따른 패들 중앙에 딱 달라붙은 공의 위치 값으로 위치를 최종 가공한 후에 반환하고 있는 것을 알 수 있다. 그렇다. 사실 property 데코레이터의 본래 활용목적은 이렇게 속성값이 반환시 어떤 추가적인 가공이 필요할 때 사용하기 위한게 주 목적인 것이다.
이제 다른 나머지의 멤버함수에 구현체 대해서는 차근히 하나하나 살펴보면서 알고리즘적인 부분을 이해하면 될 것 같고, 마지막으로 51, 61라인에 사용된 collidepoint 함수에 대해 추가설명하면, 이름에서 알 수 있듯이 충돌검사를 하되, 기존엔 Rect 기반, 즉 사격형 기반으로 충돌검사를 했다면, Point 기반 즉, 픽셀단위 기반으로 충돌을 체크한다는 것이다. 픽셀단위로 확인한다니 더 정밀하게 충돌검사 할 수 있게단 기대가 된다. 그러나, 세상이치가 모든게 다 좋을 수 만은 없고, 약점이 있듯이, 이 경우도 그런데 더 세밀한 검사를 위해 더 많은 연산으로 컴퓨터의 자원을 많이 사용하게 되고, 최악의 경우 게임 전체 실행속도가 늦어지는 단점이 발생할 수 있음을 염두해 둬야 한다. 그래서, 우리는 항상 어느정도의 타협점(tradeoff)에서 코딩을 하게 되는 것이다. 참고로 이 함수는 파이게임제로의 모태가 되는 파이게임 라이브러리 Rect 객체의 멤버함수 이다.
class Game():
def __init__(self, player):
self.active_player = player
self.score_left = 0
self.score_right = 0
self.in_progress = False
self.computer_acting = False
# position paddles in the middle of the screen
middle = HEIGHT/2 - PADDLE_HEIGHT/2
self.left_paddle = Paddle(20, middle)
self.right_paddle = Paddle(WIDTH-40, middle)
self.set_ball(self.ball_pos)
@property
def ball_pos(self):
if self.active_player == LEFT_PLAYER:
return (20 + PADDLE_WIDTH + 10, self.left_paddle.centery)
else:
return (WIDTH - 35 - PADDLE_WIDTH, self.right_paddle.centery)
def set_ball(self, pos):
# a ball is set on the paddle of last player that got a point
dt = 5 if self.active_player == LEFT_PLAYER else -5
self.tennis_ball = TennisBall(pos, dt)
def position_ball(self):
# used when the player moves tha paddle and
# game is not in progress
self.tennis_ball.x, self.tennis_ball.y = self.ball_pos
def score_for_left(self):
self.in_progress = False
self.active_player = LEFT_PLAYER
self.score_left += 1
self.set_ball(self.ball_pos)
def score_for_right(self):
self.in_progress = False
self.active_player = RIGHT_PLAYER
self.score_right += 1
self.set_ball(self.ball_pos)
def proceed(self):
self.tennis_ball.move()
# bounce from the walls
if self.tennis_ball.y <= 40:
self.tennis_ball.dy = -self.tennis_ball.dy
if self.tennis_ball.y >= HEIGHT - 40:
self.tennis_ball.dy = -self.tennis_ball.dy
# bounce from the paddles
if self.left_paddle.collidepoint(self.tennis_ball.pos):
self.tennis_ball.dx = -self.tennis_ball.dx
if self.right_paddle.collidepoint(self.tennis_ball.pos):
self.tennis_ball.dx = -self.tennis_ball.dx
# if we didn't bounce, then that is a score
if self.tennis_ball.x <= 0:
self.score_for_right()
if self.tennis_ball.x >= WIDTH:
self.score_for_left()
if self.score_left == 11 or self.score_right == 11:
self.in_progress = False
# computer movement
def computer_launch(self):
self.in_progress = True
self.computer_acting = False
def computer_stop_acting(self):
self.computer_acting = False
def computer_move(self):
# move towards the center of the screen when the ball is
# travelling toward the enemy
if self.tennis_ball.dx > 0:
target_y = HEIGHT / 2
else:
# when the ball is on other side of screen, just move
# in general direction
if self.tennis_ball.x > WIDTH / 2:
delta = int(WIDTH * 0.25)
if self.tennis_ball.dy < 0:
target_y = self.tennis_ball.y - delta
else:
target_y = self.tennis_ball.y + delta
else:
# the ball is on our side, move with it
rnd = random.randint(40, 200)
if self.tennis_ball.dy < 0:
target_y = self.tennis_ball.y - rnd
else:
target_y = self.tennis_ball.y + rnd
target_y = max(40, min(target_y, HEIGHT - 80))
animate(
self.left_paddle,
y=target_y,
duration=.50,
on_finished=self.computer_stop_acting
)
def computer_move_randomly(self):
# move the paddle randomly during one second before launching the ball
target_y = random.randint(40, HEIGHT - PADDLE_HEIGHT - 80)
distance = abs(self.left_paddle.y - target_y)
duration = max(0.1, distance / 200.0)
self.computer_total_duration += duration
if self.computer_total_duration + duration < 1.0:
on_finished = self.computer_move_randomly
else:
on_finished = self.computer_launch
animate(
self.left_paddle,
y=target_y,
duration=duration,
on_finished=on_finished
)
def computer_act(self):
if self.in_progress:
# predict where the ball will move and move towards it
self.computer_move()
elif self.active_player == LEFT_PLAYER:
# move randomly for a bit, then shoot the ball out
if not self.computer_acting:
self.computer_acting = True
self.computer_total_duration = 0.0
self.computer_move_randomly()
def draw(self):
# slightly gray background
screen.fill((64, 64, 64))
# show the score for the left player
screen.draw.text(
'Computer: {}'.format(self.score_left),
color=MAIN_COLOR,
center=(WIDTH/4 - 20, 20),
fontsize=48
)
# show the score for the right player
screen.draw.text(
'Player: {}'.format(self.score_right),
color=MAIN_COLOR,
center=(WIDTH/2 + WIDTH/4 - 20, 20),
fontsize=48
)
# a dividing line
screen.draw.line(
(WIDTH/2, 40),
(WIDTH/2, HEIGHT-40),
color=MAIN_COLOR)
if self.score_left == 11:
screen.draw.text(
'COMPUTER WINS!!!',
color=MAIN_COLOR,
center=(WIDTH/2, HEIGHT/2),
fontsize=96
)
elif self.score_right == 11:
screen.draw.text(
'PLAYER WINS!!!',
color=MAIN_COLOR,
center=(WIDTH/2, HEIGHT/2),
fontsize=96
)
else:
self.left_paddle.draw()
self.right_paddle.draw()
self.tennis_ball.draw()
여기까지해서 퐁 게임에 대한 것은 최종 마치도록 하겠다. 이번 보너스 절의 경우, 여러분 수준에서 내용이해에 조금 어려웠을 수 있겠단 생각이 든다. 그러나 어디까지나 말그대로 보너스이다. 모두가 다 이해하길 바라는 것이 아니고 여러분들 중에 누군가에게는 다소 수준보다 높은 지식이 도움이 될 것이라 추가된 것이고, 여러분들 중에 당장 이해를 다 못했을지라도 걱정하지 말라. 그것은 자연스러운 현상이고, 후추에 좀 더 여러 방면에서 지식이 더 자라났을 때, 다시 드려다보기를 몇 차례 더 하다보면 완전히 다 자기 것으로 소화될 날이 올 것이 틀림없다. 그럼, 여기서 마무리 하고, 다음 과에서 게속 객체지향 공부를 이어나가도록 하겠다.