퐁 게임의 객체지향형 버전을 만든다는 것은 구조적으로 어떠한 형태를 띄어야 하는 것일까? 파이게임제로 라이브러리를 베이스로 사용하고 있기 때문에 필수 콜백함수들(draw, update 함수 등)을 통한 게임루프의 구조는 그대로 가져갈 수 밖에 없다는 걸 전제해야 한다. 그러나, 기존의 절차지향 버전에서 존재했던 (글로벌)함수들 기반의 절자지향형 코드들은 이제 객체형태로 변형되어 그 객체간의 협력만으로 게임이 잘 동작하게 만드는 것이 목표라 하겠다.
저 그럼 퐁 게임에 필요한 객체식별부터 시작해 보자. 우리 게임무대에 등장인물들은 기존의 절차지향에서 사용했던 Rect기반 객체 중심으로 찾아보면 쉬운데, "반사판 1", "반사판 2", "공", "점수", 이렇게 4개면 충분하다. 사실 객체지향 패러다임으로 시도해보는 첫 게임으로서 구지 복잡한 게임을 예제삼을 필요없이 이정도 간단한 난이도 부터 시작하는게 적당하다.
그럼, 먼저 반사판들의 역할만 놓고 생각해보자. 두 반사판의 목표는 상하 이동하면서 볼을 놓히지 않게 잘 반사해 내는 것이다. 두 반사판 모두 똑같은 목표와 똑같은 제약을 갖는다. 단지, 각각의 반사판을 조작하기 위한 키보드의 키가 다를 뿐이다. 그렇다. 이 지점이 객체지향의 코드재사용성의 장점이 드러날 수 있는 부분이다. Bar객체를 클래스 형태로 1개로 정의하고, 이 클래스 Bar로부터 두 개의 객체(반사판1, 반사판2)를 만들어 사용하면 된다.
게임이 맨 처음 시작했을 때 화면에 등장하는 객체들은 무엇인가? 위에 언급된 4개의 객체("반사판1", "반사판2", "공", "점수") 전부 다 일 것이다. 그럼, 이들 객체의 생성은 누가할 것인가? 엔트리 블록코딩에서는 엔트리(시스템) 자체가 내부적으로 자동으로 해주었다면, 이젠 그것도 우리가 직접 코딩으로 해야하는 것이다. 객체들이 화면에 그려지기 위해서는 draw 콜백함수 안에서 각 객체의 draw 메소드를 호출해야 할 것이고, 그 말은 draw 콜백함수 이전에 최소한 생성이 되어 있어야 하는 것을 전제한다. 그럼, 방금 언급된 내용의 기본적인 코딩을 해보려고 하는데, 처음부터 코드가 너무 복잡해 보여 이해를 어렵게 만들지 않기 위해 당장은 객체에 대한 정의를 제외하고(있다고 가정하고), 그를 활용하는 코드만을 확인해 보겠다.
pong_oop.py
import random
import math
from pong_actors import Score, Ball, Bar
# 게임화면
TITLE = 'pong'
WIDTH = 800
HEIGHT = 600
# 반사판
BAR_H = 100
BAR_W = 15
GAP_FROM_SCR = 20
# 볼
BALL_RADIUS = 10
VELOCITY = 3
SPEED_UP = 1.05
# 점수
FINAL_SCORE = 11
# 주인공 객체들 생성
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()
객체간의 상호작용을 목적으로 외부에 공개하는 멤버변수 및 함수와는 달리 이렇게 객체 내부적으로만 사용하려는 목적의 멤버변수 및 멤버함수를 지칭한다.
참고로 객체의 클래스를 정의하면서, 아래의 2~4라인처럼 객체의 이름 하단에 그 객체에 목적과 사용에 대해 간단히 주석으로 적어두는 것은 파이썬 코딩에 있어서 하나의 관습이기도 하면서 동시에 가독성을 높히는 좋은 습관이 될 수 있다.
class Score():
'''
화면에 점수와 승패의 UI 표현
'''
def __init__(self, final_score):
self._final_score = final_score
self._b1_score = -1
self._b2_score = -1
그 다음으로 최종승패의 UI를 그리는 멤버함수 _gameover_draw 를 추가했는데 멤버함수의 이름에 _(언더바)가 있는 이유는 위에서 언급된 목적과 다르지 않을 것으라 유추할 수 있고, 예상한대로 해당 함수는 객체 외부에서 접근할 수 없고 내부적으로만 사용하는 목적이라는 것을 나타내기 위한 관습적인 코딩의 약속이다. 참고로 이를 객체지향 패러다임에서는 위에서 언급되었던 것처럼 비공개 멤버함수라는 용어로 지칭한다.
추가로 현재시점의 최종 게임승패를 분별하는 is_game_over 멤버함수, 최종 승패 이전의 매 경기마다의 승자에게 점수를 주는 add 멤버함수, 게임을 완전 새로 시작하기 위해 점수를 다시 원점으로 돌리는 reset 함수가 추가되었다. 그리고, 마지막으로 draw 멤버함수의 목적은 위에서 언급했듯이 실제 화면에 점수 UI 를 그리는 용도이다.
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()
한 절에서 너무 많은 양을 기술하여 내용소화에 부담을 느끼지 않도록 남은 두 객체에 대해서는 다음 절에서 계속 이어 설명하겠다.
23-26라인까지 점수, 공, 2개의 반사판을 생성하고 있는데, 기존 절차지향버전에서 배우객체를 생성할 때 Rect 객체를 통해 생성했었는데, 이제는 우리가 직접 만든 사용자 객체인 Score, Ball, Bar라는 클래스를 사용해 생성하고 있다는 것이다. 일반적으로 한 파일 안에 너무 많은 코드를 적는 것은 지양되고, 의미있게 여러 파일로 나눠 모듈화(modulization) 하는 것이 더 큰 프로젝트를 관리하는데 효과적이다. 따라서, 사용자 객체들을 정의하는 클래스 정의문은 아래처럼 예들들어, pong_actors.py 라는 파일 안에 별도로 분리시키고, 이를 pong_oop.py 안에서 import해 사용하는 방식을 취하는게 좋으나, 여러분의 수준에서 당장은 여러 파일을 오가면서 코드를 확인하고 디버깅하는게 어려울 수 있다 판단하여 객체지향 코딩을 배우는 것에 가장 초점을 맞추기 위해 한 파일(pong_oop.py) 안에 모든 코드를 포함하는 식으로 코딩할 예정이다.
이해를 돕기 위해 상세구현 없는 식별된 객체들의 아주 기본적인 뼈대로부터 시작하는게 좋겠다. 먼저 점수(Score)객체이다. 화면에 각 게임유저의 현재점수와 승패의 UI를 나타내기 위한 목적으로 공이나 반사판처럼 지속적인 움직임을 갖는 속성이 필요한 것 아니고, 고정위치에서 표현되기 때문에 Rect 라는 부모객체를 상속하지 않은 것을 알 수 있다. 그렇게 때문에 상속된 객체는 항상 호출이 필요한 super().__init()__ 부모객체를 초기화하기 위한 초기화 함수도 호출하지 않는다. 생성시 파라미터값으로 받아드리는 것은 최종 승패를 가리는 점수인 final_score 값만 필요하다. 6번 라인에서 그 값을 객체의 내부의 속성값(멤버변수라 지칭)인 self._final_score 로 재할당하는 것을 알 수 있다. 해당 멤버변수값 앞에 _(언더바 또는 언더스코어)를 붙힌 이유는 앞 절에서 설명되었던 바와 같이 객체 내부적으로만 사용하기 위한 목적이다. 참고로 이를 객체지향 패러다임에서는 비공개 멤버변수라는 용어로 지칭한다.