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

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

아래와 같이 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

예를들어, 각 객체에서 구현한 다음의 구현내역을 살펴보면 다음과 같은데, 총알(대포알)객체의 경우, 총알의 중심좌표(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

이미 지난 시간 탱크의 이동제약에 관해 고려에서 언급된 부분인데 벽을 뚫고 이동하지 하지 못하는 벽 관련 제약으로 이 부분은 두 객체(탱크와 벽)의 충돌확인 후 이동의 제약이고, 해당 내용은 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

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

battle_city_oop.py
import random
from actors import Tank

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 = Tank("tank_blue", (400, 575), 90, walls, (WIDTH, HEIGHT))

# 적 탱크 생성
enemies = []
MAX_ENEMIES = 3
for i in range(MAX_ENEMIES):
    enemies.append(Tank("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

Was this helpful?