8.8 (보너스) 파이게임제로 예제버전 2

패들(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% 동일하다.

class TennisBall():
    def __init__(self, start_pos, dt):
        self.x, self.y = start_pos
        self.dx = self.dy = dt
        self.pos = (self.x, self.y)

그럼, 저렇게 코딩하면 될 것을 왜 구지 데코레이터를 붙혀서 따로 분리해서 코딩했는가 라고 했을 때, 게임을 만든 저자가 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()

여기까지해서 퐁 게임에 대한 것은 최종 마치도록 하겠다. 이번 보너스 절의 경우, 여러분 수준에서 내용이해에 조금 어려웠을 수 있겠단 생각이 든다. 그러나 어디까지나 말그대로 보너스이다. 모두가 다 이해하길 바라는 것이 아니고 여러분들 중에 누군가에게는 다소 수준보다 높은 지식이 도움이 될 것이라 추가된 것이고, 여러분들 중에 당장 이해를 다 못했을지라도 걱정하지 말라. 그것은 자연스러운 현상이고, 후추에 좀 더 여러 방면에서 지식이 더 자라났을 때, 다시 드려다보기를 몇 차례 더 하다보면 완전히 다 자기 것으로 소화될 날이 올 것이 틀림없다. 그럼, 여기서 마무리 하고, 다음 과에서 게속 객체지향 공부를 이어나가도록 하겠다.

Last updated

Was this helpful?