I. 전투 프로그래밍이란?

얼마전에 필자의 회사에서 만들고 있는 게임의 개발에 참여할 스탭모집 공고를 낸 적이 있다. 공고는 몇 개의 부문으로 이루어져 있는데, 그중의 한 부문이 ‘전투 프로그래머’를 모집하는 것이었다. 그 공고를 낸 이후 사람들과 이야기를 나누면서 ‘전투 프로그래밍이 무엇인가? 회사에서 전투 프로그래머에게 구체적으로 무슨 일을 시키게 되는 것인가?' 에 대한 명확한 설명이 필요하다는 것을 느꼈다. 그래서 전투 프로그래밍에 대한 본 개론서를 쓰게 되었다.

전투 프로그래밍이란 것에 대한 어떤 별도의 엄밀한 학술적 정의가 따로 있는 것은 아니다. 다만 FinalFantasy 같은 RPG의 엔딩 스탭롤을 보면 프로그래머의 파트중에 Battle Programmer 라는 란이 따로 있는 것을 볼 수 있다. FF 같은 게임은 전투모드와 돌아다니는 모드로 크게 구분이 되기 때문에 전투모드에서 생길 수 있는 여러가지 일들을 프로그래밍한 전담자를 아마 전투 프로그래머라고 불렀을 것 같다.
왜 다른 이름도 많은데 하필이면 전투 프로그래머라고 부르는 것일까? 물론 절대적으로 전투 프로그래머라는 호칭이 존재하는 것은 아니다. 다른 회사에서는 AI 프로그래머라고 부르기도 하고 또 다른 호칭이 존재할지도 모른다. 다만, RPG 계열에서는 AI 이든 다른 무엇이든 모든 활동은 전투로 귀결되기 때문에 편의상 그렇게 부르게 된 것뿐이다.

전투 프로그래머가 해야 하는 일은 크게 2가지이다. 첫째로는 게임내의 각종 요소가 지능을 갖고 움직이도록 AI 프로그래밍을 하는 것이고, 두번째는 그 요소가 살아움직이는 것처럼 표현되도록 연출 프로그래밍을 하는 것이다. 즉 내용과 표현이라는 두개의 관점으로 게임의 핵심 요소들을 프로그래밍하는 것이다.

그렇다고 전투 프로그래머가 모든 프로그래밍을 하는 것은 아니다 그래픽 엔진을 만들거나, 내부용 기반 라이브러리를 만드는 것, 툴이나 파일포맷을 제작하는 것, 서버나 클라이언트의 네트워크 모듈을 만드는 것, 각종 정보를 표시할 UI 를 만드는 것 등의 많은 업무는 전투프로그래머가 담당하지 않는 분야이다.

전투 프로그래머와 다른 프로그래머의 일을 분리하는 것이 바람직할까? 한명의 프로그래머가 모든 일을 해야 하는 경우라면 일을 분리할 수가 없을 것이다. 하지만 대부분의 팀에서는 여러명의 프로그래머가 일을 나누어 처리하게 된다. 전투프로그래머에게 필요한 스킬은 다른 프로그래머들이 가져야 할 스킬과 상당히 구분이 된 전문성이 필요한 분야이다.

전투프로그래머는 기획자와 프로그래머의 경계선상에서 존재하는 사람이다. 전투프로그래머가 하는 일은 게임의 가장 핵심적인 플레이 요소를 채워넣는 것이니만큼, 누구보다도 게임적 감각이 뛰어나야 한다. 또한 기획자와 전투 프로그래머 간의 원활한 커뮤니케이션을 위해서는 다른 게임에서 구현되었던 요소들을 참고하고 비유하는 활동이 필수적이다. 그렇기 때문에 다른 어떤 프로그래머보다도 기존에 다양한 장르의 게임을 플레이했던 경험이 필요하다.

MMORPG 처럼 프로그래밍이 서버와 클라이언트로 나뉘어질 때에는 전투프로그래머가 어느 쪽을 담당하여야 할까? 서버와 클라이언트 양쪽 다 전투프로그래머가 필요한 분야이다. 서버에서는 게임의 내부적 구현을 위해 전투프로그래머가 필요하고, 클라이언트에서는 그 표현을 위해 전투프로그래머가 필요하다. 필자가 권하고 싶은 방법은 클라이언트, 서버, 전투 이렇게 3개의 파트로 프로그래밍 팀을 분리하는 것이다. 클라이언트쪽 팀은 순수한 엔진과 UI, 네트워크, 리소스 관리등을 담당하고, 서버쪽도 패킷 처리와 네트워크 제어, 월드의 오브젝트 관리, DB 처리, 각 오브젝트의 시야 처리등을 담당하며, 실제적인 전투에 관련된 부분은 따로 떼어내어 전투프로그래머가 클라이언트와 서버쪽의 코드와 데이터를 모두 책임지게 하는 것이다. 전투 프로그래밍이 서버와 클라이언트쪽의 손발이 잘 맞지 않으면 전투가 잘 구현되기 어렵기 때문이다.

II. 전투 프로그래머가 알고 다루어야 할 프로그래밍 기초

위에서도 언급했듯이, 전투프로그래밍은 기획과 프로그래밍의 사이에 위치한 것이다. 가장 기초적인 사항은 기획자와 프로그래머가 사용할 기획서를 쓰는 것이다.
다음 사항들은 전투 프로그래머뿐만 아니라 기획자 (전투기획자)도 반드시 이해해야 하는 사항들이다.

        메시지
        FSM (다계층 FSM 포함)
        애니메이션 제어
        이펙트 제어
        레퍼런스

경험이 부족한 기획자들은 기획서를 작성할 때 모호한 일상적 용어를 사용하는 경향이 있다. 그 이유는 아이디어를 형식화된 언어나 표기법을 이용해서 정리하고 설명하는 연습이 부족하기 때문이다. 메시지나 FSM 등은 프로그래머들이 프로그래밍을 위해 만든 개념들이지만 프로그래밍을 배우지 않은 기획자들도 얼마든지 이해하고 적용할 수 있는 개념들이다. 프로그래머와 기획자들은 상호 커뮤니케이션을 위해 기획서를 작성할 때 위의 개념들을 사용하여 명확하게 형식화된 정리를 할 필요가 있다.

1. 메시지

메시지는 다수의 오브젝트가 상호작용하는 것을 나타내기 위해서 사용하는 것이다. RPG 에서 오브젝트간 일어날수 있는 상호작용의 예는 ‘때린다’, ‘회복을 시켜준다’, ‘버프를 걸어준다’, ‘민다’ 등 여러가지가 있을 수 있다. 메시지는 그러한 상호작용에 들어가는 행동을 코드화하고 한정하는 의미가 있다.

메시지의 중요한 특성중 하나는, 메시지를 보내는 측과 메시지를 받는 측의 기획안이나 코드를 서로 분리시킨다는 데에 있다. 왜 두가지를 분리시키는 것이 중요할까? 그 이유는 메시지를 보내고 받는 오브젝트의 종류가 다양해지고, 상호작용의 조합이 다양해질 수 있도록 하기 위해서이다.

프로그래머의 관점에서 예를 들어보도록 하겠다. ‘공격한다’라는 개념을 프로그래밍한다고 가정해보자. 플레이어가 몬스터를 공격한다고 했을때에 구체적으로 게임 프로그램 내부에서는 많은 일이 일어나게 된다
먼저, 공격력을 계산해야 한다. 보통 공격력은 플레이어 자체가 갖고 있는 속성치, 즉 힘이나 민첩성, 레벨등의 요소와, 플레이어가 사용하는 무기, 예를 들면 칼 자체의 공격력, 공격속성, 그리고 부가적인 아이템 예를 들면 힘을 +1 시켜주는 반지나 화염속성을 부가해주는 부적 등 다양한 요소에 의해 영향을 받게 된다.
그러한 공격력은 몬스터에게 전달되서 몬스터 자체의 방어력, 몬스터가 장착한 아이템의 방어력, 회피율, 속성무시 혹은 속성 부가등을 감안하여 최종적인 데미지가 계산되고, 그 데미지만큼 몬스터가 가진 체력을 깎는 절차를 거치게 된다.
또한 거기에서 끝나지 않고, 몬스터의 속성에 따라 공격자에게 역으로 반격 데미지가 전달되거나 (고슴도치 갑옷 같은 물리적 반격속성이 있다던가) 혹은 몬스터가 체력이 0 이 되어 죽게 되었을 때, 해당하는 경험치를 다시 공격자에게 전달하거나 하는 절차가 추가적으로 생길 수 있다.
이러한 다양한 처리를 하는 코드를 실제 C++ 언어로 기술한다고 생각해보자. 어느 클래스의 어느 멤버 함수가 이 일을 수행하게 만들 것인가?

메시지 방식에서는 다음과 같은 식으로 코드를 분리해서 수행한다
CPlayer :: Attack
공격력, 공격속성, 명중률 계산
Monster 에게 ATTACK 이라는 메시지를 보냄 (메시지 안에 공격력, 속성, 명중률이 포함)
Cmonster :: OnMsg
공격이라는 메시지를 받음. 몬스터 내부의 규칙에 따라 적절한 메시지 핸들러가 메시지를 받음
Cmonster :: OnAttack
몬스터 자신의 방어력, 방어속성, 회피율 계산
메시지에서 받은 수치와 자신의 수치를 차감하여 최종 데미지 여부와 데미지 수치 계산

즉, ATTACK 이라는 메시지를 준비하는 부분 (플레이어 측)과, ATTACK 이라는 메시지를 받아 처리하는 부분 (몬스터 측) 의 코드가 나뉘게 되는 것이다. 위에 설명한대로 반격을 하거나 경험치를 주는 부분이 있다면 Cmonster::OnAttack 에서 다시 반격용 ATTACK 메시지를 만들어서 공격자에게 다시 보내거나., 또는 EXPUP 이라는 경험치 부여용 메시지를 만들어서 공격자에게 보내주는 것으로 처리하게 된다.

그냥 Attack 이라는 하나의 함수에서 공격력을 계산하고, 상대방의 체력을 소모시키는 처리를 다 해버리지 않고, 저렇게 2개 이상의 함수로 나누고, 그 중간을 메시지 송수신으로 처리하는 것의 장점은 무엇일까?

장점은, 맞는 처리를 다양화시킬 수 있다는 점이다. 예를 들면 어떤 몬스터는 그냥 맞기만 하고, 어떤 몬스터는 맞고나면 반격을 하고, 어떤 몬스터는 맞으면 에너지가 깎이는 대신 뒤로 한칸 밀리고, 어떤 몬스터는 맞으면 특정한 대사를 한다던가 하는 식으로 처리가 다양해질 수 있다. 그런 처리는 전적으로 맞는 쪽에서 알아서 하는 것이 되어야지, 때리는 쪽이 판단하게 되어서는 안된다. 예를 들어 때리는 오브젝트의 종류가 10가지가 있고, 맞는 오브젝트의 종류가 10 가지가 있다고 가정하자. 각 오브젝트가 다른 오브젝트를 때릴 때 생길 수 있는 경우의 수는 10*10, 즉 100 가지의 경우가 생길 수 있다. 이것을 때리는 측과 맞는 측의 코드를 분리 함으로써 10+10, 즉 20가지로 코드의 가짓수를 줄이는 것이 목적인 것이다.
물론 메시지를 쓰지 않고도 코드의 가짓수를 줄일 수 있는 다른 방법들도 다수 존재하지만, 메시지 방식을 사용하면 일관성이 있으면서 지속적인 확장이 가능한 구조로 만들어진다는 것이 장점이다. 이에 대한 더 자세한 설명은 OOP 나 C++ 책들을 더 참고하기 바란다.

더 중요한 장점은, 기획자들의 애매모호한 일상적 언어를 메시지라는 도구를 통해 형식에 맞출 수 있다는 점이다. 몬스터가 맞았을 때 어떤 놈은 반격을 할 수 있어야 한다 라는 말을, 어떤 몬스터는 맞는 메시지 핸들러에서 다시 공격메시지를 만들어 공격자에게 보낸다. 라는 말로 형식화가 가능하고, 이러한 형식화된 문장은 보통 일상적 문장보다 훨씬 더 간단하게 C++ 코드로 바꿀 수 있다.

게임 내에 어떤 메시지가 등장할 것인가? 각 메시지에는 어떤 파라메터가 들어갈 것인가? 를 결정하는 것은 게임기획의 가장 핵심이라는 것을 잊지 말자. 어떤 새로운 기능을 추가한다는 것은 어떤 메시지를 추가한다는 것과 마찬가지로 생각해도 무방할 것이다.

2. FSM

FSM 은 Finite State Machine, 즉 유한 상태 기계의 약자이다. 이것은 다양한 상태로 표현되는 오브젝트의 행동을 기술하기 위해 사용하는 개념이다.

OOP 에서는 어떤 오브젝트를 표현할 때, 속성과 행동이라는 두가지 개념으로 분리해서 표현한다. 어떤 오브젝트가 어떤 속성을 지녔는가라는 것은 클래스의 멤버변수라는 개념을 사용해서 쉽게 표현할 수 있다. 하지만 어떤 오브젝트가 어떤 행동을 하는가를 표현하는 것은 간단히 멤버함수로 표현하기 어렵다. 왜냐하면 행동을 하는 것은 상태에 따라 다르기 때문이다.

FSM 은 어떤 오브젝트가 가질 수 있는 상태들이 어떤 것이 있는가를 정의하고, 각 상태에서 다른 상태로 바뀌는 조건은 무엇인가를 기술하는 것이다.

FSM 은 간단히 if 문이나 switch 문을 이용해서 구현할 수도 있지만, 좀 더 편리한 처리를 위해서 클래스나 매크로를 사용해 구현할 수 있다. 보다 자세한 사항은 AI Game Programming Wisdom 의 FSM 관련 부분을 참고하기 바란다.

FSM 의 상태를 디자인할 때에는 다음과 같은 사항을 유의하여야 한다.
첫째로, 상태는, 평소에 Update 하는 사항이 무엇인가에 따라 나뉘어져야 한다는 점이다. 어떤 상태를 만들었는데, 그 상태는 들어가자마자 Update 없이 다음 State 로 가게 되어 있다면 그 상태는 존재할 이유가 없는 것이다. 그런 경우에는 상태를 만들지 말고, 그 전 상태나 이후 상태에서 조건문으로 처리되는 것이 좋다.
둘째로, 한 상태에서 경우에 따라 Update 하는 사항이 바뀌게 되어 있다면, 그 상태를 경우에 따라 다시 두개 이상의 상태로 쪼개어야 한다는 점이다. 모처럼 FSM 을 이용해서 상태를 만들었는데, 상태 안에서는 FSM 으로 쪼개기 이전처럼 코딩이 되고 있다면 FSM 을 제대로 쓰는 것이라고 할 수 없을 것이다.

예를 들어 설명해보겠다.

아이템을 줍는 경우
잘못된 경우: 아이템을 주으러 가는 상태와 아이템을 줍는 상태, 기본 상태로 나뉘어 있다. 그런데 아이템을 줍는 상태는 들어오자마자 아이템 줍기를 하고 다시 기본상태로 나간다
해결책: 아이템을 주으러 가는 상태와 아이템을 줍는 상태를 합치고, 합쳐진 상태에서의 OnLeave 에서 아이템 줍기 처리를 한다

스킬을 캐스팅 하고 사용하는 경우
잘못된 경우: 스킬 사용이라는 상태에서 캐스팅을 하는 것과 실제 스킬을 사용하는 것이 합쳐져 있다
해결책: 스킬 캐스팅이라는 상태와 스킬 사용중이라는 상태를 분리한다

FSM 의 한 상태에서 다음 상태로 넘어가는 경우는 크게 두가지로 나뉘어진다. 첫번째는 상태 Update 문의 내부 코드에서 어떠한 조건이 충족되어 다음 상태로 넘어가게 되는 것이고 두번째는 상태의 외부 영향에 의해 다른 상태로 바뀌게 되는 것이다. 이에 대한 설명은 이후에 더 자세히 할 것이다.

3. 다계층 FSM

FSM 을 이용하면 다양한 행동을 상태라는 것으로 나누어서 일관성 있게 처리할 수 있다. 하지만 좀 더 복잡한 행동과 전략, 인공지능을 구현하기 위해서는 단순한 FSM 만으로는 이것을 해결하기 어렵다.

게임 안에 등장하는 어떤 캐릭터가 있다고 가정하자. 그 캐릭터는 다음과 같은 행동을 FSM 으로 나누어 표현하고 있다

        서있기
        걸어가기
        뛰어가기
        공격하기
        아이템을 잡기

이것만 가지고도 간단한 행동은 표현할 수가 있다. 그런데 이러한 행동들보다 좀 더 상위의 개념, 즉 전술을 생각해보자.

        적을 죽인다
        도망간다
        아이템을 확보한다
        특정 목표지점으로 이동한다

예를 들면, 똑 같은 이동하기라는 행동을 한다고 해도, 적을 죽이기 위해서 쫒아가는 이동이 있는가 하면, 도망가기 위해서 이동을 할 수도 있고, 아이템을 먹기 위해서 이동할 수도 있는 것이다. 또한 공격도 적을 죽이기 위해서 공격을 한 것일 수도 있고, 도망을 가다가 공격을 받아서 어쩔 수 없이 반격을 한 것일 수도 있다. 그냥 서 있는 것도 적을 잡고 나서 다음 적을 찾기 위해서 대기하느라 서 있을 수도 있고, 적에게서 도망가다가 적과 거리가 어느정도 떨어져서 한숨을 돌리느라 서 있을 수도 있다. 또한 같은 전술을 수행하면서도 여건에 따라 실제 행동은 뛰어가기가 될 수도 있고 걸어가기가 될 수도 있다

이러한 상태들은 서로 부분별로 조합할 때 좀 더 코드의 양은 적게 하면서 좀 더 높은 차원의 인공지능을 구현할 수 있다. 이러한 전술적보다 더 높은 개념, 즉 전략도 생각할 수 있다

        동굴에서 아이템을 찾아온다
        성을 방어한다
        적의 성을 빼앗는다
        특정 몬스터를 10마리 이상 사냥한다

전략은 전술보다 더 상위의 개념이기 때문에 똑 같은 전략, 예를 들면 동굴에서 아이템을 찾아오는 것을 하더라도, 그 내용에는 적을 죽이거나 도망가거나 아이템을 확보하는 전술로 각각 변할 수 있으며, 각각의 전술 안에서는 서있고, 이동하고 공격하는 등의 행동이 역시 각각 변할 수 있다.
이렇게 복잡 다양하고 시나리오가 있는 행동을 하기 위해서는 여러 레이어로 FSM 을 구성해서 각 레이어가 다른 레이어에게 영향을 주는 방식으로 FSM 을 구성할 수 있다. 이것을 다계층 FSM 이라고 하는 것이다.

다계층 FSM 에서는 각 계층이 다른 계층에 영향을 미치게 된다. 한 계층이 상위 계층에 영향을 미칠 수도 있고, 반대로 하위 계층에 영향을 미칠 수도 있다.
예를 들어서 내부적으로 체력을 체크한 결과 현재 체력이 최고 체력의 20% 아래로 내려가게 되서 전술적 목표가 적을 죽인다에서 도망간다로 바뀌었다고 하자. 현재 하던 행동이 공격하기였다면, 뛰어가기 (도망가기 위해서) 로 바뀌게 된다. 이것은 상위 계층이 하위 계층에 영향을 주는 경우로 볼 수 있다.
반대로 아이템을 확보한다라는 전술을 위해서 뛰어가고 아이템을 잡는 행동을 한다고 하자. 아이템을 잡는 행동이 끝난 후에는 아이템을 확보한다라는 전술이 완료된다. 이것은 하위계층이 상위계층에 영향을 주는 경우로 볼 수 있다.
다계층 FSM 은 잘 디자인하면, 매우 효과적으로 인공지능 오브젝트들을 창조할 수 있게 된다.

4. 월드

게임 안에는 플레이어 혼자 존재하지 않는다. 다른 상대들이 있으며, 갈 수 있는 지형, 갈 수 없는 지형, 장애물, npc, 몬스터, 아이템, 보물상자등 다양한 오브젝트가 존재한다. 게임 내에 존재하는 이러한 오브젝트와 배경물들의 데이터베이스를 우리는 월드라고 부른다.

데이터베이스가 하는 기본적인 4대 행동은 CRUD, 즉 Create, Read, Update, Destroy 이다. 월드 자체도 데이터베이스의 일종이기에 월드에서 일어나는 일도 마찬가지이다.

Create: 월드 안에 새로운 오브젝트가 생겨난다. 적이 일정시간 주기로 랜덤한 위치에서 스폰되는 것도 Create 이며, 몬스터가 죽으면 바닥에 아이템을 떨구는 것도 Create 이다. 갖고 있던 아이템을 바닥에 내려놓는 것도 월드의 입장에서는 Create 이다. 인벤토리는 월드와는 별개의 존재이기 때문이다. 새로운 플레이어가 접속해서 내 옆에 등장하는 것도 월드의 입장에서는 Create 이다. 파이어월 마법을 써서 적을 공격하는 것도 파이어월이라는 오브젝트가 월드 상에 Create 되고, 파이어월이 적에게 공격 메시지를 보내는 것으로 이해할 수 있다.

Read: 월드 안에서 행동을 하기 위해서는 다른 오브젝트들이 어디에 있는지 알아야 한다. 월드 전체보다는 일정 시야나 반경 내에 있는 오브젝트들을 골라내는 것은 전투프로그래밍에서 월드에 대한 인공지능의 기본이다.
예를 들어 몬스터 AI 를 만든다고 하자. 몬스터가 가만히 돌아다니는 것이 전부가 아니라, 필요할 때에는 선공을 해야 할 경우도 있다. 선공을 한다면 어떤 플레이어를 대상으로 공격할 것인가? 가장 가까운 플레이어를 공격할 것인가? 아니면 범위 내 가장 약한 플레이어를 공격할 것인가? 아니면 특정 행동을 하는 (예를 들면 범위마법을 시전하려고 하는 플레이어 마법사를) 플레이어를 더 우선적으로 공격할 것인가?
이러한 활동의 기본은 오브젝트가 월드에 일정한 조건을 주고 질의를 하여 그 결과중에서 골라내는 것이다. 이것은 월드가 구성된 방식에 따라 달라질 수 있지만, 공통적으로 어느 위치를 기준으로, 어느 거리 반경 내에 있는 어떤 타입 (플레이어, 혹은 몬스터, 혹은 직업별)을 쭉 가져온 다음에, 그 가져온 결과중에서 가장 조건에 부합하는 것이다.

Update: 게임 내에서 여러가지 행동이 일어나고 나면 (메시지를 보내거나 FSM 의 상태 업데이트에 의해) 각 오브젝트가 가진 속성값이 변화하게 된다. 예를 들면 공격 메시지를 받은 몬스터 오브젝트는 체력 속성값이 낮아지게 될 것이다. 이러한 변화는 다시 월드에 기록된다. 중요한 것은 메시지로 구성된 시스템에서는 한 오브젝트의 속성값 변화는 자기 자신만 할 수 있어야 한다는 것이다. 몬스터 오브젝트가 스스로 자기 체력을 깎는 식이 되어야지, 플레이어가 몬스터 오브젝트의 체력을 함부로 건드리거나 해서는 안된다.

Destroy: 활동을 마친 오브젝트는 월드 내에서 없어져야 한다. 하지만 월드 상에서 오브젝트가 없어질 때에는 주의가 필요하다.
그 이유는 다른 오브젝트들이 없어질 오브젝트를 계속 참조하고 있을 수 있기 때문이다. 예를 들어서 플레이어가 몬스터를 타겟으로 쫓아가고 있다고 하자. 그런데 또 다른 플레이어가 몬스터를 죽여서 몬스터가 월드상에서 사라졌다고 하자. 그렇다면 몬스터를 쫓아가고 있던 원래의 플레이어는 더 이상 쫓아갈 몬스터가 없어지는 예외상황이 생기게 된다. 이러한 예외상황을 제대로 다루지 않으면 잘못된 포인터 연산 같은 오류가 발생하게 된다. 이러한 문제점을 막기 위해서는 오브젝트가 월드에서 사라질 때 즉시 사라지지 말고, 자신을 참조하고 있는 다른 오브젝트가 참조를 유지하는 동안 좀비상태로 잠시 대기할 필요가 있다. 참조를 유지하던 다른 오브젝트는 그 오브젝츠를 참조할 때 만약 좀비상태로 변했다고 하면 참조를 풀고 더 이상 그 오브젝트를 참조하지 말아야 한다.
이러한 활동을 레퍼런스 카운팅이라고 부른다. 각종 예외상황에 대응해서 안정적인 프로그램을 만들기 위해서는 꼭 필요한 개념이므로 반드시 숙지해두기 바란다.
Update 와 마찬가지로 Destroy 는 오브젝트 스스로 하는 것이다. 예를 들어서 적을 한번에 완전히 사라지게 만드는 마법을 구현한다고 해도, 적을 플레이어가 멋대로 Destroy 시켜버린다던가 하는 것은 곤란하다. 대신 KILL 이라는 메시지를 보내고 적은 KILL 이라는 메시지를 받았을 때 즉시 좀비상태로 들어가서 자신에 대한 레퍼런스가 모두 해제된 것을 확인하고 스스로를 Destroy 하는 식으로 구현하는 것이 바람직하다.

5. 애니메이션 제어

지금까지는 전투프로그래밍의 내용적 부분인 메시지와 FSM, 그리고 월드 에 대해서 알아보았다. 이것은 겉보기와는 무관한 순수하게 내용적인 부분이므로, 텍스트 머드게임이건 MMORPG 게임이건 공통적인 부분이다. 이후의 부분은 그래픽적으로 전투의 내용을 표현하기 위한 방법들을 알아보도록 하자.
표현에 대한 부분들은 절대적으로 게임 엔진 혹은 미들웨어의 영향을 받는 부분들이다. 그러므로 여러분의 팀이 사용하는 엔진이나 미들웨어의 설명서를 우선 숙지할 필요가 있다.

애니메이션을 프로그래밍할 때에는 절대적으로 애니메이터와의 협력작업이 필수적이다. 애니메이터와 원활한 공동작업을 하기 위해서는 몇가지 사항에 대한 사전합의를 해야 한다.

1. 좌표계
맥스에서 사용하는 좌표계와, 프로그래머들이 흔히 이용하는 DirectX 의 예제에서 사용하는 좌표계에는 차이가 있다. X,y,z 중 어느쪽이 위를 나타내고 +가 위인지, -가 위인지 등에 대한 사항은 미리 맞춰져 있어야 한다. 그리고 숫자가 어느정도를 의미하는 것인지 서로 합의해두면 좋다. 캐릭터가 100 정도 앞으로 간다고 하면 대강 현실세계 기준으로 몇미터정도 갔다는 것인지, 20 프레임동안 공격동작이 지속된다고 하면 몇초를 의미하는 것인지 등등…

2. 애니메이션에 이동이 들어가는 경우, 월드좌표로 처리할 것인가? 애니메이션으로 처리할 것인가?
예를 들어 걸어가는 동작을 만든다고 하자. 게임 제작에 대한 경험이 부족한 애니메이터는 아무생각 없이 맥스 뷰포트 상에서 앞으로 걸어가는 동작을 만들 것이다. 애니메이션 파일 상에서 직접 이동을 해서, 시작 프레임과 끝 프레임의 위치가 달라지도록 말이다. 이렇게 만든 애니메이션 파일을 그냥 엔진에 넣고 돌리게 되면 모션을 보정하지 않는 한, 제대로 나오지 않는다. 게임에서의 위치는 오브젝트의 월드상 위치와, 오브젝트의 모델 좌표계상의 위치가 결합되어 나오는 것인데, 프로그래머는 오브젝트를 월드좌표에서 움직이고 있고, 애니메이터는 오브젝트를 모델 좌표계상에서도 움직이고 있으니 움직임이 이중으로 겹쳐져서 제대로 나올 수 없는 것이다.
그렇다고 이 문제를 해결하기 위해서, 애니메이터가 오브젝트의 중심 노드의 포지션 키값을 홀라당 날려버리면 뭔가 움직임이 이상해지게 된다. 사람이 뛰어갈 때에는 앞뒤로 미세하게 움직임이 있기 마련인데 포지션 키값을 날리면서 이 부분까지 날리면 움직임이 밋밋해지게 된다.
이것을 해결하기 위한 방법은 움직임을 월드쪽으로 몰아줄 것인가, 아니면 애니메이션에서의 움직임이 자동으로 월드로 반영되도록 할 것인가등의 사항을 결정하여야 한다. 걷기나 뛰기뿐만 아니라 다른 애니메이션을 플레이할 때에도 이러한 부분은 주의해서 사용하여야 한다.

3. 프로그래밍적인 움직임과 애니메이터적인 움직임의 분리
위에서 말한 부분의 연장선이라고 할 수 있다. 좀 더 복잡한 움직임이 필요해질 경우가 많이 있다. 가장 대표적인 경우는 점프동작이다. 점프의 움직임을 표현 하는 것은 중력가속도에 의해 영향을 받는 포물선 운동이 된다. 이 포물선 움직임은 애니메이터가 맥스에서 직접 만들 수도 있지만, 여러가지 문제점을 안고 있다. 점프를 하는 세기를 다양하게 하기 어렵고, 높은 곳에서 낮은 곳으로 뛰어내린다던가 그 반대로 올라간다던가 하는 다양한 외부환경에 적용하기 어려워진다. 이러한 움직임은 애니메이션을 여러벌 만들어서 선택해서 플레이하는 식의 미봉책으로는 한계가 있다. 이러한 움직임 역시 올바른 해결책이 되기 위해서는 애니메이션에서 포물선 이동을 하지 말고프로그램 적으로 포물선의 움직임을 하게 하고, 애니메이션에서는 수직으로 움직이는 속도나 높이별로 애니메이션을 만들어서 프로그램이 애니메이션을 선택하게 만들어야 한다. 이것을 더욱 일반화시키고자 한다면 Rag-doll 물리학을 이용해서 사용하는 쪽으로 추진해야 한다.

4. 애니메이션을 파일 단위로 어떻게 끊을 것인가?
필자의 의견으로는 애니메이션 파일 하나에 모든 동작을 담는 것보다는 각 동작별로 파일을 분리하는 것이 바람직하다고 생각한다. 파일이 많아져도 보통의 경우 패키지를 만들어서 하나의 파일로 뭉치게 되기 때문에 별 문제가 안된다. 한 파일에서 프레임별로 여러 개의 애니메이션을 합치게 되면, 애니메이션 블렌딩 때문에 애니메이션의 다음 동작이 현재 동작의 끝부분에 영향을 주게 되는 일이 생기기 때문이다.

5. 캐릭터의 각 부위별로 다른 애니메이션을 적용하는 경우
FPS 같은 게임을 만들게 되면 캐릭터의 상체와 하체가 각각 다른 애니메이션을 해야 할 필요가 생긴다. 하체는 각 방향별로 뛰어가는 행동을 하면서 상체는 총을 재장전하는 애니메이션을 하거나 총을 쏘는 애니메이션을 하거나 하는 식으로 조합을 하게 된다. 이 경우 흔히 사용하는 방식은 애니메이션에 마스킹을 하는 것이다. 즉 총을 쏘는 애니메이션을 만든 다음, 하체부분의 노드에는 적용하지 않음 마스킹을 지정해놓고 걸어가는 애니메이션 위에 덮어쓰면 총을 쏘면서 걸어가는 애니메이션이 나오는 식이 되는 것이다.

6. Additive 애니메이션과 보통 애니메이션의 구분
MMORPG 에서는 다른 애니메이션과 달리 특별하게 처리되어야 하는 애니메이션이 있는데, 바로 맞기 애니메이션이다. 보통의 애니메이션은 애니메이션이 플레이되는 도중에 다른 애니메이션을 플레이 하면 두개의 애니메이션이 전환되지만, 맞기 애니메이션은 하던 애니메이션을 그대로 플레이 하면서 맞는 것은 맞는대로 보여주어야 한다. 맞기 애니메이션을 보통 처럼 전환되는 식으로 플레이 한다고 하자. 플레이어가 공격 동작을 하다가, 맞기동작을 하면 공격 동작이 끝까지 플레이 되지 않게 되어서 전투가 이상해진다. 공격 애니메이션 동작의 타이밍에 맞추어서 이펙트를 발생시키거나, 데미지 카운트가 뜨게 하는 부가처리가 있다면, 공격 애니메이션이 맞기 때문에 중간에 끊기게 되는 것은 더욱 심각한 부작용이 된다. 그렇다고 맞는 애니메이션 자체를 플레이를 아예 안해버리면 타격감이 없어져 밋밋하게 되어버린다. 이에 대한 해결책은 Additive 애니메이션이다. Additive 애니메이션은 기존 애니메이션에 더해져서 플레이 되는 방식을 의미한다. Additive 애니메이션은 베이스 포즈 (모델링을 해서 본 세팅을 한 포즈) 를 기준으로 만들어지는데, 각 노드별로 PRS 정보를 떼어내서 현재의 애니메이션이 플레이되고 있는 각 노드에 그 수치를 더해주는 식이 된다. 자세한 사항은 사용하고 있는 엔진의 설명서를 참조하기 바란다.

7. 두개 이상의 캐릭터가 같이 움직여야 하는 애니메이션을 어떻게 만들 것인가
액션게임에서 잡아 던지기 같은 움직임을 만들게 되면, 두개의 캐릭터가 동시에 호흡을 맞추어 움직여야만 한다. 가장 간단한 방법은, 두 캐릭터가 함께 이동하는 장면을 만들어 놓은 다음에, 한 캐릭터를 hide 해서 A 캐릭터만 있는 동작을 export 하고, 다시 HIDE/UNHIDE 를 해서 B 캐릭터만 있는 동작을 Export 한 다음에 프로그램적으로 합쳐지게 하는 것이다. A 캐릭터가 잡아 던지는 동작은 A 캐릭터의 FSM 상에서 ‘잡아 던지는 상태’ 로 처리되고, B 캐릭터가 던져지는 동작은 B 캐릭터의 FSM 상에서 ‘던져지는 상태’ 로 처리되어야 한다.

8. 애니메이션과 이펙트의 분리
캐릭터가 칼을 던지거나, 마법을 써서 손에서 불덩이가 나간다던가 하는 동작 역시 통짜 애니메이션으로는 해결이 안되고, 적절한 분리를 필요로 하는 일이다. 칼을 던지는 동작과 칼이 날아가는 동작이 서로 분리되어야 하고, 마법을 준비하는 동작과 손에서 나간 불의 동작도 역시 분리되어야 한다. 더욱이 날아간 칼이나 불덩이는 적을 향해 날아가던가 포물선 운동을 하던가 해야 하기 때문에 전적으로 프로그램적으로 움직여야만 한다. 생각해볼 수 있는 방법은, 캐릭터 애니메이션에 손부분에는 칼이나 불이 발사될 지점을 알아볼 수 있도록 더미노드를 배치해놓고, 칼이나 불이 발사되어야 하는 프레임 넘버를 애니메이션의 프로퍼티 같은 방식을 써서 지정해 놓은 다음에, 어느 노드에서 어떤 종류의 물체 (칼, 불덩이) 가 몇 프레임 째에 발사되는지를 지정하는 식으로 처리되어야 한다.

9. IK, 기타 Controller 의 적용
버파나 철권, 닌자가이덴 같은 게임의 캐릭터를 보면 지형에 굴곡이 있는데 캐릭터의 양 발이 지면의 높이에 따라 높이가 맞춰지는 것을 볼 수 있다. 이러한 움직임은 애니메이터가 모든 경우의 수를 따져서 미리 만들기 어렵기 때문에 역시 프로그램적인 힘을 빌려야만 한다. 양 발의 좌표에서 지면의 높이정보를 읽어서 그 정보만큼 발을 위로 올려주는 식으로 IK 를 구현하는 것이다. 자세한 것은 사용하는 애니메이션 엔진을 참조하기 바란다.
격투게임에서 상대방의 눈을 쳐다보거나 FPS 게임에서 캐릭터가 들고 있는 총구의 방향이 캐릭터의 조준에 맞춰지던가 하는 것들도 각각 상황에 맞는 Controller 를 사용해야 한다.

III. 전투 프로그래밍의 구체적 활동

전투 프로그래밍이란 위에서 언급한 형식을 이용하여 작성된 기획서에 따라 ‘적절한 타이밍에 적절한 일이 일어나도록 하는 것’이다.

적절한 타이밍의 예는 다음과 같다

        일정한 시간이 경과
        플레이하던 애니메이션의 종료
        땅바닥에 튀겼을 때
        메시지를 받았을 때
        패킷을 받았을 때
        마우스나 키보드 입력
        오브젝트가 특정 위치에 도달
        내부 변수가 특정 조건 만족
        다른 레이어의 State 의 변화

그에 따른 적절한 행동의 예는 다음과 같다

        내부 변수의 변경
        다른 State 로의 전환
        다른 오브젝트에게 메시지 송신
        이펙트 생성
        다른 오브젝트 생성
        애니메이션 시작
        사운드 출력
        화면 효과

IV. 테스트

전투 프로그래밍에는 절대적으로 시도 & 수정의 반복이 필수적이다. 특히 표현에 대한 부분은 여러가지 타이밍적인 요소를 고려해야 하고 가장 연출적으로 멋진 장면을 만들기 위해서는 반복적인 실행을 하게 된다.
이러한 작업을 효율적으로 하기 위해서는 테스트 환경을 잘 꾸며놓는 것이 좋다. 예를 들어서 몬스터의 FSM 중 하나를 맞추기 위해서 수십번의 수정 & 실행을 해야 하는데, 한번 실행을 할 때마다 네트워크 로그인을 해야 하고, 거대한 월드를 읽어야 하고, 몬스터가 나오는 지점까지 이동해야 하는 등등의 지루한 작업을 해야 한다면 작업의 효율은 엄청나게 떨어질 수 밖에 없을 것이다. 그러므로 효율적인 작업을 위해서는 다음과 같은 환경이 필요하다.

1. 네트워크에 접속하지 않고 자신의 컴퓨터에서 게임에 접속할 수 있게 한다. 이를 위해서는 클라이언트의 네트워크 모듈을 확장하여 마치 서버에 접속한 것처럼 흉내를 내게 해주는 더미 로컬 서버 모드를 만들어주면 효과적이다. 보통의 개발팀에서 개발도중에는 서버가 불안정하기 마련이다. 테스트를 하고 싶어도 서버 접속이 안되서 테스트가 안되는 일이 종종 생기게 되는데, 이것은 큰 시간의 낭비이다.
2. 가장 빨리 읽을 수 있는 간단한 배경을 만들어놓고, 테스트는 그곳에서 할 수 있도록 한다. 위에서 말한 더미 로컬 서버에서 맵을 임의로 지정할 수 있게 하면 될 것이다. 커다란 맵을 로딩하는 시간이 테스트 시간을 잡아먹어서는 안된다.
3. 테스터가 원하는대로 오브젝트를 불러낼 수 있게 한다. 핫키를 지정해서 특정 키를 누르면 특정 오브젝트가 튀어나오게 하거나, 혹은 콘솔 커맨드 창을 만들어서 커맨드로 오브젝트를 생성시킬 수 있게 한다
4. 화면에 있는 오브젝트의 내부적인 속성을 직접 눈으로 볼 수 있게 한다. 예를 들면 현재 이 오브젝트는 어떠한 FSM 상태에 있고, 체력이 얼마인지 캐릭터의 하단에 보여주면 테스트 할 때 매우 편리하다.
5. 각종 수치정보들은 가급적 코드에 직접 넣지 말고 xml 같은 텍스트 에디터로 고칠 수 있는 외부 데이터파일에 담아놓는다. 이렇게 하면 프로그래머 대신 기획자나 테스터들도 쉽게 문제점을 대신 해결해줄 수 있다.
6. 각종 치트 커맨드들을 충분히 만들어 놓는다. 특정 지점으로 이동해야만 어떤 테스트를 할 수 있다고 하면, 원하는 지점으로 워프를 할수 있게 해주거나, 이동속도를 몇배로 빠르게 해주는 치트 커맨드, 혹은 슈퍼 점프를 가능케 해주는 치트 커맨드를 만들어놓는 것이 큰 도움이 된다. 또 강력한 몬스터와의 전투를 테스트 하고 싶다면, 플레이어가 무적이 되게 해주거나, 플레이어가 임의로 레벨업을 할 수 있게 하는 치트커맨드들을 만들어놓도록 하자. 많은 프로그래머들은 이런 커맨드를 만드는 시간이 귀찮아서 이보다 훨씬 더 많은 시간을 아무생각 없이 테스트하는데 낭비하지만, 이것은 엄청난 시간의 낭비이다. 각종 치트 커맨드들을 만들어서 테스트하는 것은 전투프로그래머의 특권이라고 생각하자.
7. 아니, 치트 커맨드를 만들면 테스트에 큰 도움이 된다는 말은 취소다. 치트 커맨드는 반드시 먼저 만들어야만 한다. 치트커맨드를 만들지 않는 전투 프로그래머는 전투 프로그래밍을 할 자격이 없다!

V. 마지막으로

이 글을 읽고 전투 프로그래밍이 바로 나를 위한 일이라고 느꼈는가? 그렇다면 주저말고 recruit@imc.co.kr 로 응모 메일을 보내기 바란다. 단 메일을 보내기 전에 AI Game Programming Wisdom 1, 2 편과 Game Programming Gems 의 AI Section 들 정도는 미리 읽어두도록.

imcgames 의 김학규입니다