레이디안

From YYpBD's MediaWiki

Jump to: navigation, search

레이디안 이러케 만들어따!!! - 0회  


레이디안 이러케 만들어따!!! - 0회



                                    deadfish@shinbiro.com


주위에서 이제 그만 강좌 업뎃할때도 되지않았느냐고 자꾸 물어봐서
(뭐 풍아저씨가 가장 집요했지만 -.-) 그래서 밥벌이와 조금 거리가 먼
게임 타이틀인 [레이디안]에 대한 잡담 겸 제작 후기 겸 소스분석을
해 볼까 합니다.
[레이디안]의 전 소스를 공개하려고 했지만, 나름대로의 저작권이 걸려있어서
법적으로 골치 아픈것은 싫기 때문에 일부씩 발췌해서 올립니다.
그러니 알아서 보시기 바랍니다.
대부분의 내용은 이미 앞선 강좌에서 말씀드린 부분과 같습니다.
그냥 쭉~~~ 읽어 보시기 바랍니다.


1. 제작의 기초단계

현재부터는 프로그래머의 입장에서 기술하겠습니다.
뭐 다른파트도 중요한것은 마찬가지지만 직업이 직업이기땀시

레이디안은 본인으로써는 프로그래머로는 처음으로 상용게임을 만드는
것이었기 때문에, 노하우도 전혀 없는 상태에서 모든지 맨땅에 헤딩하면서
몸으로 느끼고 만들었었던 타이틀입니다.

나름대로의 경험이 참 많았던 타이틀이었는데요...

일단, 처음 제작하려고 할때 다른 모든사람도 그렇듯이 매우 막막했었습니다.

과연 무엇부터 만들어야 하는것인가???

도움이 되었던 것은 과거 몇개의 공개게임을 만들면서 쌓은 경험이었는데요..
일단 [툴]을 제작해야 만들기 편하다는 것을 알고 있었던 것이 가장 큰
도움이었음다.

그럼 무엇부터 만들어야 하는가 하는 문제에 있어서..
결론은 [쉬운것 부터] 였답니다. 후후...

그래서 쉬운것 부터 만들었는데...
과연 쉬운것이 무엇이엇는지.. 다음 강좌에 실무적인 내용을...

그럼.












레이디안 이러케 만들어따 (1)  


레이디안 이러케 만들어따 ----- (1)



                                        deadfish@shinbiro.com


--- 쉬운것 부터 만들었다 ---

예. 쉬운것부터 만들었슴니다.

가장 단순한 에디티인, Tile Editor 를 만들었는데요..

타일 에디터를 만드는 법은 아래의 강좌를 뒤져보면 제가 쓴 부분이
있을겁니다.

가장 단순하게 그림을 불러와서 쪼개서 저장하는 것이엇죠.

처음엔 정말 단순하게 그림을 쪼갰었는데.
그다음에 추가한 기능이 같은 그림이 있을때 같은 그림을 소거하는
루틴이었습니다.

그후에 추가된 것이 0번 압축을 사용했었고요..

그럼 오랜만에 소스라도..^^;;;;


타일을 압축하는 루틴임다.
무척 간단하죠? ^^;;;;;;;

BYTE* CObject::Encode(BYTE* Temp)
{
BYTE* New;
int loop;  
int i, table;
short offset=0;
int y;
char pattern[1024];
BYTE* bTemp;
BYTE* copy;
char sb, pb;
bTemp = new BYTE[4096];  // 한라인을 만들기 위해 최대버퍼를 설정합니다.

for(y=0; y {
  for(loop=0, table=0; loop  {
   for(i=0; Temp[loop]==0 && loop// 0번색이 있다면 0번색의 갯수만큼 카운트합니다.
   if(loop>=XSize)break;
// 만약 카운트 한것이 가로길이보다 길다면 다 0번이므로 더이상 진행할 필요가
// 없죠.
   pattern[table++]=(char)i;
// 카운트숫자를 버퍼에 집어넣습니다. 이것은 0번의 숫자임다.
   for(i=0; Temp[loop]!=0 && loop// 그담엔 0번색이 아닌 색의 숫자를 카운트 합니다.
   pattern[table++]=(char)i;
// 오케~~~ 숫자를 알았으면 버퍼에 집어넣슴다.
  }
  bTemp[offset++]=table>>1;
// 패턴의 갯수를 저장합니다. 2로 나눈이유는
// 그냥 쉽게 하려고 한겁니다. 하여간 1라인에 들어가는 패턴의 수입니다.
// 이때 패턴은 0번색들과 0번이 아닌색들의 조합으로 이뤄집니다.
  copy = Temp;
// 두개의 패턴을 한번에 저장합니다.
  for(loop=0; loop  {
   sb = pattern[loop++];
   pb = pattern[loop++];
   bTemp[offset++] = sb;
   bTemp[offset++] = pb;
// bTemp에 0번색의 갯수와 0번색 아닌놈의 갯수를 저장함다.
   if(pb!=0)
   {
    memcpy(bTemp+offset, copy+sb, pb);
    offset+=pb;
   }
// 그담에 0번색 아닌놈들을 카피해 넣는거죠.
   copy+=sb+pb;
// copy는 원본이미지의 포인터이므로, 카피한만큼 증가시키는거죠.
  }
  Temp+=XSize;
// 한줄 끝났음다. 아휴~~ 힘들당.
}
New = new BYTE[offset+2];
*New = offset & 0x00ff;
*(New+1) = (offset & 0xff00)>>8;
memcpy(New+2, bTemp, offset);
delete bTemp;
// 새로운 버퍼에다가 현재 타일의 크기(전체 바이트수)를 헤더로 하는 새로운
// 구조체로 해야하는데 귀찮아서 그냥 배열에 쑤셔넣습니다. 훗.
return New;
}

오오~ 허접한 0번 압축 루틴입니다.
[레이디안]은 타일도 0번 압축을 사용해서 만들어 졌습니다.
이유는 단순하게 '타일에도 투명색을 집어넣어보자' 였습니다.^^;;
그래픽 출신이라 도트노가다를 최소한으로 줄이고자, 타일의 조합방식을 택한것이
었습니다.

그담에 볼 루틴은 같은 이미지가 있는지 검색하는 루틴입니다.
소스 왕 지저분입니다.


// 동일 타일 검색/삭제 함수
void CObject::Optimize()
{
BYTE* Src;
BYTE* Dest;
BYTE* Check;
int i, j;
int Del;
short iSrc, iDest;

// Object Tile Optimize
// 무지단순 루프돌면서 같은 타일있나 검색입니다.
for(i=0; i {
  Src = TileTemp[i];
  iSrc = (*(short*)Src);
  for(j=(i+1); j  {
   Dest = TileTemp[j];
   iDest = (*(short*)Dest);
// 두개의 크기가 같은 경우만 검사를 하는거죵.
// 타일의 첫 두바이트는 타일의 크기를 나타내기 때문입니다.
   if(iSrc == iDest)
   {
// memcmp를 무시하지 마시기 바랍니다. 상상외로 빠른 검색임다.
// 단, i와 j가 같으면 지우면 안되겠죠. 지우면 닭입니다.
    if((memcmp(Src+2,Dest+2, iSrc) ==0) && i!=j)
    {
     TileTemp.RemoveAt(j);
// 전체크기가 하나 줄었기 때문에 현재의 위치는 하나앞의 위치가 됩니다.
// 현재 자리가 지워졌기 때문이죠. 위의 TileTemp는 CArray 템플릿을
// 사용한겁니다.
     j--;
    }
   }
  }
}

// Append Data
// 이번에 자른 데이터들을 저장용 배열에 추가합니다.
TileArray.Append(TileTemp);
TileTemp.SetSize(0);
}

와~ 단순하죠?
원래는 더 복잡합니다만..
(에디터용 원본 참조테이블이 존재합니다.)
그러면 더 복잡해지기때문에 뺐습니다.
하여간, 타일 에디터는 예전에 나왓던 강좌정도의 난이도에서
이정도만 높아진 정도입니다.

서비스로 0번압축 타일 찍는 함수도 하나...

입력값은 가로,세로 위치, 타일번호, 찍힐 타겟의 가로,세로 크기. 찍힐 타켓포인터
이정도 입니다.

void CObject::PutTile(int x, int y, int num, int dXSize, int dYSize, BYTE* Dest)
{
if(x<0 || y<0 ||(x+XSize)>dXSize || (y+YSize)>dYSize )return;
// 타일이 화면밖에 찍힌다면 찍을 필요가 없겠죠?
int loop;
char skip, put;
char index;
BYTE* Src = TileArray[num];
Dest += y*dXSize+x;
Src+=2;
// 타일은 좌우 또는 상하 클리핑 따위는 안하고 무식하게 찍는검다.
for(loop=0; loop {
// index는 전체 패턴의 갯수라고 할수 있겠죠? (한 라인에서)
  index = *Src;
  Src++;
  skip=0;
  while(index--)
  {
// 0번의 갯수는 skip에 저장됩니당.
   skip+=*Src;
   Src++;
   put=*Src;
   Src++;
// 찍힐 타켓을 skip만큼 옮겨서 put 갯수만큼 찍어버립니다.
// 아주 단순하게 하면서 매우 빠른 출력속도를 나타내는 루틴이죠.
   memcpy(Dest+skip,Src,put);
   Src+=put;
   skip+=put;
  }
  Dest+=dXSize;
}
}


간단하게 0번 압축으로 타일 찍는 법을 알려드렷습니다.
저야 구시대 프로그래머라 이런 방식을 좋아하긴 합니다만, 이 강좌를
보시는 분들은 이런 구질구질한 방법 쓸필요없이 하드웨어 가속을
최대한 이용한 방법을 사용하시는 편이 나을겁니다.

그럴경우, 저장하는 포맷과 화면에 찍는 포맷이 달라서 변환해 주는 방법을
공부하셔야 할겁니다..

타일에디터는 그냥 pcx화일을 읽어서 잘게 쪼개는 역할만 햇기때문에
별다른 에디터와 인터페이스에 대한 사전 지식없이도 만들기 쉽더군요.

그런데 문제는 스프라이트란것을 만들때 였습니다.
우웅~ 스프라이트란 것이 [크기가 제한되지 않은 투명색을 가진 그림]이란
정의를 가진다고 정의하고 (물론 제 맘대로의 정의임다)

거기에 맞춰서 작업을 햇읍니다.

처음 부딪힌 문제는 마우스 컨트롤 문제였는데요..
좀 버벅이다가, 간단하게 플래그로 해결해 버렸습니다.^^;;

아래에 있는 부분이 마치 MFC로 짠것 같은 API 마우스 루틴 소스 코드입니다.

void CSprEdit::OnLButtonDown(int x, int y)
{
if(Pcx!=NULL)
{
  if(x>=Pcx->GetXSize()) x = Pcx->GetXSize();
  if(y>=Pcx->GetYSize()) y = Pcx->GetYSize();
  Sx = x;
  Sy = y;
  Ex = x;
  Ey = y;
  Click = TRUE;
}
}
왼쪽 버튼을 눌렀을때, 그림의 최대크기 바깥이라면 최대크기만큼만 하도록
조종하면서 누름 플래그를 켭니다.
자름 사각형의 크기는 이때 초기화 됩니다.(마우스 좌표로..)

void CSprEdit::OnLButtonMove(int x, int y)
{
if(!Click) return;
RECT rec;
rec.left = min(Sx, Ex);
rec.right = max(Sx, Ex);
rec.top = min(Sy, Ey);
rec.bottom = max(Sy, Ey);
rec.left -=1;
rec.right+=1;
rec.top-=1;
rec.bottom+=1;
if(x>=Pcx->GetXSize()) x = Pcx->GetXSize();
if(y>=Pcx->GetYSize()) y = Pcx->GetYSize();
Ex = x;
Ey = y;
InvalidateRect(m_hWnd,&rec,FALSE);
}

마우스가 움직이게 된다면, 계속 새로운 좌표를 받을수 있음다.
그렇다면? 지난번에 받은 좌표와 새로 받은 좌표를 비교해서
크기가 작은점을 시작점으로 큰점을 끝점으로 만듭니다.
물론 새로받은 점이 그림보다 크다면, 그림만큼만 되도록 합니다.
그리고나서 자름사각형이 화면에 보여야 하므로, 자름사각형보다
1만큼 크기가 큰 사각형으로 화면을 invalidate 합니다.


void CSprEdit::OnLButtonUp(int x, int y)
{
if(!Click)return;
if(x>=Pcx->GetXSize()) x = Pcx->GetXSize();
if(y>=Pcx->GetYSize()) y = Pcx->GetYSize();
Ex = x;
Ey = y;
RECT rec;
rec.left = min(Sx, Ex);
rec.right = max(Sx, Ex);
rec.top = min(Sy, Ey);
rec.bottom = max(Sy, Ey);
Click = FALSE;
InvalidateRect(m_hWnd,&rec,FALSE);
CutSpr(rec);
ShowSpr();
}

마우스 버튼이 올라가면 이젠 정말 그림이 잘려야 합니다.
CutSpr이 자르는 함수인데요..
내용은 위의 타일 압축함수와 같습니다.
마찬가지로 사각영역만큼 그림에서 메모리 카피한후에
압축하는 거죠.
그런후에 화면에 자른 그림을 보여줍니다.

뭐 지금 생각하면 단순하지만, 처음 접할때는 정말 막막하더군요.

스프라이트는 이외의 대부분의 기능은 아래에 있는 에디터 강좌에 나와있으므로
(사실 위의 내용도 다 소스에 있음다.)
구차한 설명은 뒤로하죠.

하여간, 에디터를 만들고 있는 겁니다.

그럼 다음시간에...








레이디안 이러케 만들어따 (2)  


레이디안 이러케 만들어따 ----- (2)



                                        deadfish@shinbiro.com


--- 편집툴을 만들자! ---

자르는 두개의 툴을 만들고나니, 이 툴으로 만들어낸 데이터들중 필요없는
데이터들을 잘라내고, 다른 화일에서 카피해오고, 화일을 append 하는 기능
등을 가진 에디터가 필요하단 사실을 알게 되었습니다.

아래 에디터 강좌에는 언급되지 않은 내용일지도 모르는데요..

그런 에디터가 필요하게 되어서, 부랴부랴, 타일에디터에도 원본화일처럼
보이게 하는 더미데이터를 추가하고, 스프라이트 에디터도 스프라이트를
복사하고, 붙이고, 잘라버리는 기능을 가진 에디터를 추가하게 되었습니다.

스프라이트 편집툴은 나중에 스프라이트가 중심점 좌표를 갖게 됨에 따라
중심점 좌표 편집기능까지 들어가게 됩니다.
(1.5버젼에서는 스프라이트 그림 편집까지 가능하게 되죠. -.-;;;;)

하여간, 이런식으로 자질구래한 에디터가 포함되기에 이릅니다.

타일에디터와 스프라이트 에디터들이 완성됨에 따라, 제작은 더욱 활기를
띄게 되어서, 편집툴의 최고봉!

[맵툴] 제작에 들어가게 됩니다.

레이디안의 설계사상은 적은 타일을 가지고 최대한 뽀다구 나게(!!) 였습니다.
그래서 다중레이어를 선택햇는데요..

이 다중레이어라는게, 별것 아니고, 그냥 포토샵에 있는 레이어와 마찬가지
기능을 하고 있습니다.

아래에 써놓은 RPG강좌나 맵툴 강좌에 중복되는 내용이 있기때문에,
가타부타 잔소리는 늘어놓지 않겠습니다만, 맵툴의 강력함이 매우 중요했었고
그럼으로써, 맵툴이 게임의 메인에디터의 기능을 수행하게 되어 버리고 말았습니다.

맵에 게임에 필요한 모든정보가 적용되었던 것이죠.

더나아가서, 맵에는 추가정보를 많이 저장했엇는데요..

일례로, 타일이름들(여러개의 타일을 읽도록 했엇죠.), 알파테이블 화일 이름
스프라이트 화일 이름, NPC화일이름 등, 게임에 필요한 (화면에 표시하는 모든)
화일들의 정보를 저장하게 됩니다.
즉, 맵 하나만 부르면 게임을 진행할 수 있게 해 놓은거죠.

맵에디터를 만드는 실전적인 부분은 아래의 맵에디터 강좌를 보세요.
나름대로 자세합니다.

자.. 나름대로 열심히 맵에디터를 만들었습니다.

이렇게 하니 어리버리 한달이 잘 지나가더군요.
그런데 여기서 난관이... -.-

맵에디터에 캐릭터를 박아 넣어야 했던 겁니다.
아래의 에디터강좌를 보시면, 단순히 맵에 타일과 스프라이트만을 찍는 것을
알수 있을겁니다.

캐릭터를 만들려고 보니, 스프라이트화일의 구조상 애니메이션을 처리하기엔
너무 단순한 구조라는 것이 판명되었습니다.
그냥 단순 배열구조였거든요.

그래서 결국 생각한 것이, 애니메이션을 에디팅하는 툴을 만드는 것이엇습니다.
간단한 애니메이터였죠.

애니매이션이란것이 대단한 것이 아니고, 해당동작에 해당 방향에 배열을 만들어서
배열안에 스프라이트의 번호를 주루룩~ 저장하는 방식이었습니다.

자~ 그러면 애니메이션을 저장하는 정보체가 과연 어떻게 생겼는지 잠시 소개를...

struct Mo{
CArrayCenter;
CArrayUp;
CArrayDown;
CArrayLeft;
CArrayRight;
CArrayLUp;
CArrayRUp;
CArrayLDown;
CArrayRDown;
char Flip[9];
};

#define Max  20  // 동작 가지수 (기본, 정지, 걷기 등)
#define FCen 0
#define FUp  1
#define FDown 2
#define FLeft 3
#define FRight 4
#define FLUp 5
#define FRUp 6
#define FLDown 7
#define FRDown 8

Mo Motion[Max];


뭐 이런식으로 생겼습니다.
Mo란 구조체에는 9방향(센터도 잇음니다, 그냥 넣엇엇죠.)의 각 애니메이션
프레임들이 저장된 배열이 있읍니다.
그리고 이것을 Max개만큼 가지고 있죠.
즉, 뛰기란 모션이 있으면 거기에 9방향이 있고, 그리고 그중에서 몇번째
프레임이 있다~.. 그리고 몇번째 프레임에는 스프라이트 번호가 있고,
그 스프라이트 번호를 참조하여 해당 스프라이트를 화면에 출력하면 되겠죠.

결국 이 애니메이션 화일은 하나의 스프라이트를 포함하고 있는 구조가 됩니다.
아웅~ 머리아프죠?

별것 없어보이는데 간단하게 3차원 배열이 되어버리죠^^
물론 2차원으로 만들고자 노력해서 2차원으로 보입니다.

참, 저기에 CArray가 또 보이네요. 참 한번 배운것 끝까지 쓰죠?
MFC에 보시면 나옵니다.
[레이디안]에서 유일하게 쓴 MFC구조체 입니다. ^^;;;

하여간, 배열의 일종인데, 그냥 포인터의 무한배열임다. 뭐 그렇게 아시고...
Flip이란 변수는 좌우 Flip을 하느냐에 대한 bool 변수입니다.
동작은 최대 20가지까지 되고 프레임길이는 제한이 없군요.

1.5버젼에서는 동작갯수의 제한을 없앴습니다.
물론 방향은 9방향으로 고정되어 잇습니다만.. 훗

애니메이션 에디터는 뭐 간단합니다. 대신 애니메이션 데이터를 저장하고 로드하는
루틴을 간단히 소개하고 애니메이션 에디터를 끝마치죠.


BOOL CChAni::OpenFile(char*AniName, BOOL f)
{
FILE* Fp;
short* Temp;
int iTemp1;
int i, j, k;

struct stcHeader
{
  char Name[16];
  char Flage[16];
};

stcHeader Head;
Fp=fopen(AniName, "rb");
if(Fp==NULL)return FALSE;
fread(&Head, sizeof(struct stcHeader), 1, Fp);
// 화일의 헤더입니다.
if(strcmp(Head.Name,"GAB Ani V1.4")!=0) return FALSE;
// 해당 동작의 효과를 지정하는데 그것을 읽는 군요.
fread(Effect, sizeof(char)*Max, 1, Fp);
// 반투명의 비율을 읽어옵니다. 이것도 동작마다 다릅니다.
fread(AlphaDeep, sizeof(char)*Max, 1, Fp);
// 20개의 동작이니 20개까지는 읽어야죠...
for(i=0; i// 맨처음 가운데 부터 읽겠죠? 순서가 그렇게 되니..
// 그럼 먼저 프레임의 길이를 읽습니다.
  fread(&j, sizeof(int), 1, Fp);
// 프레임이 있다면 읽는거죠.
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
// 하나씩 읽어서 배열에 추가합니다. 이것은 링크드 리스트로 해도 됩니다.
    Motion[i].Center.Add(Temp);
   }
  }
// 다음부터는 다~~ 똑같음다. 노가다 루틴이군요. 쩝
  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].Up.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].Down.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].Left.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].Right.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].LUp.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].RUp.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].LDown.Add(Temp);
   }
  }

  fread(&j, sizeof(int), 1, Fp);
  if(j!=0)
  {
   for(k=0; k   {
    Temp = new short;
    fread(&iTemp1, sizeof(int),1,Fp);
    *Temp = iTemp1;
    Motion[i].RDown.Add(Temp);
   }
  }
  fread(Motion[i].Flip, 9, 1, Fp);
}
// 이궁~ 여긴 현재 애니메이션의 이동속도를 나타냅니다.
fread(&Speed, sizeof(char),1,Fp);
// 이동할때거리인데, 이것 필요없더군요. -.-
fread(&Length, sizeof(char),1,Fp);
// 애니메이션과 연계된 스프라이트화일의 이름입니다.
fread(SprName,100,1,Fp);
fclose(Fp);
// 입력플래그가 참이면 게임용 루틴이므로, 디렉토리를 씨디로 합니다.
// 뭐 이런것은 몰라도 됩니당.
if(f)
  SprName[0] = AniName[0];
// 마지막으로 딸린 스프라이트화일을 읽습니다.
return LoadSprite();
}

저장은 읽는 루틴의 반대.. -.-;;;
아웅~ 머리아프죠? 노가다 루틴...

그런데 오늘 강좌는 조금 깁니다.
조금 더 나가보죠..

맵에다가 애니메이션을 찍을려고보니, 바로 애니메이션을 찍을수가 없더군요.
애니메이션은 순수 그림과 그에따른 움직임의 정보만을 가질뿐 정작 게임에
필요한 정보들은 가지고 잇지 않은겁니다.

여기서 기획서라는 것에 힘을 빌렸어야 했죠.
뭐 기획서라는 것이 별로 미비한 관계로 탁상공론과 회의끝에 몇가지가 결정되었
는데요.

맵에 박는 캐릭터는 주인공은 안박힙니다. 당연하게도 주인공은 한번 읽으면 끝이고
매번 맵이 읽힐때마다 새로 읽히는 놈들만 맵에 연관되게 되어서 맵이 읽힐때
같이 읽혀야 하지요.
그래서, 맵에 연관되는 캐릭터들은 몬스터(몹)과 NPC가 됩니다.
여기서 평화적으로 먼저 NPC 구조체를 알아보죵.

struct NPC
{

char AniName[100];  // 연계된 애니메이션화일 이름입니다.
int Schedule;       // NPC가 어떤식의 패턴을 가지게 될지를 결정하는 변수
int Script;   // 이 NPC와 대화하면 이 스크립트번호의 스크립트가
                        // 자동으로 호출되죠. 대화이벤트시 사용됩니다.
short ChunkNum;     // 이것들은 대부분 좌표정보입니다.
char Xpos;   //
char Ypos;
char SmallX;
char SmallY;
short AniNum;       // 애니메이션 동작번호임다. 저장되지는 않는 정보죠.
char iDirect;  // 방향
char Frame;   // 프레임!
int LastTime;  // 마지막으로 움직였던 시간.. 뭐 동기화에 사용됩니다.
BOOL Talk;   // 대화중인가? 대화중이면 멈춰있어라.
      // 대화할땐 움직이면 안돼겟죠?
CChAni* AniFile; // 애니메이션의 포인터임다.
  // 구조체 외부에서 로드하기때문에 포인터만 가지고 있게 됩니다.
char tChunk;  // 스크립트에 의해 강제이동하게 될때 사용되는
      // 목표지점입니다.
char tXpos;
char tYpos;
short Index;  // 강제이동등을 할때 해당 NPC를 다른 NPC와 구분해주는
      // 번호입니다. 중요한 NPC에만 사용되죠.
};

이정도로 데이터를 준비했답니다.
오오~ 물론 이후로도 게임을 작성하면서 추가된 부분은 꽤 됩니다만,
이정도면 게임 만드는데는 별 지장없더군요.

이정보들을 마치 엑셀처럼 에디트하길 바랬습니다만...
실력이 없다보니 그렇게는 못하고, 그냥 다이얼로그 기반으로 해서 간단하게
입력창을 만들어서 입력받는 형식으로 만들어 버렸답니다.
그렇게 하니 간단하더군요.
(덕분에 그후 에디터는 무조껀 다이얼로그 기반으로... -.-)

아웅~ 구조체는 가장 중요한것이 save/load임다.
그런데 위에있는 정보 저장 못할정도는 아니시겠죠? ^^;;;;;

에디터에서는 구조체에대해서만 주절거리고, 실제적으로 움직이는 부분은 게임에
들어가서 설명드리도록 하죠

NPC와 거의 동등하게 나오는 놈이 몹입니다.
NPC와는 다르게 몹은 조금 다른데요.. 뭐 구조체가 크게 다른것은 아닙니다만.
가장 큰 차이점이 동일하게 생긴 놈이 화면에 많이 나올수 있다는 점입니다.
NPC의 경우는 똑같이 생겼더라고 사실상 구조체는 완전 다르겠죠. 다 다른소리를
해야하니깐요.
하지만, 몬스터의 경우는 다릅니다.
동일하게 생기면 내용도 동일합니다. 약간의 난수로 데이터를 변경할수도 있지만
일반적으로 같은거죠.
즉, 마을에서는 그림이 같아도 A,B는 다른사람으로 만들어서 따로 찍어야 합니다.
하지만, 전투필드에서 몹은 A란 놈 하나만 있을경우 여러개의 구조체를 만들필요
없이 화면에 A만 찍어대면 되죠.
하지만, 화면에 찍힌만큼 구조체는 생성되어야합니다. 서로 독립적으로 움직이고
죽고 싸우기 때문이죠.
그래서, 게임상에서는 몹이 있는 맵을 읽을때는 몹을 화면에 찍힌만큼 복사하여
구조체를 자동생성하는 방법을 취합니다.

후우~ 말이 이상하게 꼬이네요.
하여간 그런건데요.. 몹은 그래서 NPC와는 조금 다르게 구조체를 가집니다.


struct MON{
char AniName[100];  // 애니메이션 이름이겠죠?
char Name[40];  // 몹에 이름도 지어줍시다^^
int ItemNum;  // 죽을때 아이템이 튀어나오면 정말 좋죠?
int Num;   // 이건 그냥 더미정보
short ChunkNum;  // 여기부턴 위치정보
char Xpos;
char Ypos;
char SmallX;
char SmallY;
short AniNum;  // 동작번호
char Frame;   // 프레임수
char iDirect;  // 방향
int LastTime;  // 동기화용 변수
int* ParaData;  // 몹의 세부데이터가 들어가는 배열
char Mode;   // 공격, 평상시, 피격등을 구분
CChAni* AniFile; // 애니메이션구조체 포인터
CArray MagicData; // 소지 마법
RECT rect;   // 충돌 사각형임다.
};

NPC와 별로 다른것은 없죠?
단지 ParaData란 형태가 있는데요.. 이것은 뭐랄까...
Hp/Mp등을 저장하는 배열입니다. 크기가 얼마가 될지, 에디터에서는
모르기때문에 일단 포인터로 지정해 놓은거죠.
저장할때는 Num이라는 변수에 잠시 크기가 저장됩니다.
로드된 후에는 전혀 필요가 없기때문에 더미정보로만 사용되긴 하죠.^^
물론 에디터에서는 사용됩니다.

다른 특이한 점은 충돌사각형이 존재한다는 점입니다.
왜 존재하는가? 하는 문제에 대해서는 간단히 대답해 드릴수 있는데
없는 NPC가 이상한거다! 란 거죠^^
NPC의 경우 거대한 사람이 등장하지 않기때문에 그냥 자동생성이 가능한겁니다.
하지만, 몹의 경우 거대몹이 등장하죠.
[레이디안]을 해보시면 알겠지만, 데몬같은경우는 꽤 큼지막합니다.
그래서 충돌사각형이 따로 저장됩니다.
처음에는 직접 사각형을 에디팅하려고 했었는데요.. 결국은 귀찮아서
이것도 자동생성 해버립니다. 훗.

뭐 이정도 되겠네요. 이런정도를 가지고 맵을 만들게 됩니다.

다음시간에는 기타 게임데이터를 만드는 에디터란것이 있는데 그걸 소개하죠.











레이디안 이러케 만들어따 (3)  


레이디안 이러케 만들어따 ----- (3)



                                        deadfish@shinbiro.com

--- 기타등등 기타등등 ----

후우~ 안되는 말빨 세우느라 정말 힘듭니다.
개 썰렁 강좌도 어언 3회입니다.
이런 페이스라면 언제 끝날지 모르겠습니다.
그래도 천천히 진행할랍니다. -.-;;;


자~ 오늘은 게임에 사용되는 여타 기타등등의 데이터를 편집하는 툴을 만든 얘기를
하겠습니다.

기타툴은 사실상 따로 필요가 없는 것 같기도 하지만, 여러모로 편리하더군요.
첫번째 말씀드릴 것이 파라미터 에디터입니다.

간단합니다. 파라미터를 에디트 하는 겁니다.
무한배열, 즉 링크드리스트인데요.. 형태는 아래와 같죠.

struct PA{
char Name[80];
int iMin;
int iMax;
int iUse;
};

위와 같은놈을 여러개 연결해서 파라미터들을 관리하는 겁니다.
이름은 그냥 Hp, Mp 이런거고요..
min/max는 최소/최대값이겠죠.
use란 수치는 더미인데, 레이디안에서는 해당 파라미터의 수치를 올릴때,
사용되는 경험치로 사용되었답니다.
레이디안을 해보신 분이라면, 어떤식으로 쓰였는지 상상이 되실겁니다.

이런식으로 수치들을 데이터로 외부화일로 뺀이유는 무척 간단합니다.
코딩하기 싫어서였죠. -.-;;
테스트해보면서 계속 수정되는 것이 위와같은 수치엿는데요.. 그걸 코딩해버리면
아주 골치아파지더군요.
거기다가, 파라미터의 숫자도 유동적이라, 몬스터나, 마법, 아이템, 주인공등에서
사용되는데, 이것들이 언제 바뀔지 몰라서 말이죠.. 훗.
(뭐 이게 확실한 설계와 준비, 기획 없이 프로그래밍을 하게되면서 고생한 점이라고 할까나요? 기획이 완료되기전에 에디터프로그래밍을 끝냈기 때문에..^^)

위의 파라미터들을 포함한 것이 아이템과 마법입니다.

struct Items{
char Name[40];
char Comment[100];
int SprNum;
int Num;
int* UpArray;
};

이름과 스프라이트같은것이 나오죠?
그거야 뭐 화면에 아이템의 이름이 나와야 하고, comment라고 설명도 나와야 하고
(게임에 화면 하단에 설명이 나옵니다.^^)
스프라이트번호는 화면에 찍히는 아이템 그림이고요...Num는 파라미터의 갯수죠
에디팅할때, 파라미터화일의 정보가 바뀌어서 파라미터 갯수가 바뀌면 이것과 비교해서 UpArray의 크기를 변화시키는 겁니다.
UpArray는 파라미터들이 들어가는 곳의 포인터죠. 그냥 숫자들이 주루룩 들어가긴 합니다만^^
에디터에서는 숫자만 넣고, 게임에서는 그걸 코딩해서 사용하긴 해야합니다.
게임이야 기획이 끝나야 만드는 것이기때문에 게임마다 특화시킬 필요가 있지만,
에디터는 확장성 위주라서 데이터 형태 자체도 무언가 개선의 여지가 많이 남아있는
형태가 되어버리는 군요.

struct MAG{
char Comment[100];
char Name[40];
int Num;
char iMotion;
char iDirect;
int* ParaData;
};

마법도 매 한가지 입니다. 대신 마법은 애니메이션이 되기때문에 애니메이션화일을 불러서 사용합니다.
동작과 방향을 가지고 있으면, 최대 20*9가지의 마법을 한화일에서 처리할수있는거죠.
레이디안에는 180개 이하의 마법이 나왔기 때문에 다행히도 화일 하나만 사용할수 있었습니다.

후우~ 데이터형태 다 알려드려버렸습니다.
레이디안 데이터 변형해서 게임하나 새로 만드실수 있겠군요. 흐흐흐.

에디터에서 가장 중요한것이 데이터형태이고요.. 물론 기획하시는 분들이 머리깨지는 부분도 데이터형태입니다.
물론 프로그래머와 생각하는 방향이 다르긴 합니다만.^^
기획자가 계속적으로 데이터를 수정하면서 게임을 진행해 볼 수 있도록 에디터를 만들어야 편합니다.

그래야 기획자가 '저기요~~ 이거 너무쎈데 약하게 해주면 안될까염?' 하는 따위의
말을 안들을수가 있지요. 에디터 사용법 알려주고 알아서 고치게 하면 되거든요.^^
레이디안때는 제가 직접 데이터를 입력해서 별로 소용이 없었지만,
(워낙 제작인원이 적다보니...)
씰때는 다 시켜먹었답니다. '아~ 그거 알아서 고쳐서 해요~~~'
아주 편하더군요. 훗.

덤으로 주인공 데이터도 한번 써볼려고 지금 소스를 보니 장난아니게 지저분한
클래스군요. -.-;;;
다른것은 다 구조체인데, 이것만 우째 클래스다냐... 하여간, 이런 데이터들을 만드는 에디터들을 만들면서 한 일이 무엇이냐~~ 하면
처음부터 간단한 통합환경을 염두에 두고 제작을 한것입니다.

대부분의 툴은 스프라이트 툴 따로, 맵툴 따로, 타일 툴 따로 만드는게 보통입니다.
솔직히 화일을 여러개 만드는 것이 좀 귀찮아서 화일 하나에다가 다넣어버렸습니다.
즉, 큰 창이 하나 열리고, 모든 에디터는 그창의 차일드형태로 되도록 만든거죠.
물론 이렇게 하다보니, MFC로는 불가능해서(물론 가능도 하지만, 실력이 없는 증거죠) 그냥 간단하게 API로 작업을 했습니다.

기본적인 컨셉은 8용신전설 제작당시에 밉스소프트웨어에서 만들었던 RPG메이커였고요.. 여담이자만, RPG메이커는 상용으로 판매했더군요.
제가 만든 GabEditer도 좀만 손보면 상용이 될수도 있단 소리로 들리긴 햇었는데
소스를 한번 ㅎㅜㅀ어보고 버그때문에 포기했엇다는..^^;;;;

하여간, 그담에 만든것이 게임을 진행하는데 가장 필요한 스크립트 에디터였습니다.
물론 만들당시에는 레이디안이 설마 리얼타임 스크립트를 사용하랴~ 하고 만들엇는데, 결국에는 리얼타임 스크립트를 사용하더군요. 게임 메인프로그래밍할때 그것으로 또 고생좀 햇었죠.^^

일단, 전 스크립트형식을 [예약어] [파라미터] [문자열] 의 형태로 하기로 했습니다
왜냐? 라고 물으면, 별 생각없이 그렇게 한겁니다.
C문법을 그대로 하면, 스크립트를 짜는 사람이 상당히 어려워 할것 같기도 했고
뭐 그런식으로 어렵게 만들필요가 없다고 생각했기 때문이죠.
(뭐 실력도 없긴 합니다만 말이죠. 쩌비)

// 예약어 초기화
#define _IF_THEN   1 // 조건_시작
#define _ELSE_THEN   2 // 아니면
#define _IF_END    3 // 조건_끝
#define _SCRIPT_START  4 // 스크립트_시작
#define _SCRIPT_END   5 // 스크립트_끝
#define _FLAG_ON   6 // 플래그_켜기
#define _FLAG_OFF   7 // 플래그_끄기

자~ 일단 간단하게 예약어를 정의했음다.
다른 사람들 같으면 예약어와 포맺자체도 무언가 에디터를 사용해서 만들겠죠.
그렇지만, 레이디안에서는 노가다 코딩의 진수!
그냥 정의 했음다^^

컴파일이란것을 햇는데요.. 컴파일이란게 뭐 그리 어려운것은 아니더라고요..
일단 한줄을 읽은후에 의미별로 단어를 자르고, 그걸가지고 예약어와 비교한후
같다면, 형식에 맞춰서 구조체에 넣고.. 그리고 나서 그것들을 저장하면 되더군요.


int CScript::GetString()
{
CurPos = 0;
if(InText==NULL)return -1;
char ch;
int pos;
pos = 0;
while(1)
{
  if(feof(InText))return -1;
  ch = fgetc(InText);
  if(ch==0x1a)return -1; // EOF이면 종료
  if(ch==0xD)break; file://한줄끝이면 리턴
  Line[pos++]=ch;
}
ch = fgetc(InText);
Line[pos]=NULL;
return strlen(Line);
}

짜자자자장~ 짜장... -.-;;; 한줄을 읽는 함수입니다.
그냥 fgets로 하셔도 상관없습니다. token 이란 함수도 존재합니다.
하지만, 전 그당시 그걸 몰랐기 땀시 이런 노가다 코드를.....
배.껴.서. 사용했던 것입니다! 무하하. -.-;;;
영진님 스크립트강좌에 나온내용을 뽀려서 만든거죠. 비슷할겁니다.

다음은 한줄 읽은것을 분해하는 함수임다.


int CScript::Skip()
{
int i;
int len = strlen(Line);
for(i=CurPos; i CurPos=i;
return i;
}

필요없는것을 스킵하는 함수군요. 32라면? 스페이스, 공백문자죠? 그런거면
넘어가 버리는 거죵. tab도 처리했던것 같은데.. 그건 seal에서 한것 같군요.

int CScript::GetWord(char* Word)
{
int i,p=0;
BOOL Flag = 0;
int len = strlen(Line);
Skip();
for(i=CurPos; i {
  if(Line[i]=='"' && Flag==0)
  {
   Flag=1;
   i++;
  }
  else
  {
   if(Line[i]=='"' && Flag==1)
   {
    Flag=1;
    i++;
   }
  }

  if(Line[i]==',' && Flag==0)
  {
   i++;
   break;
  }
  if(Line[i]==' ' && Flag==0)
   break;
  Word[p]=Line[i];
}
Word[p]='\0';
CurPos = i;
return p;
}

대강 소스를 보시면 알겠지만, 의미별로 스트링을 자르는 루틴입니다.
Word에 넣어서 리턴하죠.
리턴값은 길이...
뭐 맴버변수들로 대부분 처리해서 리턴값은 무의미 하긴 합니다.

"란 부호를 만나면 다시 "부호를 만나기전까지 계속적으로 버퍼에 문자를
카피해 넣고요.. 아니면, ,부호나 공백문자가 나오면 리턴해버립니다.
뭐 그런 구조의 함수임다. 간단! 아싸~ 깔끔 깔끔~ 깔끔 깔끔~(깔끔요정)

대강 몇개로만 줄여본 컴파일 함수입니다.
int CScript::Encode()
{
int Top = 0;
char Word[100];
char Number[10];
char Strings[100];
char* Temp = NULL;
Command cTemp;
cTemp.String=NULL;
cTemp.SubScript=0;
int LineNum=0;
int iTemp;
while(1){
  memset(Line, 0, 200);
  memset(Word, 0, 100);
  memset(Number, 0, 10);
  memset(Strings, 0, 100);
  if(GetString()==-1)break;
  LineNum++;
  cTemp.SubScript=0;

  GetWord(Word);
  if(strcmp(Word,"조건_시작")==0 || strcmpi(Word, "if_then")==0)
  {
   cTemp.SubScript = _IF_THEN;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 참조플래그번호
   GetWord(Number);
   cTemp.Para[1]=atoi(Number); // 구문 번호
  }

  if(strcmp(Word,"아니면")==0 || strcmpi(Word, "else_then")==0)
  {
   cTemp.SubScript = _ELSE_THEN;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 구문번호
  }

  if(strcmp(Word,"조건_끝")==0 || strcmpi(Word, "end_if")==0)
  {
   cTemp.SubScript = _IF_END;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 구문번호
  }

  if(strcmp(Word,"스크립트_시작")==0 || strcmpi(Word, "script_start")==0)
  {
   cTemp.SubScript = _SCRIPT_START;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 스크립트 구분번호
   Top++;
   if(CheckNumber(SCommend, Top, cTemp.Para[0]))return LineNum;
   SCommend[Top]=cTemp.Para[0];
  }
  if(strcmp(Word,"스크립트_끝")==0 || strcmpi(Word,"script_end")==0)
  {
   cTemp.SubScript = _SCRIPT_END;
  }
  if(strcmp(Word,"플래그_켜기")==0 || strcmpi(Word,"flag_on")==0)
  {
   cTemp.SubScript = _FLAG_ON;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 켤 플래그번호
  }
  if(strcmp(Word,"플래그_끄기")==0 || strcmpi(Word,"flag_off")==0)
  {
   cTemp.SubScript = _FLAG_OFF;  // 끌 플래그번호
   GetWord(Number);
   cTemp.Para[0]=atoi(Number);
  }

  if(cTemp.SubScript==0)
  {
   CloseTextFile();
   CloseSaveFile();
   return LineNum;
  }
  int i;
  fwrite(&cTemp.SubScript,sizeof(short),1,OutScp);
  for(i=0; i<5; i++)
   fwrite(&cTemp.Para[i], sizeof(int), 1, OutScp);
  if(cTemp.String==NULL)
  {
   i=0;
   fwrite(&i, sizeof(int), 1, OutScp);
  }
  else
  {
   i=strlen(cTemp.String);
   fwrite(&i, sizeof(i), 1, OutScp);
   fwrite(cTemp.String, i, 1, OutScp);
  }

  if(Temp!=NULL)
  {
   cTemp.String=NULL;
   delete Temp;
   Temp = NULL;
  }
}
CloseTextFile();
CloseSaveFile();
return 0;
}

와~ CloseTextFile 은 그냥 화일 닫는 겁니다.
별거 아니니 신경쓰지 마시고..
Command cTemp;
란게 있죠?
그럼 Command가 어떤 구조체인지만 알아보고 스크립트 컴파일은 마칩니다.


struct Command
{
short SubScript;
int Para[5];
char* String;
};

간단하죠? 명령어, 파라미터 3개, 문자열 포인터!
아싸~ 깔끔 깔끔~ 깔끔 깔끔~


그럼 오늘은 이만. -.-;;;;;;









레이디안 이러케 만들어따(4)  


레이디안 이러케 만들어따 ----- (4)



                                        deadfish@shinbiro.com

---- 실행모듈을 만들자~ ----


지난번엔 작업용 에디터를 만드는 부분까지 소개를 했을껍니다.
일단, 작업용 에디터를 만들고 나서부터, 세부기획작업에 들어갔습니다.

실제적인 데이터는 에디터를 통해서 제작되지만, 실행모듈은 각각의
게임에 대해 특화될 부분이 있습니다.

예를 들어, 파라미터가 int배열이므로, 여러개의 수치가 들어갑니다.
하지만, 그 수치들이 0번위치에 있는 수치가 무엇을 의미하는지는
각 게임에 따라 다를수 있습니다.
에디터는 어느게임을 만드느냐에 따라 약간의 수정만을 가하면 되지만,
실행모듈은 그렇지 않더군요.

이번 강좌는 실제적인 프로그래밍 보다는 기획서와 기획과정에 대해서
알아보는 편이 나을거라고 생각합니다.
그리고 기획을 프로그래밍으로 옮기는 과정을 설명드리지요.

레이디안은 공식적인 기획문서가 확실히 정립되어서 만든 게임은
아닙니다.
물론 에디터를 열심히 만들고있을때, 지원사를 찾으려고 만든게 있긴
합니다만, 그중에 일부를 발췌해서 게임에 사용했던 걸로 기억합니다.

레이디안의 첫 기획은 어쩌면 퇴마전설과 같은 시뮬레이션 형식의
전투방식이었습니다.
하지만, 여러가지 문제로 인해서 결국은 액션 RPG로 귀결 되었지요.
(그때 그 기획안을 밀어붙였으면 어떻게 되었을까.. 재미있게 되었을지도
모르지요. 헤헤..^^;;)

기획서를 쓰는 방식에 대해서 잠깐 설명을 드리자면,
제가 같이 일한 팀에서는 (가람과바람이겠죠? ^^)일단 기획회의를 전체적으로
하지요.

전체적인 회의가 끝나면 회의내용을 정리하고 전체회의에서 대강 대명제로
정해진 부분에 대한 세부내용을 각각의 담당자가 정리합니다.
이때, 정리한 부분에 대한 데이터는 전체회의에서 정해준 수치에 따릅니다.
전체회의에서 정할 부분에 대해서 잠시 말씀드리자면...

게임제목 (-.-)
주장르 (복합장르일 경우가 많으니까요)
참고로 할 게임 (어느부분은 무슨겜, 어느부분은 무슨겜..)
게임의 주요 재미요소 및 부각시킬 부분
(레이디안의 경우 액션적 요소였죠.)
게임에 사용되는 마법의 전체적인 가짓수와 분류방법
(확산형, 발사형으로 나누었습니다.)
아이템의 대강의 가짓수와 분류방법
(먹는 아이템, 입는 아이템, 기타 이벤트 아이템 등등이겠죠?)
주인공의 파라미터(Hp/Mp 등등.. 여러가지죠?)
게임 특유의 시스템 (레이디안의 경우 레벨업 시스템이 특색있었죠^^)

이정도가 정해지면, 각 파트별로 나눠서 세부적인 것을 정합니다.
마법의 종류와 모양/방법/세부파라미터 등을 정하는 거죠.
근데, 레이디안은 이 세부적인부분에 대한 사항을 정리하지 못하고
그냥 막바로 작업에 들어갔습니다.
작업을 하면서 차츰 작업을 했는데요.. 이부분은 참 아쉬운 부분이었죠
체계적으로 기획을 하고나서 작업을 진행시켰으면, 좀더 풍부하게
사용할수있는 많은 아이템을 사장시켜버렸거든요.
(참고로 레이디안엔 유니크아이템만 300여가지 등장합니다.)

기획이 부실함에 따라, 재미요소와 부각시킬 부분에 대한 확실한 부각요소가
부족하게 되는 것은 필연적인 부분이더군요.
여러분도 나름대로 기획할때 부각시킬부분에 대해서 확실한 차별성을
두도록 고려하십시요.

게임의 묘미는 부각과 간소화 그리고 다양함의 추구입니다.
서로 대치되는 것 같지만, 이 세요소가 적절히 어울려졌을때,
안정감있는 기획과 재미있는 게임이 나옵니다.
(Seal도 기획서가 없기는 마찬가지였지만,위의 세요소에 대한 법칙을
충실히 지켰기때문에 인기는 없어도 그나마 재미는 있는 게임이 나왔었죠.)

하여간, 기획에서 정해진 부분에 대한 세부데이터를 가지고 에디터작업에
들어갑니다.
실제로 제일처음 입력하는 부분은 파라미터에 대한 부분입니다.

// item parameter define
#define _IPidentify  0
#define _IPbulletnum 1
#define _IPbulletspeed 2
#define _IPbulletlength 3  // 사정거리 도트단위
#define _IPattack  4
#define _IPdefence  5
#define _IPm_identify 6
#define _IPhp   7
#define _IPmp   8
#define _IPki   9
#define _IPintel  10
#define _IPweight  11

자~ 이것이 레이디안에 사용된 아이템의 파라미터입니다.
기획자가 파라미터의 갯수와 작용을 정하고, 에디터로 편집을 하게됩니다.
그럼 그것을 참고로 프로그래머는 실제적으로 define 작업에 들어갑니다.
우웅~ 머리아프죠?
이렇게 정해놓고 각각의 수치를 어떻게 사용하는지는 직접 코딩해야 합니다.
이 각각의 파라미터의 작용을 앞으로 모든 게임에서 같게 한다고 하신다면^^
한번 만들어놓고 끝날텐데, 사실 게임이 그렇지 못하기때문에
(발더스 게이트가 아니지 않습니까???) 이부분은 매번 바뀌는 부분입니다.


#define _MPmotion 0   // 애니메이션 모션
#define _MPnum  1   // 총알번호
#define _MPdirect 2   // 총알방향수
#define _MPspeed 3   // 속도
#define _MPlength 4   // 이동거리 도트단위
#define _MPdamage 5   // 데미지
#define _MPidentify 6 // 속성
#define _MPclass 7 // 종류
#define _MPusemp 8
#define _MPsound1 9 // 소리
#define _MPsound2 10 // 소리

이건 마법의 파라미터... 아이템과 비슷하지만 조금 틀리죠??? ^^

이런식으로 집어넣는데요.. 주인공의 경우도 이렇겠죠?
뒤쪽에 있는 번호는 뭐냐고요?
숫자로 정렬되어있죠? int* ParaData 변수에서 이것이 배열이기때문에
배열안의 값을 말하죠. 즉 총알번호를 알려면 ParaData[_MPnum] 하면
되는거죠. 결국은 알아보기 편하게 하는 것밖에 의미가 없습니다만..
가독성도 프로그래밍에서 중요한 부분이기에^^;;;;

// Hero Parameter Define
#define _HPfacenum  0
#define _HPhp   1
#define _HPhp_max  2
#define _HPmp   3
#define _HPmp_max  4
#define _HPki   5
#define _HPki_max  6
#define _HPexper  7
#define _HPstamina  8
#define _HPstamina_max 9
#define _HPsack   10
#define _HPluck   11
#define _HPleftstr  12
#define _HPrightstr  13
#define _HPskill  14
#define _HPdefence  15
#define _HPattack  16
#define _HPintel  17
#define _HPconcen  18
#define _HPfame   19
#define _HPjob   20
#define _HPai   21
#define _HPatt_plus  22
#define _HPdef_plus  23

주인공의 파라미터 디파인.. ^^;;;
주인공은 꽤 많군요. 레이디안은 원래 파라미터가 많이 쓰이는 게임이었습니다
처음부터 액션게임으로 생각되어진것이 아니고, 시뮬적인 요소를 많이
가지고 있었기때문에.. 나중에 기획을 바꿀때 생각이 엉겨서 미쳐 간소화를
못한거죠.^^;;
(간소화하려고 해도 시나리오와 맞물리는 것이 꽤 되다보니.. 쩝)

이번강좌는 짧게 여기까지 하기로 하죠..
세미나 준비도 해야하고.. 개인적인 문제도 좀있고 해서..
간만에 강좌를 쓴주제에 말이 많군요. -.-;;;

다음은 실제로 에디터를 제외하고 게임을 만들어 나가도록 하죠.
하나하나 차근 차근.^^

그럼.







레이디안 이러케 만들어따(5)  


/******************************************

정말 간만의 강좌입니다. 세미나 등등해서 정신이 없었던 관계로..

빨리 완성되어야 할텐데요... 흠흠.

****************************************/



레이디안 이러케 만들어따 ----- (5)



                                        deadfish@shinbiro.com

----- NPC를 움직여 보자! --------

레이디안이 첫 상용타이틀이다보니 나름대로 주의깊게 프로그래밍을
하게 되더군요.
그래서, 처음부터 차근차근 하는 의미로 스크롤과 캐릭터이동부터
작성을 하게 되었습니다.

맵을 읽고 쓰는 루틴은 이미 에디터에 있었기 때문에 별 문제가 되지
않았습니다.
다만, 루틴의 문제상, 링크된 화일의 디렉토리까지 전부 저장하는 관계로
메인을 작성할때 디렉토리 또는 드라이브명을 없애는 루틴을 만들 필요가
있었습니다.

그래서, 모든 읽는 루틴을 두가지로 나눠서 작성하게 되었습니다.

또 한가지 문제는 맵에 저장되는 몬스터와 NPC때문이었는데.. 이들을
한꺼번에 리스트화해서 사용하기 때문에, 맵마다 필요한 화일이 다른경우가
많았습니다.
이때, 필요없는 화일까지 로드하는 것은 에디트할때는 편리하지만,
실제게임에서는 속도저하의 요소가 되기때문에 어쩔수 없이 루틴을 수정해야
했습니다.

다음은 그런식으로 수정된 몬스터 및 NPC를 읽는 루틴입니다.
화일이름을 입력받는 것은 동일 드라이브에서 읽어야 하기때문에 화일이름의
제일 첫번째 문자 (드라이브명)이 필요하기 때문이지요.

// 게임용 Npc/Monster list 읽는 함수
// Npcmap에는 각 Npc나 Monster의 종류번호가 저장되기 때문에
// 같은 종류의 Monster가 두마리 있을 경우 이상한 움직임을 보이게 된다.
// 이를 방지하기 위해 임시저장소에 Npc/Monster리스트 화일을 읽은후에
// 중복 카피해 주는 방법으로 게임용 Array를 만든다.
// 또한 사용되지 않는 애니메이션 화일은 읽어 들이지 않게된다.
void CChunkmap::gLoadNpc(char* fName)
{
CNpclist* NpcTemp;
CMonster* MonTemp;
NPC* nTemp;
MON* mTemp;
MON* mTemp2;
BYTE* Flags;
CChAni* Temp;
char* Name;
int* iTemp;
int i, End;
int j, k, l, m;
IDAT* itTemp;
Chunk* ChunkTemp;
if(Flag) // 일반맵일 경우
{
  NpcTemp = new CNpclist;  // 임시저장소에 메모리 할당
  if(Npclist!=NULL)
   delete Npclist;
  Npclist = new CNpclist;  // Npclist에 메모리를 할당
  if(!NpcTemp->LoadFile(fName)) // 임시저장소에 화일의 내용을 읽는다.
  {
   delete Npclist;
   Npclist = NULL;
   return;
  }
//  memcpy(NpcName->Name, fName, 200);
  End = NpcTemp->GetSize(); // Npclist의 총갯수
  Flags = new BYTE[End];  // Npclist중에 사용유무를 체크하기 위한 행렬
  for(i=0; i<End; i++)
   Flags[i]=FALSE;
  NpcAniArray.RemoveAll();
  NpcAniArray.SetSize(End); // Npc용 애니메이션 저장소의 갯수만큼의 메모히 할당
  l=0;
  for(i=0; i<ChunkArray.GetSize(); i++)
  {
   ChunkTemp = ChunkArray.GetAt(i); // 청크 루프
   for(j=0; j<ChunkX; j++)    // Y 루프
   {
    for(k=0; k<ChunkY; k++)   // X 루프
    {
     if(ChunkTemp->Npcmap[k][j]!=-1) // 루프를 돌면서 Npc맵에 Npc가 있는지 확인
     {        
      nTemp = new NPC;
      memcpy(nTemp, NpcTemp->GetAt(ChunkTemp->Npcmap[k][j]), sizeof(struct NPC));
      // 사용되는 Npc를 임시저장소에서 Npclist에 메모리 카피한다.
      nTemp->ChunkNum = i;
      nTemp->Xpos = j;
      nTemp->Ypos = k;
      nTemp->AniNum = ChunkTemp->Npcmap[k][j];
      nTemp->iDirect = FDown;
      nTemp->RealX = (((i%XSize)*ChunkX + j)<<5)+nTemp->SmallX;
      nTemp->RealY = (((i/XSize)*ChunkY + k)<<5)+nTemp->SmallY;
      Flags[ChunkTemp->Npcmap[k][j]]=TRUE;
      Npclist->Add(nTemp);
      ChunkTemp->Npcmap[k][j]=l; // Npclist번호 세팅
      l++;
     }
    }
   }

  }
  for(i=0; i<End; i++) // 사용하는 Npc의 Ani화일만 선별해서 읽음
  {
   if(Flags[i])
   {
    Temp = new CChAni(AlphaTable);
    Name = NpcTemp->GetAt(i)->AniName;
    Name[0] = fName[0];
    if(Temp->OpenFile(Name,TRUE))
     NpcAniArray[i] = Temp;
   }
   else
   {
    NpcAniArray[i] = NULL;
   }
  }
  NpcAniNum = End;
  End = Npclist->GetSize();
  for(i=0; i<End; i++)
  {
   nTemp = Npclist->GetAt(i);
   Npclist->SetAni(i, NpcAniArray.GetAt(nTemp->AniNum));
  }
  delete Flags;
  delete NpcTemp;
}
else
{
  MonTemp = new CMonster;
  if(Monlist!=NULL)
   delete Monlist;
  Monlist = new CMonster;
  if(!MonTemp->LoadFile(fName))
  {
   delete Monlist;
   Monlist = NULL;
   return;
  }
  End = MonTemp->GetSize();
  Flags = new BYTE[End];
  for(i=0; i<End; i++)
   Flags[i]=FALSE;
  NpcAniArray.RemoveAll();
  NpcAniArray.SetSize(End);
  l=0;
  for(i=0; i<ChunkArray.GetSize(); i++)
  {
   ChunkTemp = ChunkArray.GetAt(i);
   for(j=0; j<ChunkX; j++)
   {
    for(k=0; k<ChunkY; k++)
    {
     if(ChunkTemp->Npcmap[k][j]!=-1)
     {
      mTemp = new MON;
      mTemp2 = MonTemp->GetAt(ChunkTemp->Npcmap[k][j]);
//      memcpy(mTemp, MonTemp->GetAt(ChunkTemp->Npcmap[k][j]), sizeof(struct MON));
      memcpy(mTemp->AniName,mTemp2->AniName, 100);
      memcpy(mTemp->Name, mTemp2->Name, 40);
      mTemp->ItemNum = mTemp2->ItemNum;
      mTemp->Num = mTemp2->Num;
      iTemp = new int[mTemp2->Num];
      memcpy(iTemp, mTemp2->ParaData, sizeof(int) *(mTemp2->Num));
      mTemp->ParaData = iTemp;
     // 금전에 대한 랜덤성 추가.
      if(mTemp->ItemNum==-1)
      {
        mTemp->ParaData[_MONPmoney]+=(rand()%20);
      }
      // 경험치에 랜덤추가.
      mTemp->ParaData[_MONPexper]+=(rand()%10);
      // 마법 데이타 추가하기. 줴엔장.
      for(m=0; m< mTemp2->MagicData.GetSize(); m++)
      {
       itTemp = new IDAT;
       itTemp->iIndex = mTemp2->MagicData.GetAt(m)->iIndex;
       itTemp->iCost = mTemp2->MagicData.GetAt(m)->iCost;
       mTemp->MagicData.Add(itTemp);
      }
      mTemp->DMode = _MDNormal;
      mTemp->ChunkNum = i;
      mTemp->SmallX =0;
      mTemp->SmallY =0;
      mTemp->Xpos = j;
      mTemp->Ypos = k;
      mTemp->LastTime = 0;
      mTemp->Mode = _MFStand;
      mTemp->Frame=0;
      mTemp->AniNum = ChunkTemp->Npcmap[k][j];
      mTemp->iDirect = FDown;
      mTemp->RealX = ((i%XSize)*ChunkX + j)<<5;
      mTemp->RealY = ((i/XSize)*ChunkY + k)<<5;
      mTemp->DMode = _MDNormal;
      Flags[ChunkTemp->Npcmap[k][j]]=TRUE;
      Monlist->Add(mTemp);
      ChunkTemp->Npcmap[k][j]=l;
      l++;
     }
    }
   }

  }
  for(i=0; i<End; i++)
  {
   if(Flags[i])
   {
   Temp = new CChAni(AlphaTable);
   Name = MonTemp->GetAt(i)->AniName;
   Name[0] = fName[0];
   if(Temp->OpenFile(Name,TRUE))
    NpcAniArray[i] = Temp;
   }
   else
   {
    NpcAniArray[i] = NULL;
   }
  }
  NpcAniNum = End;
  End = Monlist->GetSize();
  for(i=0; i<End; i++)
  {
   mTemp = Monlist->GetAt(i);
   Monlist->SetAni(i, NpcAniArray.GetAt(mTemp->AniNum));
  }
  delete Flags;
  delete MonTemp;
}
}

위의 루틴을 최적화 한것이 씰에 사용된 NPC/몬스터 로드 함수이고요..
씰에서는 몬스터까지 NPC로 사용되어서 좀 더 편하게 만들수 있었습니다만, 레이디안에서는
몬스터와 NPC를 분리한 관계로 다르게 읽습니다.

간단한 알고리즘은 이렇습니다.

먼저 가상의메모리를 잡아서 화일의 내용을 읽습니다.
그리고 이미 읽은 맵화일을 참고로 해당NPC가 맵에 사용되었는지 맵 전체를 스캔하여
검사합니다.
그리고, 사용된 경우는 맵에 NPC번호를 수정해주고, 그 번호에 맞춰서 NPC정보를 복사해서
맵의 NPC구조체에 집어넣습니다.
모든 NPC구조가 새로 만들어진 이후에 사용된 NPC의 그림화일만 골라서 읽습니다.
이때, NPC에 그림화일의 포인터를 넘겨줘서 링크시킵니다.

몬스터의 경우도 마찬가지입니다.
다만, 몬스터의 경우 데이터의 일부를 random하게 변형시킵니다. 그래야 몬스터답게 만들어지죠.

하여간, 몬스터를 읽엇으니 맵의 구조가 에디트한것과는 조금 다르게 변형된 겁니다.
그럼 읽은 맵위에 NPC를 걷게 만들어 볼까염?

한명의 NPC가 맵위에서 걷는 루틴입니다.

먼저, 대화를 할때나 이벤트 동작시엔 동작이 계속 루프됩니다.
수동으로 대화가 끝나면 수동으로 바꿔줘야 하지요. (스크립트에서 제어)
링크된 그림화일의 속도에 맞추어서 이동하게 됩니다.
이동하게 되는 방식은 몇가지 패턴을 미리 프로그래밍 해놓습니다.
랜덤으로 마구 움직이는 경우, 위/아래로만 움직이는 경우, 좌/우로만 움직이는 경우,
가만히 서있는 경우, 지정좌표를 찾아 가는 경우 이 정도의 패턴이 존재합니다.
지정좌표를 찾아가는 경우는 지정좌표까지 가면 자동으로 가만히 서있는 패턴으로
바뀝니다. 지정좌표를 찾아가는 패턴은 이벤트 연출시에 사용됩니다.
패턴에 의해 방향이 결정되었으면, 방향에 따라 움직이는 함수를 호출하여 움직입니다.

void CNpclist::MoveOne(CChunkmap* Chunkmap, int Num, CHerolist* Herolist)
{
BOOL i;
int Xdiff, Ydiff;
int nMotion;
char DirectNum;
NPC* nTemp = NpcArray.GetAt(Num);
CChAni* cTemp;
cTemp = nTemp->AniFile;
if(cTemp == NULL )
{
  nTemp->Frame = 0;
  return;
}
if(nTemp->Talk>=_NM2Talk) // 대화시나 다른 동작시는 루프돈다.
{
  if(nTemp->Talk == _NM2Talk)
   nMotion = 1;
  else
   nMotion = nTemp->Talk;
  nTemp->Frame++;
  if(nTemp->Frame>=cTemp->GetSize(nMotion, nTemp->iDirect))
   nTemp->Frame = 0;
  return;
}

DWORD Speed = (DWORD)(1000/cTemp->GetSpeed());
if(Speed>=(GetCurrentTime() - nTemp->LastTime))
{
  return;
}
nTemp->LastTime = GetCurrentTime();
nTemp->Talk = _NM2Move;
switch(nTemp->Schedule)
{
case _NMRANDOM:
  if(rand()%9==2)
  {
   nTemp->Frame = 0;
   i = TRUE;
   while(i)
   {
    switch(rand()%5)
    {
    case 1:
     if(nTemp->iDirect!=1){ nTemp->iDirect = 1;  nTemp->Frame=0; i = FALSE; }
     break;
    case 2:
     if(nTemp->iDirect!=2){ nTemp->iDirect = 2;  nTemp->Frame=0; i = FALSE; }
     break;
    case 3:
     if(nTemp->iDirect!=3){ nTemp->iDirect = 3;  nTemp->Frame=0; i = FALSE; }
     break;
    case 4:
     if(nTemp->iDirect!=4){ nTemp->iDirect = 4;  nTemp->Frame=0; i = FALSE; }
     break;
    }
   }
   return;
  }
  else{nTemp->Frame++;}
  break;
case _NMSTOP:
  nTemp->iDirect = FDown;
  nTemp->Frame++;
  if(nTemp->Frame>=cTemp->GetSize(0,nTemp->iDirect))
   nTemp->Frame=0;
  break;
case _NMUPDOWN:
  if(rand()%9==2)
  {
   nTemp->Frame = 0;
   i = TRUE;
   while(i)
   {
    switch(rand()%5)
    {
    case 1:
     if(nTemp->iDirect!=1){ nTemp->iDirect = 1;  nTemp->Frame=0; i = FALSE; }
     break;
    case 2:
     if(nTemp->iDirect!=2){ nTemp->iDirect = 2;  nTemp->Frame=0; i = FALSE; }
     break;
    }
   }
   return;
  }
  else{ nTemp->Frame++; }
  break;
case _NMLEFTRIGHT:
  if(rand()%9==2)
  {
   nTemp->Frame = 0;
   i = TRUE;
   while(i)
   {
    switch(rand()%5)
    {
    case 3:
     if(nTemp->iDirect!=3){ nTemp->iDirect = 3;  nTemp->Frame=0; i = FALSE; }
     break;
    case 4:
     if(nTemp->iDirect!=4){ nTemp->iDirect = 4;  nTemp->Frame=0; i = FALSE; }
     break;
    }
   }
   return;
  }
  else{ nTemp->Frame++; }
  break;
case _NMTRACE:
  Xdiff = (nTemp->tXpos +(nTemp->tChunk%Chunkmap->XSize)*ChunkX)
   - ((nTemp->RealX)>>5);
  Ydiff = (nTemp->tYpos +(nTemp->tChunk/Chunkmap->XSize)*ChunkY)
   - ((nTemp->RealY)>>5);
  if(abs(Xdiff)>=1 || abs(Ydiff)>=1)
  {
   if(abs(Xdiff)>abs(Ydiff))
   {
    if(Xdiff>0)  DirectNum = FRight;
    else   DirectNum = FLeft;
   }
   else
   {
    if(Ydiff>0)  DirectNum = FDown;
    else   DirectNum = FUp;
   }
   if(DirectNum==nTemp->iDirect)
   {
    nTemp->Frame++;
   }
   else
   {
    nTemp->iDirect = DirectNum;
    nTemp->Frame = 0;
   }
  }
  else
  {
   nTemp->Schedule = _NMSTOP;
   nTemp->Frame = 0;
  }
  break;
}
if(nTemp->Schedule!=_NMSTOP)
{
  switch(nTemp->iDirect)
  {
  case 1:
   MapUp(Chunkmap, Num,Herolist);
   break;
  case 2:
   MapDown(Chunkmap, Num,Herolist);
   break;
  case 3:
   MapLeft(Chunkmap, Num,Herolist);
   break;
  case 4:
   MapRight(Chunkmap, Num,Herolist);
   break;
  }
}
}

다음은 화면위로 NPC가 위로 움직일 경우의 함수의 예입니다.
청크방식을 써서 자료형은 간단해지고, 크기의 수정도 용이해졌지만,
아쉽게도 그 안에서 움직이려면 상당히 복잡하군요. ^^;;;
타일단위 이동이 아닌 도트단위 움직임으로 구현되어 있기때문에, 충돌체크가 좀
복잡합니다.

void CNpclist::MapUp(CChunkmap* Chunkmap, int Num, CHerolist* Herolist)
{
NPC* nTemp = NpcArray.GetAt(Num);
int iChunk = nTemp->ChunkNum;
int Xpos = nTemp->Xpos;
int Ypos = nTemp->Ypos;
int SmallX = nTemp->SmallX;
int SmallY = nTemp->SmallY;
CChAni* ccTemp = nTemp->AniFile;
nTemp->iDirect = FUp;
if(nTemp->Frame>=ccTemp->GetSize(0,nTemp->iDirect))
  nTemp->Frame=0;
SmallY-=ccTemp->GetLength();//*ccTemp->GetSpeed()/30);
if(SmallY<8)
{
이부분이 바로 충돌체크! 위의 타일의 속성이 충돌속성이거나
좌상단 타일 또는 우상단의 타일의 속성이 충돌속성인 경우는
움직이지 못하죠.
물론 지금타일을 벋어나기 바로 전 순간에 체크하는 겁니다.
if(Chunkmap->GetAttrib(iChunk, Xpos, Ypos-1)>=64
  ||SmallX<12&&(Chunkmap->GetAttrib(iChunk, Xpos-1, Ypos-1)>=64)
  ||SmallX>20&&(Chunkmap->GetAttrib(iChunk, Xpos+1, Ypos-1)>=64)
  )
  {
   Stand(Num);
   return;
  }

}
충돌하지 않고 위로 올라갔으면 캐릭터의 좌표를 완전 바꿉니다.
청크가 바뀔수도 잇으니 처리가 조금 복잡하죠.
if(SmallY<0)
{
  SmallY+=32;
  if((Ypos-1)<0)
  {
   if(nTemp->Schedule==_NMTRACE)
   {
    iChunk -= Chunkmap->GetXSize();
    Ypos = ChunkY+Ypos-1;
   }
   else
   {
    nTemp->Frame = 0;
    nTemp->iDirect = FDown;
    Ypos = 0;
    SmallY=0;
   }
  }
  else
   Ypos-=1;
}
CHero* hTemp;
int i;
int End = Herolist->GetSize();
RECT hRect, nRect, rTemp;
int RealY = (((iChunk/Chunkmap->XSize)*ChunkY + Ypos)<<5)+SmallY;
SetRect(&nRect, NpcArray[Num]->RealX-16, RealY-16,
     NpcArray[Num]->RealX+16, RealY+8);

현재 위치에 주인공이 있는가 검사합니다. 이때는 사각 충돌로 체크합니다.
한명이라도 충돌하면 리턴~ 해버리고 가만히 서있게 됩니다.
for(i=0; i<End; i++)
{
  hTemp = Herolist->GetAt(i);
  CopyRect(&hRect, &(hTemp->rect));
  OffsetRect(&hRect, hTemp->RealX, hTemp->RealY);
  if(IntersectRect(&rTemp, &hRect, &nRect) && nTemp->Schedule!=_NMTRACE)
  {
   Stand(Num);
   return;
  }
}
End = NpcArray.GetSize();
NPC* nTemp2;
for(i=0; i<End; i++)
{
  nTemp2 = NpcArray.GetAt(i);

  SetRect(&hRect, nTemp2->RealX-16, nTemp2->RealY-16,
     nTemp2->RealX+16, nTemp2->RealY+8);
NPC끼리도 충돌체크를 합니다. 하지만, 지정좌표를 따라가고 있는 경우는 체크하지 않는데
이 이유는 이벤트시까지 체크해 버리면, 이벤트가 끝나지 않는 버그상태가 연출될 수 있기
때문입니다.
  if(IntersectRect(&rTemp, &hRect, &nRect)&& i!=Num && nTemp->Schedule!=_NMTRACE)
  {
   Stand(Num);
   return;
  }
}
if(Chunkmap->GetNpcmap(iChunk, Xpos, Ypos)!=-1)
{
  Stand(Num);
  return;
}
nTemp->ChunkNum = iChunk;
nTemp->Ypos = Ypos;
nTemp->SmallY = SmallY;
nTemp->RealY = RealY;
}

제대로움직엿으면 움질인 좌표를 NPC에 넣어줍니다. 이전에 리턴되어버리면 좌표수정은
안되는 셈이 되어버리는거죠.

오늘은 짧게 함수 두개만... ^^;;;
일단 NPC를 움직여 봤습니다.
그럼 다음시간엔 주인공 캐릭터를 움직이는 루틴을 만들겠습니당.
그럼.



레이디안 이러케 만들어따 ----- (6)



                                        deadfish@shinbiro.com

이 시리즈가 시작한지도 꽤 돼었는데 전혀 업데이트가 없어서
오랜만에 바쁜와중에도 키보드를 두드립니다.
솔직히 너무 시간이 빠듯하기 때문에 여유있게 쓰질 못해서 제대로
이해나 할수 있을런지...

----- 주인공을 움직여 보자! --------

앞 강좌에서 NPC를 움직이는 법을 설명했습니다.
NPC를 움직일수 있다면 주인공도 마찬가지이지요.
일단 중요한 것은 조종할 주인공을 제대로 움직이는 것일텐데요..
NPC루틴을 보면 MapUp, MapDown 따위의 함수를 볼수 있습니다.
이것은 캐릭터의 위치를 갱신해주면서 충돌체크따위를 해주는 함수로
이미 그 사용법이나 해당 코드는 보셧을 겁니다.

그렇다면 주인공은 어떻게 되느냐? 하면????
MoveOne에서 인공지능에 의해 움직여줬던 부분을 키입력에 대응시키면 됩니다.
자~ 그러면 여기서 레이디안에 사용된 걷는 키입력 함수를 봅시다.
아~~ 정말 짜증나게 긴 소스네요..
언제나처럼 간단한 주석만이 있을 뿐입니다.
공부하고 싶으신분은 열시미 봐주세요~


// 걷는 키입력 
BOOL CTest::WalkKey()
{
static BYTE lastkey1 = 0;
static BYTE lastkey2 = 0;
static BYTE lastkey3 = 0;
int iTemp;
CHero* hTemp = Herolist->GetLeader();
// 키입력을 받는다.
DWORD Speed = (DWORD)(1000/hTemp->AniFile->GetSpeed());
if(Speed>=(GetCurrentTime() - hTemp->LastTime))
{
  Update2();
  return TRUE;
}
hTemp->AttackDelay--;
if(hTemp->AttackDelay<0)hTemp->AttackDelay=0;

DownParameter();
// 주인공 동기화에 맞추어 동작한다.
if(Chunkmap->GetFlag())
  MoveHeros();
else
  FightHeros();

if(!(hTemp->Mode==_HMNormalWalk || hTemp->Mode==_HMAttackWalk || 
  hTemp->Mode==_HMNormalRun || hTemp->Mode==_HMAttackRun ||
  hTemp->Mode==_HMStand1 || hTemp->Mode==_HMStand2 || hTemp->Mode==_HMGrogi))
{
  hTemp->LastTime = GetCurrentTime();
  UpdateHero(Herolist->Leader);
  MapMove(4);
  return TRUE;
}

BYTE keystate[256]; 
BYTE* DKey = DInput->GetDKey();
BYTE* FKey = DInput->GetFKey();
memcpy(keystate, DInput->Keys, 256);

// 같은방향으로 연속 두번 누르면 달리도록 하기위한
// 함수들
lastkey1 = lastkey2; // 이전 이전에 입력된 키
lastkey2 = lastkey3; // 이전에 입력된 키
lastkey3 = 0;   // 이번에 입력된 키
if(keystate[DKey[0]])lastkey3 = DKey[0];
else if(keystate[DKey[1]])lastkey3 = DKey[1];
else if(keystate[DKey[2]])lastkey3 = DKey[2];
else if(keystate[DKey[3]])lastkey3 = DKey[3];
else if(keystate[FKey[0]] || keystate[FKey[1]] || keystate[FKey[2]] || keystate[FKey[3]])lastkey3 = 1;

char run = 1;
// 이전 이전과 이번에 입력된 키가 같고 이전키가 안눌렸다면
// 그렇다면 뛰는거닷!
if((lastkey2 == 0 && (lastkey1 == lastkey3) && lastkey1!=0)
  ||(hTemp->Mode==_HMNormalRun || hTemp->Mode==_HMAttackRun))
  //(lastkey2 == lastkey3 && (hTemp->Mode==_HMNormalRun || hTemp->Mode==_HMAttackRun)))
{
  if(hTemp->iParaData[_HPstamina]>_MIN_STAMINA)
   run=2;
  // 모드 바꿔주기
}
if(DInput->IsDash() && hTemp->iParaData[_HPstamina]>_MIN_STAMINA) run = 2;
// 왼쪽 위
if(keystate[DKey[0]] && keystate[DKey[2]])//&& Chunkmap->GetFlag())
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, (-1)*_WLength2*run, (-1)*_WLength2*run);
   if(iTemp!=-1 )
   {
    if( Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapLeftUp(Chunkmap, (-1)*_WLength2*run); 
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapLeftUp(Chunkmap, (-1)*_WLength2*run); }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, (-1)*_WLength2*run, (-1)*_WLength2*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
    {
     hTemp->MapLeftUp(Chunkmap, (-1)*_WLength2*run);
    }
    else
    {
     hTemp->iDirect = FLeft;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapLeftUp(Chunkmap, (-1)*_WLength2*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength2*run/2);
  return TRUE;
}

// 오른쪽 위
if(keystate[DKey[0]] && keystate[DKey[3]])//&& Chunkmap->GetFlag())
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, _WLength2*run, (-1)*_WLength2*run);
   if(iTemp!=-1)
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapRightUp(Chunkmap, _WLength2*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapRightUp(Chunkmap, _WLength2*run); }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, _WLength2*run, (-1)*_WLength2*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
    {
     hTemp->MapRightUp(Chunkmap, _WLength2*run);
    }
    else
    {
     hTemp->iDirect = FRight;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapRightUp(Chunkmap, _WLength2*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength2*run/2);
  return TRUE;
}

// 왼쪽 아래
if(keystate[DKey[1]] && keystate[DKey[2]])//&& Chunkmap->GetFlag())
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, (-1)*_WLength2*run, _WLength2*run);
   if(iTemp!=-1)
   {
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapLeftDown(Chunkmap, (-1)*_WLength2*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapLeftDown(Chunkmap, (-1)*_WLength2*run); }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, (-1)*_WLength2*run, _WLength2*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
    {
     hTemp->MapLeftDown(Chunkmap, (-1)*_WLength2*run);
    }
    else
    {
     hTemp->iDirect = FLeft;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapLeftDown(Chunkmap, (-1)*_WLength2*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength2*run/2);
  return TRUE;
}


// 오른쪽 아래
if(keystate[DKey[1]] && keystate[DKey[3]])//&& Chunkmap->GetFlag())
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, _WLength2*run, _WLength2*run);
   if(iTemp!=-1)
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapRightDown(Chunkmap, _WLength2*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapRightDown(Chunkmap, _WLength2*run); }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, _WLength2*run, _WLength2*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
    {
     hTemp->MapRightDown(Chunkmap, _WLength2*run);
    }
    else
    {
     hTemp->iDirect = FRight;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapRightDown(Chunkmap, _WLength2*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength2*run/2);
  return TRUE;
}

if(keystate[DKey[0]])
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, 0, (-1)*_WLength*run);
   if(iTemp!=-1)
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapUp(Chunkmap, (-1)*_WLength*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapUp(Chunkmap, (-1)*_WLength*run); }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, 0, (-1)*_WLength*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
     hTemp->MapUp(Chunkmap, (-1)*_WLength*run);
    else
    {
     hTemp->iDirect = FUp;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapUp(Chunkmap, (-1)*_WLength*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength*run/2);
  return TRUE;
}

if(keystate[DKey[1]])
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, 0, _WLength*run);
   if(iTemp!=-1 )
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapDown(Chunkmap,_WLength*run); 
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapDown(Chunkmap,_WLength*run);  }
  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, 0, _WLength*run,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
     hTemp->MapDown(Chunkmap, _WLength*run);
    else
    {
     hTemp->iDirect = FDown;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapDown(Chunkmap,_WLength*run); }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength*run/2);
  return TRUE;
}

if(keystate[DKey[2]])
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, (-1)*_WLength*run, 0);
   if(iTemp!=-1 )
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapLeft(Chunkmap, (-1)*_WLength*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapLeft(Chunkmap, (-1)*_WLength*run);   }

  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, (-1)*_WLength*run, 0,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
     hTemp->MapLeft(Chunkmap, (-1)*_WLength*run);
    else
    {
     hTemp->iDirect = FLeft;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapLeft(Chunkmap, (-1)*_WLength*run);  }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength*run/2);
  return TRUE;
}

if(keystate[DKey[3]])
{
  hTemp->LastTime = GetCurrentTime();
  if(Chunkmap->GetFlag())
  {
   iTemp = Npclist->CheckNpc(hTemp, _WLength*run, 0);
   if(iTemp!=-1 )
   { 
    if(Npclist->CheckNpc(hTemp, 0, 0)!=-1)
     hTemp->MapRight(Chunkmap, _WLength*run);
    else
     StandHero(Herolist->Leader);
   }
   else
   { hTemp->MapRight(Chunkmap, _WLength*run);   }
  }
  else
  {
   iTemp = Monster->CheckMonster(hTemp, _WLength*run, 0,Chunkmap->XSize);
   if(iTemp!=-1)
   {
    if(Monster->CheckMonster(hTemp,0,0,Chunkmap->XSize)!=-1)
     hTemp->MapRight(Chunkmap, _WLength*run); 
    else
    {
     hTemp->iDirect = FRight;
     StandHero(Herolist->Leader);
    }
   }
   else
   { hTemp->MapRight(Chunkmap, _WLength*run);  }
  }
  WalkSound();
  CheckScript();
  MapMove(_WLength*run/2);
  return TRUE;
}

// 숨쉬기 모드로...
if( (lastkey3 ==0 && lastkey2 == 0)||
  (lastkey3 ==1 && lastkey2 == 0)||
  (lastkey3 ==0 && lastkey2 == 1)||
  (lastkey3 ==1 && lastkey2 == 1))
  StandHero(Herolist->Leader);
MapMove(4);
hTemp->LastTime = GetCurrentTime();
return TRUE;
}

와~ 길기도 하군요.
소스를 잠깐 설명해 볼까요?
처음에 보면 Update2 란 함수가 보이죠?
그것은 키입력없이 캐릭터를 업데이트 하는 것으로, 즉 1/2의 속도로 키입력을 받는단
소리입니다.
매 프레임마다 받을 필요까진 없으니까요.

그다음에 lastkey 라는 변수들이 보이시죠?
이것은 대쉬를 위해 마련된 변수들로 -> _ -> 이런식으로 오른쪽으로 두번 클릭했을때
대쉬로 동작을 바꾸기 위해 사용되었습니다.
또는 이전에 뛰던 동작이면 무슨키를 누르건 뛰는것으로 되는건 당연하겠죠?

그 이후에는 8방향으로 움직였던 레이디안이니 만치 8방향에 대해 각각대응을 
해줬습니다.
8방향을 대응시키다보니 함수가 길어졌는데..
지금보니 레이디안때는 함수를 무척 길게 썼군요..
점점 살아오면서 머리가 단순해지다보니 함수가 짧아지던데.. 하하.. T.T

먼저 direct input에서 들어온 키보드 배열에서 방향을 입력받습니다.
레이디안은 키보드를 외부세팅프로그램에 의해 세팅을 할수 있엇기 때문에
BYTE DKey[4]라는 추가변수가 필요하지요.

레이디안은 전체적으로 전투맵과 일반맵으로 구분되어져 있었기 때문에
이 구분은 map에 flag변수로 처리되었습니다.
즉, Flag 가 True일 경우 일반맵, 아닐경우는 전투맵이었죠.
일반맵일경우 주인공이 갈 위치에 NPC가 잇는지 체크합니다.
만약 없으면 그쪽방향으로 움직이고, 아니라면 제자리에  서잇게 합니다.

전투맵일경우에도 마찬가지로 작동합니다. 일반 슈팅게임이라면 적과 충돌할
경우 데미지를 입어야 하겠지만, RPG라서 그렇게 하면 게임이 너무 어려워지더군요.
(이미 충분히 어렵단 소리를 들은지라.. -.-;;;)
이렇게 움직이는 것까진 똑같지만,
주인공이 다른점은 주인공이 움직이게 되면 맵도 따라서 스크롤 되는 것입니다.



-- 맵 스크롤 --

void CTest::MapMove(int num2)
{
int num;
CHero* hTemp = Herolist->GetLeader();

int mXtemp;
mXtemp = (((iChunk/Chunkmap->GetXSize())*ChunkY+Ypos)<<5) + SmallY;
num = (hTemp->RealY-mXtemp);
if(num<_TOPM)MapUp(min(((_TOPM)-num)>>2,32));
if(num>_BOTTOM)MapDown(min((num-(_BOTTOM))>>2,32));

mXtemp = (((iChunk%Chunkmap->GetXSize())*ChunkX+Xpos)<<5) + SmallX;
num = (hTemp->RealX-mXtemp);
if(num<_LEFT)MapLeft(min(((_LEFT)-num)>>2,32));
if(num>_RIGHT)MapRight(min((num-(_RIGHT))>>2,32));
}

맵을 스크롤 하는 함수입니다.
_TOPM 과 같은 변수들은 좌우,상하의 여유분을 나타냅니다.
즉, 여유분이 적어지면 해당방향으로 화면을 이동시키는 것이지요.
맵의 청크과 맵타일크에의한 실제좌표계산법은 구지 설명하지 않아도 될만큼
간단할겁니다.

그럼 실제로 좌표를 움직이는 함수를 잠깐 살펴볼까요?

// 맵 스크롤 루틴들
void CTest::MapUp(int _Scroll)
{
if(Ypos<=0&&SmallY==0&&iChunkGetXSize())
{
  if(SmallY!=0)
  {
   SmallY=0;
   ReDrawBack();
  }
  return;
}
SmallY-=_Scroll;
if(WeatherOut && Weather!=NULL)
  Weather->Move(0,-1*_Scroll);
if(SmallY<0)
{
  SmallY+=32;
  if((Ypos-1)<0)
  {
   if((iChunk-Chunkmap->GetXSize())>=0)
   {
    iChunk -= Chunkmap->GetXSize();
    Ypos = ChunkY+Ypos-1;
   }
   else
   {
    Ypos=0;
    SmallY = 0;
    ReDrawBack();
    return;
   }
  }
  else
  Ypos-=1;
  if(WeatherOut && Weather!=NULL)
   Weather->Move(0,-32);
  ReDrawBack();
}

}
실제로 화면좌표를 이동시키는 함수입니다.
ReDrawBack이란 함수는 캐릭터보다 아래에 있는 바닥1,바닥2, 알파1의 맵들을 찍는
함수로 여기에 찍힌 그림은 다음타일로 이동할때까지 갱신하지 않습니다.
그렇게 함으로써 약간의 속도향상이 있습니다.

몇글자 쓰지도 않았는데 소스가 많다보니 용량이 꽤 돼는군요.
일단 요번회는 여기서 마치고..
다음에 이어쓰도록 하지요.
아직 주인공은 다 움직인게 아닙니다!!

그럼~




레이디안 이러케 만들어따 ----- (7)



                                        deadfish@shinbiro.com

----- 주인공을 움직여 보자2 --------

지난번에는 주인공을 움직여만 봣습니다.
하지만, 배경은 스크롤 되었지만, 실제적으로 주인공의 좌표가 움직인적은
없었을 겁니다.
CHero란 클래스안에있는 주인공의 좌표를 움직여주는 함수를 알아봅시다.

hTemp->MapUp(Chunkmap, _WLength2*run);
같은 함수를 이전 강좌에서 발견할 수 잇을것이다.
이 함수는 과연 어떻게 생겼을까?


#define _WLength 8

BOOL CHero::MapUp(CChunkmap* Chunkmap, int Num, BOOL f, int Count)
{
Xps = 0;
if(f||Ypos<=0&&SmallY==0&&iChunkGetXSize())
{
  if(iDirect!=1)
  {
   iDirect=1;
   Frame = 0;
  }
  Stand(Chunkmap);
  return TRUE;
}

BOOL ret = MoveUp(Chunkmap, Num/2,Count);
if(ret)
{
  if(iDirect!=1)
  {
   iDirect=1;
   Frame = 0;
  }
  Stand(Chunkmap);
}
else
{
  if(Mode!=_HMNormalWalk && Mode!=_HMNormalRun && Mode!=_HMAttackWalk && 

Mode!=_HMAttackRun)
   Frame=-1;
  if(abs(Num)>_WLength)
  {
   if(!Chunkmap->GetFlag())
   {
    num++;
    iParaData[_HPstamina]-=(int)(0.2*num);
    if(num>=5)num=0;
    if(iParaData[_HPstamina]<0)iParaData[_HPstamina]=0;
   }
   if(Chunkmap->GetFlag())
    Mode = _HMNormalRun;
   else
    Mode = _HMAttackRun;
  }
  else
  { 
   if(Chunkmap->GetFlag())
    Mode = _HMNormalWalk;
   else
    Mode = _HMAttackWalk;
  }

  if(iDirect!=1)
  {
   iDirect=1;
   Frame = 0;
  }
  else
   Frame ++;
  if(AniFile!=NULL && Frame>=AniFile->GetSize(Mode, 1))
   Frame = 0;
}
return ret;
}

무척간단하다. 실제적으로 해주는 일이라고는 주인공의 애니메이션 처리밖에 없다.
걷거나 뛰는 것에 대해서 그리고 맵이 전투맵이냐 일반맵이냐에 따라 주인공의 애니메이션을
교체해 주는 역할을 하는 함수이다.
하지만 여기서 눈여겨볼만한 부분은 단 하나이다.

BOOL ret = MoveUp(Chunkmap, Num/2,Count);

바로 이것! 이함수가 바로 주인공을 움직이는 함수이다.
이 함수는 일단, NPC의 MapUp함수와 하는 일이 동일하지는 않다.
NPC의 경우 움직이고자 하는 좌표에 방해물이 있을경우 (즉, 맵에 충돌이 잇을경우)
돌아가지 않고 막혀버렸었다.
하지만, 주인공일 경우는 그러한 맵이라도 밀려서라도 앞으로 또는 옆으로 가야한다.
바로 MoveUp함수에서 그러한 일을 처리한다.
무한루프에 빠지지 않도록 Count만큼만 길을 찾는다.

함수를 보자.

BOOL CHero::MoveUp(CChunkmap* Chunkmap, int Num, int Count)
{
if(Count>=4)return TRUE;
SmallY+=Num;
RECT hRect;
CopyRect(&hRect, &rect);
OffsetRect(&hRect, SmallX, SmallY);
char Plus =1;
if(hRect.top<0)
{
if((Chunkmap->GetAttrib(iChunk, Xpos, Ypos-Plus)>=64 || 

Chunkmap->GetNpcmap(iChunk, Xpos, Ypos-Plus)!=-1)
  ||hRect.left<0&&(Chunkmap->GetAttrib(iChunk, Xpos-1, Ypos-Plus)>=64|| 

Chunkmap->GetNpcmap(iChunk, Xpos, Ypos-Plus)!=-1)
  ||hRect.right>=32&&(Chunkmap->GetAttrib(iChunk, Xpos+1, Ypos-Plus)>=64|| 

Chunkmap->GetNpcmap(iChunk, Xpos, Ypos-Plus)!=-1))
  {
  SmallY-=Num;
  if(Chunkmap->GetAttrib(iChunk,Xpos-1,Ypos)<64
   && Chunkmap->GetAttrib(iChunk,Xpos-1,Ypos-1)<64)
   return MoveLeft(Chunkmap, Num,Count+1);
  else if(Chunkmap->GetAttrib(iChunk,Xpos+1,Ypos)<64
   && Chunkmap->GetAttrib(iChunk,Xpos+1,Ypos-1)<64)
   return MoveRight(Chunkmap, -1*Num,Count+1);
  return TRUE;
  }
}
if(SmallY<0)
{
  SmallY+=32;
  if(Ypos==1 && (iChunk-Chunkmap->GetXSize())<0)
  {
   SmallY=0;
   RealY = ((Ypos+(iChunk/Chunkmap->XSize)*ChunkY)<<5)+SmallY;
   return TRUE;
  }

  if((Ypos-1)<0)
  {
   if((iChunk-Chunkmap->GetXSize())>=0)
   {
    iChunk -= Chunkmap->GetXSize();
    Ypos = ChunkY+Ypos-1;
   }
   else
   {
    Ypos=0;
    SmallY-=32;
    SmallY-=Num;
   }
  }
  else
   Ypos-=1;
}
RealY = ((Ypos+(iChunk/Chunkmap->XSize)*ChunkY)<<5)+SmallY;
return FALSE;
}


아래쪽의 좌표을 옮겨주는 부분은 같다. 하지만, 위쪽의 맵과의 충돌을 체크하는 부분은
매우 틀리다.
즉, 위로 갈경우, 위, 왼쪽위, 오른쪽위 까지 자신이 갈수있는 곳을 체크하여 갈수 있다면
한번 더 Move방향() 함수들을 호출한다. 대신 이것은 거의 재귀호출에 가깝게 되므로,
일정 한계를 정할 필요가 잇다.
그래서 4번이상은 체크를 하지 않도록 매루프마다 Count를 증가시키고 4이상이 되면 바로
리턴시키도록 하고 있다.
밀림처리까지 끝났으므로, 실제적인 키입력에 의해 주인공이 움직이는 부분은 모두 끝난
것이다. 
다음강좌에서는 게임에서 맵화일을 어떻게 읽고 어떻게 사용하는지에 대해 알아보겠습니다.
그럼 언제가 될진 모르지만, 다음기회에...^^ 




레이디안 이러케 만들어따 ----- (8)



                                        deadfish@shinbiro.com

//-------------------------------------------
// 여전히 강좌가 늦고 있습니다.. 회사 프로젝트가 마감단계라..
// 매일 테스트 하는것도 고역이군요. 흑.
//
////////////////////////////////////////////

처음 의도와는 달리 완전 프로그램 강좌가 되어버리는 것 같군요.
이래저래 심기일전해서 다시 써야할 것 같습니다.^^
언젠가 다시 탈고하는 날이 오겠죠 뭐. 

하여간, 벌써 8회입니다. (맨날 소스로 도배를 해서 분량에 비해
회수가 많군요.)

이번회에는 맵툴에서 저장된 정보를 게임에서 어캐 쓰는지 알아보죠.

처음 제작할때는 몰랐는데, 맵툴에 의해서 저장된 대부분의 정보중
npc의 정보와 monster의 정보는 그대로 쓰일수가 없더군요.
이유가 뭔고.. 하니.. monster정보는 한종류당 하나만 존재하는데
맵에는 그 한종류가 여러마리 존재합니다.
즉, 그대로 사용해 버리면, 1번 몬스터가 맞으면 1번 몬스터의
종류를 가진 모든 몬스터가 맞아 버리게 되는 현상이 벌어지죠.
(물론 그렇게 만들 바보는 없겠죠?)

그래서, 아쉽게도, 맵을 읽을때 맵내에 찍혀있는 몬스터데이터를
적절해 변환해야 했습니다. 상당히 프로세스를 잡아먹는 작업이지만
게임이 제대로 되려면 어쩔수 없는 일이죠. 맵을 읽어서 전투를 하고자 했을때
가장 골치 아프게 했던 부분입니다. NPC의 경우 한맵에 두명의 NPC가 존재하는 것은
거의 없기때문에 별 문제가 아니었는데, 레이디안은 액션롤이다보니 
그렇게 되더군요. (물론 액션롤이 아닌 FF식 롤이라면 뭐 이런식의 작업이
불필요하겠죠.)

그렇게 하기 위해서는 두가지가 필요합니다. 일단 맵을 만들때 사용
되었던 몬스터의 데이터가 필요합니다. 이것을 원본이라고 부릅시다.
이 원본을 일단 메모리에 적재합니다.
글구나서 맵을 읽습니다. 맵내에 몬스터 정보는 몬스터맵에 몬스터들의 인덱스만을 저장한 것입니다.
몬스터 인덱스를 저장했기 때문에 이 몬스터 인덱스는 원본의 몬스터들을 참조해야 하죠.
즉, 몬스터 인덱스에 3번이 저장되어 있다면, 원본의 4번째(0부터 시작하니까요)몬스터가 찍혀 있는 겁니다.
그러므로, 원본을 참조해서 출력이 가능합니다, 하지만, 이전 강좌에 나온 몬스터 구조체를 보시면 아시겠지만, 몬스터 구조에는 좌표가 들어가게 되는데, 이 좌표를 3번 인덱스를 사용하는 모든 몬스터가 같이 사용할 수 없습니다. 에너지라든가 그런것도 마찬가지겠죠. (이유는 위에 설명한 대로...) 그래서, 간단하게 원본을 참조해서 새로운 배열을 만드는 겁니다.
한마디로 인덱스의 갯수만큼, 즉 맵에 등장하는 몬스터만큼 배열을 생성한 후에 원본에 있는 내용을 복사해 줍니다. 이때, 좌표만은 매번 바뀌므로, 좌표는 다시 계산을 해주어야 합니다.
그럼 소스를 보시죠.^^


// 게임용 Npc/Monster list 읽는 함수
// Npcmap에는 각 Npc나 Monster의 종류번호가 저장되기 때문에
// 같은 종류의 Monster가 두마리 있을 경우 이상한 움직임을 보이게 된다.
// 이를 방지하기 위해 임시저장소에 Npc/Monster리스트 화일을 읽은후에
// 중복 카피해 주는 방법으로 게임용 Array를 만든다.
// 또한 사용되지 않는 애니메이션 화일은 읽어 들이지 않게된다. 
void CChunkmap::gLoadNpc(char* fName)
{
CNpclist* NpcTemp;
CMonster* MonTemp;
NPC* nTemp;
MON* mTemp;
MON* mTemp2;
BYTE* Flags;
CChAni* Temp;
char* Name;
int* iTemp;
int i, End;
int j, k, l, m;
IDAT* itTemp;
Chunk* ChunkTemp;
if(Flag) // 일반맵일 경우
{
  NpcTemp = new CNpclist;  // 임시저장소에 메모리 할당
  if(Npclist!=NULL)
   delete Npclist;
  Npclist = new CNpclist;  // Npclist에 메모리를 할당
  if(!NpcTemp->LoadFile(fName)) // 임시저장소에 화일의 내용을 읽는다.
  {
   delete Npclist;
   Npclist = NULL;
   return;
  }
//  memcpy(NpcName->Name, fName, 200);
  End = NpcTemp->GetSize(); // Npclist의 총갯수
  Flags = new BYTE[End];  // Npclist중에 사용유무를 체크하기 위한 행렬
  for(i=0; i    Flags[i]=FALSE;
  NpcAniArray.RemoveAll();
  NpcAniArray.SetSize(End); // Npc용 애니메이션 저장소의 갯수만큼의 메모히 할당
  l=0;
  for(i=0; i   {
   ChunkTemp = ChunkArray.GetAt(i); // 청크 루프
   for(j=0; j    {
    for(k=0; k     {
     if(ChunkTemp->Npcmap[k][j]!=-1) // 루프를 돌면서 Npc맵에 Npc가 있는지 확인
     {        
      nTemp = new NPC;
      memcpy(nTemp, NpcTemp->GetAt(ChunkTemp->Npcmap[k][j]), sizeof(struct NPC));
      // 사용되는 Npc를 임시저장소에서 Npclist에 메모리 카피한다.
      nTemp->ChunkNum = i;
      nTemp->Xpos = j;
      nTemp->Ypos = k;
      nTemp->AniNum = ChunkTemp->Npcmap[k][j];
      nTemp->iDirect = FDown;
      nTemp->RealX = (((i%XSize)*ChunkX + j)<<5)+nTemp->SmallX;
      nTemp->RealY = (((i/XSize)*ChunkY + k)<<5)+nTemp->SmallY;
      Flags[ChunkTemp->Npcmap[k][j]]=TRUE;
      Npclist->Add(nTemp);
      ChunkTemp->Npcmap[k][j]=l; // Npclist번호 세팅
      l++;
     }
    }
   }

  }
  for(i=0; i   {
   if(Flags[i])
   {
    Temp = new CChAni(AlphaTable);
    Name = NpcTemp->GetAt(i)->AniName;
    Name[0] = fName[0];
    if(Temp->OpenFile(Name,TRUE))
     NpcAniArray[i] = Temp;
   }
   else
   {
    NpcAniArray[i] = NULL;
   }
  }
  NpcAniNum = End;
  End = Npclist->GetSize();
  for(i=0; i   {
   nTemp = Npclist->GetAt(i);
   Npclist->SetAni(i, NpcAniArray.GetAt(nTemp->AniNum));
  }
  delete Flags;
  delete NpcTemp;
}
else
{
  MonTemp = new CMonster;
  if(Monlist!=NULL)
   delete Monlist;
  Monlist = new CMonster;
  if(!MonTemp->LoadFile(fName))
  {
   delete Monlist;
   Monlist = NULL;
   return;
  }
  End = MonTemp->GetSize();
  Flags = new BYTE[End];
  for(i=0; i    Flags[i]=FALSE;
  NpcAniArray.RemoveAll();
  NpcAniArray.SetSize(End);
  l=0;
  for(i=0; i   {
   ChunkTemp = ChunkArray.GetAt(i);
   for(j=0; j    {
    for(k=0; k     {
     if(ChunkTemp->Npcmap[k][j]!=-1)
     {
      mTemp = new MON;
      mTemp2 = MonTemp->GetAt(ChunkTemp->Npcmap[k][j]);
//      memcpy(mTemp, MonTemp->GetAt(ChunkTemp->Npcmap[k][j]), sizeof(struct MON));
      memcpy(mTemp->AniName,mTemp2->AniName, 100);
      memcpy(mTemp->Name, mTemp2->Name, 40);
      mTemp->ItemNum = mTemp2->ItemNum;
      mTemp->Num = mTemp2->Num;
      iTemp = new int[mTemp2->Num];
      memcpy(iTemp, mTemp2->ParaData, sizeof(int) *(mTemp2->Num));
      mTemp->ParaData = iTemp;
     // 금전에 대한 랜덤성 추가.
      if(mTemp->ItemNum==-1)
      {
        mTemp->ParaData[_MONPmoney]+=(rand()%20);
      }
      // 경험치에 랜덤추가.
      mTemp->ParaData[_MONPexper]+=(rand()%10);
      // 마법 데이타 추가하기. 줴엔장.
      for(m=0; m< mTemp2->MagicData.GetSize(); m++)
      {
       itTemp = new IDAT;
       itTemp->iIndex = mTemp2->MagicData.GetAt(m)->iIndex;
       itTemp->iCost = mTemp2->MagicData.GetAt(m)->iCost;
       mTemp->MagicData.Add(itTemp);
      }
      mTemp->DMode = _MDNormal;
      mTemp->ChunkNum = i;
      mTemp->SmallX =0;
      mTemp->SmallY =0;
      mTemp->Xpos = j;
      mTemp->Ypos = k;
      mTemp->LastTime = 0;
      mTemp->Mode = _MFStand;
      mTemp->Frame=0;
      mTemp->AniNum = ChunkTemp->Npcmap[k][j];
      mTemp->iDirect = FDown;
      mTemp->RealX = ((i%XSize)*ChunkX + j)<<5;
      mTemp->RealY = ((i/XSize)*ChunkY + k)<<5;
      mTemp->DMode = _MDNormal;
      Flags[ChunkTemp->Npcmap[k][j]]=TRUE;
      Monlist->Add(mTemp);
      ChunkTemp->Npcmap[k][j]=l;
      l++;
     }
    }
   }

  }
  for(i=0; i   {
   if(Flags[i])
   {
   Temp = new CChAni(AlphaTable);
   Name = MonTemp->GetAt(i)->AniName;
   Name[0] = fName[0];
   if(Temp->OpenFile(Name,TRUE))
    NpcAniArray[i] = Temp;
   }
   else
   {
    NpcAniArray[i] = NULL;
   }
  }
  NpcAniNum = End;
  End = Monlist->GetSize();
  for(i=0; i   {
   mTemp = Monlist->GetAt(i);
   Monlist->SetAni(i, NpcAniArray.GetAt(mTemp->AniNum));
  }
  delete Flags;
  delete MonTemp;
}
}
위의 함수는 맵에 따라 몬스터와 NPC를 따로 읽어들이게 되어있습니다.
원래대로 하려면, NPC와 몬스터는 같은 구조체를 사용해야 하지만 (플래그 처리로 구분하면 되죠)
레이디안 작업시에는 경험도 없었고, 시간도 없는 관계로 두 구조를 분리했습니다.
(결과적으로 시간만 더 걸려버리기만 했죠. 같은 구조를 두개 만든 거니까요.)

맵 읽는 부분에서 머리아팠던 부분을 알아봤습니다.
다음에는 맵을 출력할때 머리 아팠던 부분을 알아볼까요? ^^



레이디안 이러케 만들어따 ----- (9)



                                        deadfish@shinbiro.com


------------------------------------------------------------
//
// 요즘 생활이 말이 아닙니다. 개인적으로 하는 프로젝트도 있고..
// 회사일도 마무리 단계고.. 거기다 강좌까지 마무리 하려는 욕심에..
// 강연회다, 컨퍼런스다.. 사람 정신없게 하는군요. 빨리 마무리되고
// 뭔가 정신 날만한 사건이 일어나야 할텐데 말이죠. ㅋㅋㅋ
///////////////////////////////////////////////////////////

지난번에는 맵을 읽는 부분중에 한부분에 대해서 잠깐 알아봤습니다.
그럼, 이번에는 맵을 출력하는데 머리아팠던 부분을 살펴보죠..

레이디안은 일반적으로 가로X세로를 배열로 만들어서 저장하는 방식으로
제작되지 않았습니다. 로딩을 최적화 시키고, 에디팅을 편하게 하고자
Chunk라는 개념을 도입했는데요.. (원래는 맵을 굉장히 크게 해서 분할 로딩을
하고자 만든 거였습니다만.. 기획자가 필요없다고해서.. T.T)
Chunk라 함은 맵을 일정한 Chip으로 잘게 나눈겁니다. 레이디안의 경우 한화면분량

을
한 Chunk로 했습니다. Chunk안에는 한화면에 들어갈 정보들이 들어가게 되지요.
Chunk의 구조를 알아봅시다.

#define ChunkX 20
#define ChunkY 15

struct Chunk
{
//          [ysize][xsize]
short Badak1[ChunkY][ChunkX]; 
short Badak2[ChunkY][ChunkX];
short Alpha1[ChunkY][ChunkX];
short Animap[ChunkY][ChunkX];
short Npcmap[ChunkY][ChunkX];
short Badak3[ChunkY][ChunkX];
short Alpha2[ChunkY][ChunkX];
short Unused[ChunkY][ChunkX];
char  Attrib[ChunkY][ChunkX];
char  Frame[ChunkY][ChunkX];
int   iEvent[ChunkY][ChunkX];
};

레이디안은 32*32타일을 사용했으니, 한화면은 20*15개의 타일로 이루어지죠.
이렇게 하면, 분할로딩하게 될때 굉장히 편하기때문에, Swarp방식으로 만들고자 한

것인데..
레이디안의 맵은 작기때문에 구조의 기능을 제대로 살리지 못했죠. -.-;;

하여간, 이런구조를  가로*세로 만큼 가지고 있게 되어있습니다.
즉, 3차원 배열이 되는거네요.
이렇게 되기때문에, 일반적인 출력루틴보다 출력루틴이 상당히 복잡해져 버리고 말

았습니다.
제대로 하려면 루프를 세번돌아야 하기때문에 출력이 느려지고 말죠.
그래서 노가다 최적화를 하고 말았습니다. 0.1초라도 빨라져야 하기때문에 말이죠. 

-.-;;

최적화한 바닥출력함수입니다. 레이어가 많은 관계로, 매 레이어마다 루프를 돌게

되면 속도상
이득이 전혀 없으므로, 최대한 같이 찍을수 있는 레이어는 같이 찍도록 했습니다.
바닥 레이어는 바닥1, 바닥2, 알파1을 차례로 출력하는군요.

//int Chunk, int Xpos, int Ypos, int dXSize, int dYSize, BYTE* Dest
// 게임전용 바닥1, 바닥2, 알파1 출력 함수
//  
void CChunkmap::gPutBackGround(int iChunk, int Xpos, int Ypos, int dXSize, 

int dYSize, BYTE* Dest)
{
int i, j, k, l;
short* Badak1Y;
short* Badak2Y;
short* Alpha1Y;
int CheckX;
int CheckY;
if(iChunk>=ChunkArray.GetSize())return;
Chunk* Temp = ChunkArray[iChunk];
CheckX = 0;
CheckY = 0;
for(j=Ypos; j {
  Badak1Y = Temp->Badak1[j];
  Badak2Y = Temp->Badak2[j];
  Alpha1Y = Temp->Alpha1[j];
  for(i=Xpos; i   {
   if(*(Badak1Y+i)>-1)
    Object->PutTile2(CheckX, CheckY, 

*(Badak1Y+i), dXSize, dYSize, Dest);
   if(*(Badak2Y+i)>-1)
    Object->PutTile2(CheckX, CheckY, 

*(Badak2Y+i), 
       dXSize, dYSize, 

Dest);
   if(*(Alpha1Y+i)>-1)
    Object->PutAlpha2(CheckX, CheckY, 

*(Alpha1Y+i), dXSize, dYSize, Dest, AlphaTable->GetAlpha60());
   CheckX+=32;
  }
  CheckX = 0;
  CheckY+=(dXSize<<5);
}

CheckX = (i-Xpos)<<5;
CheckY = 0;

if((iChunk+1) {
  Temp = ChunkArray[iChunk+1];
  for(l=Ypos; l   {
   Badak1Y = Temp->Badak1[l];
   Badak2Y = Temp->Badak2[l];
   Alpha1Y = Temp->Alpha1[l];   
   for(k=0; k    {
    if(*(Badak1Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak1Y+k), dXSize, dYSize, 

Dest);
    if(*(Badak2Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak2Y+k), dXSize, dYSize, 

Dest);
    if(*(Alpha1Y+k)>-1)
     Object->PutAlpha2(CheckX, CheckY,
      *(Alpha1Y+k), dXSize, dYSize, 

Dest, AlphaTable->GetAlpha60());
    CheckX+=32;
   }
   CheckX = (i-Xpos)<<5;
   CheckY+=(dXSize<<5);
  }
}

CheckX = 0;
CheckY = (dXSize*(j-Ypos))<<5;

if((iChunk+XSize) {
  Temp = ChunkArray[iChunk+XSize];
  for(l=0; l   {
   Badak1Y = Temp->Badak1[l];
   Badak2Y = Temp->Badak2[l];
   Alpha1Y = Temp->Alpha1[l];   
   for(k=Xpos; k    {
    if(*(Badak1Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak1Y+k), dXSize, dYSize, 

Dest);
    if(*(Badak2Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak2Y+k), dXSize, dYSize, 

Dest);
    if(*(Alpha1Y+k)>-1)
     Object->PutAlpha2(CheckX, CheckY,
      *(Alpha1Y+k), dXSize, dYSize, 

Dest, AlphaTable->GetAlpha60());
    CheckX+=32;
   }
   CheckX = 0;
   CheckY+=(dXSize<<5);
  }
}

CheckX = (i-Xpos)<<5;
CheckY = (dXSize*(j-Ypos))<<5;

if((iChunk+1+XSize) {
  Temp = ChunkArray[iChunk+1+XSize];
  for(l=0; l   {
   Badak1Y = Temp->Badak1[l];
   Badak2Y = Temp->Badak2[l];
   Alpha1Y = Temp->Alpha1[l];   
   for(k=0; k    {
    if(*(Badak1Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak1Y+k), dXSize, dYSize, 

Dest);
    if(*(Badak2Y+k)>-1)
     Object->PutTile2(CheckX, CheckY,
      *(Badak2Y+k), dXSize, dYSize, 

Dest);
    if(*(Alpha1Y+k)>-1)
     Object->PutAlpha2(CheckX, CheckY,
      *(Alpha1Y+k), dXSize, dYSize, 

Dest, AlphaTable->GetAlpha60());
    CheckX+=32;
   }
   CheckX = (i-Xpos)<<5;
   CheckY+=(dXSize<<5);
  }
}
}

위를 보시면 아시겠지만, 루프를 최대한 줄이기 위해, 청크가 한화면 분량이라는 

것을 이용하여
4번 출력하게 만들었습니다.
즉, 주인공이 현재 있는 좌표를 판단하여 출력할 기준점을 찾아냅니다.
이 기준점이 포함된 청크, 오른쪽 청크, 아래청크, 오른쪽 아래청크.. 이렇게 4개

의 청크를
출력하게 됩니다. 기준점부터 찍기 시작하기때문에, 기준점 왼쪽의 청크는 출력되

지 않죠.
주인공의 왼쪽청크를 출력하게 될일은 거의 없습니다.
청크마다 x,y에 대해 루프를 돕니다. 루프안에서는 바닥1, 바닥2, 알파1이 순차적

으로 버퍼에 출력됩니다.
위와 같은 방법으로 오브젝트와 바닥3, 알파2를 루프돌리면 됩니다.
다만, 오브젝트의 경우 거대 오브젝트가 존재하기때문에 문제가 생겼는데요.. 
결국은 가로사이즈 320, 세로사이즈 320정도만 출력하기로 했습니다.

그다음은 오브젝트 출력쪽이었는데, 일단 크기를 제한하긴 했지만, 주인공을 어떻

게 출력할까에 대해
고민이 되었습니다.
무조건 순서대로 출력하면, y좌표가 위에 있는 놈이 먼저 찍혀야 되는데 그렇게 안

돼는 경우가 생기거든요.
그래서 주인공은 일단 매 프레임마다 y좌표로 소팅을 합니다.

void CChunkmap::gPutNormalObject(int iChunk, int Xpos, int Ypos, int dXSize, 

int dYSize, 
       BYTE* Dest, 

CHerolist* Herolist, char PutBar)
{
int i, j;
int End = Herolist->GetSize();
int h;
short k;
char l;
short att;
int nSmall;
int mXpos, mYpos;
int mChunk;
BOOL nPut = TRUE;
CHero* hTemp;
if(iChunk>=ChunkArray.GetSize())return;

// Hero 위치 소트 
int *Temp = new int[End];
int *Temp2 = new int[End];
int *Sorted = new int[End];
short m, n;
// 그냥 단순한 버블소트를 호출함다.
SortHero(Herolist,Temp, Temp2, Sorted, End);
// char Status;
// 가로,세로로 좀 더 넓은 범위를 스캔합니다.
// 느리지만 그런데로 찍는게 별로 없어서 느려지진 않더군요.
for(j=-6; j   for(i=-6; i   {
   nPut = TRUE;
   mChunk = iChunk;
   mXpos=i+Xpos;
   mYpos=j+Ypos;
   while(mXpos<0)
   { mChunk-=1;  mXpos+=ChunkX; }
   while(mYpos<0)
   { mChunk-=XSize; mYpos+=ChunkY; }
   while(mXpos>=ChunkX)
   { mChunk+=1;  mXpos-=ChunkX; }
   while(mYpos>=ChunkY)
   { mChunk+=XSize; mYpos-=ChunkY; }
//
//  소트된 주인공의 위치를 판별하여 출력합니다.
//              같은 좌표에 npc가 있을경우 npc와 y위치를 비교하여
//              어떤것을 먼저찍을지 결정합니다.
//
   for(h=0; h    {
    hTemp = Herolist->GetAt(Sorted[h]);
//    Status = hTemp->iParaData[_HPai]/10;
    

if(hTemp->iChunk==mChunk&&hTemp->Xpos==mXpos&&hTemp->Ypos==mYpos)
    {
     if(GetNpcmap(mChunk, mXpos, 

mYpos)>-1)
     {
      nSmall = 

Npclist->GetAt(GetNpcmap(mChunk, mXpos, mYpos))->RealY;
      if( nSmall < 

hTemp->RealY)
      {
       

gPutNpcAni(GetNpcmap(mChunk,mXpos, mYpos),iChunk, i, j, dXSize, dYSize, 

Dest);
       nPut = FALSE;
      }
     }
     hTemp->PutHero(i, j, dXSize, dYSize, 

Dest, Shadow, Flag, 
      ResSprite, Herolist->Item, 

Herolist->MagicAni, IconSprite);
    }
   }
   if((GetNpcmap(mChunk, mXpos, mYpos)>-1)&&nPut)
   {
    gPutNpcAni(GetNpcmap(mChunk,mXpos, 

mYpos),iChunk, i, j, dXSize, dYSize, Dest);
   }
   k = GetAnimap(mChunk, mXpos, mYpos);
   att = GetAttrib(mChunk, mXpos, mYpos);
   if(k>-1 && MapAniFile!=NULL)
   {
    l = GetFrame(mChunk, mXpos, mYpos);
    m = k/9;
    n = k%9;
    if(l>=MapAniFile->GetSize(m, n)||l<0) l=0;
    // 속성에 의해서 열리고 닫히고에 대한 그림을 

구분 출력한다.
    switch(att)
    {
    case _Open_Gate:
     MapAniFile->gPutAni(16+(i*32), 

(j+1)*32, n, m, 1, dXSize, dYSize, Dest);
     break;
    case _Close_Gate:
     MapAniFile->gPutAni(16+(i*32), 

(j+1)*32, n, m, 0, dXSize, dYSize, Dest);
     break;
    case _Open_Box:
     MapAniFile->gPutAni(16+(i*32), 

(j+1)*32, n, m, 1, dXSize, dYSize, Dest);
     break;
    case _Close_Box:
     MapAniFile->gPutAni(16+(i*32), 

(j+1)*32, n, m, 0, dXSize, dYSize, Dest);
     break;
    default:
     MapAniFile->gPutAni(16+(i*32), 

(j+1)*32, n, m, l, dXSize, dYSize, Dest);
     break;
    }
    SetFrame(mChunk, mXpos, mYpos, ++l);
   }
  }
delete Sorted;
delete Temp;
delete Temp2;
}

이렇게 오브젝트를 출력합니다. 레이디안에서는 오브젝트를 두번 출력했습니다.
이유는 바닥3이 찍히고 나서 주인공을 알파로 출력함으로써, 반투명 효과를 
주기 위해서죠.
즉, 벽뒤나 나무뒤에 있을때, 주인공이 알파로 출력되게 하는 겁니다.
최적화를 하기 위해서는 좀더 복잡한 루틴을 사용해야 하지만,
50%알파로 주인공이 있는 위치에 동일 스프라이트를 알파로 찍어주면 됩니다.
원본 이미지의 손상없이 제대로 나옵니다.
(벽에 안가렸을때도 제대로 나옵니다. 전혀 뭉개지는것이 없죠.^^)
그 루틴의 위의 루틴에서 주인공만 찍는거니 소개할 필요가 없겠죠?

오브젝트를 출력한 후에 그 위에 바닥3과 알파2를 출력합니다.
뭐 간단하게 위에 있는 gPutBackGround() 함수와 동일하게 만들면 됩니다.

여기서 최적화의 한 포인트...
배경중 gPutBackGround()는 매번 다시 찍을 필요가 없습니다.
위에 함수를 보신분은 아시겠지만, 가로, 세로로 한 타일씩 더 찍습니다.
즉, 한타일을 더 찍음으로써, 다음타일로 스크롤 하기전까지는 위의 배경이
유효한 원본 그림이 될 수 있습니다.
위의 타일을 버퍼에 출력한 후, 매 프레임마다 출력버퍼로 카피해 줍니다.
그런 후에 출력버퍼에서 오브젝트와 foreGround작업을 해주고..
나서 화면에 뿌려주면 됩나다.

흐음.. 출력 최적화에 대해선 나중에 기회가 되면 더 자세히 강좌하도록 하고요.
오늘은 일단 이정도로 마치죠.
여기까지가 화면 출력할때 골치아팠던 부분입니다.
화면 출력할때 속도가 맘대로 안나오면 정말 가슴이 아프죠..^^
나름대로의 최적화 방법을 생각하세요.
보통 2D게임은 맵당 리소스가 적게는 5M에서 많게는 60~100M정도 됩니다.
이정도 용량을 전부 하드웨어 가속하도록 비디오 메모리에 적재할 수는 없겠죠.
소프트로 최적화 하는 방법을 찾으셔야 합니다.^^
그럼 다음엔 다른 내용으로...
(뭘 쓰나...)




레이디안 이러케 만들어따 ----- (10)



                                        deadfish@shinbiro.com


------------------------------------------------------------
// 헤에~~ 진짜 바빠도 너무 바쁘군요. 항상 녹초.. -.-
// 일이 그렇게 많은 것도 아닌것 같은데, 신경쓸 일이 너무 많다보니..
// 아무래도 메모하는 습관을 길러야 할것 같네요..
// 정리를 안하고 사는 습성으로 아웅~~~~
////////////////////////////////////////////////////////////////////

자~ 지난번에 올라간 강좌가 언제쩍에 올라갔는지 기억조차 가물가물 합니다그려.
하여간 시간이 날때 쓰려고 마음먹은 강좌가 몇개 남았는데, 이대로 가다간 
아무래도 끝내기 어려울것 같아서.. 일단 끝내고자 씁니다..^^

이번글에서는 몬스터의 A.I에 대해서 간단히 설명하고자 합니다.

레이디안에는 참, 단순한 몬스터들만 나옵니다. ^^
다 저의 실력이 없음을 한탄할 노릇인 멍청한 놈들만 나오는데...
그래도 나름대로는 A.I라는 것을 가지고 있습니다.

항상 게임을 만들때는 온라인 대전게임을 제외하고는 대부분 컴퓨터가 자코로 존재합니다.
레이디안도 마찬가지인데, 이런 자코들은 한결같이 몇가지 패턴에 의해 움직입니다.
레이디안에서 정의한 것을 보면 다음과 같습니다.
#define _MAiNoAttack 0  // 공격 안한다.
#define _MAiNormal 1  // 랜덤으로 움직이다가 사정거리내에서 공격한다.
#define _MAiTrace 2  // 일정범위안의 주인공들을 쫓아온다.
#define _MAiNormalMagic 3  // 랜덤하게 움직이다가 랜덤하게 마법을 쓴다.
#define _MAiTraceMagic 4  // 주인공을 쫓아오면서 마법을 쓴다.
#define _MAiNormalShoot 5  // 랜덤하게 움직이다가 사정거리내에서 주인공에게 총알공격을한다.
#define _MAiTraceShoot 6  // 주인공을 쫓아다니면서 총알을 쏜다.
#define _MAiNormalMS 7  // 랜덤하게 움직이다가 마법을 쓰거나 타격.
#define _MAiTraceMS 8  // 주인공을 쫓아와서 마법을 쓰거나 타격.
9종류죠?
대부분의 몬스터의 패턴을 정의했다고 생각합니다만, 액션에서는 좀더 많은 패턴이
필요하긴 하죠. 레이디안은 솔직히 미완성적인 면이 많은 게임이라, 패턴도 그리 많지는
않았습니다.
몬스터의 패턴을 보면 종류가 대단히 많은것 같지만, 트리구조로 생각하면 아주 편합니다.
1. 움직임 : a. 랜덤, b. 주인공추적 
2. 공격방법 : a. 타격, b. 마법, c.장거리, d. 마법 및 타격
이렇게 2*4의 패턴으로 이루어진 것이죠.
실제 프로그래밍은 6개의 함수로 이루어집니다. (물론 함수는 여러개를 합쳐놔서 그렇게 많지는 않습니다.)


이놈이 한놈의 몬스터를 움직이는 함수입니다.
BOOL CMonster::MoveOne(CChunkmap* Chunkmap, int Num, CHerolist* Herolist, CBullet* Bullet)
{
MON* nTemp = MonsterArray.GetAt(Num);
CChAni* cTemp;
cTemp = nTemp->AniFile;
if(cTemp == NULL)return FALSE;
// 몬스터의 움직임을 시간당 제어해서 빠르게 움직이거나 느리게 움직이는 걸 계산합니다.
DWORD Speed = (DWORD)(1000/cTemp->GetSpeed());
if(Speed>=(GetCurrentTime() - nTemp->LastTime))
{
  Update2(Chunkmap, Num, Herolist);
  return TRUE;
}
nTemp->LastTime = GetCurrentTime();

// 몬스터가 죽었으면 1000 프레임(초당 30프레임이므로 33초정도)후에 리스폰 시켜줍니다.

if(nTemp->DMode==_MDDead)
{
  nTemp->DFrame++;
  if(nTemp->DFrame>1000 && (rand()%10)>4)UnDead(Num, Chunkmap);
  return TRUE;
}
// 되살아나기 시작하고나서 7프레임이 지나면 완전히 살아납니다.
if(nTemp->DMode==_MDUnDead)
{
  nTemp->DFrame++;
  if(nTemp->DFrame>7)UnDead(Num, Chunkmap);
  return TRUE;
}

// 걷거나 서있는 것이 아니면 무언가 특별한 동작중입니다. 이런 특별동작에 대해서 처리합니다.
if(nTemp->Mode!=_MFWalk && nTemp->Mode!=_MFStand)
{
// 몬스터가 맞는 동작이면 맞는 방향에 대해 조금 밀려줍니다. 타격감을 주기 위함이죠.
  nTemp->Frame++;
  if((nTemp->Mode==_MFDamage)||(nTemp->Mode==_MFDead&& nTemp->Frame<=4))
  {
   switch(nTemp->hDirect)
   {
   case 1:
    MUp(Chunkmap, Num,Herolist);
    break;
   case 2:
    MDown(Chunkmap, Num,Herolist);
    break;
   case 3:  case 5:  case 7:
    MLeft(Chunkmap, Num,Herolist);
    break;
   case 4:  case 6:  case 8:
    MRight(Chunkmap, Num,Herolist);
    break;
   }
  }
// 죽는 애니메이션이 끝났으면 완전히 죽여버립니다. 실제 메모리를 날리는 건 아니고, 플래그처리만 해서
// 다시 리스폰 될 수 있도록 처리합니다.
  if((nTemp->Mode==_MFDead)&&(nTemp->Frame>=cTemp->GetSize(nTemp->Mode, nTemp->iDirect)))
  {
   Dead(Num, Chunkmap);
   return FALSE;
  }
// 애니메이션이 다 끝났으면 서있는 동작으로 변합니다. 이렇게 되면 다시 A.I계산이 진행됩니다.
  if(nTemp->Frame>=cTemp->GetSize(nTemp->Mode, nTemp->iDirect))
  {
   nTemp->Mode=_MFStand;
   nTemp->Frame=0;
   return TRUE;
  }
// 마법을 사용하는 도중이었으면, 마법이 생성될 시간동안 기다린후 마법객체에 마법을 추가합니다.
  if(nTemp->Mode==_MFMagic)
  {
   if(nTemp->Frame>=
    Magic->AniFile->GetSize(nTemp->Damage, nTemp->Hit)+1)
    nTemp->Frame = nTemp->AniFile->GetSize(nTemp->Mode, nTemp->iDirect)-1;
  }

// 마법이나 공격을 하고 있는 동안이었으면, 자신이 공격하고 있는 방향에 주인공들이 있는지를 체크해서
// 충돌계산을 합니다.
  if(nTemp->Mode==_MFAttack || nTemp->Mode==_MFMagic)
  {
   CheckAttackOne(Num,Herolist,Chunkmap->XSize, Bullet);
   return TRUE;
  }

  return TRUE;
}
// 특별한 애니메이션이 아니고 걷거나 서있는 경우에는 가장 가까이 있는 주인공을 찾아서 공격합니다.
// 만약 사정거리안에 아무도 없다면 FALSE를 리턴합니다.
if(CheckAttack(Num, Herolist,Chunkmap->XSize, Bullet))return TRUE;

// 추적&마법의 A.I들은 일정시간마다 워프를 합니다.
if(nTemp->ParaData[_MONPpattern]==_MAiTraceMagic)
{
  if(MonsterJump(Num, Chunkmap, Herolist))return TRUE;
}
// 공격을 하지 않았으면, 패턴대로 움직입니다.
// 움직이는 패턴은 추적과 랜덤입니다.
switch(nTemp->ParaData[_MONPpattern])
{
case _MAiNoAttack: case _MAiNormal: case _MAiNormalMagic:
case _MAiNormalShoot: case _MAiNormalMS:
  if(RandMove(nTemp))return TRUE;
  break;
case _MAiTrace: case _MAiTraceMagic: case _MAiTraceShoot:
case _MAiTraceMS:
  if(TraceMove(nTemp, Herolist, Chunkmap->XSize))return TRUE;
  break;
}
// 움직일 방향이 정해졌으면 방향대로 움직입니다.
switch(nTemp->iDirect)
{
case 1:
  MapUp(Chunkmap, Num,Herolist);
  break;
case 2:
  MapDown(Chunkmap, Num,Herolist);
  break;
case 3:
  MapLeft(Chunkmap, Num,Herolist);
  break;
case 4:
  MapRight(Chunkmap, Num,Herolist);
  break;
}
return TRUE;
}

몬스터가 한마리 움직이려면 꽤나 많은 과정을 거치네요.
과정을 본다면 이렇습니다.
1. 몬스터가 죽어있는 경우 일정시간을 기다린후 리스폰시킨다.
2. 공격중이면 공격중 계속해서 주인공과 충돌계산을 해서 주인공에게 공격을 가한다.
3. 공격중이 아니면 가까운 주인공을 찾아서 공격한다.
4. 가까이에 적이 없으면 주어진 패턴대로 움직인다.
자~ 대강 알아보긴 했는데, 정작 알고싶은 함수들은 설명이 안됀것 같네요.
그럼 가장 가까이에 있는 주인공을 찾아서 공격하는 CheckAttack 함수를 보죠.

BOOL CMonster::CheckAttack(int Num, CHerolist* Herolist, int mXSize, CBullet* Bullet)
{
BOOL ret;
if((rand()%5)>2)return TRUE;
MON* nTemp = MonsterArray.GetAt(Num);
if(nTemp->Mode == _MFDead || nTemp->Mode == _MFDamage)return FALSE;
switch(nTemp->ParaData[_MONPpattern])
{
case _MAiNormal: case _MAiTrace:
  ret = CheckNormal(Num, Herolist, mXSize);
  break;
case _MAiNormalShoot: case _MAiTraceShoot:
  ret = CheckShoot(Num, Herolist, mXSize);
  break;
case _MAiNormalMagic: case _MAiTraceMagic:
  ret = CheckMagic(Num, Herolist, mXSize);
  break;
case _MAiNormalMS: case _MAiTraceMS:
  if(rand()%3)
  {
   ret = CheckNormal(Num, Herolist, mXSize);
   if(!ret)
    ret = CheckMagic(Num, Herolist, mXSize);
  }
  else
  {
   ret = CheckNormal(Num, Herolist, mXSize);
  }
  break;
}
return ret;
}
// CheckAttack함수는 A.I패턴별로 함수를 호출하는 역할만 합니다. 간단하죠.
// 호출되는 함수중 타격몬스터용 CheckNormal 함수를 보면 다음과 같습니다.
BOOL CMonster::CheckNormal(int Num, CHerolist* Herolist, int mXSize)
{
MON* nTemp = MonsterArray.GetAt(Num);
CHero* hTemp;
int iChunk = nTemp->ChunkNum;
int i;
int End = Herolist->GetSize();
RECT hRect[6] , nRect, rTemp;

// 기본크기 설정
CopyRect(&nRect, &(nTemp->rect));
OffsetRect(&nRect, nTemp->RealX, nTemp->RealY);
int length = nRect.bottom - nRect.top+8;
length = max(length, 32);
for(i=0; i {
  hTemp = Herolist->GetAt(i);
  CopyRect(&hRect[i], &(hTemp->rect));
  OffsetRect(&hRect[i],hTemp->RealX,hTemp->RealY);
}
// 자신의 사방에 대해 자신의 공격 사각형과 주인공들의 충돌사각형의 충돌계산을 합니다.
// 여기서 충돌하면 그쪽에 대해 공격합니다.
// 여기서는 공격사각형이 실제 충돌하는 것보다 길게 되어있습니다.
// 약간의 예측이라고나 할까요??
OffsetRect(&nRect, 0, -length); // 위쪽에 있을때
for(i=0; i {
  if(IntersectRect(&rTemp, &hRect[i], &nRect)&&(char)(Herolist->GetAt(i)->iParaData[_HPai]/10)!=_HFDead)// Hero가 죽었어도 충돌처리는 한다.
  {
   if(nTemp->iDirect!=FUp)
   {
    nTemp->iDirect = FUp;
    nTemp->Frame = 0;
   }
   else
    Attack(Num, i, Herolist);
   return TRUE;
  }
}

OffsetRect(&nRect, 0, length<<1); // 아래쪽에 있을때
for(i=0; i {
  if(IntersectRect(&rTemp, &hRect[i], &nRect)&&(char)(Herolist->GetAt(i)->iParaData[_HPai]/10)!=_HFDead)
  {
   if(nTemp->iDirect != FDown)
   {
    nTemp->iDirect = FDown;
    nTemp->Frame = 0;
   }
   else
    Attack(Num, i, Herolist); // Hero와 충돌처리 
   return TRUE;
  }
}

OffsetRect(&nRect, -length, -length); // 왼쪽에 있을때
for(i=0; i {
  if(IntersectRect(&rTemp, &hRect[i], &nRect)&&(char)(Herolist->GetAt(i)->iParaData[_HPai]/10)!=_HFDead)
  {
   if(nTemp->iDirect != FLeft)
   {
    nTemp->iDirect = FLeft;
    nTemp->Frame = 0;
   }
   else
    Attack(Num, i, Herolist); // Hero와 충돌처리 
   return TRUE;
  }
}

OffsetRect(&nRect, length<<1, 0); // 오른쪽에 있을때
for(i=0; i {
  if(IntersectRect(&rTemp, &hRect[i], &nRect)&&(char)(Herolist->GetAt(i)->iParaData[_HPai]/10)!=_HFDead)// Hero가 죽었어도 충돌처리는 한다.
  {
   if(nTemp->iDirect !=FRight)
   {
    nTemp->iDirect = FRight;
    nTemp->Frame = 0;
   }
   else
    Attack(Num, i, Herolist); // Hero와 충돌처리 
   return TRUE;
  }
}
return FALSE;
}
// 이놈은 장거리공격용 몬스터의 거리측정루틴입니다.
BOOL CMonster::CheckShoot(int Num, CHerolist* Herolist, int mXSize)
{
MON* nTemp = MonsterArray.GetAt(Num);
CHero* hTemp;
int iChunk = nTemp->ChunkNum;
int i;
int End = Herolist->GetSize();
int Xminus;
int Yminus;

for(i=0; i {
// 몬스터와 주인공들간의 거리를 계산한다.
  hTemp = Herolist->GetAt(i);
  Xminus = (hTemp->RealX - nTemp->RealX);
  Yminus = (hTemp->RealY - nTemp->RealY);
// 주인공들이 어느방향쪽에 있는지 찾아낸후, 맞을 수 있는 각도면
// 총을 쏘게됩니다.
// 즉, y축 위치의 차이가 32보다 작다면 x축위치가 400보다 작을때 좌,우로 쏘게됩니다.
  if(abs(Yminus)<32)
  {
   if(Xminus>0 && abs(Xminus)<400) // Right
   {
    if(nTemp->iDirect!=FRight)
    {
     nTemp->iDirect = FRight;  nTemp->Frame = 0;
    }
    else Attack(Num, i, Herolist);
    return TRUE;
   }

   if(Xminus<0 && abs(Xminus)<400)
   {
    if(nTemp->iDirect!=FLeft)
    {
     nTemp->iDirect = FLeft;  nTemp->Frame = 0;
    }
    else Attack(Num, i, Herolist);
    return TRUE;
   }
  }
// 반대로, x의 차가 32보다 작으면 위,아래로 쏘게되죠.
  if(abs(Xminus)<32)
  {
   if(Yminus>0 && abs(Yminus)<300)
   {
    if(nTemp->iDirect!=FDown)
    {
     nTemp->iDirect = FDown;  nTemp->Frame = 0;
    }
    else Attack(Num, i, Herolist);
    return TRUE;
   }

   if(Yminus<0 && abs(Yminus)<400)
   {
    if(nTemp->iDirect!=FUp)
    {
     nTemp->iDirect = FUp;  nTemp->Frame = 0;
    }
    else Attack(Num, i, Herolist);
    return TRUE;
   }
  }
}
return FALSE;
}

핵심적인 부분에 대해서는 다 알아봤군요.
나머지 랜덤으로 움직이는 부분은 뭐 알아 볼 필요도 없고, 주인공을 추적하는 것은 단순한 추적알고리즘만을
사용했습니다. 씰에서는 a*알고리즘을 사용했었지만요. 이때는 a*를 잘 몰랐었거든요.^^
몬스터의 패턴은 더 많이 지정 할 수 있지만, 대부분이 공격하는 방법에 대한 패턴일겁니다.
또한, 움직임에 대한 패턴도 늘어날 수 있겠죠. 패턴프로그램은 상당한 노가다 작업이기때문에, 꽤 시간이 걸리는
부분입니다.
단지 시간만 요하는 부분이니깐 그냥 열심히 하면 됩니다.^^
다음엔 스크립트를 어떻게 구현했었나에 대해 쓰죠.



레이디안 이러케 만들어따 ----- (11)



                                        deadfish@shinbiro.com
스크립트 인코딩/디코딩

////////////////////////////////////////////////
//
// 얼마만인지 모르겠군요.. 하여간 간만에 글을 씁니다.
// 크러쉬며 외주며 나르실리온이며.. 작년한해는 정말 바빴군요.^^
//
//////////////////////////////////////////////

RPG의 생명은 스크립트에 있다~~ 라고 감히 말할 수 있습니다.
스크립트는 게임의 플래그를 참조, 또는 변형하여 게임을 직접 플레이할 수 있도록
만들어주는 것들입니다. 
게임이 소규모라면 간단하게 프로그래밍 해버리면 되겠지만, 게임의 규모가 커지면
당연히 외부 데이터 화일로 빼서 다른사람에게 작업을 맡겨야 하기때문에, 간이 컴파일러를
작성해야합니다.
게임내에서만 쓰이는 언어가 만들어지는 것이죠.
이 스크립트언어는 간단하게 숫자로만 이루어질수도 있는데, 그것보다는 명령어와 그에 딸린
간단한 숫자나 문자열로 이루어 질 수 있습니다.

레이디안에 사용된 함수들을 잠깐 볼까요??

////////////////// 스크립트의 예 /////////////////
;
조건_시작 300 10
대화창그리기 320 480
대화얼굴 0 0 0 1
대화출력 "아싸"
키기다리기
대화지움
아니면 10
대화창그리기 320 480
대화얼굴 1 0 0 1
대화출력 "아싸아~~"
키기다리기
대화지움
조건끝 10

/////////////////////////////////////////////////////
뭐 이런식으로 간단한 스크립트가 사용되었습니다.
이 스크립트는 컴파일, 바이너리화일화 되어서 저장됩니다.

인코딩부분은 쉬우니 간단하게 인코딩부분을 함수로 보여드리지요.
간단하게 한줄을 읽은후 첫문자열을 비교하여 명령어와 같으면 다음에 오는 문자열들을
적당히 잘라서 저장하는 형식입니다.

struct Command
{
short SubScript;
int Para[5];
char* String;
};

int CScript::Encode()
{
int SCommend[10000];
int Top = 0;
char Word[100];
char Number[10];
char Strings[100];
char* Temp = NULL;
Command cTemp;
cTemp.String=NULL;
cTemp.SubScript=0;
int LineNum=0;
int iTemp;
while(1){
  memset(Line, 0, 200);
  memset(Word, 0, 100);
  memset(Number, 0, 10);
  memset(Strings, 0, 100);
  if(GetString()==-1)break;
  LineNum++;
  cTemp.SubScript=0;

  GetWord(Word);
  if(strcmp(Word,"조건_시작")==0)
  {
   cTemp.SubScript = _IF_THEN;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 참조플래그번호
   GetWord(Number);
   cTemp.Para[1]=atoi(Number); // 구문 번호
  }

  if(strcmp(Word,"아니면")==0)
  {
   cTemp.SubScript = _ELSE_THEN;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 구문번호
  }

  if(strcmp(Word,"조건_끝")==0)
  {
   cTemp.SubScript = _IF_END;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 구문번호
  }

  if(strcmp(Word,"스크립트_시작")==0)
  {
   cTemp.SubScript = _SCRIPT_START;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 스크립트 구분번호
   Top++;
   if(CheckNumber(SCommend, Top, cTemp.Para[0]))return LineNum;
   SCommend[Top]=cTemp.Para[0];
  }
  if(strcmp(Word,"스크립트_끝")==0)
  {
   cTemp.SubScript = _SCRIPT_END;
  }
  if(strcmp(Word,"플래그_켜기")==0)
  {
   cTemp.SubScript = _FLAG_ON;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 켤 플래그번호
  }
  if(strcmp(Word,"플래그_끄기")==0)
  {
   cTemp.SubScript = _FLAG_OFF;  // 끌 플래그번호
   GetWord(Number);
   cTemp.Para[0]=atoi(Number);
  }
  if(strcmp(Word,"아이템습득")==0)
  {
   cTemp.SubScript = _FIND_ITEM;
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 습득 아이템 번호
   GetWord(Number);
   cTemp.Para[1]=atoi(Number); // 가격
   GetWord(Number);
   cTemp.Para[2]=atoi(Number); // 플래그
  }
  if(strcmp(Word,"마법습득")==0)
  {
   cTemp.SubScript = _FIND_MAGIC;  
   GetWord(Number);
   cTemp.Para[0]=atoi(Number); // 습득 마법 번호
   GetWord(Number);
   cTemp.Para[1]=atoi(Number); // 가격
   GetWord(Number);
   cTemp.Para[2]=atoi(Number); // 플래그
   GetWord(Number);
   cTemp.Para[3]=atoi(Number); // 습득 캐릭터
  }
  if(strcmp(Word,"대화출력")==0)
  {
   cTemp.SubScript = _TALK;
   iTemp = GetWord(Strings)+1;
   Temp = new char[iTemp];
   memcpy(Temp, Strings, iTemp);
   cTemp.String=Temp;      // 글줄
  }


  if(cTemp.SubScript==0)
  {
   CloseTextFile();
   CloseSaveFile();
   return LineNum;
  }
  int i;
  fwrite(&cTemp.SubScript,sizeof(short),1,OutScp);
  for(i=0; i<5; i++)
   fwrite(&cTemp.Para[i], sizeof(int), 1, OutScp);
  if(cTemp.String==NULL)
  {
   i=0;
   fwrite(&i, sizeof(int), 1, OutScp);
  }
  else
  {
   i=strlen(cTemp.String);
   fwrite(&i, sizeof(i), 1, OutScp);
   fwrite(cTemp.String, i, 1, OutScp);
  }

  if(Temp!=NULL)
  {
   cTemp.String=NULL;
   delete Temp;
   Temp = NULL;
  }
}
CloseTextFile();
CloseSaveFile();
return 0;
}

몇가지 명령어만 컴파일되도록 잘랐는데, 대강의 함수들은 이런식으로 구성됩니다.
저장도 순서대로 그대로 저장되지요.

게임에서 이 만들어진 스크립트들을 호출하는 방법은 여러가지입니다.
일단 스크립트간의 구별은 스크립트 번호로 합니다.
스크립트번호는 함수이름과 동일하다고 생각하시면 됩니다.

스크립트를 호출하는 것은 이 스크립트번호를 호출하는 것인데,
레이디안에는 맵에 스크립트번호가 저장됩니다. 이벤트번호라고 들어가는 그것입니다.
이벤트번호위에 주인공(리더)가 올라가면 이벤트가 있느것으로 간주하고 해당 스크립트 번호를 찾아서
스크립트를 실행합니다.
또 한가지는 NPC에 이벤트번호가 저장됩니다. 즉, NPC와 대화를 하면 해당 스크립트가 호출되어
마치 말을 하는 것처럼 되는 것이지요.

그럼 스크립트를 찾는 함수를 쭈욱~~ 뿌려봅시다.

BOOL CScript::Search(int Num)
{
Command* cTemp;
char* Temp;
CurPos = 0;
while(1)
{
  cTemp = ReadCommand();
  if(cTemp==NULL)return FALSE;
  if(cTemp->SubScript==_SCRIPT_START && cTemp->Para[0]==Num)break;
  Temp = cTemp->String;
  if(Temp!=NULL)
   delete Temp;
  delete cTemp;
}
while(1)
{
  cTemp = ReadCommand();
  if(cTemp->SubScript==_SCRIPT_END)break;
  CommandArray.Add(cTemp);
}
CloseScpFile();
return TRUE;
}

간단하죠? 처음부터 쭈루룩~ 화일에서 스크립트번호만을 읽어들인후 스크립트번호가 동일한 것만을
찾아냅니다. 스크립트번호는 동일한것이 두개가 들어 갈수 없으므로, 절대 한가지만 있겠죠.
없으면 실행이 안돼는 것이고요.
이런식으로 해서 찾아서 TRUE를 리턴하게 되고, CScript객체내에는 해당 화일의 포인터 또는 Command객체들의
포인터를 가지고 있게됩니다.

그후에 루프를 돌면서 한 명령어씩 순차적으로 처리해 나가는 것이죠.
이때 루프는 따로 돌리는 것이 아니고 Idle loop(아무 윈도 명령어가 없을때 도는 루프, 게임루프와 동일)를 뜻합니다.

스크립트의 명령어들은 대부분 게임에 사용되는 기능들을 호출하기때문에 게임에따라 특화되기 마련입니다.
어떤게임에서든 쓰이는 것은 비교문 정도겠죠. (if문)
그럼 if문에 대한 함수를 뿌립니다.

// if flg가 true이면 리턴, 아니면 else가 나올때까지 스크립트를
// 스킵시켜준다. 
BOOL CTest::if_then(int Num, int Section)
{
int SubScript;
int* Para;
Stack[++Top] = GameFlag[Num];
if(Stack[Top])return FALSE;
while(1)
{
  if(!Script->NextCommand())
  {
   Script->RemoveAll();
   ScpComment = FALSE;
   if(TalkNpc>=0)
   {
    NPC* nTemp = Npclist->GetAt(TalkNpc);
    nTemp->Talk = _NM2Move;
    TalkNpc=-1;
   }
   return TRUE;
  }
  SubScript = Script->GetSubScript();
  Para = Script->GetPara();
  if(SubScript==_ELSE_THEN && Section==Para[0])return TRUE;
}
}

위 함수는 스택만 아시면 간단히 이해하실 수 있는 함수입니다.
잘 모르시겠으면, 스택에 대해 아실 수 있는 자료구조책을 좀 뒤져보세요. 꽤 자세히 나올겁니다.

else도 마찬가지입니다.

// flag가 false이면 리턴, 아니면 end if가 나올때까지
// 스크립트를 스킵시켜준다.
BOOL CTest::else_then(int Section)
{
int SubScript;
int* Para;
Stack[Top] = !Stack[Top];
if(Stack[Top])return FALSE;
while(1)
{
  if(!Script->NextCommand())
  {
   Script->RemoveAll();
   ScpComment = FALSE;
   if(TalkNpc>=0)
   {
    NPC* nTemp = Npclist->GetAt(TalkNpc);
    nTemp->Talk = _NM2Move;
    TalkNpc=-1;
   }
   return TRUE;
  }
  SubScript = Script->GetSubScript();
  Para = Script->GetPara();
  if(SubScript==_IF_END && Section==Para[0])return TRUE;
}
}

if와else를 만들때 처음에는 스택을 생각하고 만들지 않았다가, if문들이 중첩될 상황
"if(i......)if(j.........)if(k.........)" 와 같은 상황이 일어났기때문에 어쩔 수 없이
스택구조를 생각하게 되었던 것입니다. 그외에도 Seal를 프로그래밍할 때는 많은 곳에 스택이
사용되었습니다. Queue구조는 잘 사용되지는 않았는데, Stack은 많이 사용되더군요.
재미있는 구조이므로, 꽤 사용할 곳이 많을 겁니다.

지금까지 레이디안을 만들때 사용되었던 스크립트의 일부를 소개했습니다.
뭐 제가 만들면서 어려웠던 부분들만 추려서 소개하다보니, 방법론이나 그런것은 쓰질 못했는데...
언젠간 쓰게 되겠죠.^^
그럼~





레이디안 이러캐 만들었다. ----------------------- ( 12 )


      김병철 
      ( deadfish@shinbiro.com )

12화 디버깅/최적화

지금까지 대강 대강 게임 만들었던 경험 중에 조금 어려웠던 부분이나, 중요하다고 생각되는 부분에대해
여러가지 이야기를 해 보았다.
게임 프로그래밍에 있어서 가장 중요한 것은 이래저래 말이 많지만 디버깅이다.
디버깅에 실패한 게임은 수도 없이 많다.. 대부분 버그라는 수렁에 빠져서 정말 심각한 불명예를
가진 게임들인데, 디버깅이라는게 정말 어렵기 때문이다.
개발자들 누구나 버그 많은 게임을 만들기는 싫기 마련이다.

레이디안에 사용된 디버깅 트릭을 알아보자.

1. 노가다 디버깅
레이디안은 거의 모든 디버깅을 노가다로 해결했다. 도스시절부터 프로그래밍을 해오던 사람들은
요즘 나오는 바운스체커니 디버거니 하는 것들의 사용법에 대해서는 그리 익숙하지 않을 것이다.
필자도 마찬가지인데.. 레이디안때는 더더욱이 경험이 부족한 때라서 디버깅에 많은 힘을 기울였고,
대부분이 노가다 디버깅이었다. 버그가 일어나는 곳에서 MessageBox라든가, 아니면 Printf문으로
화면에 현재 상태를 출력함으로써 논리상 버그를 찾았던 것이다.

논리상 버그를 찾는 방법은 해당 변수를 계속적으로 체크해 나가는 것이다. 요즘은 듀얼모니터를
사용하면 해당변수를 계속적으로 한쪽화면에서 디버그 화면으로 확인해 나가면서 작업할 수 있지만,
레이디안때는 장비도 부족했고, 듀얼모니터를 제대로 지원하는 카드조차 없었기때문에, 할 수 없는 일이었다.

하지만, 노가다로 일일히 의심가는 곳에 출력문을 삽입하고 출력된 값을 비교하는 것은
모든 디버깅의 기초적인 부분으로 반드시 해봐야 하는 것이다. 작업된 하드웨어 외의 다른 하드웨어,
다른 OS에서 제대로 실행되는 지에 대한 것, 또는 다른 환경에서 왜 제대로 안돼는지에 대해서 알고
싶다면, 노가다로 작성된 로그파일 정도밖에 없는 것이다.

2. 메모리 누수를 막아라.
레이디안때도 그랬고, 씰때도 그랬고, 크러쉬작업할때도 마찬가지 였지만, 메모리 누수는 정말 사람을
돌아버리게 하는 버그이다.
메모리 누수는 정말 세심히 체크하지 않으면, 안돼는 부분이라... 여기서 그때 알아낸 노하우를 약간 써보자.

일단, 포인터사용을 최대한 줄인다. 크기를 미리 알수있는 것은 무조껀 배열로 만들고,
동적인할당이 필요한 것만 포인터를 사용한다. 포인터는 정말 메모리 잡아먹는 귀신이다.

두번째로, 포인터변수는 항상 초기화를 NULL로 시킨다. NULL인지 아닌지로 해당 변수에 메모리가 할당되었는지
아닌지를 체크할 수 있다.

세번째, 포인터변수는 사용되지 않는 순간 즉시, NULL인지 확인하고 delete한다.

네번째, 포인터변수를 사용하는 클래스는 파괴자에 모든 포인터변수의 메모리를 free하도록 신경써준다.
포인터변수안에 포인터변수가 들어있을 경우에는 더더욱이 loop를 돌면서 확실히 메모리를 비워줘야한다.

struct aa{
aa():temp(NULL){};
int* temp;
};

class bb{
public:
aa* m_aa;
int size;
bb():m_aa(NULL)
  {
  };
~bb()
  {
   
   if(m_aa)
   {
    for(int i=0;i     {
     if(m_aa[i].temp)
     delete[] (m_aa[i].temp);
    }
    delete[] m_aa;
   }
  };
};

항상 위와 같은형식으로 파괴자를 작성하도록 하면, 왠만해서는 클래스내의 메모리누수는 잡을 수 있다.

다섯번째, 맵등 자주 메모리를 재할당하는 클래스의 경우, 내부변수만 새로 읽는 것이 아니라, 확실히
클래스를 지우고 새로 읽어들이는 편이 낫다. 메모리에 쓰레기값이나 이전정보를 가지게 되는 경우가
종종있는데, 확실히 초기화하지 못할 바에는 클래스자체를 지우고 새로 생성하는 편이 낫다.

3. static을 조심하라.
static으로 클래스내에 변수를 사용할 경우, static변수들은 같은 메모리를 사용한다. 즉

class kk{
static int a;
}; 

kk a;
kk b;
a.a = 1;

이라고 한다면, b.a도 같은 1이란 값을 가지게 된다. 거기에다가, a클래스를 소멸시키고 다시 생성한다고해도
내부변수는 고정된 위치만을 사용하기때문에 소멸시킨 a클래스의 값을 가지게 된다.
그러므로, static보다는 맴버변수만 사용해야되고, 만약 static함수를 사용할 일이 있어서 반드시 static변수를
쓰게된다면, 해당 클래스는 전체에 단 하나만 존재하는 유니크한 클래스여야한다. (관리클래스같은경우)
스크립트엔진을 작성하다보면, 함수포인터를 많이 사용하게 되는데, 이 함수포인터를 사용하려면, 반드시
static함수를 사용해야한다. 하지만, 이때 스크립트 클래스는 두개 만들수없다. (꼬인다. 정말) 이점 명심하기
바란다.

3. 출시 3개월전부터는 코드구조를 바꾸지 마라.
이건 절대 지켜야할 문제이다.
코드를 안정화 시키려면, 최소 3개월이전부터 설계된 구조를 바꾸는 일을 해서는 안됀다.
왠만하면 처음 설계한 코드대로 게임을 끝까지 작성하면 좋으련만... 사람일이 뜻같지 않아서, 그렇게는 안됀다

.
언제 어떤일로 설계가 변경될지 모르게 된다.
그렇지만, 출시 3개월전에 코드의 설계를 변경하게되면, 치명적인 버그가 생길 수 있다는 점을 명심하기 바란다

.
그리고 최소 1개월은 코드의 추가가 있으면 안됀다. 기능의 추가라든가 하는것은 1개월전부터는 최소화하고
절대적으로 코드 안정화와 최적화에 힘을 기울여야한다.

4. 오류체크는 잊지마라.
보통 최적화를 위해 오류체크부분을 없애는 경우가 종종있다.
하지만, 프로세스타임을 많이 잡아먹는 그래픽 출력부분을 제외한 논리적인 부분에서 오류체크를 안하게되면,
코드의 안정성은 심각하게 타격을 받게된다.
메모리가 할당되었는지, 메모리가 제대로 free됐는지 여부등은 확실히 오류를 체크해줘야한다.
대부분의 게임이 이러한 부분에 최적화랍시고 오류체크를 빼는 바람에 막바지에 고생하게 되어버리고 만다.
논리적인부분에서는 오류체크를 한다고 해도 느려지지 않는다. 
버그로 뻗는것보다는 조금 느린편이 낫다고 생각하지 않는가????

5. 최적화에 목숨걸지 말자.
위에말과 일맥상통하는 말이다. 논리적인 알고리즘적 최적화를 제외하고, 오류체크라든가, 루프등을 없앰으로써
얻는 최적화의 효과는 극히 미미하다. 알고리즘을 분석하고 알고리즘자체를 변경시키는 편이 훨씬낫다.
알고리즘을 변경할 수 없다고 종종, 디버깅하기 어렵게 오류체크라든가, 루프없애기, 암호문만들기 
(어디서보니 코드를 압축하면 빨라진다고 한다.) 등을 하는 사람들이 있는데, 그런데 노력하느니, 출력알고리즘
자체를 조금이라도 손보는 편이 정신건강상 이롭고, 디버깅도 쉬워진다.

6. 최적화는 최후에
최적화는 디버깅이 어느정도 끝나고 코드가 안정화 된 시점에서 해야한다.
코드가 안정화되지 않은 시점에 최적화를 하게되면, 버그가 갑자기 많아져버리게 되는 경우가 종종있다.
절대로 최적화는 최후에 하라. 최적화가 덜되었다고 게임이 재미없는 것은 아니다. 하지만,
디버깅이 덜되면, 게임은 재미없어진다. 코드를 작성하는 것보다 디버깅이 어렵다. 

7. 설계를 확실히, 그리고 디버깅이 가능하도록 코드를 작성하자.
설계를 바꾸지 않아도 되도록, 클래스들을 설계할때 여러가지를 생각하고, 기획서를 숙지하고 만들도록한다.
부실한 설계는 프로그램을 필요이상으로 복잡하게 만들게 되고, 그것은 버그를 양산하는 길이다.
최초설계를 튼튼히 하도록한다.
그리고, 처음부터 디버깅이 가능하도록 코드를 작성하도록한다.
코드를 너무 암호문으로 길게 작성한다던가 한다면, 나중에 가독성이 떨어져서 디버깅이 나쁘다.
디버깅이 끝난시점에서는 코드를 짧게 줄일수도 있겠지만, 처음에는 가독성 위주로 코딩하는 습관을 기른다.

레이디안이나 여타 다른프로젝트를 하면서 느꼈던 점들을 대강 썼다.
점점 부실해져가는 글을보니, 아무래도 이번 레이디안... 씨리즈는 여기서막을 내려야 할 것 같다.
다음번엔 좀 더 좋은 내용의 강좌를 썼으면 좋겠다.

맞춤검색