(2012년에 예전 블로그에 쓴 글을 옮겨왔습니다)
게임 기획 공부모임에서 “UML 실전에서는 이것만 쓴다(UML for Java Programmers)”를 읽었다.
어제 지인으로부터 “2012년에 UML 공부라니!?”라는 소리를 들었는데, 맞는 말이다. 사실 UML은 손꾸락이고 달은 따로 있었다능. UML 표기법을 배우는게 목적이 아니라 UML의 창시자들이 UML이라는 언어(Unified Modeling LANGUAGE)에 담아내고자 했던 아이디어들을 기획자의 관점에서 공부해보자는 것이 스터디의 목적이었다. UML 표기법 읽을 줄 안다/모른다가 중요한게 아니다.
아무튼, 책 요약은 됐고 요번에는 책을 끝낸 기념으로 간단한 실습을 해봤다(사실은 숙제였음). 실습 주제는 간단한 게임을 하나 골라서 “역기획”을 해보는거다. 나는 만만해보이는 팩맨(Pacman)을 파봤다.
몇 가지 전제:
- 내 맘대로 한다.
- 내가 보기에 중요하거나 특이한 부분에만 집중한다. 내 맘대로 한다.
- 결과물이 실제 팩만과 다를 수 있다. 내 맘대로 한다.
초기 아이디어
2차원 평면 미로가 있고, 팩맨이 콩을 먹고 다니는 게임입니다. 콩을 다 먹으면 다음 판으로.

그냥 점만 먹으면 재미가 없으니 1) 미로를 복잡하게 만들거나 2) 시간 제약을 두거나 하는 식으로 재미요소를 주고 싶다. 근데 이런건 평범하니까 좀 특이하게 팩맨을 쫓아다니는 귀신들을 넣어서 새로운 장르를 개척해보자. 팩맨은 귀신에게 잡히면 죽는다. Maze Chasing 장르 탄생.

그렇다면 팩맨은 도망만 다니느냐? 그건 아니지. 슈퍼콩을 몇 개 배치하자. 슈퍼콩을 먹으면 잠시동안 팩맨이 귀신을 잡을 수 있게 됨. 뽀빠이의 시금치 같은거임.

설계
아이디어 좋으니 이제 형식적으로 표현해보자. 이 때 주요 명사들을 뽑아내면서 시작하면 무난하다. 미로(Maze), 팩맨(Pacman), 콩(Pellet), 슈퍼콩(Super Pellet), 귀신(Ghost). 얘네들이 대표적인 클래스가 된다.

음, 귀신들이 모두 동일하게 움직이는 것보다, 각자 개성있게 움직이면 더 재밌을 것 같다. 각각 이름도 붙여주자. Chaser, Ambusher, Fickle, Stupid. 얘네들은 귀신의 일종이니까 일반화 관계(generalization)로 표현하면 좋겠다.

공통점을 좀 더 뽑아 보자:
- 콩과 슈퍼콩의 공통점은? 팩맨이 먹을 수 있다(Eatable).
- 팩맨과 귀신의 공통점은? 움직일 수 있다(Movable).
- 콩과 슈퍼콩과 팩맨과 귀신 모두의 공통점은? 미로 상의 특정 좌표에 위치한다(Placeable).
어떻게 표현할까? 아까와 같이 일반화 관계를 써보면 이렇게 된다.

아니면 이런 식도 가능하다. 슈퍼콩을 콩의 일종인 것으로 보고, Movable은 Placeable의 일종인 것으로 보는거다. 말로 풀어보면 이렇다:
> 미로에 놓일 수 있는 것들(Placeables)이 있는데, 여기에는 콩(Pellet)이랑 움직이는 애들(Moveables)이 있다. 콩 중에는 슈퍼콩(Super Pellet)이라는 것이 있다. 움직이는 애들에는 팩맨(Pacman)이랑 귀신(Ghost)이 있어.

아참 잠깐 곁다리로, 위 그림에서 네 종류의 귀신, Eatable 등을 표현하지 않고 있는데 그건 그냥 귀찮아서…가 아니라, “현재 논의와 무관하므로” 뺀거다. 모델링을 할 때 가장 중요한 것 중 하나죠. 모델은 현실을 그대로 담아내는 것이 아니라, 중요한 것을 드러내고 중요하지 않은 것을 감추는 것.
다시 원래 이야기로 돌아가자. 좀 더 창의력을 발휘해보면 이런 식으로 생각할 수도 있다. “먹는다”는 것을 어떻게 보느냐에 따라, 귀신이 팩맨을 “잡는 것”을 “먹는 것”으로 볼 수 있다. 또한 팩맨이 슈퍼콩을 먹으면 귀신을 “먹을” 수도 있다. 그렇게 생각하면 미로 위의 모든 것은 먹을 수 있는(Eatable) 애들이 된다. 밴다이어그램으로 치면 Placeable과 Eatable이 서로의 부분집합이니까 결국 상등(set Eatable = set Placeable)이다. 그러면 합칠 수도 있겠다. 결국 위에서 그린 Eatable 빠진 그림이랑 같아진다.
다시 곁다리. 갑자기 왠 집합? 사실 객체지향적 사고랑 집합론적 사고는 통하는 면이 많다. 클래스(class)란 비슷한 개체들을 모아서 그 특성을 정의한 것인데, 그렇다는 말은 결국 클래스가 있으면 이 클래스가 정의하고 있는 특성에 부합되는 사례들(instances)이 있을거다. 어떻게 보면 조건제시법에 의해 정의된 집합이랑 비슷하다.
이 때 서로 다른 두 정의(즉 두 개의 클래스)에 해당하는 사례가 서로 완전히 겹치면 클래스를 하나로 합쳐도 된다고 생각할 수 있다. 물론 합쳐도 되는 것이지 합쳐야 하는 것은 아니다. 집합도 마찬가지. 왜냐하면 이 완전한 합치가 정의 자체에서 따라나오는 필연적 속성인지(이 경우 합치는게 대체로 좋겠지. “대체로”인 이유는… 동일한 대상을 여러 측면에서 정의했을 때의 장점이 있으므로), 현재 열거된/발견된 사례들이 우연히 그러한 특성을 갖게 된 것인지(이 경우 합쳐버리면 성급한 일반화가 된다)에 따라 다르니까.
그래서 실제로는 어떻게 하느냐? 애자일 방법론에서 추천하는 방식(제가 따르고자 노력하는 방식)은 1) 적당히 고민하고 좀 더 진도를 나가보는 것, 2) 언제 바뀌더라도 되도록 적은 비용으로 변화를 수용할 준비를 할 것이다.
그래서 제가 실제로 팩맨을 설계한다고 치면… 음 그냥 일단은 초기 설계와 비슷하게 Eatable, Moveable, Placeable로 나누고, 진도 나갈 것 같다. 어쩌면 Placeable을 제일 위에, 그 아래에 Eatable과 Moveable을 놓는 식일 수도 있고. 계속 팩맨과 귀신을 Eatable로 규정해서 묶어버리는게 확실히 좋은지 아닌지 고민도 하겠지. 아무튼 대충 다시 이 그림으로 돌아온다. (실제 코드에서는 Eatable, Moveable, Placeable을 각각 인터페이스로 만들고 generalization이 아니라 realization 관계를 쓸 것이다)

중간 정리
주요 개념들을 일반화 관계로 엮어내는 다양한 방법을 이리저리 고민해봤다. 여기에서 살펴본 방식은 엄청나게 다양한 방법 중 극히 일부에 불과하다. 내 생각에 중요한 교훈은
- “대상을 어떻게 바라보느냐”에 따라 다양한 방식의 일반화가 가능하다는 것
- 일반화를 잘 하면 좀 더 일관되고 체계적인 기획을 할 수 있다는 것
- 어떻게 일반화를 하느냐에 따라 서로 관련 없는 것으로 보이던 것들을 엮어낼 수 있고 이에 따라 여러 가지 유익한 통찰을 얻을 수 있다는 것
- 어차피 진행되다보면 초기 기획/설계/아키텍쳐 등은 바뀔 수 밖에 없으므로 너무 깊게 고민하지 않고 일단 진도를 나가보고 피드백을 받는 것
등이다.
귀신들의 상태 변화
이제 귀신들의 상태 변화와 상황에 따른 행동 패턴 변화 등, 귀신들의 개성있는 전략을 어떻게 표현할지 생각해보자.
우선 상태 변화 먼저. 귀신은 크게 네 가지 상태를 갖는다. 형상시에는 팩맨을 쫓아가는 모드(chasing)와 자신의 담당 영역 주변을 배회하는 모드(scattering) 사이를 주기적으로 오간다. 팩맨이 수퍼콩을 먹으면 때 쫄아서 도망가는 모드(frightened)로 바뀐다. 수퍼콩을 먹은 팩맨과 접촉하하면 유령집으로 퇴각하는 모드(retreating)로 바뀐다. 슈퍼콩 약발이 떨어지면 다시 쫓아가는 모드(chasing)로 바뀐다.
상태의 변화를 나타낼 땐 State Transition Diagram을 쓰면 좋다. State Transition Diagram이 표현하고자 하는 핵심 개념은 “상태(state)”와 “사건(event)”이다. 어떤 상태에서 어떤 사건이 발생하면 상태가 어떻게 바뀌는지를 보여줄 때 좋다.

위 그림에서 타원은 각각의 상태를 말한다. 작고 검은 동그라미는 시작 상태를 말한다. “상태”와 “사건” 개념에 맞춰 귀신의 상태변화를 설명해보자.
귀신의 초기 상태는 Chase다. 시작상태(검은 동그라미)에서 아무 사건도 발생하지 않더라도 자동으로 Chase 상태로 전환된다고 표현하면 된다. 시간의 흐름에 따라 Scatter 상태와 Chase 상태를 오가는 것은 어떻게 표현하면 될까? 어떤 타이머가 있고 이 타이머가 주기적으로 Chase On, Chase Off 사건을 발생시킨다고 보면 되겠다. Chase 상태에서 Chase Off 사건이 발생하면 Scatter 상태로 변경되고, Scatter 상태에서 Chase On 사건이 발생하면 Chase 상태로 바뀌는 거다.

사건이 발생했을 때의 상태 변화는 화살표로 나타낸다. 화살표에 붙은 글귀는 사건을 나타낸다. 간단하다.
다음은 팩맨이 수퍼콩을 먹었을 때, 수퍼콩 약발이 떨어졌을 때의 상태 변화다. 수퍼콩을 먹으면 Power Up 사건이, 약발이 떨어지면 Power Down 사건이 발생한다고 표현하면 되겠다.

위 다이어그램은 Chase 상태에서건, Scatter 상태에서건 팩맨이 수퍼콩을 먹으면 Frightened 상태로 가야하고, 약발이 떨어지면 (기존 상태와 무관하게) Chase 상태로 돌아가야 한다는 규칙을 표현하고 있다.
마지막으로 수퍼콩을 먹을 상태에서 팩맨에게 잡히면(touched 사건) Retreating 상태가 되어 귀신집으로 돌아가고, 귀신집에 도착하면(retreated 사건) 다시 Chase 상태로 바뀌는 것을 표현해보자.

한편, Chase 상태일 때의 움직임은 유령의 종류에 따라 다르다고 했었는데, 이 부분을 좀 더 살펴보자. 지난 글에서 그렸던 클래스 다이어그램을 다시 가져와보면 이렇다.

여기서 잠깐. 만약 네 마리 유령이 색상만 다를 뿐, 움직임 전략을 포함한 나머지 모든 것은 동일하다면? 그래도 “네 종류의 유령이 있다”라고 생각할까, 아니면 유령은 한 종류인데 색깔만 다를 뿐이다라고 생각할까? UML 스타일로 말하자면, “유령 클래스에는 네 개의 하위 클래스가 있다”고 볼 것인지, “유령 클래스의 모든 인스턴스는 색상이라는 속성을 갖는다”라고 볼 것인지를 선택해야 한다. 그림으로 그려보면 이렇다.

대체로 우측 방식을 택하겠지. 그렇다면 대체 “움직임 전략”이 뭐길래 이게 포함되면 네 종류의 유령, 이게 빠지면 한 종류의 유령으로 생각하게 되는 걸까? “색상”처럼 “움직임 전략”도 그냥 하나의 속성으로 보면 아래와 같이 그려볼 수 있겠다.

이제 유령은 한 종류인데, 각 유령 인스턴스는 서로 다른 “색상”과 “전략” 속성을 가질 수 있다고 말할 수 있다. 그림이 단순해진 것 같지만 그거슨 훼이크, 사실 그렇지는 않다. 유령이 한 종류인 대신 전략이 여러 종류니까.

물론 좀 더 생각을 해보면 서로 다른 전략으로 보이는 것도 사실은 하나의 전략인데 속성만 다른 것으로 간주할 수 있다.
예를 들어 팩맨의 움직임을 고려하지 않고 현재 팩맨이 있는 곳으로 움직이는 전략(Simpleton이라고 하자)과 팩맨의 이동 방향을 고려하여 현재 위치의 두 칸 앞으로 움직이는 전략(Look-ahead라고 하자)은 두 개의 전략으로 볼 수도 있지만, “몇 칸 앞을 볼 것인가”를 속성으로 갖는 하나의 전략에 변수만 다른 것(각각 0과 2)으로 생각할 수도 있겠다. 이와 유사하게 Melee Attack과 Range Attack은 두 종류의 어택으로 볼 수도 있지만 모든 어택은 Range Attack이고 Melee Attack은 속성이 range 0으로 설정된 Range Attack일 뿐이라고 생각할 수도 있다.
이러한 식의 일반화를 매개변수화(parameterizing)라고 부른다. 얼핏 보면 질적인 차이로 보이지만 어찌어찌 잘 매개변수화를 해 보면 양적인 차이로 바꿀 수 있다.
이런 사례를 공부하고 싶으면 각종 게임에 들어있는 “캠페인 에디터”, “맵 에디터” 등을 보길 권한다. 이런 류의 “에디터”들은 LUA 등으로 스크립팅을 하는 일을 최소화하면서 레벨 디자이너 등에게 최대한의 자유도를 주기 위한 목적으로 “과도하다 싶을 정도로” 매개변수화를 해놓고 있다. 보고 있노라면 이건 그냥 뭐 엑셀 시트 ㅋ
다시 “움직임 전략”이야기로 돌아가자(위에서 매개변수화에 대한 이야기를 하긴 했지만 일단은 모든 움직임을 하나로 일반화하는 것이 가능한지, 효율적인지 등을 따져보기엔 이른 감이 있으니 별개의 종류로 보겠다).
움직임 전략에 대해 조금 더 파보기
“움직임 전략”도 “색상”과 같은 속성으로 보자고 했는데, 이제 이 “움직임 전략”이라는 것에 대해 좀 더 생각해보면 좋겠다. 움직임 전략이란 뭘까? 대충 이렇게 정의해자. “게임의 현재 상황(configuration)에 따라 유령의 움직임을 결정하는 로직. 게임의 현재 상황이란 1) 팩맨의 위치, 2) 다른 유령들의 위치, 3) 자신의 위치, 4) 남은 콩의 갯수, 5) 자신의 상태(scatter, chase, frightened, retreating)”.
움직임 전략을 이런 식으로 정의하면 사실 게임에는 “네 개의 전략”만 있는 것이 아니라 일곱 개의 전략이 있는 거다. 이 중 Scatter 상태일 때의 움직임, Frightened 상태일 때의 움직임, Retreating 상태일 때의 움직임은 유령의 종류에 상관없이 동일하고, Chase 상태일 때의 움직임은 유령의 종류별로 다르다고 생각하면 되겠다. 그렇게 따져보니 “전략”이라는 말은 좀 이상하다. 그냥 이동 규칙(Movement Rule)이라고 하자. 그려보면 이렇게 된다.

주어진 문제(유령의 움직임)를 어떤 측면에서 바라볼지, 어떻게 일반화할지, 어떻게 나누고 합칠지 등을 고민하는 과정에서 뜻하지 않게 위와 같은 결과가 나왔다. 그리고 그 결과에 따라 “움직임 전략”이라는 개념 보다는 “움직임 규칙”이라는 이름이 더 적절하다는 생각이 들었다. 생각을 하다 보면 이런 일은 흔하다. 주어진 문제를 자연스럽게 모듈화하는 방법을 찾다 보니 내가 애초에 규정했던 문제의 틀이나 관련 어휘들이 적절치 못했다는 것을 알게 되는 것이다.
일단 요기까지. 끗.
다시 강조하지만 UML은 손꾸락이고 달은 따로 있다. UML 표기법 읽을 줄 안다/모른다가 중요한게 아니고, 사고 과정이 중요하다고 생각한다.