# 9.5 객체지향으로 개발하기 2

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

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

{% code lineNumbers="true" %}
```python
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
```
{% endcode %}

{% hint style="info" %}
참고로 경계를 확인해야하는 화면의 크기는 미리 정해놓지 말고, 객체를 상속하는 쪽에서 각자 경계를 확인 할 자신이 원하는 화면의 크기를 알려주도록 한다. &#x20;

5번 라인에 self.s\_width, self.s\_height = screen 라는 표현을 처음 보았을 수도 있는데, 이 코드 의미는 screen 라는 변수가 하나이지만, 그 안에 값은 2개 이상일 수 있는데 예를들어, 리스트나 투플일 수 있다는 것이다. 그래서, 이 코드는 그 리스트나 투플 안에 각각의 값을 앞에서부터 하나씩 꺼내어 새로운 변수(self.s\_width, self.s\_height)로 각각 옮겨 담는 것으로 이해하면 될 것이다. 파이썬에서는 이러한 과정을 **unpack** 이란 용어로 사용한다.
{% endhint %}

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

{% code lineNumbers="true" %}
```python
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
```
{% endcode %}

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

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

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

<pre class="language-python" data-title="actors.py" data-line-numbers><code class="lang-python">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
        
<strong>
</strong><strong>class Bullet(Actor, CheckOutOfScreen):
</strong>    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 &#x3C; 0 or \
            self.y > self.s_height or self.y &#x3C; 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 &#x3C; 0 or \
            self.bottom > self.s_height or self.top &#x3C; 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
</code></pre>

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

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

{% code title="battle_city_oop.py" lineNumbers="true" %}
```python
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) 
```
{% endcode %}

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

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