8.5 객체지향으로 개발하기 1

퐁 게임의 객체지향형 버전을 만든다는 것은 구조적으로 어떠한 형태를 띄어야 하는 것일까? 파이게임제로 라이브러리를 베이스로 사용하고 있기 때문에 필수 콜백함수들(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()
pong_actors.py
class Score():
    def __init__():
        pass
    def _gameover_draw():
        pass
    def is_game_over():
        pass
    def reset():
        pass
    def draw():
        pass
    
class Ball(Rect):
    def __init__():
        super().__init__()
        pass
    def collide_wall():
        pass
    def reset():
        pass
    def draw():
        pass
    
class Bar(Rect):
    def __init__():
        super().__init__() 
        pass
    def up():
        pass
    def down():
        pass
    def collide_ball():
        pass

비공개(private) 메소드 또는 변수란?

참고로 객체의 클래스를 정의하면서, 아래의 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()

한 절에서 너무 많은 양을 기술하여 내용소화에 부담을 느끼지 않도록 남은 두 객체에 대해서는 다음 절에서 계속 이어 설명하겠다.

Last updated

Was this helpful?