http://creaty.net/ : 크리티 프로젝트 프로모션사이트
안녕하세요. 제프입니다. 오랜만에 다시 강좌가 이어졌네요.
이번에는 지난번에 말씀드렸던 게임의 감초 NPC에 대해 알아보고, 만들어보는 시간을 가져보겠습니다. 우선 게임에서 NPC란 무엇일까요?
논플레이어 캐릭터
논플레이어 캐릭터(non-player character, NPC)는 게임에서 사람이 직접 조작하지 않는 캐릭터를 말한다.
원래는 테이블 롤플레잉 게임에서 비롯된 용어로 컴퓨터 롤플레잉 게임에서도 사용되며, 종종 다른 장르의 컴퓨터·비디오 게임에서도 사용된다.
NPC는 플레이어가 하지 않는 캐릭터를 맡음으로서 통해 세계의 존재감을 높여주는데, 단순히 구경꾼이나 상인일 수도 있고, 동료나 적일 수도 있다. 게임 특성에 따라서는 퀘스트를 부여하기도 하고 길드를 만들어주거나 일부 아이템을 공짜로 주는 등 게임을 즐기는 플레이어에게 부가적인 도움을 주기도 한다.
출처 : 위키백과
위키백과에서는 위와같이 말하고 있습니다. 즉 게임상에 존재하는 케릭터중에 사람이 아닌 컴퓨터가 조작하는 캐릭터를 따로 지칭하는 말이라는 것이죠.
이것은 지난번에 저희가 만들었던 몬스터 클래스도 일종의 NPC가 되는 것입니다. 하지만 오늘 만들어볼 NPC는 흔히 플레이어에게 도움을 요청하거나 스토리를 설명해주고, 또는 플레이어와 함께 적을 물리치는 등 다양한 일을 하는 NPC입니다.
NPC를 만들려면 NPC가 어떤 일을 하는지 알아봐야겠죠? NPC는 간단히 다음과 같은 일을 할 수 있습니다.
- 대사를 한다.
- 움직인다.
- 퀘스트를 부여한다.
- 아이템을 주고 받는다.
- 주인공의 파티멤버가 된다.
- 주인공과 싸운다.
- 다른 마을 주민과 잡담을 나눈다.
- 기타 등등...
휴~ 정말 하는일이 많은 NPC로군요. 어쨌든 여기서 중요한 것은 NPC는 어떤 일이든 할 수 있다는 점에 주목하여 아래와 같이 다시 정리해보겠습니다.
- 대사를 말할 수 있다. 특정 분기를 만나면 대사가 달라진다.
- 특정 이벤트를 일으킬 수 있다.(기타 행동정의)
- 이동할 수 있다.
사실 대사를 하고, 퀘스트 또는 아이템을 주고받고, 파티멤버가 되는 이런 모든 것들은 일련의 이벤트입니다.
이미 정의되어있는 이벤트를 불러올 수 있다면 이 NPC는 무슨일이든 할 수 있는 것이죠. 또한 스스로 위치를 움직일 수 있어야 합니다.
그것을 기초로 클래스를 구성해보면 아래와 같습니다.
class NPC {
int HP;
int Pos_x, Pos_y;
int state;
(void (*)()) Event[100];
Event_size;
public:
NPC() {
HP = 100; Pos_x = Pos_y = state = 0; Event_size = 0;
for (int i = 0; i < 100; i++)
Event[i] = null;
}
~NPC();
bool move_to(x, y) {
Pos_x = x; Pos_y = y;
cout << "NPC가 " << Pos_x << ", " << Pos_y << " 지점으로 이동합니다." << endl;
}
bool dialog();
int Set_Event( void (*func) () );
int Delete_Event(int index);
bool excute(int index);
}
이 NPC에서 Pos_x, Pos_y는 보시다시피 NPC의 위치를 말하고 move_to 함수를 통해 이동이 가능하도록 되어있습니다.
이제 중요한 dialog는 상황에 맞는 대사를 말하도록 해야하므로 현재 상황을 저장하는 변수로 state를 추가해두었습니다.
지금까지는 지난번의 몬스터 함수보다도 간단한 구조라 별달리 설명해 드릴게 없네요. 대화 함수로 넘어가보겠습니다.
bool NPC::dialog()
{
switch(state) {
case 0:
cout << "[홀그렌] 흥, 난 너와 할 얘기가 없어." << endl;
state++;
break;
case 1:
cout << "[홀그렌] 왜 자꾸 귀찮게 해" << endl;
state++;
break;
case 2:
cout << "[홀그렌] 너 나한테 관심 있니?(T/F)" << endl;
char -nput[255];
cin >> -nput;
if (-nput[0] == 'T')
state++;
else
state = 0;
break;
case 3:
cout << "[홀그렌] 나,난 딱히 니가 마음에 들어서 받아준건 아니라구..." << endl;
cout << "[System] [홀그렌]을 파티로 맞았다." << endl;
state++;
break;
default:
cout << "[홀그렌] ..." << endl;
break;
}
}
온라인게임 라그나로크의 무기강화 NPC인 홀그렌이 찬조출연했네요.
보통 게임들은 위와 같이 분기를 통해 대사가 바뀌며, 그에따라 이벤트도 다양하게 적용되는 모습을 보여주고 있습니다.
다만, 이런식으로 각 대사를 일일히 분기넣어가며 조절하는 방법보다는 대사 스크립트를 통해 한줄씩 출력하고 대사 내에서 이동할 대상을 지정하여 이런 분기를 처리합니다.
그런 스크립트 시스템을 만드는 것은 상당히 방대한 양이라, 이번 포스팅 하나에 모두 설명해드리기가 벅차므로, 기본적으로 사용되는 C++ 정보들을 설명해드리고나면 그때 다시 언급하도록 하겠습니다.
단 이때 주의할 점은 프로그램의 흐름이 아닌, 유저가 지속적으로 선택해나가는 것을 상상하면서 만드시면 되겠습니다.
(위의 예제에서도 단순히 switch문만 사용하지만, 저것으로 인해 유저는 계속해서 말을 걸게 되겠지요.)
다음은 이벤트구현 부분입니다. 이벤트구현은 Set_Event함수를 통해 외부함수를 입력받을 수 있게 하였고, 그것을 인덱스값으로 구동/삭제할 수 있도록 하였습니다.
int NPC::Set_Event( void (*func) () )
{
for (int i = 0; i < 100; i++) // 0부터 99까지 배열을 검사
{
if (Event[i] == null) // 비어있으면
{
Event[i] = func; // 현재 이벤트를 입력
return i; // 인덱스 반환
}
}
return -1; // 오류
}
함수포인터는 예전 강좌에서 한번 등장한적이 있었습니다. 함수포인터는 포인터 자체를 가리키는 포인터로써 그것을 이용해 함수를 구동할수도 있다고 말씀드렸습니다.
이때 주의하실 점은 이때 사용하는 함수는 클래스 외부에 있는 것으로 판단하여 클래스 내부 변수를 전혀 읽어낼 수 없으므로, 이것에 대한 노출 함수를 만들던지, 이벤트 함수도 클래스로 정의해 friend로써 내부 인자를 열람할 권한을 주는 것도 좋습니다.
함수포인터는 역시 포인터와 동일하기 때문에 아래처럼 간단하게 실행 / 삭제가 가능합니다.
int NPC::Delete_Event(int index)
{
Event[index] = null; // 삭제
}
bool NPC::excute(int index)
{
Event[index](); // 실행
}
이렇게 외부 함수를 추가할 수 있도록 함으로써, 다양한 행동을 정의하여 다양한 NPC들이 각기 다른 반응을 보일 수 있도록 할수 있습니다.
사실 이렇게 외부함수를 이용하는 방법은 객체지향 관점에서는 피해야할 부분이기도 합니다만, 차차 강좌를 진행하면서 이런 장점을 살리면서도 객체지향적인 설계할 수 있도록 극복하는 방법도 알아나가실 겁니다.
지금까지 NPC의 기본적인 모습을 만들어보았는데 어떠셨나요. 사실 이번 강좌는 주제가 너무 애매하여 제가 생각해도 내용이 부실한 것 같아서 정말 부끄럽네요. 앞으로 더욱 분발하겠습니다.
다음시간에는 배열의 한계를 극복한 첫번째 노력, 연결리스트에대해 알려드리겠습니다.
사실 배열은 최대 크기가 고정되어있고, 조금만 실수하면 메모리관련 오류가 일어날 수 있어서 조심해야하는 부분이 있었죠. 다음시간에 배울 연결리스트는 그런 단점을 해소할 수 있는 좋은 대안이 될 것입니다.
그럼 다음시간에 뵙죠.
"[홀그렌] 나,난 딱히 니가 마음에 들어서 받아준건 아니라구..." ㅋㅋㅋㅋㅋㅋㅋ