이제 나머지 공(Ball) 객체와 반사판(Bar) 객체를 살펴보자. 먼저, Ball 객체이다. Rect 객체를 상속했는데, Rect의 실체는 파이게임제로 라이브러리의 모체인 파이게임 안에 존재하는 객체이다. 주 용도는 우리가 Actor객체에서 다음과 같은 내장된 위치정보를 활용할 수 있음을 배웠는데, 사실은 이 위치정보의 본체는 Rect객체에 있는 것이고, Actor객체가 이 Rect객체를 상속받아 사용하고 있기 때문에 Actor객체도 저 위치속성값들이 사실은 원래 부모의 속성값들임에도 불구하고 마치 본래부터 자기에게 있었던 것마냥 사용하고 있는 것이다.
7번라인에서 Rect 부모객체의 생성시 넘겨야 할 4개의 초기값(x좌표, y좌표, 넓이, 높이)을 넘기면서, 부모객체를 초기화하고 있다. 여기서, (x, y)좌표를 (0, 0)으로 넘겼는데, 사실은 어떤 값을 넘기더라도 크게 의미가 없는 것은 그다음 9번 라인에서 곧바로 이 객체의 중심좌표값을 start_pos 파라미터값으로 대치하기 때문이다.
공 객체를 생성할 때 넘겨져야 할 파라미터값들 중 하나로 점수(score)객체가 넘겨져야 하는데, 이는 공 객체 안에서 점수객체와 의존성이 생기는 것으로 공 객체가 점수객체에게 어떤 행동을 요청하거나 또는 그가 외부에서 값을 바꿀 수 있도록 허락한 속성값들을 직접 바꾸는 등의 조작을 할 수 있음을 의미한다. 여기서는 전자의 경우로 collide_wall 함수에서 공이 좌우(화면)벽과의 충돌하는 상황은 곧 매 경기의 승패가 나는 상황이고, 이때 승리쪽에 점수값을 add 공개 메소드를 호출하여 증가시키게 된다.
공개 함수 또는 공개 메소드(public method)란?
어떤 객체의 멤버함수 중 다른 객체와의 상호작용을 목적으로 다른 객체가 호출하여 특정 행동을 유도할 수 있도록 외부에 공개된 멤버함수를 지칭하는 말. 그 반대말은 비공개(private) 메소드라고 한다.
class Ball(Rect):
'''
공을 동작(위치, 움직임)과 충돌검사
'''
def __init__(self, start_pos, velocity, score):
super().__init__(0, 0, BALL_RADIUS * math.sqrt(2), BALL_RADIUS * math.sqrt(2))
self.center = start_pos
self._velocity = velocity
self.vx = self.vy = self._velocity
self._score = score
def reset(self):
if self._score.is_game_over():
self.vx = self.vy = 0
else:
self.vx = self.vy = self._velocity
self.vx *= random.choice([-1, 1])
# 중심선에서 좌우로 랜덤하게 던지기
self.center = (WIDTH/2 , random.randint(BALL_RADIUS*2, \
HEIGHT - BALL_RADIUS*2))
def move(self):
# vx와 vy만큼 속도로 공을 이동시키기
self.move_ip(self.vx, self.vy)
def collide_wall(self):
# 위쪽 또는 아래쪽 벽
if self.top < 0 or self.bottom > HEIGHT:
self.vy = -self.vy # 속도의 y축 방향을 반대로하기
sounds.wall.play()
# 왼쪽 벽
if self.left < 0:
self._score.add('b2') += 1
sounds.die.play()
self.reset()
# 오른쪽 벽
if self.right > WIDTH:
self._score.add('b1') += 1
sounds.die.play()
self.reset()
def draw(self):
screen.draw.filled_circle(self.center, BALL_RADIUS, 'white')
다음으로 반사판 객체를 살펴보자. 반사판 객체도 Rect객체를 부모로부터 상속받았다. 멤버함수들의 구성을 천천히 살펴보면 객체를 상하로 움직이는 행동을 요청할 수 있는 함수들을 별도로 두고 있으며, 이 행동시 제약으로는 함수 내부적으로 반사판의 화면 밖을 넘어가지 않도록 하는 것을 보장하며 움직이고 있다.
역시나 이 객체도 공(Ball) 객체라는 외부 객체를 ball 이라는 파마리터값으로 전달받으면서, 공 객체와 의존성을 갖게 되는데 이의 목적도 이전의 공 객체에서 점수객체에 의존하는 것과 유사한데 반사판이 공과의 충돌검사하면서 공이 반사되는 움직임을 만들기 위해 공의 진행방향을 반대쪽 방향으로 전환시키는 처리를 수행하기 위한 목적이다.
class Bar(Rect):
def __init__(self, x, y, ball):
super().__init__(x, y, BAR_W, BAR_H)
self.ball = ball
def up(self):
if self.y > 0:
self.y -= 7
def down(self):
if self.y + self.height < HEIGHT:
self.y += 7
def collide_ball(self):
if self.colliderect(self.ball):
# 10픽셀 앞으로 먼저 튀어오르기
if self.ball.vx < 0: # bar1 이면
self.ball.x += 10
else: # bar2 이면
self.ball.x -= 10
self.ball.vx = -self.ball.vx * SPEED_UP # 속도의 x축 방향을 반대로하기
''' 공이 윗측 진입하면서 반사판 윗측에 부딪힐 때 또는
공이 아래측 진입하면서 반사판 아래측에 부딪힐 때는 진입방향 그대로 반사 '''
if (self.ball.vy > 0 and self.ball.centery < self.centery) or \
(self.ball.vy < 0 and self.ball.centery > self.centery):
self.ball.vy = -self.ball.vy * SPEED_UP
sounds.bar.play()
def draw(self):
screen.draw.filled_rect(self, 'white')
이로써 우리가 만들고자 했던 퐁 게임의 객체지향 버전의 최종판을 완성했다. 어떤가 남이 만들어준 객체만을 활용하는게 아니라 나만의 사용자 객체를 만들고 이들 간의 협력을 통한 객체지향 패러다임으로 개발하는 것이 신선한 경험이었을 것이라 믿는다. 다만, 아직 객체지향 복잡하기만 하고 큰 장점을 모르겠다라고 느낄 수 있을텐데 사실은 이 게임이 워낙 단순한 구조를 갖고 있어서 객체지향의 큰 장점이 드러날 순 없었다. 그럼에도 불구하고, 엔트리 블록코딩에서 객체개념으로 생각하고 코딩했을 때의 편한함(?)의 기억이 조금 복기되는 느낌을 갖게 되었다면, 그것만으로도 객체지향 패러다임에 성공적으로 입문했다고 할 수 있다. 아직 첫 술에 배부르긴 이루다. 다음 게임부터는 계속해서 객체지향으로 게임제작을 시도함으로써 점점더 객체지향 개념에 익숙해져 갈 것이다.
이제 마지막으로 지금까지 코딩내용의 전체 소스코들 첨부하는 것으로 이 과를 마무리 하겠다. 이미 게임초기에 언급했던 내용이지만, 모든 소스코드가 파일 1개로 만들어져 있음을 기억하자.
pong_oop.py
import random
import math
# 게임화면
TITLE = 'pong'
WIDTH = 800
HEIGHT = 600
# 반사판
BAR_H = 100
BAR_W = 15
GAP_FROM_SCR = 20
# 볼
BALL_RADIUS = 10
VELOCITY = 5
SPEED_UP = 1.08
# 점수
FINAL_SCORE = 11
class Score():
'''
화면에 점수와 승패의 UI 표현
'''
def __init__(self, final_score):
self._final_score = final_score
self._b1_score = -1
self._b2_score = -1
def _gameover_draw(self):
if self._b1_score == self._final_score:
winner = 'Player1'
elif self._b2_score == self._final_score:
winner = 'Player2'
if winner:
screen.draw.text(winner + ' Win!!', (WIDTH/3, HEIGHT/2 - 50), color='blue', fontsize=70)
screen.draw.text('Press Space to play again', (WIDTH/4, HEIGHT/2 + 50), color='skyblue', fontsize=50)
def is_game_over(self):
if (self._b1_score == self._final_score or self._b2_score == self._final_score) \
or (self._b1_score == -1 and self._b2_score == -1):
return True
else:
return False
def add(self, who):
if who == 'b1':
self._b1_score += 1
elif who == 'b2':
self._b2_score += 1
def reset(self):
self._b1_score = 0
self._b2_score = 0
def draw(self):
screen.draw.text(str(self._b1_score), (WIDTH/4, GAP_FROM_SCR), color='yellow', fontsize=60)
screen.draw.text(str(self._b2_score), ((WIDTH/4)*3, GAP_FROM_SCR), color='yellow', fontsize=60)
if self.is_game_over():
self._gameover_draw()
class Ball(Rect):
'''
공을 동작(위치, 움직임)과 충돌검사
'''
def __init__(self, start_pos, velocity, score):
super().__init__(0, 0, BALL_RADIUS * math.sqrt(2), BALL_RADIUS * math.sqrt(2))
self.center = start_pos
self._velocity = velocity
self.vx = self.vy = self._velocity
self._score = score
def reset(self):
if self._score.is_game_over():
self.vx = self.vy = 0
else:
self.vx = self.vy = self._velocity
self.vx *= random.choice([-1, 1])
# 중심선에서 좌우로 랜덤하게 던지기
self.center = (WIDTH/2 , random.randint(BALL_RADIUS*2, \
HEIGHT - BALL_RADIUS*2))
def move(self):
# vx와 vy만큼 속도로 공을 이동시키기
self.move_ip(self.vx, self.vy)
def collide_wall(self):
# 위쪽 또는 아래쪽 벽
if self.top < 0 or self.bottom > HEIGHT:
self.vy = -self.vy # 속도의 y축 방향을 반대로하기
sounds.wall.play()
# 왼쪽 벽
if self.left < 0:
self._score.add('b2')
sounds.die.play()
self.reset()
# 오른쪽 벽
if self.right > WIDTH:
self._score.add('b1')
sounds.die.play()
self.reset()
def draw(self):
screen.draw.filled_circle(self.center, BALL_RADIUS, 'white')
class Bar(Rect):
def __init__(self, x, y, ball):
super().__init__(x, y, BAR_W, BAR_H)
self.ball = ball
def up(self):
if self.y > 0:
self.y -= 7
def down(self):
if self.y + self.height < HEIGHT:
self.y += 7
def collide_ball(self):
if self.colliderect(self.ball):
# 10픽셀 앞으로 먼저 튀어오르기
if self.ball.vx < 0: # bar1 이면
self.ball.x += 10
else: # bar2 이면
self.ball.x -= 10
self.ball.vx = -self.ball.vx * SPEED_UP # 속도의 x축 방향을 반대로하기
''' 공이 윗측 진입하면서 반사판 윗측에 부딪힐 때 또는
공이 아래측 진입하면서 반사판 아래측에 부딪힐 때는 진입방향 그대로 반사 '''
if (self.ball.vy > 0 and self.ball.centery < self.centery) or \
(self.ball.vy < 0 and self.ball.centery > self.centery):
self.ball.vy = -self.ball.vy * SPEED_UP
sounds.bar.play()
def draw(self):
screen.draw.filled_rect(self, 'white')
# 주인공 객체들 생성
score = Score(FINAL_SCORE)
ball = Ball((WIDTH/2, HEIGHT/2), VELOCITY, score)
bar1 = Bar(GAP_FROM_SCR, HEIGHT/2 - BAR_H/2, ball)
bar2 = Bar(WIDTH - BAR_W - GAP_FROM_SCR, HEIGHT/2 - BAR_H/2, ball)
bars = [bar1, bar2]
def draw():
screen.clear()
screen.draw.line((WIDTH/2, GAP_FROM_SCR), (WIDTH/2, HEIGHT - GAP_FROM_SCR), \
color='grey')
if not score.is_game_over():
ball.draw()
bar1.draw()
bar2.draw()
score.draw()
def update():
# 반사판
if keyboard.a:
bar1.up()
if keyboard.z:
bar1.down()
if keyboard.up:
bar2.up()
if keyboard.down:
bar2.down()
for bar in bars:
bar.collide_ball()
# 게임시작 조건
if score.is_game_over():
if keyboard.space:
score.reset()
ball.reset()
else:
# 공
ball.move()
ball.collide_wall()
26번 라인의 move 함수 안에 사용한 move_ip 함수 호출방법에 대해 유심히 살펴보면 self 를 통해 호출하고 있는데, 이 역시도 상속에 의한 결과로 move_ip 함수가 Ball 객체 안에 정의된 멤버함수가 아님에도 불구하고, 마치 자신에게 존재하는 것처럼 호출하고 있다. 즉, move_ip 함수는 결국 부모인 Rect객체 안에 존재하고 있으며, 자녀인 공 객체가 자신의 일부처럼 사용할 수 있는 것이다.