8.7 객체지향으로 개발하기 2

우리는 지난 6절에서 모든 객체에게 필요한 것은 아닌, 탱크와 총알 객체에게 필요한 화면경계 제약을 객체지향 관점에서 어떻게 구현하면 좋을지를 고민하면서 끝이 났던 것을 기억할 것이다. 이를 구현하기 위해서는 인터페이스라는 개념이 필요했는데 지난 장에서 이 부분을 잘 이해하였으니, 이제 이를 실제적으로 적용해보자.

아래와 같이 CheckOutOfScreen 이란 추상클래스를 정의하고, 그안에 실제 화면 경계를 벗어났는지를 확인하는 is_out_of_screen 추상메소드를 추가하여 이 인터페이스를 상속하는 쪽에서 이 메소드를 직접 구현하도록 하였다. 왜냐면 화면경계를 벗어났는지 여부의 기준이 객체 각자마다 다를 수 있기 때문이다.

from abc import ABC, abstractmethod

class CheckOutOfScreen(ABC):
    def __init__(self, screen):
        self.s_width, self.s_height = screen

    @abstractmethod
    def is_out_of_screen(self):
        pass

참고로 경계를 확인해야하는 화면의 크기는 미리 정해놓지 말고, 객체를 상속하는 쪽에서 각자 경계를 확인 할 자신이 원하는 화면의 크기를 알려주도록 한다.

5번 라인에 self.s_width, self.s_height = screen 라는 표현을 처음 보았을 수도 있는데, 이 코드 의미는 screen 라는 변수가 하나이지만, 그 안에 값은 2개 이상일 수 있는데 예를들어, 리스트나 투플일 수 있다는 것이다. 그래서, 이 코드는 그 리스트나 투플 안에 각각의 값을 앞에서부터 하나씩 꺼내어 새로운 변수(self.s_width, self.s_height)로 옮겨 담는 것으로 이해하면 될 것이다.

예를들어, 각 객체에서 구현한 다음의 구현내역을 살펴보면 다음과 같은데, 총알객체의 경우, 총알의 중심좌표(self.x, self.y)를 기준으로 화면 경계를 넘었나/안넘었나를 따진다면, 탱크객체의 경우, 탱크객체 이미지의 좌우상하(self.left, self.right, self.top, self.bottom)을 기준으로 경계를 따지고 있어서(7~12라인), 객체마다 그 경계를 삼는 기준이 다를 수 있다.(22~27라인)

class Bullet(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self.angle = angle

    def is_out_of_screen(self):
        if self.x > self.s_width or self.x < 0 or \
            self.y > self.s_height or self.y < 0:
            return True
        else:
            return False 
            

class Tank(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self._original_pos = 0
        self.angle = angle

    def is_out_of_screen(self):
        if self.right > self.s_width or self.left < 0 or \
            self.bottom > self.s_height or self.top < 0:
                return True
        else:
            return False
            
    def move(self):
        self._original_pos = self.pos
        if self.angle == 180:
            self.x -= 2
        elif self.angle == 0:
            self.x += 2
        elif self.angle == 90:
            self.y -= 2
        elif self.angle == 270:
            self.y += 2
        
        # 화면경계 확인
        if self.is_out_of_screen():
            self.pos = self._original_pos

참고로 17번 라인의 부모객체의 초기화는 코드가 전에는 super().__init__ 메소드 였으나, 이제는 특정 객체의 이름과 함께 호출해야 하는데, 이유는 2개 이상의 부모객체로부터 상속을 받았기 때문에, 어느 부모객체를 초기화 하려는지를 이름을 통해 명시를 해줘야 하기 때문이다.

🔢 탱크객체의 move 메소드에도 변화가 있는 것을 감지할 수 있을 것이다. 30번 라인에서 현재 위치를 기억해두고, 40~42라인을 통해 이동한 지점에 화면경계를 넘어서는지를 객체의 자신의 is_out_of_screen 메소드로 확인하고, 만약, 넘어서게 되면 그쪽으로 이동할 순 없고 다시 제자리로 원위치하는 부분을 추가되어 화면경계 넘어서의 이동을 제약하는 목적을 달성하고 있다.

이미 지난 시간 탱크의 이동제약에 관해 고려에서 언급된 부분인데 벽을 뚫고 이동하지 하지 못하는 벽 관련 제약으로 이 부분은 두 객체(탱크와 벽)의 충돌확인 후 이동의 제약이고, 해당 내용은 move 메소드에 추가되어야 할 것이다. 이 내용까지 포함해 지금까지 구현내용을 한번꺼번에 보여주면 다음과 같다.

actors.py
from pgzhelper import *
from abc import ABC, abstractmethod


class CheckOutOfScreen(ABC):
    def __init__(self, screen):
        self.s_width, self.s_height = screen

    @abstractmethod
    def is_out_of_screen(self):
        pass
        

class Bullet(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self.angle = angle

    def is_out_of_screen(self):
        if self.x > self.s_width or self.x < 0 or \
            self.y > self.s_height or self.y < 0:
            return True
        else:
            return False 
            

class Tank(Actor, CheckOutOfScreen):
    def __init__(self, img_name, pos, angle, walls, screen):
        Actor.__init__(self, img_name, pos)
        CheckOutOfScreen.__init__(self, screen)
        self._original_pos = 0
        self.angle = angle
        self._walls = walls

    def is_out_of_screen(self):
        if self.right > self.s_width or self.left < 0 or \
            self.bottom > self.s_height or self.top < 0:
                return True
        else:
            return False
            
    def move(self):
        self._original_pos = self.pos
        if self.angle == 180:
            self.x -= 2
        elif self.angle == 0:
            self.x += 2
        elif self.angle == 90:
            self.y -= 2
        elif self.angle == 270:
            self.y += 2
        
        # 화면경계 확인
        if self.is_out_of_screen():
            self.pos = self._original_pos

        # 벽더미 확인
        if self.collidelist(self._walls) != -1:
            self.pos = self._original_pos
            
            
class MyTank(Tank):
    def __init__(self, img_name, pos, angle, walls, screen):
        super().__init__(img_name, pos, angle, walls, screen)

class EnemyTank(Tank):
    def __init__(self, img_name, pos, angle, walls, screen):
        super().__init__(img_name, pos, angle, walls, screen)

🔢 59~60 라인에서 탱크가 벽더미와 충돌이 있는지의 여부는 collidelist 메소드의 활용을 통해 충돌여부 확인이 가능하고, 다만, 충돌의 대상이 되는 벽더미들 객체들의 인스턴스 정보(구체적으로는 생성된 객체의 메모리 상에 위치정보)는 29, 34 라인에서처럼 탱크 객체를 생성할 때 파라미터로 넘겨받아서 활용할 수 있다.

탱크객체들의 이동관련 기능구현이 완료되었으므로, 이를 실제로 사용하는 쪽(battle_city_oop.py 내의 update 함수)에서의 코드까지 포함해서 살펴보면 우리의 구현코드가 어떻게 동작하는지 더 입체적으로 보일 것이다.

battle_city_oop.py
import random
from actors import MyTank, EnemyTank

WIDTH = 800
HEIGHT = 600

enemy_move_cnt = 0
ENEMY_MOVE_DELAY = 20

# 50x50 크기의 벽의 더미 생성
walls = []
WALL_SIZE = 50
for x in range(int(WIDTH / WALL_SIZE)):
    # 탱크가 위치 할 첫 행과 마지막 행 총 2행을 비워두기 위해 -2 하여 생성
    for y in range(int(HEIGHT / WALL_SIZE - 2)):
        if random.randint(0, 100) < 50:  # 적정 수의 벽을 생성
            wall = Actor("wall", anchor=("left", "top"))
            wall.x = x * WALL_SIZE
            wall.y = y * WALL_SIZE + WALL_SIZE  # 맨 첫 행 비우기 위해 전체적으로 아래로 밀기
            walls.append(wall)

# 주인공 탱크 생성
tank = MyTank("tank_blue", (400, 575), 90, walls, (WIDTH, HEIGHT))

# 적 탱크 생성
enemies = []
MAX_ENEMIES = 3
for i in range(MAX_ENEMIES):
    enemies.append(EnemyTank("tank_red", (400, 25), 270, walls, (WIDTH, HEIGHT)))


def draw():
    screen.blit("grass", (0, 0))  # 배경이미지 그리기
    tank.draw()
    for enemy in enemies:
        enemy.draw()
    for wall in walls:
        wall.draw()

def update():
    global enemy_move_cnt

    # 주인공 탱크
    if keyboard.left:
        tank.angle = 180
        tank.move()
    elif keyboard.right:
        tank.angle = 0
        tank.move()
    elif keyboard.up:
        tank.angle = 90
        tank.move()
    elif keyboard.down:
        tank.angle = 270
        tank.move()

    # 적 탱크
    for enemy in enemies:
        choice = random.randint(0, 3)
        if enemy_move_cnt > 0:
            enemy_move_cnt -= 1
            enemy.move()
        elif choice == 0:  # 움직임 지연 초기화
            enemy_move_cnt = ENEMY_MOVE_DELAY
        elif choice == 1:  # 랜덤방향 결정
            enemy.angle = random.randint(0, 3) 

코드에서 유심히 볼 것은 새롭게 등장한 update 함수인데 주인공 탱크를 게임유저의 키보드 조작으로 조정하고, 적 탱크를 자동조정하기 위한 코드 부분은 절차지향 때의 알고리즘하고 동일하여 이해에 큰 어려움은 없을 것이다. 기존 절차지향과의 차이점은 기존의 글로벌 함수호출로 기능동작이 이뤄졌던 부분이 이제는 다 객체 호출로 대치되고 있어 우리가 목표하는 바를 이뤄가고 있다는 것이다.

그럼, 이제 다음으로 탱크들의 총알발사에 대한 것으로 다음 절에서 이를 디자인해보고 구현해보도록 하자.

Last updated