본문 바로가기

C++ Programming

유닛 추상클래스

스타크래프트(StarCraft)라는 시뮬레이션 게임을 보면 아주 많은 유닛들이 등장한다. 갑자기 특정 게임 얘기를 꺼내 이 게임을 모르는 사람들은 당황스러울지도 모르겠지만 믿을만한 통계에 의하면 이 책을 읽는 사람의 96%는 스타크래프트를 해 본 적이 있고 나머지 3.8%는 해 보지는 않아도 게임을 알고는 있다고 하니 예를 들어도 무난할 것 같다. 게임에 등장하는 유닛들을 특성별로 클래스화한다면 아마 다음과 같은 계층이 만들어질 것이다. 실제로는 더 많은 중간 계층이 존재하겠지만 간단하게 개념적인 계층을 만들어 보자.

모든 유닛들은 특징별로 뛰어 다니는 것들, 날아다니는 것들, 땅속으로 숨을 수 있는 것들로 일차 분류할 수 있다. Running 클래스에 속한 마린과 탱크는 땅 위를 아장 아장 걸어다니고 날 수 없다는 공통점을 가지고 있으면서 또한 스팀팩, 시지 모드같은 고유의 동작을 가지기도 한다. 뮤탈, 레이스, 캐리어 등은 날아다닌다는 면에서는 공통적이므로 Flying으로부터 상속을 받으며 마찬가지로 클로킹, 쓰리쿠션, 인터셉터 발사 등등 각각의 특성들을 추가로 가질 것이다.

이런 모든 유닛들은 공통적으로 좌표와 에너지 상태라는 속성을 가지며 이동할 수 있고(Move), 공격도 하고(Attack), 에너지가 떨어지면 죽기도(Die)한다. 이 외에도 스스로를 그리기도 하고 사라지기도 하며 가끔 말을 하는 경우도 있어 더 많은 공통 속성을 추출할 수 있다. 그래서 모든 유닛의 공동 조상으로 Unit클래스를 루트로 선언했는데 이 클래스는 아마도 다음과 같은 모양을 가지고 있을 것이다. 모든 유닛의 가장 기본적인 동작을 순수 가상 함수로 포함하고 있다.

 

class Unit

{

protected:

     int x,y;

     int energy;

public:

     virtual void Move(int x, int y)=0;

     virtual void Attack(int x, int y)=0;

     virtual void Die()=0;

};

 

Unit 클래스는 너무 일반적인 유닛을 표현하기 때문에 이 클래스의 인스턴스를 실제로 만들 수는 없다. 즉 Unit은 게임 유닛이 되기 위한 최소한의 요구 조건만을 명시하는 추상 클래스이다. 여기에 어떤 식으로 이동하고 어떤 식으로 공격하는지 좀 더 구체적인 특성이 정의되어야 게임에 등장하는 실제 유닛이 될 수 있다. 모든 유닛은 Unit으로부터 상속을 받아야 하며 Unit에 선언되어 있는 순수 가상 함수를 자신의 특성에 맞게 반드시 재정의해야 한다. 그래야 구체 클래스가 되어 객체를 만들 수 있다.

Unit 클래스와 실제 유닛들의 중간 계층인 Running, Flying 등은 뛰어다니고 날아다니는 유닛의 공통적인 특성을 표현할 뿐 실제로 구체적인 동작을 묘사할 수는 없으므로 역시 추상 클래스이다. 이 클래스들은 또 나름대로의 순수 가상 함수들을 선언하고 있을 것이다. 예를 들어 Flying은 이동시 목적지까지 바로 날아갈 수 있지만 Running은 장애물을 피해 최단 거리를 찾아 이동해야 하므로 FindShortestPath 따위의 동작을 필요로 한다.

이런 계층 구조에서 최상위의 루트 클래스인 Unit은 실제 객체를 만들지는 못하지만 모든 유닛의 대표 타입으로 사용된다. Unit * 타입의 변수를 선언하면 이 변수로 존재하는 모든 유닛을 다 가리킬 수 있다. 스타크래프트는 한 번에 12개의 유닛을 선택하여 동시에 명령을 내릴 수 있는데 이때 선택된 유닛들의 목록은 Unit *pSel[12] 배열로 간단하게 기억할 수 있다. 최상위 루트 클래스를 가리키는 포인터 타입이면 못 가리킬 유닛이 없지 않은가? 12개가 선택된 상태에서 사용자가 이동 명령을 내렸다면 이때 다음 코드로 선택된 모든 유닛에게 명령을 내릴 수 있다.

 

for (i=0;i<12;i++) pSel[i]->Move(x,y);

 

각각의 유닛이 목표 지점까지 이동하는 방식은 서로 다르다. 질럿은 뒤뚱 뒤뚱 걸어갈 것이고 뮤탈은 가로 질러 날아가고 캐리어는 아주 여유 부리면서 천천히 기는 듯 날아간다. 하지만 Move 함수 자체가 다형적으로 동작하기 때문에 선택된 유닛의 종류를 판단할 필요없이 Move라는 함수만 호출하면 선택 객체의 실제 Move 함수가 호출되어 정의된 특성대로 정확하게 동작할 것이다. 공격이나 사망 처리도 마찬가지로 다형적으로 동작하는 가상 함수이므로 해당 유닛의 타입을 일일이 구분할 필요없이 가상 함수만 호출하면 모든 처리는 동적으로 결합되는 가상 함수가 알아서 처리한다. 핵폭탄이 터졌을 때의 처리는 다음 루프면 된다.

 

for (pUnit=첫 유닛 ~ 생성된 모든 유닛까지) {

     if (pUnit->x, y 좌표가 핵폭탄 범위 안이면) {

          pUnit->Die();

     }

}

 

생성되어 있는 모든 유닛을 순회하면서 좌표를 점검하여 핵폭탄 사정 거리안일때 Die만 호출하면 다들 각자의 방법으로 알아서 사망하시므로 더 이상 신경쓸 게 없다. 게다가 스타크래프트가 확장되어 새로운 유닛이 추가되었다 하더라도 Unit 추상 클래스의 기본 요건을 반드시 만족해야 하므로 게임 운영 코드는 크게 수정할 필요없이 그대로 적용되는 이점도 있다.

실제로 스타크래프트라는 게임이 C++로 만들어졌는지, 다형성을 사용하는지는 확인해 본 바 없다. 그러나 분명한 것은 C++로 다형적인 코드를 작성하면 이런 게임을 쉽게 만들고 관리할 수 있다는 것이다. 그래픽 환경이라면 그럴싸한 예제를 한 번 만들어 볼 수도 있겠지만 콘솔 환경에서 Z, M, C 따위의 문자로 유닛을 표현하는데는 한계가 있어 지금은 하지 않기로 한다. 다음에 여러분들이 그래픽 환경을 배우게 되면 미니 스타크래프트를 한 번 만들어 보기 바란다.

 

여기까지 C++의 가장 기본적인 문법들에 대해 모두 연구해 보았다. 이 시점에서 25장에 있는 OOP의 특징들인 캡슐화, 추상화, 정보 은폐, 상속, 다형성의 개념들을 다시 한 번 읽어 보자. 처음에는 무슨 말인지 도통 이해가 가지 않았겠지만 이제 어렴풋이나마 이해가 될 것이다. 공부해 봐서 알겠지만 짧은 정의로 간단하게 설명하기는 참 어려운 개념들이다.

아직도 이해가 잘 가지 않는다면 예제를 통해 좀 더 연구해 보고 그럭 저럭 이해가 된다면 C++을 잘 모르는 친구를 앞에 앉혀 놓고 설명을 해보자. 그 친구가 알아 듣든 고개만 갸웃거리고 있든 객체 지향이란 바로 이런 것이야 라는 설명을 자신있게 할 수 있다면 현재 단계에서는 충분하다. 객체 지향 프로그래밍이 확실이 몸에 베려면 역시 좀 더 많은 실습이 필요하다.