# 9.3 객체지향으로 개발하기 1

배틀시티 게임의 객체지향형 버전에선 이전에 비해 객체지향 디자인(OOD: Objected Oriented Design)적인 면을 더 고려해 보는 훈련을 해보면 좋겠다. 객체지향적으로 만든다는 것은 구조적으로 어떠한 형태를 띄어야 하는 것일까? 파이게임제로 라이브러리를 베이스로 사용하고 있기 때문에 필수 콜백함수들(draw, update 함수 등)을 통한  [게임루프의 구조](../game_loop)는 그대로 가져갈 수 밖에 없다는 걸 전제해야 한다는 것을 이해하고 있을 것이다. 다만, 그 게임루프에서 개임의 주인공 객체간의 협력으로 게임이 잘 동작하게 만드는 것이 우리의 최종 목표라 하겠다.&#x20;

저 그럼 배틀시티 게임에 필요한 객체식별부터 시작해 보자. 우리 게임무대에 등장인물들은 배우(Actor)객체 중심으로 찾아보면 쉬운데, "주인공 탱크", "적 탱크", "벽 더미", "대포알", "폭발장면"이 있다. 크게 구분해 보면, "주인공", "적", "장애물", "무기" 라는 역할을 한다고 말할 수 있다. 우리의 고민은 여기서 다시 시작된다. _이 게임을 예제수준에서 먼저 절차지향형으로 개발해 본 딱 그정도만 기능구현하고 더이상 추가구현 없는 전혀 건드리지 않을(?) 게임으로 생각하는지, 아니면 개인적인 의욕이 있어 이 게임을 더 개선해서 더 발전된 형태의 버전업이 되는 게임, 예를들어 적으로서 탱크만이 아닌 다양한 형태의 다른 적이 등장한다던지, 내가 사용하는 무기도 대포알 뿐만 아니라 다른 형태의 공격무기도 갖게 할 것인지, 지금은 장매물이 벽 더미이지만, 그 외의 다른 형태의 장애물도 생각하는지 등등에 따라 당장의 상속의 깊이에서부터 객체지향 설계의 스케일이 달라지기 때문이다._

스케일을 너무 크게 잡고 설계 하는 부분에 대해서는 애시당초 이 책이 고려한 학습 난이도에 비해 높고, 분량이 많아져서 기회가 되면 이 책의 다음 레벨의 책에서 더 진도를 나가기로 하고, 이 책에서는 적절한 수준으로 타협하여 설계하도록 하겠다. 그럼, 먼저 주인공과 적의 역할만 놓고 생각해보자. 주인공과 적의 목표는 이동하면서 서로 상대방을 포쏘기 공격해 맞추는 것이다. 이 때 둘의 사용하는 무기(대포알)가 동일하고 이동하는 방법 역시 전후좌우의 직진이동만 할 수 있어 둘 다 똑같은 제약을 갖는다. 그렇다. 탱크라는 한 공통 객체를 만들어 그 객체로부터 재활용해 주인공 탱크/적 탱크 각각을 생성할 수 있는 것이다.

그럼, 이를 또 한편으로 탱크객체가 해야할 **책임(외부의 객체의 사용자에게 제공하게 될 정보 또는 요청에 의한 행동)**&#xC5D0; 대해서 고민해보자. 그런데 **책임들을 정하기에 앞서 먼저는 객체들 간에 협력에 대해 생각하는게 우선인데, 협력에 대해 고민하면 자연스럽게 객체의 책임을 이끌어 내기 때문이다.** 즉, _어떤 객체가 탱크객체에게 어떤 행동의 요구를 할 것인가에 대한 것을 고민해보자. 가장 본질적인 요구는 각 객체을 생성(creation)하는 것에서부터 출발할 수 있다._ _각 객체를 첨부터 무조건 다 만들어 놓고 시작하는게  아니라, 특정 객체가 특정 객체의 생성을 특정 시점에 요구할 수 있는 것이다._ 예를들어 스페이스 바를 눌러 대포를 쏘는 시점에 "총알" 라는 객체가 생성되고 날아가기 시작하면 되는 것처럼 말이다.

게임이 맨 처음 시작했을 때 화면에 맨 처음 등장하는 객체들은 무엇인가? "주인공 탱크", "적 탱크", "벽 더미" 이다.  그리고, 객체는 아닌 단순 이미지인 배경 이미지가 존재해야 할 것이다. 그럼, 이들 객체의 생성은 누가할 것인가? 엔트리 블록코딩에서는 엔트리(시스템) 자체가 내부적으로 자동으로 해주었다면, 이젠 그것도 우리가 직접 코딩으로 해야하는 것이다. 객체들이 화면에 그려지기 위해서는 draw 콜백함수 안에서 각 객체의 draw 메소드를 호출해야 할 것이고, 그 말은 draw 콜백함수 이전에 최소한 생성이 되어 있어야 하는 것을 전제한다. 다음의 battle\_city\_oop.py 라고 명명한 파일에서 방금 언급된 내용의 기본적인 코딩을 해보도록 하겠다.

{% code title="battle_city_oop.py" lineNumbers="true" %}
```python
import random
from actors import Tank

WIDTH = 800
HEIGHT = 600

# 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))

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


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

:1234: 벽 더미 생성코드는 기존의 절차지향 버전에서의 것과 동일하여 설명이 필요없다. 19-26라인까지 주인공 탱크와 3대의 적 탱크를 생성하고 있는데, 특이한 것은 기존에는 배우객체를 생성할 때 Actor 클래스를 통해 생성했었는데, 이제는 우리가 직접 만든 사용자 객체인 MyTank, EnemyTank라는 클래스를 사용해 생성하고 있다는 것이다. 그 사용자 객체들은 battle\_city\_oop.py 파일 안에 함께 코딩할 수도 있지만, **한 파일 안에 너무 많은 코드를 적는 것은 지양되고, 의미있게 여러 파일로 나눠 모듈화(modulization) 하는 것이 더 큰 프로젝트를 관리하는데 효과적이며 일반적이다.** 따라서, _우리의 사용자 객체들은 아래처럼 actors.py 라는 파일 안에 존재하며, 이를 battle\_city\_oop.py 안에서 불러 사용하기 위해 2번 라인에서 from-import 문을 사용해 actors 모듈(이를 통해 우리는 파일명이 곧 모듈명이 된다는 것을 알 수 있음) 안에 클래스들을 import 하고 있다._

{% code title="actors.py" lineNumbers="true" %}
```python
from pgzhelper import *

class Bullet(Actor):
    def __init__():
        super().__init__()
        pass
    
class Explosion(Actor):
    def __init__():
        super().__init__()
        pass

class Tank(Actor):
    def __init__():
        super().__init__() 
        pass
    def move():
        pass
    def fire():
        pass
```
{% endcode %}

:1234: 이해를 돕기 위해 상세구현 없는 식별된 객체들의 아주 기본적인 뼈대로부터 시작하는게 좋겠다. 먼저 탱크 객체부터 차근히 고민해보자. 탱크 객체는 Actor객체를 상속했기 때문에 기존의 Actor처럼 간주될 수 있기 때문에 기존의 일반적인 배우 객체의 생성 때처럼 객체의 외관 이미지와 최초 화면상의 위치정보의 두 파라미터 값으로 생성할 수 있으나, 잘 고민해보면 게임 시작시 적 객체와 주인공 객체가 서로 마주보는 상태로 게임이 시작해야 하므로, 객체생성시 초기 파마미터로 아예 angle 속성값도 함께 넘겨 초기화하면 좋겠다. 참고로 angle 속성값은 원래 오리지널 Actor 객체의 속성엔 존재하지 않지만 actors.py 상단에서 pgzhelper 모듈을 import 했기 때문에 Actor객체 안에 원래 존해하는 추가 속성처럼 인식이 가능하게 되었다는 것을 잘 알고 있을 것으로 안다. 총 3개의 파라미터에서 img\_name, pos 값은 부모객체인 오리지널 Actor객체의 초기화에 사용되도록 값을 넘기고, angle값은 파생객체의 자신의 속성값으로 설정한다. 그럼 지금까지 언급된 내용을 코드로 표현해 보면 다음과 같다.

```python
class Tank(Actor):
    def __init__(self, img_name, pos, angle): 
        super().__init__(self, img_name, pos)
        self.angle = angle
```

:1234: 그 다음으로는 탱크 이동에 관한 move 메소드의 구현을 고민해 보자. 부모는 자식들이 모두 공유하는 공통된 기능에 대한 것을 구현하는 것이 의미 있는데 그래야만 부모를 상속한 자녀가 부모 것을 그대로 쓰면 되기 때문이다. 현재까지 찾아진 탱크의 공통동작이라고 할 수 있는 것은 근본적인 탱크 이동에 대한 것으로 이 move 메소드가 호출될 때 마다 현재 탱크가 바라보고 있는 방향을 기준으로 정해진 크기만큼 직진이동 한다는 것이다. 이 부분까지 포함해 다시 전체적인 코드로 표현하면 다음과 같다.&#x20;

```python
class Tank(Actor):
    def __init__(self, img_name, pos, angle): 
        super().__init__(self, img_name, pos)
        self.angle = angle
     
    def move(self):
        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
```

탱크의 이동에 대해 우리가 간과한(?)이 하나 있는데, 탱크이동에 제약이 있다는 것이다. 이 제약은 기존 절차지향으로 개발할 때도 다 인지가 되었던 것으로 탱크가 벽을 통과해 이동할 수 없고, 보이는 전체 게임화면을 넘어 이동해서도 안된다. _두 제약은 이동제약이란 측면에선 유사하지만 실제 구현관점에선 화면경계 제약은 단순 범위제약이고, 벽 관련 제약은 두 객체(탱크와 벽)의 충돌확인 후 이동의 제약이라 서로 내용이 다르다._&#x20;

여기서 전자의 경우는 꼭 탱크객체에게만 유효할까? 다른 객체에는 필요없을까? 를 생각해보면, '총알' 객체에게도 필요한 부분이다. 왜냐하면, 총알객체가 화면경계 밖을 넘어서자마자 객체를 삭제(메모리에서 제거)해야 하기 때문이다. **이렇게 모든 객체에게는 필요하지 않지만, 일부 객체에겐 필요한 기능을 구현하기 위해 객체지향 프로그래밍에서 반드시 알아야하는 또다른 개념이 하나 등장하는데 추상 클래스(abstract class) 또는 인터페이스(interface)라는 개념**을 배워야 할 시점이 왔다. 이 부분은 이론적 이해가 필요해 내용이 길어질 수 있으므로 다음 장에서 좀 더 상세히 설하도록 하겠다.
