꽃미남 프로그래머 김포프가 창립한 탑 프로그래머 양성 교육 기관 POCU 아카데미 오픈!
절찬리에 수강생 모집 중!
프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 알 수 없는 사용자
안녕하세요. 랩하는좀비, 줄여서 랩좀비군입니다.
이제 올해로 개발 5년차에 들어섰네요. 하지만 그동안 발매한 게임은 없고(...) 회사에서 열심히 이것 저것하면서 좋은날을 위한 대비를 하고 있습니다. 언젠가는 클베도 하고 오베도 하면서 삶의 보람을 느끼...ㄹ지도 모르겠습니다.

아무튼, 앞으로의 제 글은 알고보면 쉬운, 하지만 이제 게임프로그래밍에 들어선 소년, 소녀들에게는 조금은 까다로운 것들에 대해서 설명해 볼까 합니다. 수준 높은 이야기는 다른 분들이 열심히 해 주실 것이기 때문에 저처럼 다른 분들의 글이 도대체 뭐하는 소리야? 라는 생각이 드시는 분은 저만 따라오세요. 그러면 님도 나와 같은 초보. ㅋㅋㅋ

실없는 소리는 여기까지.
오늘은 FXAA를 엔진에 적용하는 법에 대해서 알아 볼까 합니다. 어떤 단어의 약자인지는 제목을 읽으면 알기 때문에 생략. 대충 번역하면 '속도가 짱 빠른 안티얼라이징의 근사' 정도가 되겠습니다. 뭐 쉽게 말해서 안티얼라이징을 하고 싶은데 그냥 속도 빠르고 나름 성능이 나오는 게 뭐다? FXAA 입니다. MLAA, DLAA, SMAA 등등 여러 가지 AA등이 있지만 오늘은 누구나 쉽게, 심지어는 유저들조차도 쉽게 적용할 수 있는 FXAA를 게임에 적용시키는 방법에 대해서 설명해 볼까 합니다. 다음에 여유가 되면 SMAA를 적용해 보도록 하지요.

우선 알아야 할 것이 FXAA는 Screen공간에서 처리되는 PostEffect의 일종입니다. 포토필터나 칼라그레이딩 등등과 똑같은, 즉, 렌더링 파이프라인과는 독립된 형태의 후처리 효과라는 것이지요.  다른 것들과는 완전 분리되어 있는 소스이기 때문에 엔진에 적용하기도 쉽습니다. 그래서인지 몰라도 FXAA는 nVidia사에서 친히 Shader코드를 업데이트 시켜 주고 있습니다. - 좀 더 찾아보니 nVidia 그래픽카드에 더 잘 최적화되는 형태로 만들고 있다고 하더군요.

자, 이제 스텝을 밟아가면서 적용해 봅시다.

1. 소스를 다운 받는다.
 


압축된 파일도 아니고 그냥 코드 파일(Fxaa3_9.h) 1개입니다. .확장자가 .h로 되어 있지만 안에 들어 있는 내용은 Shader코드입니다. 확장자를 변경하고 싶다면 뭐 아무렇게나 변경하셔도 됩니다.

2. FXAA를 include할 코드를 만든다.

Fxaa3_9.h를 인클루드 할 소스 코드를 작성합니다. Fxaa3_9 코드 안에 바로 technique를 만들어서 작성해도 되지만, 매 번 업데이트될 수 있는 코드이므로 그것은 좋지 않은 선택입니다. 파일을 하나 만들어서 include하세요.
#ifndef __FXAA_FILTER_FX__
#define __FXAA_FILTER_FX__

#include "Fxaa3_9.h"

3. define 셋팅. 
include했으니 이제 PC에서 사용하기 위한 설정을 해 봅시다.
Fxaa3_9.h에는 XBOX와 PS3용 소스까지 모두 들어 있거든요.
다음과 같이 define을 해 주면 됩니다.

#ifndef __FXAA_FILTER_FX__
#define __FXAA_FILTER_FX__

#define FXAA_PC 1
#define FXAA_HLSL_3 1

#include "Fxaa3_9.h"
FXAA_PC  : PC용으로 사용할 꺼임.
FXAA_HLSL_3 : shader 3.0모델을 사용할 꺼임.

DirectX10이나 11을 사용하시는 분이라면 FXAA_HLSL_4와  FXAA_HLSL_5도 가능합니다. 입맛에 맛게 설정해 주세요.

4. 함수 호출하기.
그렇다면 이제 함수를 호출해 주면 되겠네요. 호출하는 인자는 다음과 같습니다.
float4 PS_FxAAFilter(in float2 UV : TEXCOORD0) : COLOR
{
	float4 disableParm = float4(0.0f, 0.0f, 0.0f, 0.0f);
	return FxaaPixelShader(UV,				//픽셀의 UV값.
						   disableParm, 	//사용하지 않습니다. 아무 값이나 넣으세요.
						   targetTex, 		//FXAA를 적용할 Texture
						   g_texelSize, 	//텍셀의 크기.xy, x에는 1.0f/pixelWidth, y에는 1.0/pixelHeight
						   disableParm);	//사용하지 않습니다.
}

5. Texture 처리
끝난 것 같지만 끝이 아닙니다. FXAA를 처리하기 위해서는 적용할 Texture의 Alpha값에 해당 픽셀의 Luminance를 저장해 줘야 합니다.  Luminance를 어떻게 구하냐고요? 표준이 있습니다. RGB의 각각의 값에 float3(0.299, 0.587, 0.114)요 값을 곱해서 더해 주면됩니다. 이 변수는 HDR할 때 등등 자주 나오는 값이니 익숙해지면 건강에 좋습니다.
//픽셀의 밝기를 계산해 주어야 FXAA가 제대로 됨.
texColor.a = dot(texColor.rgb, float3(0.299, 0.587, 0.114));
또 중요한 게 한 개 있습니다.
바로 감마 코렉션인데요. 감마 코렉션에 대해서는 이제 해묵은 주제니 넘어가고 -요청이 있다면 한 번 다루겠습니다만...없을 듯. FXAA를 처리하는 곳에서 픽셀의 RGB는 Linear한 공간에 있어야 하지만 Luminance가 저장된 Alpha값은 감마 공간에 있어야 합니다. 이 부분만 잘 하시면 이제 끝!

 어떄요? 참 쉽죠? 이건 뭐 초딩도 10분이면 할 수 있는...
그럼 다음 시간에 만나요.

반응형
,
Posted by 알 수 없는 사용자

0.  창업이 대세다!!
라고 사람들이 줄기차게 주변에서 이야기합니다.  일면 맞는 말이고 틀린 말이기도 하죠. 

맞는 말입니다. 
창업하기 좋은 시기인 것만은 사실입니다. 어느때보다 vc(venture capital) 투자도 활발한 시기이고, pc internet base에서 smart device internet base로 환경이 급속도로 변하고 있죠. 2년전에 메신져 만들어서 1천만 회원 모은다고 하면 미친놈 소리 듣기 딱 좋았지만(네이트온이 정to복)인데, 카카오톡이랑 틱톡이 1-2천만의 회원을 모집했습니다.  누군가의 말처럼 10년만에 다시 찾아온 기회의 시기일지도 모릅니다.  이럴때 창업은 안한다면 도대체 언제 하라는겁니까? 

그렇지만 틀린 말입니다.  
창업하는데에 시기가 다가 아닙니다. 창업을 하고 사업을 성공 시키기 위해서는 넘어야 할 산이 무척이나 많습니다. 시기라는 이점은 나에게만 주어지는게 아니라, 다른 모든 사람들에게도 주어지는 것입니다. 한마디로 나에게만 강점이 되는 바가 아니라는 것이죠. 따라서 창업 그 자체가 목적이 아닌, 사업의 성공을 위해서라면 수 많은 장벽을 극복하고 사업의 성공 그 자체를 잘 추구해야 한다는 것입니다. 

1.  그래. 나도 게임회사를 만들어서 내 게임을 만들겠어.
라고 생각하고 나서부터 해야 할 일은 생각해보면 사실 사업 시작하게 하는데 엄두가 나지 않습니다.

게임 개발 자체는 일단 자신이 있다고 칩시다. 그건 그냥 내가 가진 기술이나 엔지니어 운영이나 수급능력, 그리고 게임 자체의 아이디어나 개발 계획에 대한 것이겠죠.

그러나 그것을 만들기위해 실제적으로 필요한 것은 결국 '실무'적인 부분입니다.

게임 만드시면서 아마 게임 만드는 실무는 충분히 경험해보신 분들이 회사를 만드시겠다고 생각 할 것입니다.  그런데, 회사, 더구나 주식회사로 회사를 만들어서 운영하시려면 주식회사라는 놈 자체에 대해서 잘 알고 어떻게 회사 그 자체를 운영해야 할지에 대해서도 충분히 잘 아셔야 할 것입니다.

자금은 어떻게 돌릴 것인지, 초창기 지분은 어떻게 할 것인지, 투자를 받게 된다면 어떤 투자가들한테 받을 것인지, 또 어떤 조건으로 투자를 받을 것인지, 회사 경영권은 어떻게 보호를 할 것인지, 그리고 같이 회사를 만든 사람들에게는 어떤 식으로 보상을 할 것인지, 회사를 운영하다가 잘 안된다고 관두고 나가려는 사람들이 있다면 그들에게 준 지분은 또 어떻게 할 것인지, 투자 이후에 자금은 어떻게 집행해나갈 것인지, 법률 문제는 어떻게 해결해 나갈 것인지, 회계적인 처리 및 관리는 어떻게 할 것인지, 회사 설립 초기에야 사이 좋았던 파트너들과의 사이가 나빠지게 되면 어떻게 할 것인지, 내가 지금 자금 운영하거나 회계처리하고 있는 방식이 문제가 없는 것인지, 투자자들은 어떻게 대응해야 할 것인지, 주식회사 정관은 도대체 뭘 의미하는 것인지, 등등

그냥 앉은 자리에서 막 떠오르는데로 이야기를 했는데도 이정도입니다. -_-  도대체 고려해야 할 점이 한 두가지가 아니라는 것이죠.  그런데 이런 문제에 대해서 무지한 상태에서 그냥 회사를 만들어서 그냥 법무사, 회계사 한테 일을 다 맡겨놓고 난 잘 모르겠다고 하고 회사를 그냥 운영한다면 나중에 사업이 잘 되더라도 해결해야할 문제가 매우 많아 집니다.  심지어 가끔 회사 창립자는 나인데, 회사가 성공하고 나니까 갑자기 나는 쫓겨나게 되는 경우도 있습니다.  분명히 초창기 투자가였는데, 나한테 떨어지는 수익은 얼마 없는 경우도 있죠. 회사를 위해서 최선을 다했는데, 회계적으로 생각치도 않게 불법을 저지른 경우도 있죠. 잘 챙겨놓지 않은 지분때문에 회사에 신규 투자를 받기도 힘들고, 그렇다고 매각하기도 만만치 않아지게 일이 꼬이는 경우도 다반사입니다.

결론적으로 말하자면, 게임 회사를 만드시고 운영하시려면 게임만 잘 만들어서는 안됩니다. 그건 기본이고, 그보다 더 기본은 회사를 어떻게 움직여야 하는지에 대한 지식이 필수입니다.

영화 <소셜 네트워크>를 보시면, 그냥 아무 생각없이 잘못 싸인한 계약서 한장에 자기 지분율이 사라져버릴 수도 있고, 숀 패닝이 잘 도와준 덕에 vc와의 투자 조건이 회사에 매우 유리하게 적용되기도 합니다.  자본주의의 회사 시스템이라는게 잘 이용하면 큰 무기가 되지만, 잘못하게 되면 자신에게 큰 독으로 다가옵니다.

2.  그래도 난 창업을 하고야 말겠어.
라고 생각하시고 계신다고요?  그렇다면 별 수 없습니다.  회사를 어떻게 만들어나갈지를 게임을 어떻게 만들지와 함께 잘 고민하고 많이 생각하셔야 합니다.
모르면 배우면 됩니다. 

그냥 변호사한테 맡겨놓겠다고요? 그냥 믿을 만한 회계사무실이 있다고요? 법무사가 그런거 다 알아서 해주는거 아니냐고요? 그들이 물론 전문가인 것만은 사실입니다. 그렇지만 그사람들은 여러분이 잘 컨트롤 할 수 있는 만큼만 일을 합니다. 여러분이 잘 모르면 그사람들은 그사람들 편의에 맞춰서 일을 합니다. 아니 왜 돈 주고 일 시키면서 제대로 시키지 못해서 나중에 피해볼 일을 하십니까? 

제가 쓰는 연재는, 주변에 개발자로써 뛰어나신 분들이 회사를 만드시면서 이런 문제에 봉착하시는 경우를 많이 봐왔고, 그런 문제를 잘 풀고 해결해나가야 진정으로 게임의 성공을 넘어서 회사의 성공까지 이뤄내실 수 있기에 그런 부분에 약간이라도 도움이 되고자 글을 쓰려고 합니다.  가능하면 회사의 설립부터, 회사가 ipo 하거나 혹은 m&a를 통해 매각이 되는 경우까지 그 중간에 거칠 수많은 일을 회사 설립시부터 차근차근 하나씩 밟아 나가며 쓰려고 합니다.  

 개발사이드에서 고민하실때는 모르셨던, 운영이나 투자사이드의 일도 잘 아셔야 합니다.  게임이 성공해서 그게 정말로 회사를 만든 분들에게 부를 가져다 주려면 그 게임을 어떤 회사의 틀 안에 담아야 하는지도 잘 고민해야합니다.

개발자 여러분.  창업 화이팅입니다.  방패와 칼 모두 들고 험난한 게임판에 뛰어 들어보죠.
반응형
,
Posted by 김포프

동기
최근에 한동안 동고동락했던 게임엔진에서는 모든 것을 문자열(string)로 참조했었습니다. 물론 문자열 비교를 계속 해야하니 꽤 느린 방법이었지요. 그래서 이걸 빠르게 하기 위해 해쉬값(정수, int)을 사용했었더라죠.

이놈이 대충 이렇게 동작했습니다. 일단 엔진에 싱글턴으로 해쉬 문자열 매니저가 있고요, 게임실행 도중에 문자열을 사용할 때마다 이 매니저를 통해서 해쉬 값을 받는데, 그때마다 해쉬 값과 문자열이 해쉬문자열 매니저안에 저장됩니다. 그리고 문자열을 비교할 때는 그냥 해쉬 값만 비교하고, 실제 문자열을 char*가 필요할 때는 hashStringManager->GetString(hashKey); 이런 식으로 호출해 주면 되었죠.

개인적으로는 이 시스템이 그닥 맘에 들지 않았는데요......-_- 그 이유는:

  • 메모리 낭비가 심하다:
    • 해쉬문자열 매니저에 저장된 놈 중에 정작 게임에서 char* 문자열로 형태로 사용하는 놈은 10% 정도 밖에 안되었습니다.
    • 따라서 90%는 그냥 룩업(look-up) 키처럼 해쉬값(int)만 사용할 뿐이었죠.
    • 즉, 90% 에 대해선 실제 char* 을 저장해 둘 필요가 없었습니다. 그냥 해쉬값만 있으면 충분했죠.
    • 그래서 한 생각이... .이 90%에 대해서는 굳이 해쉬값을 게임 실행중에 계산할게 아니라 오프라인에서(툴이나 컴파일 도중에) 만들어 주면 되겠다고....
  • 멀티쓰레딩을 할 때도 레이스(race) 컨디션이 없도록 하려다보니 해쉬문자열 매니저가 꽤 느려졌습니다. lock을 추가했는데, 특히 리소스 로딩도중에 여러 쓰레드가 이 lock을 걸려고 하다보니 이게 꽤 bottleneck이 걸리더라구요?
  • 실행파일이나 게임리소스 데이터파일에 들어가있는 문자열은 hex 에디터 하나만으로도 쉽게 볼 수 있으니....좀 더 해킹당할 위험이 클껄요...?
그래서 이보다 좀 나은 방법을 찾아보자..... 하고 시도했던 게 바로... 이 밑에 아주 장황하게 설명하는 놈입니다... 

일단 문자열의 종류를 2가지로 나누자
위에서 90%와 10%로 나눴던 문자열을 다른 포맷으로 저장하는게 우선입니다.

1. char* 문자열 (10%)
게임속에서 char* 형태로 사용해야 하는 문자열들은 (예전과 비슷하게) char[]포맷으로 저장합니다.

  • 게임실행중에 로딩해야할 파일의 이름들 (단, 파일이름마저 해쉬로 만들어서 1HDA3820.ext 라는 형식으로 저장하는 경우에는 예외겠죠... 근데 정말 이렇게까지 하는 게임들이 몇이나 있을까요.....? 머엉.... -_-)
  • 화면에 출력해야할 문장들: 게이머들에게 0xFFFFF, 0x888888 또는 0x000000 따위로 대화창을 보여주고 싶진 않겠죠....? (아래 사진을 보니 그래도 될거 같긴....  ^_^)

Image Source: icanhascheeszburger.com



대부분의 게임에서 쓰이는 문자열 중에 순수 char* 문자열이 필요한 경우는 대충 10% 정도 될테니까... 굳이 해쉬값을 계산하는 대신 곧바로 strcmp()로 한글자씩 문자열을 비교해도 될거 같은데요. 뭐, 정 해쉬값을 사용하려면 위에서 설명드렸던 해쉬 문자열 매니저를 사용해도 되구요. (최소한 예전에 비해 1/10정도의 문자열만 저장할테니 메모리 낭비는 적겠죠....)

2. 해쉬값으로 표현한 문자열 (90%)
위의 10%를 제외한 다른 문자열들, 즉 char* 형태가 전혀 사용되지 않는 문자열들은 그냥 간단히 해쉬값(int)로 저장합니다. 여기에 포함되는 문자열들로는 다음과 같은 놈들이 있죠.

  • 문자열 비교에만 쓰이는 놈
  • (해쉬맵 등의) 룩업키로만 쓰이는 놈

char* 문자열을 사용하는 법은 이미 다들 아실테니.... 두번째 방법인 해쉬값으로 표현하는 문자열에 대해서만 좀더 다루겠습니다.

괜찮은 해쉬함수 고르기: x65599
해쉬함수가 뭔지는 다들 아실란가요? 모르시는 분들을 위해 간단히 말하면 다른 문자열마다 독특한('고유한'이라고도 표현합니다) 정수값을 만들어 내줄려고 "노력"하는 함수입니다. ("노력"에 따옴표를 두른 이유는 해쉬함수가 독특한 정수값을 계산해내지는 못하는 경우가 있기 때문입니다. 이렇게 문자열이 서로 다른데도 동일한 해쉬값이 나오는 경우를 두고 해쉬 충돌이 생겼다고 하지요.) 뭐든간에 각 문자열마다 독특한 해쉬값을 만들어 내었다면 문자열을 비교할 때, 그 안에 있는 글자를 하나씩 비교할 필요 없이 그냥 해쉬값 2개만 비교하면 되지요. 이것의 장점? 아무래도 빠르죠. 간단하고... ^^

그렇다면 문자열마다 고유한 해쉬값을 계산하는 방법은 뭘까요? 뭐... 이미 꽤 많은 해쉬함수들이 공개되어있지요. 각, 함수따라 해쉬 충돌이 나는 횟수도 좀 다르고, 속도도 다양합니다. 그냥 본인의 필요에 따라 가장 적절한 함수를 선택하면 됩니다. 제 개인적으로 생각하는 게임에 적합한 해쉬 함수는 다음과 같은 조건을 충족해야 합니다.
 
  • 해쉬 충돌이 거의 없어야 한다.
  • 컴파일 도중과 게임실행 중에 모두 사용할 수 있을 정도로 유연해야 한다: 예를 들어 게임실행도중에 두 문자열을 합친(concat) 뒤 그 결과물에 해쉬값을 계산하려 한다면 컴파일 시점만 아니라 런타임에서도 이 함수를 쓸 수 있어야 겠죠?
  • 게임속에서 사용할 수 있을 정도로 속도가 빨라야 한다.
그래서 인터넷질을 좀 하던 도중 Chrisis Savoie 아저씨네 블로그에서 해쉬함수를 비교해둔 차트를 찾았습니다. 차트를 쭉 둘러보니 x65599라고 불리는 해쉬함수가 저에게 가장 적합해 보이더군요. (그리고 이름도 멋지잖아요.. 앞에 X가 떡하니 붙으니.. 먼가 간지가 풀풀~ -_-;;; ) x65599는 성경책에 나오는 단어들을 모두 돌려도 해쉬 충돌이 없다더군요. (역시 위 링크 참조)

그리고 실제 코드도 한번 봤는데(바로 아래 붙여놓았음) 매우 짧더군요. -_-;; 그냥 65599를 계속 곱해주면 끝(물론 오버플로우를 이용하는 거지만....) 아하! 그래서 이름이 x65599였군요.. ㅎㅎ... (참고로 65599는 소수(prime number)입니다. 해쉬값을 계산해 낼 때는 이렇게 소수를 많이 씁니다.)

// 65599를 곱하는 해쉬함수. (Red Dragon 책에서 훔쳐옴 -0-)
unsigned int generateHash(const char *string, size_t len)
{
  unsigned int hash = 0;
  for(size_t i = 0; i < len; ++i)
  {
     hash = 65599 * hash + string[i];
  }
  return hash ^ (hash >> 16);
}


자, 그럼 그럴듯해 보이는 해쉬 함수도 골랐으니... 이제 툴과 게임코드에서 어떤 짓을 해야하는지 가볍게 살펴보죠.

툴에서 데이터 세이브하기
char * 값을 게임데이터로 저장하는 툴이 있다면, char * 대신 해쉬값(int)을 저장하도록 툴 코드를 바꿔줍니다. 뭐 그냥 저 위의 해쉬함수에 char* 를 인자로 호출한 뒤, 그 결과를 저장해 주면 됩니다. (툴이 C#처럼 다른 언어로 되어있으면  그 언어에서 똑같은 함수를 만들어주던가.. 아니면 interop으로 감싸주던가.... )

간단하죠? 이러면 데이터에서 char*는 사라집니다. 이제 게임코드쪽으로 고고고,,,

게임코드에서 컴파일시에 해쉬값 만들기
예를 들어 게임코드에서 "funny_bone"이란 이름의 조인트를 찾으려 한다고 하죠. 예전 같으면 이런 코드를 썼겠죠.

bones.find("funny_bone");


근데 이제 툴에서 "funny_bone"이란 문자열 대신에 해쉬값을 저장하니... 이제는 대신 이렇게 코드를 작성해야 합니다.

const char * boneToFind = "funny_bone";
bones.find( generateHash(boneToFind, strlen(boneToFind) );


근데 이렇게 하면 "funny_bone"이라는 문자열이 여전히 실행파일에 삽입되지 않을까요?  만약 그렇다면 예전에 쓰던 해쉬문자열 매니저보다 메모리를 적게 잡아먹을리도 없겠고.... 으음.... 그렇다면.. 여태까지 왜 글을 쓴거지? -_-;;;; 는 아니고........

위를 잘보면 문자열이 상수(const) 잖아요? 그럼 저기에 generateHash() 함수에서 하는 계산을 적용하면 나오는 그 결과 해쉬값(int)도 정해져 있을 수밖에 없죠. 즉, 똑똑한 컴파일러라면 "아하! funny_bone이란 문자열이 이미 상수로 정의되어 있고 여기에 generateHash()란 함수를 호출하면서 이런저런 계산을 하는군. 그렇다면 굳이 프로그램 실행도중에 이런 계산을 할 필요가 없겠는걸? 컴파일 도중에 미리 해버려서 그 결과인 해쉬값'만' 코드에 넣으면 되지 않을까?" 라는 논리적 사고를 할 수 있어야 한다.... 는게 제 소망이자 바램이죠.. -_-;; 만약 컴파일러가 이리 똑똑할 수 있다면 컴파일 도중에 다음과 같이 코드가 바뀔 겁니다.

// 0XF1C66FD7F이 실제 "funny_bone"의 해쉬값입니다.
bones.find( 0xF1C6FD7F );    


이런 마법은(?) 다음 두 조건만 충족된다면 가능합니다.

  • 컴파일러가 위 해쉬 함수를 인라인(inline)한다: 컴파일러가 해쉬함수를 인라인으로 삽입해주지 않으면 컴파일 도중에 해쉬값을 계산할 턱이 없지요. 그냥 함수를 호출할 테니까요. 따라서 이 조건이 반드시 충족되야 합니다. 대부분의 컴파일러에서 inline 키워드는 강제성이 없는 게 문제긴 한데... (가이드라인일 뿐)... 뭐 그닥 해결하기 어려운 문제는 아닙니다.
  • 컴파일러가 해쉬함수 안에 있는 for루프를 언롤(unroll )해 준다: 언롤이란 루프 코드가 있을 때, 각 루프 회차를 일일이 코드로 풀어서 써주는 걸 뜻합니다. 컴파일러가 컴파일시에 루프를 몇번 돌릴 지 예측할 수 있다면 이걸 일일이 풀어 써주는게 불가능하지만은 않죠....
generateHash(const char *, size_t) 함수의 인라인
우선 컴파일시에 generateHash(const char*, size_t) 함수가 인라인 될 수 있게 만들어줘야 겠죠. 그러려면 헤더파일에 함수본체를 넣는 방법이 최고입니다. 더 나아가, 문자열의 길이를 구하려고 strlen(const char *)함수를 따로 호출할 필요가 없도록 다음과 같은 매크로를 만들겠습니다.

#define HASH_STRING(str) generateHash(str, strlen(str));


이 매크로까지 들어간 hash.h 파일을 보여드리면 다음과 같습니다.

// 컴파일 타임 해쉬문자열 만들기 테스트
// author: Pope Kim (www.popekim.com)

#include <string.h>
#define HASH_STRING(str) generateHash(str, strlen(str));

// 65599를 곱하는 해쉬함수. (Red Dragon 책에서 훔쳐옴 -0-)
// 이 함수의 몸체까지 헤더파일에 넣어서 컴파일러의 인라인을 돕는다.
inline unsigned int generateHash(const char *string, size_t len)
{
  unsigned int hash = 0;
  for(size_t i = 0; i < len; ++i)
  {
    hash = 65599 * hash + string[i];
  }
  return hash ^ (hash >> 16);
}


테스트 코드
이제 테스트 코드를 만들어 컴파일러와 최적화 옵션에 따라 원하는 결과(HASH_STRING(str)이 정수로 탈바꿈 하는 것... 물론 컴파일시에...)가 나오는지 살펴보겠습니다.

이게 테스트 코드, main.cpp입니다.

// 컴파일 타임 해쉬문자열 만들기 테스트
// author: Pope Kim (www.popekim.com)


#include <stdio.h>

#include "hash.h"


int main(int args, char** argv)
{
  unsigned int hashValue = HASH_STRING("funny_bone");
  printf("해쉬 값: 0x%8x\n", hashValue);

  return 0;
}



이제 컴파일러들이 얼마나 똑똑한지 알아봅시다 -_-;

Visual Studio 2010 SP1
비주얼 스튜디오에서 Win32 콘솔 프로젝트를 만든 뒤, 최적화 옵션을 바꿔가며 테스트 해봤습니다. (제가 영문 비졀 스튜디오를 써서 옵션은 대충 영문으로 남겨둡니다 -_-)

  1. 프로젝트 설정을 Release로 바꿔줍니다.
  2. 어셈블리 파일을 출력하기 위해 Project Properties > C/C++ > Output Files > Assembler Output 옵션으로 가서 Assembly-Only Listing (/FA)을 선택 해줍니다.
  3. 최적화 플래그를 바꿔주기 위해 Project Properties > C/C++ > Optimization으로 가서 아래의 최적화 옵션들을 바꿔줘가며 컴파일을 합니다.
Disabled (/Od)
주목할 만한 것은 대략 2가지....
  • generateHash() 함수가 인라인 안되었군요.. (뭐 최적화를 전혀 안했으니 당연한...?)
  • 재미있게도 strlen() 호출은 10으로 탈바꿈 했군요. push 10이라고 된 어셈코드를 보세요...

_main PROC      ; COMDAT
; File e:\temp\x65599\x65599\main.cpp
; Line 11
 push ebp
 mov ebp, esp
 push ecx
; Line 12
 push 10     ; 0000000aH
 push OFFSET $SG-5
 call ?generateHash@@YAIPBDI@Z  ; generateHash
 add esp, 8
 mov DWORD PTR _hashValue$[ebp], eax
; Line 13
 mov eax, DWORD PTR _hashValue$[ebp]
 push eax
 push OFFSET $SG-6
 call DWORD PTR __imp__printf
 add esp, 8
; Line 15
 xor eax, eax
; Line 16
 mov esp, ebp
 pop ebp
 ret 0
_main ENDP



Minimize Size(/O1)
최적화 끈 거와 별 차이는 없습니다. 해쉬함수가 인라인되긴 했는데 여전히 루프를 돌립니다. (문자열 길이인 10하고 비교한 뒤 다시 루프 처음으로 점프(jb)하는 부분을 보시면 암)

_main PROC      ; COMDAT
; File e:\temp\x65599\x65599\main.cpp
; Line 12
 xor ecx, ecx
 xor eax, eax
$LL5@main:
 movsx edx, BYTE PTR ??_C@_0L@DDOFCBGB@funny_bone?$AA@[eax]
 imul ecx, 65599    ; 0001003fH
 add ecx, edx
 inc eax
 cmp eax, 10     ; 0000000aH
 jb SHORT $LL5@main
 mov eax, ecx
 shr eax, 16     ; 00000010H
 xor eax, ecx
; Line 13
 push eax
 push OFFSET ??_C@_0BF@DJEFNLLJ@hash?5value?5is?50x?$CF8x?6?$AA@
 call DWORD PTR __imp__printf
 pop ecx
 pop ecx
; Line 15
 xor eax, eax
; Line 16
 ret 0
_main ENDP



Maximize Speed(/O2)
오옷! 첫 줄을 봐봐요. push -238617217 이게 16진수로 0xF1C6FDF?이거든요. 모든 계산이 다 사라지고 해쉬값 하나로 탈바꿈 했군요! 이야! 역시 가능한 거였어요 -_- 흣~

; Line 13
 push -238617217    ; f1c6fd7fH
 push OFFSET ??_C@_0BF@DJEFNLLJ@hash?5value?5is?50x?$CF8x?6?$AA@
 call DWORD PTR __imp__printf
 add esp, 8
; Line 15
 xor eax, eax
; Line 16
 ret 0
_main ENDP


그리고 여기서 나온 .exe파일을 텍스트 에디터에서 열어서 funny_bone이란 문자열이 있나 찾아보니 없군요!



Full Optimization(/Ox)
이 옵션으로도 어셈블리어는 그럴듯해 보입니다.

_main PROC      ; COMDAT
; File e:\temp\x65599\x65599\main.cpp
; Line 13
 push -238617217    ; f1c6fd7fH
 push OFFSET $SG-6
 call DWORD PTR __imp__printf
 add esp, 8
; Line 15
 xor eax, eax
; Line 16
 ret 0
_main ENDP


하지만 .exe파일을 열어서 funny_bone을 찾아보니...

funny_bone이 왜 있는 건데...? 응?



이거 뭐하자는 건지... -_- Full Optimization이 사용안하는 문자열 하나 제거하지 않다니.. 참으로 웃긴 일입니다. 이 외에도 다른 테스트 프로그램을 만들어서 실험해봐도 결과는 같았습니다. 심지어 이따위 함수를 만들고 컴파일해도 exe파일안에 스트링이 그대로 있더군요.

void idiot()
{
  const char* idiot = "OMG";
}


사실 .exe 파일 안까지 뒤져볼 생각은 첨에 안했었는데 진영군(denoil)이 안되는거 아니냐고 물어와서 그거 확인해보다 찾아낸 결과입니다. 진영군이 VS 2008과 VS2010 버전에서 실험했을때도 결과는 똑같이 개판이었어요 -_-;

그래서 회사동료인 Karl하고 뒤적거리다 보니 C/C++ > Code Generation > Enable String Polling이란 옵션이 있더군요. 이걸 Yes(/GF)로 켜주면 그제서야 문자열이 exe에서 사라집디다. 뭔 이유인진 모르겠지만 이 옵션이 /O1, /O2에는 켜있는데 /Ox에는 기본적으로 꺼져있더라는....


g++
그렇다면 g++ 컴파일러는 과연 어떨까요. 테스트에 사용한 g++ 버젼은 4.5.3이고.. 컴파일러 플랙은 이렇게 했습니다.

g++ *.cpp -pedantic -Wall -S <최적화-플랙>


-S 플랙은 어셈블러 코드만 만들고 컴파일을 중지하라는 의미임..(어셈블리어를 봐야 제대로 마법을 부렸는지 확인할 수 있으니.... -_-)

-O0
-O0 플랙은 최적화를 하지말란 의미죠. 따라서 결과는 뻔한... 비졀스튜디오와 마찬가지로 strlen() 함수가 10으로 탈바꿈 해버렸단 게 좀 특이할 뿐.... 하지만 여전히 해쉬함수는 인라인 안되었습니다.

LFE4:
 .def ___main; .scl 2; .type 32; .endef
 .section .rdata,"dr"
LC0:
 .ascii "funny_bone\0"
LC1:
 .ascii "hash value is 0x%8x\12\0"
 .text
.globl _main
 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI4:
 movl %esp, %ebp
LCFI5:
 andl $-16, %esp
LCFI6:
 subl $32, %esp
LCFI7:
 call ___main
 movl $10, 4(%esp)
 movl $LC0, (%esp)
 call __Z12generateHashPKcj
 movl %eax, 28(%esp)
 movl 28(%esp), %eax
 movl %eax, 4(%esp)
 movl $LC1, (%esp)
 call _printf
 movl $0, %eax
 leave
LCFI8:
 ret


-O1
이 플래그에서는  generateHash() 함수가 인라인 됩니다만 여전히 계산은 다 합니다. 비졀 스튜디오랑 매우 비슷하군요?

.def ___main; .scl 2; .type 32; .endef
 .section .rdata,"dr"
LC0:
 .ascii "funny_bone\0"
LC1:
 .ascii "hash value is 0x%8x\12\0"
 .text
.globl _main
 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI0:
 movl %esp, %ebp
LCFI1:
 andl $-16, %esp
LCFI2:
 pushl %ebx
LCFI3:
 subl $28, %esp
LCFI4:
 call ___main
 movl $LC0, %eax
 movl $LC0+10, %ebx
 movl $0, %edx
L2:
 imull $65599, %edx, %edx
 movsbl (%eax), %ecx
 addl %ecx, %edx
 addl $1, %eax
 cmpl %ebx, %eax
 jne L2
 movl %edx, %eax
 shrl $16, %eax
 xorl %eax, %edx
 movl %edx, 4(%esp)
 movl $LC1, (%esp)
 call _printf
 movl $0, %eax
 addl $28, %esp
 popl %ebx
LCFI5:
 movl %ebp, %esp
LCFI6:
 popl %ebp
LCFI7:
 ret


-O2
-O1플래그와 결과가 같군요. (뭐 그도 그럴법한게 루프 언롤은 -O3 플래그에서나 활성회 돠거든요...)

 .def ___main; .scl 2; .type 32; .endef
 .section .rdata,"dr"
LC0:
 .ascii "funny_bone\0"
LC1:
 .ascii "hash value is 0x%8x\12\0"
 .text
 .p2align 4,,15
.globl _main
 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI0:
 movl %esp, %ebp
LCFI1:
 andl $-16, %esp
LCFI2:
 subl $16, %esp
LCFI3:
 call ___main
 movl $LC0, %eax
 xorl %edx, %edx
 .p2align 4,,7
L2:
 imull $65599, %edx, %edx
 movsbl (%eax), %ecx
 addl $1, %eax
 addl %ecx, %edx
 cmpl $LC0+10, %eax
 jne L2
 movl %edx, %eax
 shrl $16, %eax
 xorl %edx, %eax
 movl %eax, 4(%esp)
 movl $LC1, (%esp)
 call _printf
 xorl %eax, %eax
 leave
LCFI4:
 ret


-O3
드디어 결과가 나왔습니다!  movl $-238617217, 4(%esp) 보이시죠? 드디어 정수값 하나로 탈바꿈 했군요.

 .def ___main; .scl 2; .type 32; .endef
 .section .rdata,"dr"
LC0:
 .ascii "hash value is 0x%8x\12\0"
 .text
 .p2align 4,,15
.globl _main
 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI0:
 movl %esp, %ebp
LCFI1:
 andl $-16, %esp
LCFI2:
 subl $16, %esp
LCFI3:
 call ___main
 movl $-238617217, 4(%esp)
 movl $LC0, (%esp)
 call _printf
 xorl %eax, %eax
 leave
LCFI4:
 ret


exe 파일을 열어서 문자열 검색을 해봐도 없었습니다. (스크린샷은 생략)

-Os
-Os 는 크기를 제일 작게 최적화하란 플래그인데요. 역시 원하는 결과는 아닙니다.

LFE4:
 .def ___main; .scl 2; .type 32; .endef
 .section .rdata,"dr"
LC0:
 .ascii "funny_bone\0"
LC1:
 .ascii "hash value is 0x%8x\12\0"
 .text
.globl _main
 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI7:
 movl %esp, %ebp
LCFI8:
 andl $-16, %esp
LCFI9:
 subl $16, %esp
LCFI10:
 call ___main
 movl $10, 4(%esp)
 movl $LC0, (%esp)
 call __Z12generateHashPKcj
 movl $LC1, (%esp)
 movl %eax, 4(%esp)
 call _printf
 xorl %eax, %eax
 leave
LCFI11:
 ret



간단 정리
자, 이 놀라운(어쩌면 어이없는 걸지도 -_-) 꼼수를 동작하게 하려면 필요한 비졀 스튜디오 2010과 g++의 최적화 플래그를 간단히 정리.

Visual Studio 2010 SP1
  • /O2
  • /Ox (단, Enable String Pooling 옵션을 킬 것) 

g++ 4.5.3
  • -O3

디버깅
자, 그럼 코드에서 char* 문자열도 제거했으니 실행파일 용량도 작아질테고... 어랏? 근데 디버깅은 어쩌죠? 예를 들어서 "0xF1C6FD7F"란 이름을 가진 본에서 크래쉬가 낫다면..... 이게 대체 3DS Max에서 어떤 본인지 어케 알까요? 디버깅을 하려면 char* 문자열이 여전히 필요하군요...... 써글 -_-;;;

그렇다면 여태까지 한 걸 모두 접어야 할까요... 물론 그럴거면 이 글도 안썼겠죠 -_-; 이 데이터는 디버깅에만 유용한 거니까 디버깅에만 사용할 법한 꼼수를 찾아야죠. 생각해보면 사실 그닥 어려운 문제는 아닙니다. 그냥 문자열 데이터베이스 파일만 하나 있으면 되죠. 그 데이터베이스는 <해쉬키, char*>로 된 목록을 가질거고, 이제 1)게임코드에서 사용하는 모든 문자열과 2)툴에서 게임데이터로 저장하는 모든 문자열을 데이터베이스에 저장해 두기만 하면 됩니다.

디버그 문자열 데이터베이스 만들기
디버그 문자열 DB는 무슨 파일포맷으로 저장해야할까요? SQL DB 라이트도 나쁜 생각은 아니죠. 근데 전 그냥  텍스트 파일에 저장할 거 같습니다. 아무래도 SQL DB보다는 텍스트 파일이 게임엔진에서 쉽게 읽을 수 있을 거 같아서요. 뭐, 무슨 포맷을 선택하던 그냥 파일이름은 debug.string_db로 하죠.

도구에서 디버그문자열 DB 저장하기
뭐, 도구에서 할 일은 크게 없습니다. 그냥 게임데이터 파일에 새로운 문자열을 저장할 때마다 debug.string_db 파일에도 저장하면 됩니다.

끝 -_-. 룰루~

게임코드에서 디버그 문자열 DB 저장하기
그렇다면 코드 안에 있는 문자열은 어케 할까요. HASH_STRING() 매크로 안에 인자로 전해주는 문자열들이요. 뭐, 다행히 HASH_STRING()이란 매크로를 정의해뒀군요. 간단히 C#이나 파이썬 같은 스크립트 언어로 소스코드 디렉토리를 다 뒤지면서 HASH_STRING() 패턴이 보일때마다 그 안에 있는 char*를 해쉬값으로 변환해서 debug.string_db에 저장하는 코드를 짜면 됩니다. regular expression을 쓰면 와따지요. 그리고 비졀 스튜디오 프로젝트의 포스트 빌드 이벤트로 이 스크립트를 한 번씩 호출해주면 끝... 속도도 꽤 빨라요... -_-

뭐, 이건 아주 간단하진 않지만... 그닥 어렵지도 않은 문제였으니...... 끝.... -_- 룰루~

문자열 값 찾기
그럼 이제 비주얼 스튜디오에서 디버깅을 할 때 디버그 문자열들을 찾는 문제만 남았는데.... (어차피 watch 창에는 int로 된 해쉬 값밖에 안보일테니까요.)

문자역 룩업 툴
DirectX SDK에 딸려오는 DirectX Error Lookup 툴 써보신 분 있으세요? 이따위로 생겼지요.



간단히 이런 툴을 작성해도 됩니다. 툴이라고 해봤자 그냥 debug.string_db 파일을 읽어온 뒤 유저가 입력한 해쉬값과 일치하는 문자열을 찾아서 보여주는 게 전부죠. 일일이 해쉬값을 비졀 스튜디오 watch 창에서 복사해와 붙여넣는게 귀찮긴 하지만...... 쓰는데 큰 문제는 없겠죠?

Visual Studio 플러그인?
다음으로 해 본 생각은... 비주얼 스튜디오 플로그인을 만드는 겁니다. 제가 직접 비주얼 스튜디오 플러그인을 만들어 본 적이 없어서 이게 가능한지는 확실치 않은데...

비주얼 스튜디오 플러그인에서 텍스트 파일이라던가 SQL DB를 읽어올 수 있다면... 그리고 watch 창 안에 디버그 데이터를 보여주는 방법을 맘대로 주무를 수 있다면 가능할 거 같은데요? 언젠가 시간이 남는다면 한 번 제작할지도 모르겠지만....일단 전 대충 문자열 룩업 툴로 만족 -_-

디버그전용 해쉬문자열 매니저
아니면 게임코드 안에 디버그전용 해쉬문자열 매니저를 만들어도 되죠. 디버그 빌드에서만
debug.string_db 파일을 로딩하게 만들면 되니까요. 그러면 코드 안에서 쉽게 문자열을 찾아낼 수 있죠. 이건 디버그 빌드에서만 동작하는 코드고 디스크 빌드에서는 컴파일러 스위치로 뿅~ 사라져야 하는 놈...

좀 더 어이없는 생각 하나 더....
디버그전용 해쉬문자열 문자열 매니저에 대해 쓰던 도중 갑자기 떠오른 생각.... 디버그전용 해쉬문자열 매니저가 로칼라이제이션 데이터베이스하고 되게 비슷한 거 같은데요? 문자열 ID를 키로 쓰고 거기에 대응하는 실제 문자열이 char* 값으로 들어가 있는 게 전부니... 나중에 언어를 바꿔주고 싶으면 그냥 각 문자열 ID마다 다른 언어로 char* 값이 들어가있는 로칼라이제이션 DB 파일을 로딩해버리면 되니까.... 해쉬문자열 매니저와 매우 비슷....

따라서 디버그전용 해쉬문자열을 구현하기로 결정을 했다면 동일한 아키텍처를 이용해서 로칼라이제이션을 해버리면 어떨까 하는 생각... 사실 로칼라이제이션 DB에 대해서는 아는게 별로 없으므로 허무맹랑한 소리일지도 모릅니다. 그냥 이딴 생각이 들었을뿐입니다.... -_-


아악! 좀 커다란 문제가 하나....
위에 글을 쓰고 매우 기뻐하고 있었는데... 제 동료인 Noel 아저씨가 갑자기 이딴 질문을.... "그 루프 언롤은 문자열의 길이에 상관없이 잘 돌아? 졸 길면 안되지 않을까?"... 그래서 다시 한번 재빨리 테스트를 해보니....... 흙~

Visual Studio 2010 SP1
Visual Studio 2010 SP1 는 10글자까지만 제대로 동작하더군요. -_-  "funny_bone1"이라고 11글자를 넣으니 이따위 결과가...!

_main PROC      ; COMDAT

; File e:\temp\x65599\main.cpp
; Line 12
 xor ecx, ecx
 xor eax, eax
 npad 12
$LL5@main:
 movsx edx, BYTE PTR $SG-5[eax]
 imul ecx, 65599    ; 0001003fH
 inc eax
 add ecx, edx
 cmp eax, 11     ; 0000000bH
 jb SHORT $LL5@main
 mov eax, ecx
 shr eax, 16     ; 00000010H
 xor eax, ecx
; Line 13
 push eax
 push OFFSET $SG-6
 call DWORD PTR __imp__printf
 add esp, 8
; Line 15
 xor eax, eax
; Line 16
 ret 0
_main ENDP



g++
g++은 좀 납디다.. 아니 많이... g++은 무려 17글자까지! 두둥! "funny_bone12345678"이라고 17글자를 넣으니 그제서야 이런 결과가....


 .def _main; .scl 2; .type 32; .endef
_main:
LFB5:
 pushl %ebp
LCFI0:
 movl %esp, %ebp
LCFI1:
 andl $-16, %esp
LCFI2:
 subl $16, %esp
LCFI3:
 call ___main
 movl $LC0, %eax
 xorl %edx, %edx
 .p2align 4,,7
L2:
 imull $65599, %edx, %edx
 movsbl (%eax), %ecx
 addl $1, %eax
 addl %ecx, %edx
 cmpl $LC0+18, %eax
 jne L2
 movl %edx, %eax
 shrl $16, %eax
 xorl %edx, %eax
 movl %eax, 4(%esp)
 movl $LC1, (%esp)
 call _printf
 xorl %eax, %eax
 leave
LCFI4:
 ret



그래서, 뭐 어쩌라고?
위의 실험이 의미하는 바는.... 컴파일러 따라 10이나 17자 까지만 멋지게 최적화가 된다는 거지요. 다른 문자열들은 실행도중에 계산됩니다... 으음... 그래도 과연 이런 짓(?)을 할 가치가 있을까 생각을 해봤는데요. 그래도 가치는 있다고 생각합니다. 가장 큰 이유는 최소한 게임데이터 파일 안에서 문자열이 사라지니까요. 대신 다음과 같은 가이드라인을 좀 따라야겠죠.

  • 가능한 룩업키로 사용하는 문자열의 길이를 짧게 만든다.
  • 동일한 문자열에 HASH_STRING() 매크로를 여러 번 호출하지 않는다. 대신 계산은 한 번만 하고 그 값을 다른 데 저장해뒀다 필요할 때마다 불러와 사용한다. (예. 오브젝트의 멤버변수로 저장)

또 미래의 컴파일러가 루프 언롤을 좀 더 잘 해줄 수도 있구요. 한 64 글자까지만 되면 좋을텐데 말이죠....  (비졀 스튜디오 2011 Preview에서도 여전히 10글자더군요)

아니면 C+11의 constexpr를 여따 쓸 수 있을까요..... 하지만 비졀스튜디오 2011 프리뷰에서도 아직 이 놈을 지원 안하는 걸요?...... 그러니 별 소용이 -_-

제가 가장 선호하는 해결책은 MS사에서 다음과 같은 컴파일러 스위치를 추가해주는 겁니다.

inline unsigned int generateHash(const char *string, size_t len)
{
  unsigned int hash = 0;

  #pragma unroll
  for(size_t i = 0; i < len; ++i)
  {
    hash = 65599 * hash + string[i];
  }
  return hash ^ (hash >> 16);
}



이러면 len의 길이가 컴파일시에 이미 정해져 있는 경우 컴파일러가 루프 전체를 언롤해주는거죠. IBM 컴파일러에 저거랑 비슷한 컴파일러 스위치가 있다고 들었고, HLSL 컴파일러는 이미 저걸 지원하니 C++ 컴파일러에 저걸 넣지 못할 이유가 없을 듯 한데 말이죠.

마소 아저씨들! 저 컴파일러 옵션좀 추가해 주세요!

급한대로 땜빵 해법
영문 블로그에 며칠전에 이 글을 올렸었는데 그 뒤에 Mikkel Gjoel 아찌가 트위터에서 말해주기를 Humus 아찌가 이 글자 제한에 상관없이 컴파일시에 해쉬를 만들어 내는 법을 알고 있다고 귀뜸 해줬습니다.

그래서 냅따 시도해봤지요. 오오~ 잘 작동합니다. 64글자까지 실험해봤는데 다 되요! 프로그래머가 사용하기 좀 불편하다는 단점은 있는데요. 범용적인 generateHash(const char*) 함수를 특화된 generateHash(const char &(string)[N]); 함수들과 동시에 선언해둘수가 없거든요. 컴파일러가 헷갈려해요 -_-

영문 블로그에 커멘트로 달린 AltDevBlogADay 링크에서 바로 위에 지워버린 내용을 해결할 수 있는 방법을 발견했습니다.

struct ConstCharWrapper
{
    inline ConstCharWrapper(const char* str) : m_str(str) {}
    const char* m_str;
};

inline unsigned int generateHash(ConstCharWrapper wrapper, size_t len)
{
    const char* string = wrapper.m_str;

    // 요밑은 이전과 똑같은 코드
} 


그리고 비졀 스튜디오 2010이 좀 멍청해서... 다음의 두 코드가 같은 놈이란 걸 모르고.. 첫번째 놈을 해쉬값으로 바꿔주는 데 실패하더군요. 물론 제 해쉬 함수를 쓸때도요... (g++은 잘 함...)

#1

const char * const funny = "funny_bone";
unsigned int hashValue = HASH_STRING(funny);


#2

unsigned int hashValue = HASH_STRING("funny_bone");



그러나 #1 방식을 쓰면 디버그 스트링 DB를 만들려고 regular expression을 쓸 때도 개판이 나니... 첫번째 방법을 쓰면 안되겠죠. 그럼 상수 문자열을 쓸 때 다른 프로그래머들이 첫번째 방법을 쓰지 않도록 강제교육할 방법이 있어야 할텐데..... 일단 좀더 생각해봐야 겠어요.

어쨌든 이 새로운 정보를 좀 덜 짜증나게 쓸 수 있는 방법을 찾아내면 새로운 글을 올리지요. 이미... 너무 길어요.. 글이... 흙~ -_-

반응형
,
Posted by 알 수 없는 사용자
안녕하세요, 원래는 12, 27일에 기획 포스팅을 하는 스톰 서광록입니다. 그런데 비즈니스 카테고리가 비어있어서 생각 없이 올린 게임회사의 회계이야기에 재미를 붙여서 본업(?)은 제쳐두고 이 시리즈를 날짜에 관계없이 막 올리고 있군요. 어쨌거나 글을 열심히 더 올린다는데 포프님이 뭐라 하시진 않겠죠? 자, 그러면 이번 포스팅에서는  다른 업종과는 달리 게임업계에서만 볼 수 있는 특수한 상황에서의 회계처리에 대해서 이야기해볼까 합니다.


퍼블리싱 계약금은 매출일까 부채일까?

신작 온라인 게임을 개발 중인 개발사 A는 B라는 퍼블리셔와 글로벌 퍼블리싱 계약을 체결하면서 다음과 같은 계약조건에 합의하고 계약금 8억원을 받았다고 가정해봅시다.

계약기간: 3년
판권료 : 총 20억원
지불조건:
- 계약금: 8억원 (계약체결일로부터 14일 이내 지급)
- 중도금: 6억원 (CBT 개시일로부터 14일 이내 지급)
- 잔금: 6억원 (상용서비스 개시일로부터 14일 이내 지급)

자, 그러면 개발사 A의 회계 담당자는 이 8억원을 어떻게 회계처리 해야 할까요? 어쨌든 돈을 받았으나 매출 8억원이라고 계상하면 되는 걸까요?

일반적인 퍼블리싱 계약의 경우, 보통 선불 계약금은 특정 사유로 계약이 해지되면 반환해야할 의무가 있습니다. 예를 들어 A사가 정해진 기한까지 CBT 버전을 개발하지 못하거나 한다면, B사는 어느 정도 기다려주다가 계약을 해지하고 계약금 반환을 요구할 수 있겠죠. 따라서 아직은 이 계약금을 '매출'이라고 볼 수 없습니다. 매출이 되려면 '거래가 성립'되어야 하는데, 보통 실물을 거래하는 경우, 구매처에서 주문을 받아 판매처에서 상품을 전달하면 거래가 성립된 것으로 보고 판매금액에 대한 매출을 인식합니다.

하지만 게임업계에서는 대부분의 거래 대상물이 저작권, 판권, 서비스권과 같은 무형자산이기 때문에 다른 업종과는 조금 다른 점들이 많습니다. 그래서 이와 같은 경우, 계약금으로 받은 금액은 매출액이 아니라 '선수금'으로 인식합니다. 말 그대로 '미리 받은 돈'이란 거죠. 반대로 계약금을 지불한 B사의 입장에서는 '선급금', 즉 '미리 지급한 돈'으로 계상합니다. - 여기서 계상(計上)은 계산하여 장부에 올린다(기장한다)라는 의미입니다.

뭐 여기까지는 설명을 들으시면 쉽게 이해하실 겁니다. 그런데 문제는 재무회계에서 이 선수금은 반환의 의무가 소멸되기 전까지는 '부채'로 인식합니다. 그래서 A사는 계약금 8억이라는 현금 자산이 증가한 반면, 8억원의 부채도 함께 증가합니다. 반대로 B사에서는 8억원의 현금자산이 감소하는 대신 8억원의 채권이 선급금이라는 명목으로 증가하게 됩니다. B사의 입장에서는 돈은 지불했지만, 만약 A사가 게임을 완성하여 제공하지 못하면 되돌려 받아야 하니까요. 

보통은 부채나 채무라 하면 빌린 돈을, 채권은 빌려준 돈을 뜻하는 말이라고 알고 있겠지만, 회계에서는 지급해야 할 의무가 있으면 무조건 부채, 채무이고, 받을 권리가 있으면 채권으로 인식합니다. 그래서 상품을 외상으로 판매하고 아직 대금을 받지 못했다면 매출채권(외상매출금)이라고 하고, 상품을 외상으로 구매하고 대금을 지불하지 않았다면 매입채무(외상매입금)라고 합니다.

따라서, 여러분이 A사와 같은 개발사의 경영진이나 자금 관리자의 입장이라면 퍼블리싱 계약금을 '매출 수익'이라고 생각하면 안 됩니다. 게임을 완성시켜서 퍼블리셔에 제공하기 전까지는 '채무'라고 생각하는 것이 바람직합니다. 반대로 여러분이 B사와 같은 퍼블리셔에 있다면 선지급하는 계약금액의 산정과 지불에 앞서 '계약금 또는 계약 목적물의 회수 가능성'에 대해서 엄격하게 따져봐야만 합니다.

계약금을 많이 받은 것은 회계의 관점에서 보면 돈방석에 앉은 것이 아니라
많은 부채를 떠안은 셈이 됩니다.


 

개발사가 망했어요!

총 판권료 20억 중에 8억원을 지불한 B사는 A사가 CBT 버전을 완성할 때까지 기다립니다. 그런데 어느날, A사가 개발을 포기했다는 소식을 듣습니다. 알고보니 계약금으로 받은 8억원은 그동안 빌린 은행 대출과 사채를 갚느라 써버리고, 경영진이 계약금을 받은 것에 너무 들뜬 나머지, 수익도 없으면서 직원들에게 인센티브를 지급하고 고가의 렌더링 엔진을 도입하는 등 낭비를 일삼다가 자금이 오링이 되었고, 급기야는 급여가 밀리니까 개발자들이 다 나가버린거죠. 자, 과연 이런 경우 B사는 이미 지불한 8억원을 어떻게 처리해야 할까요?

보통 퍼블리싱 계약에서는 개발사가 지정된 기한내에 게임을 완성시키지 못해서 계약이 해지되는데 위약금을 물지 못할 경우, 게임의 모든 권리가 B사에 귀속된다는 단서조항 같은 것들이 계약서에 들어있습니다. 그래서 원칙적으로는 선급금(채권) 8억과 무형자산 8억 증가를 상계처리해야 합니다. 즉 계약취소시 돌려받아야 할 8억의 채권이 사라지고 '게임의 소스와 판권'이라는 무형자산이 그 자리에 대신 들어오는 거죠.

상계 (相計)
채무자와 채권자가 같은 종류의 채무와 채권을 가지는 경우에 그 채권·채무를 같은 금액으로 소멸시키는 것을 뜻하는 말. 즉 위의 사례와 같은 경우에는 8억원의 채권을 소멸시키고 대신 8억원의 무형자산을 채권 대신 받은 것으로 처리한다는 뜻


하지만 그동안 개발하던 개발자가 다 나가버렸는데 소스와 권리를 받아봐야 현실적으로는 재개발을 해서 완성시키는 건 매우 어려운 일이죠. 게다가 무형자산이 되면 매년 감가상각도 해야 한다는 점을 감안해야 합니다. 그래서 보통은 일단 채권을 그대로 두고 있다가 적절한 시점에 그것을 무형자산으로 돌려서 감가상각비를 계상하여 세금 감면 효과를 노린다든지, 회사가 아예 파산했다면 회수불가능 채권으로 인식하여 8억원 만큼을 손실로 처리하여 세금감면 효과를 노립니다. 이러한 판단은 회사의 손익과 세무와 밀접한 연관이 있는 만큼 회계, 세무 전문가의 조언을 받아야만 회사의 피해를 최소화 할 수 있습니다.


한빛소프트의 예: 헬게이트 런던

실제로 한빛소프트는 헬게이트 런던을 수출하기로 하고 해외 퍼블리셔로부터 선급으로 받은 계약금 가운데 103억원을 2008년도에 '미지급 비용'으로 계상하고 공시했습니다. 즉, 당시에 한빛소프트는 헬게이트 런던이 해외 서비스 버전을 완성시켜주지 못할 우려가 매우 크다고 판단한 것이죠. 그래서 한빛소프트의 2008년도 기말 감사보고서를 보면 영업외비용의 우발손실 항목에 103억원의 손실이 계상되어, 그해 695억의 매출액과 101억의 영업외수익을 기록했음에도 불구하고, 367억 이상의 당기순손실이 났습니다. 아래 표는 한빛소프트의 2008년 기말 감사보고서에서 중요한 항목만 발췌한 것입니다.


2008년에 367억의 당기순손실을 기록한 한빛소프트는 2009년 기말에는 전년에 비해 다소 감소한 612억원의 매출을 올렸지만, 12억의 당기순이익을 올리며 흑자로 전환합니다. 그러다가 2010년 기말에는 매출액 345억, 당기순손실 62억으로 다시 적자가 됩니다. 그런데 2010년도의 손익계산서를 보면 한빛소프트는 2008년에 헬게이트 런던의 수출을 전제로 받은 판권 계약금 중 손실처리했던 103억 가운데 80억원 가량을 영업외이익으로 환입처리하였습니다.

환입 (換入)
바꾸어 넣다라는 뜻으로, 회계에서는 이미 매출로 계상했던 금액에 반품 등이 발생하여 물건을 돌려받고 환불해주는 경우, 혹은 부채로 잡아놓은 금액의 채무가 소멸되어 해당 부채를 이익으로 바꾸어 계상하는 것 등을 의미합니다.



해당 재무제표의 주석에 보면 한빛소프트는 (헬게이트 런던의 리뉴얼 개발이 완료되어 해외 퍼블리싱 계약을 유지할 수 있게 되어) 2008년도에 미지급비용(=부채)으로 처리했던 103억 가운데 80억원은 반환할 가능성이 현저히 낮아졌다고 판단하여 영업외수익으로 환입했다고 설명되어 있습니다. 이 덕에 2010년 한빛소프트는 그 해 적자폭이 세자리수가 될 뻔 했지만 이 부채로 잡아두었던 103억 가운데 80억을 이익으로 전환하면서 2010년도의 적자폭을 줄일 수 있었던 것이죠.

그런데, 만약 한빛소프트가 헬게이트 런던의 수출 계약금으로 받은 103억을 2008년도가 아닌 2009년도에 손실처리하고 2010년에 환입처리를 하지 않았다면 3년간의 손익은 어떻게 됐을까요?


자, 이와 같이 원래는 2009년도에 12억의 흑자로 수년간의 연속적자에서(한빛은 2008년 이전에도 적자가 연속되어 왔음) 일시적인 흑자전환을 한 번 기록하고, 2010년에도 비록 적자긴 하지만 두 자리수로 막았던 것이... 103억원의 계약금을 2009년에 손실처리한 것으로 바꾸고 2010년도에 환입처리를 하지 않은 것으로 계산하니까, 2008~2010년이 3년연속 적자에, 적자폭도 매우 크게 바뀌었습니다. 게다가 2006년부터 적자였던 것을 감안하면 이렇게 될 경우 5년 연속 적자를 기록하는 것입니다.

그러나, 원래의 의도는 알 수 없지만, 2008년에 103억의 계약금을 손실처리하고 2010년에 환입처리 함으로써 결과적으로 2009년에는 흑자전환을 했고 2010년에는 적자규모를 두자리 수로 낮출 수 있었습니다.

이렇듯 게임회사의 회계처리 가운데에서는 상황에 따라 기업이 재량껏 판단할 수 있는 사안들이 있으며, 이런 사안의 최종 결정은 회사의 다른 형편과 향후 전망을 토대로 결정해야 합니다. 상장기업이나 규모가 꽤 큰 업체라면 회계, 재무 전문가들이 있기 때문에 별 문제가 없겠지만, 그렇지 않은 중소기업의 경우에는 자칫 회계처리 하나의 판단미스로 투자나 퍼블리싱 계약이 좌절되거나 자금융통이 되지 않아 곤란을 겪는 일이 발생할 수도 있다는 점에 주의해야 합니다.


투자한 게임이 출시가 안되면 이런 일도

신생 퍼블리셔 C사는 설립초기부터 공격적으로 게임포털 사업에 뛰어들기로 하고 개발중인 신작 온라인 게임 5개의 퍼블리싱 계약을 체결합니다. 그리고 게임별로 계약금 5억씩 총 25억원을 지급했다고 가정해보죠.

그러면 C사의 재무제표에는 25억의 선급금이 '비유동자산'으로 잡혀있게 됩니다. 그런데, 애석하게도, 이 다섯 개의 게임이 모두 완성되지 못해 투자금을 회수하지도 못하고 게임도 출시하지 못했다면 어떻게 될까요?

앞서 설명했듯이, C사의 회계 책임자는 25억의 선급금을 무형자산으로 전환할지, 아니면 손실처리할지를 판단해야 합니다. 원칙적으로는 개발 소스를 다 인수받아 자체적으로 개발할 의도와 능력이 있다면 무형자산으로 처리하는 게 맞고, 그냥 게임의 론칭을 포기하고 회수불가능 채권으로 인식한다면 손실처리하는 것이 맞습니다.

그래서 보통은 일단 선급금인 상태로 두었다가, 향후 경영자의 전략과 판단에 따라 위의 두가지 중 하나를 선택하게 되죠. 그런데 이 때, 이것을 손실처리하는 경우에는 세무상의 문제가 발생할 수 있다는 점에 주의해야 합니다.

재무회계와는 달리 세무회계는 그 목적이 소득과 손금을 세법에 맞게 측정하여 납세액을 결정하기 위함입니다. 보통 소득 금액은 별 문제가 없으나, 손금이란 놈이 문제가 됩니다.

손금 (損金)
기업(법인)이 사업과 관련하여 발생하거나 지출된 손실 또는 비용으로, 세법상의 개념은 기업회계상의 비용과 유사하나 반드시 동일하지는 않음.


손금은 보통 기업회계상의 손실, 비용, 지출과 거의 비슷하지만, 세법에서는 모든 손실, 비용, 지출 등이 다 손금으로 인정되는 것은 아닙니다. 특히 회수불가능 채권 손실액 등은 기업의 입장에서는 분명히 손실이 발생한 것이지만, 세법에서는 손금으로 인정하지 않는 경우도 꽤 있습니다. 만약 손실금액을 세법상 손금으로 인정받지 못한다면, 그만큼 소득이 증가한 것으로 인정하기 때문에 납세액도 늘어나는 것이죠. 이렇게 되면, 손실은 손실대로 보고 세금은 세금대로 더 내는 이중고를 겪는 셈입니다.



그런데 퍼블리싱 계약했던 게임의 계약이 해지되어 선지급 계약금이 회수 불가능해진 경우는 가시적인 실물자산이 오고간 거래가 아니기 때문에 세무당국과 같은 제3자, 더우기 게임산업에 대한 비전문가의 입장에서는 그것이 올바른 회계처리인지를 판단하기가 무척 어렵습니다.

뭐 세무소에서 그냥 손금으로 인정해주면 되는 것 아니냐고 생각하실 분도 있겠지만, 만약 그렇게 해준다면 판권 계약금이라는 명목으로 자금을 불법적으로 증여하거나 탈세의 용도로 삼는 일이 발생할 수가 있습니다. 이해를 돕기 위해 예를 들어보죠.

게임 퍼블리셔 C사의 대표는 지인으로 하여금 신생 개발사 D사를 창업하게 합니다. 그리고는 D사가 게임 하나를 서류상으로 만들어서 C사와 퍼블리싱 계약을 맺고, C사는 계약금 명목으로 D사에게 5억원을 지급합니다. 그다음 D사는 짜고치는 고스톱처럼 실제로는 발생하지 않은 용역이나 컨설팅비 등의 명목으로 다른 회사에 비용을 지불하고 돈이 떨어지면 그냥 폐업처리를 합니다. 뭐 더 간단하게 그냥 C사가 D사에게 외주개발 용역계약을 체결하고 계약금을 지급하는 방법도 있겠죠.

이런 경우는 정말 게임업계 전문가가 아니면 정상적인 거래인지 판별하기가 어렵기 때문에 세무당국에서도 거액의 금액이 손실처리 되는 경우에는 의심을 할 수 밖에 없겠죠. 심한 경우는 세무조사를 받을 수도 있습니다. 그래서 회계 담당자들은 계약금을 선지급한 퍼블리싱 계약이나 용역 계약 등이 해지될 때 그 처리에 곤란을 겪는 것입니다.

* * * * * * * * * * * *


이번 포스팅에서는 게임업계에서 일어나는 특수한 상황에서의 회계처리에 대해서 이야기해보았습니다. 다음 회에서는 최근에 신생 개발사들이 개발력 뿐만 아니라 재무 전문가를 필요로 하는 이유에 대해서 투자유치와 자금관리의 예를 들면서 써볼까 합니다.


반응형
,
Posted by 알 수 없는 사용자


1. 콘솔(Console)이란?

    게임을 개발 할 때 게임 인터페이스와 독립적으로 게임로직이나 엔진한테 명령을 내릴 수 있고 그 명령에 대한 결과를 보거나 엔진의 어떤 기능이 오작동하여 개발자가 알아야 할 정보를 볼 수 있는 장치가 필요합니다. 국내 온라인 게임 개발에서는 보통 채팅창을 통해서 이런 기능들이 작동합니다. 둠3에서는 이런 장치을 콘솔(Console)이라 부르고 다른 시스템들이 일관되게 이 콘솔을 활용하고 있습니다.



2. 콘솔 사용법


    키보드에서 탭(Tab)키 위에 있는 물결 키(~)를 누르면 위의 그림처럼 도스창과 비슷한 화면이 나타납니다. 이것을 콘솔창(Console Window)이라고 부릅니다. 게임 중에는 마우스 커서가 윈도우 밖으로 못 나가는데, 콘솔창을 띄우면 그 마우스 잠금이 풀립니다. 또 마우스 가운데 버튼으로 드래그하면 여러가지 로그들이 출력된 것을 보실 수 있습니다.

    간단하게 콘솔을 사용해보겠습니다. 게임을 실행시켜서 맵까지 들어갑니다. 몬스터를 소환해보겠습니다. 물결 키(~)를 눌러서 콘솔창을 열어 아래의 문장을입력하고 엔터를 쳐주세요.

spawn monster_demon_imp


    몬스터가 소환되고 플레이어를 공격하기 시작합니다. 피가 줄줄 깍이는데, 아래의 문장을 입력하면 플레이어는 무적이 됩니다. (다시 한번 입력하면 무적 해제가 됩니다)

god


    죽이기도 귀찮으니 아래의 문장을 입력해서 몬스터를 없앱니다.

killmonsters


    게임 치트키만 있는 것은 아닙니다. 아래의 문장을 입력하면 현재 할당된 게임 오브젝트 수와 그 클래스들의 전체 메모리 사용량을 볼 수 있습니다.

game_memory


    이처럼 콘솔에서 인식하는 명령어를 콘솔 명령어(Console Command)라고 부릅니다. 이번에는 스펙큘러 라이팅을 꺼보겠습니다.

r_skipSpecular 1


    화면이 칙칙한 것이 금방 눈에 띄네요. 다시 아래의 문장을 입력해서 스펙큘러 라이팅을 켜겠습니다.

r_skipSpecular 0


    r_skipSpecular는 방금전까지 입력한 명령어하고 형태가 약간 달랐습니다. 콘솔에는 콘솔 명령어와 함께 콘솔 변수(Console Variable)가 있습니다. 일종의 전역 변수 개념입니다. 콘솔창에서 콘솔 변수만 그냥 입력하면 그 콘솔 변수가 현재 갖고 있는 값과 처음에 설정한 기본 값 그리고 해당 변수가 어떤 역할을 하는지 설명하는 주석이 출력됩니다.콘솔 변수의 값을 변경하려면 방금전 사용해본 것 처럼 콘솔 변수 이름 옆에 적용될 값을 추가해주면 됩니다.

    콘솔 명령어와 콘솔 변수는 둠3 엔진 전체에서 널리 쓰이는 시스템입니다. 심지어 맵에디터의 실행도 콘솔을 통해서 실행합니다. (콘솔창에서 editor를 입력해보세요)



3. 클래스 소개

    다음은 콘솔을 이루는 주요 클래스들입니다.

- idCommon
- idConsole
- idCmdSystem
- idCVarSystem
- idCVar
- idCmdArgs
- idEditField


    클래스 분석에 들어가기 앞서 둠3에서 자주 나타나는 패턴 한가지를 살펴보겠습니다. 둠3에서는 엔진이 굵직굵직하게 시스템별로 클래스화 되어있습니다. 아래 코드는 엔진이 초기화되고 게임DLL쪽에 엔진 시스템들의 포인터를 넘겨주는 구조체입니다. 이름으로 미루어 보아 짐작할 수 있는 여러 시스템과 매니저가 있습니다.


    여기서 idCommon 시스템의 소스파일 구성을 살펴보겠습니다. idCommon 코드는 idCommon.h 와 idCommon.cpp 파일로 이루어져 있고 크게 다음과 같은 형태를 띄고 있습니다.

     idCommon.h 파일에서는 해당 시스템의 인터페이스만 정의하고 idCommon.cpp 파일에서 해당 인터페이스를 상속받아서 구현합니다. 구현체 클래스의 이름 뒤에는 Local을 붙입니다. 시스템 클래스들은 전역변수로 만들어서 코드 어디에서든 접근할 수 있습니다. 아래 표에 보이는 시스템과 매니저들의 구성은 모두 이와 같이 되어있습니다. 이런식의 디자인은 다형성 보다 종속성을 제거하는 것이 목적입니다. 게임 DLL쪽에 엔진 시스템의 포인터를 넘겨줄 때는 시스템들의 전역변수들을 위의 gameImport_t 구조체에 담아서 한꺼번에 넘깁니다.

 인터페이스 클래스 구현체 클래스 전역변수 
 idSys  idSysLocal  sys
 idCommon  idCommonLocal  common
 idCmdSystem  idCmdSystemLocal  cmdSystem
 idCVarSystem  idCVarSystemLocal  cvarSystem
 idFileSystem  idFileSystemLocal  fileSystem
 idNetworkSystem  idNetworkSystemLocal  networkSystem
 idRenderSystem  idRenderSystemLocal  renderSystem
 idSoundSystem  idSoundSystemLocal  soundSystem
 idRenderModelManager  idRenderModelManagerLocal  renderModelManager
 idUserInterfaceManager  idUserInterfaceManagerLocal  uiManager
 idDeclManager  idDeclManagerLocal  declManager
 idAASFileManager  idAASFileManagerLocal  AASFileManager
 idCollisionModelManager   idCollisionModelManagerLocal   collisionModelManager 




3.1 idCommon 클래스

    idCommon 클래스는 엔진의 Initialize와 Shutdown을 책임지고 시스템 전체에서 사용되는 메소드를 가지는 클래스입니다. 콘솔 출력은 시스템 전체에서 고루 사용되기 때문에 idCommon에 있는 것 같습니다. 콘솔과 관련된 주요 메소드들은 아래와 같습니다.

    Printf 메소드는 그냥 일반적인 출력입니다. DPrintf 메소드는 콘솔변수 developer가 1로 세팅되어있을 때 빨간색으로 출력됩니다. 보통 개발중인 코드에서 실시간으로 값의 변동을 보고자 할 때 사용할 것 같습니다. Warning 메소드는 출력할 메시지 앞에 노란색 "WARNING:" 을 붙여서 출력합니다. DWarning 메소드는 Warning 메소드와 같지만 DPrintf 처럼 콘솔변수 developer가 1일 경우에만 출력합니다.


3.2 idConsole 클래스

    idConsole 클래스는 콘솔창의 렌더링과 키 입력을 받아서 idCmdSystem으로 명령을 전달하는 역할을 맡습니다.


3.3 idCmdSystem 클래스
    idCmdSystem 클래스는 콘솔 명령어를 등록하고 실행시키는 역할을 합니다. 등록은 위의 AddCommand 메소드를 통해서 합니다. 비주얼 스튜디오에서 Ctrl+Alt+F 키를 눌러서 cmdSystem->AddCommand 를 전체 찾기 해보면 등록되는 모든 콘솔 명령어들을 보실 수 있습니다.

    콘솔 명령어 실행은 위의 BufferCommandText 메소드를 통해서 실행시킵니다. 첫번째 인자를 보시면 콘솔 명령어를 실행시키는 타입을 지정할 수 있습니다. CMD_EXEC_NOW는 해당 콘솔 명령어가 다 끝난 다음 BufferCommandText 함수를 리턴합니다. CMD_EXEC_INSERT와 CMD_EXEC_APPEND는 일단 버퍼에 해당 명령을 집어넣고 다음 프레임에서 실행시키는데, INSERT는 버퍼 맨 앞에 넣고 APPEND는 버퍼 맨 뒤에 넣습니다.또 버퍼에 쌓는 방식이기 때문에 세미콜론으로 구별해주면 한 줄에 여러개의 콘솔 명령어를 실행시킬 수 있습니다.

spawn monster_demon_imp ; killmonsters


    몬스터를 스폰하자마자 죽이므로 결국 아무것도 나타나지 않습니다.

    idConsole이 키 입력을 받고 idCmdSystem한테 명령을 실행시킬 때 CMD_EXEC_APPEND로 실행시킵니다. 즉 우리가 콘솔창에서 입력한 명령어는 다음 프레임에서 실행됩니다. 만약 idCmdSystem이 해당 콘솔 명령어를 찾을 수 없다면 idCVarSystem으로 넘겨 콘솔 변수가 아닌지 검사하게 됩니다.

    idCmdSystem은 꼭 idConsole 뿐만 아니라 다른 시스템 사이에서 빈번하지 않게 발생하는 요청들을 전달할 때도 사용합니다. 직접 해당 시스템의 메소드를 호출하려면 헤더 파일을인클루드 해야하고 종속성이 생겨서 컴파일 시간도 오래 걸리므로 아예 시스템을 초기화 할 때 관련 기능들을 콘솔 명령어로 등록시켜 놓고 다른 시스템이 그것을 사용하고 싶을 때 콘솔 명령을 내리는 방식으로 사용합니다.



3.4 idCVarSystem 클래스

  idCVarSystem 클래스는 콘솔 변수를 등록하고 idCmdSystem을 통해서 들어온 콘솔 변수 관련 명령들을 처리합니다. 콘솔 변수를 등록하고 값을 가져오는 메소드들은 아래와 같습니다.
    콘솔 변수의 등록과 값을 가져오는 것은 위의 메소드들로 할 수 있고 사용되기도 하지만 이 메소드들은 컨테이너를 돌면서 해당 변수를 찾아야 하기 때문에 비효율적이기도 해서 정말 많이 쓰는 방식은 직접 idCVar 클래스를 전역변수로 사용하는 방법입니다.


3.5 idCVar 클래스
    위와 같이 idCVar 클래스를 전역변수로 만들어서 사용하면 컨테이너에서 찾을 필요 없이 값을 바로 가져오거나 변경할 수 있습니다. 물론 콘솔창에서 아까 했던 방법처럼 값을 변경할 수 있습니다.

    여기서 의문이 하나 생깁니다. 전역변수로 하더라도 콘솔창에서 제어가 가능하려면 idCVarSystem에 등록되어야 하고 idCVar의 생성자에서 idCVarSystem에 등록하는 작업을 해준다고 해도 idCVarSystem의 생성 시점은 전역변수 초기화 시점 보다 한참 늦은 뒤에 하게 되는데 어떻게 등록을 할 수 있는 걸까요.
    그 해법은 idCVars의 staticVars 정적 멤버 변수에 있습니다. idCVars의 생성자는 idCVarSystem의 생성 유무를 살펴보고 아직 생성되지 않았으면 staticVars에 링크드리스트 형식으로 자신을 링크시킵니다. (그것을 위한 next 라는 멤버 변수가 있습니다) 그리고 나중에 idCVarsSystem은 자신이 생성되고 RegisterStaticVars 함수를 실행시켜서 idCVars::staticVars를 살펴보아 포인터가 유효하면 그 링크드리스트를 돌면서 자신의 컨테이너에 집어넣기를 수행합니다.

    이 방법은 MOD 개발에서도 유용합니다. 둠3는 게임쪽 코드를 DLL로 만들어서 엔진이 그것을 구동시키는 방식입니다. 이때 게임 DLL쪽에서 사용하는 idCVars 전역 변수들은 DLL이 로딩될 때 자신들끼리 링크드리스트 형태로 존재하다가 엔진에서 idCVarSystem의 포인터를 넘겨주면 그때 등록을 하게 됩니다.

    콘솔 변수 또한 콘솔 명령어와 마찬가지로 시스템간의 통신을 위한 수단으로도 사용되고 심지어 CVAR_NETWORKSYNC 플래그를 지정해주면 멀티플레이 게임에서 자동으로 동기화까지 해줍니다. 단순한 디버깅 용도를 넘어 전역적으로 사용되는 중요한 개념입니다.


3.6 idCmdArgs 클래스
    콘솔 명령어를 등록할 때 콜백 함수도 같이 넘겨주는데 그 콜백 함수가 호출될 때 인자로 들어오는 클래스입니다. 단순하게 주어진 문자열을 idLexer에 넣어서 토큰으로 분리하는 일을 합니다. 첫번째 토큰을 얻으려면 Argv(0) 함수를 호출합니다. 보통 첫번째 토큰은 콘솔 명령어 이름입니다. 두번째 토큰은 Argv(1) 함수 호출로 알 수 있고 콘솔 명령어 다음에 나오는 인자(Argument) 문자열입니다. (이런식으로 콘솔 명령어의 인자는 63개까지 지원합니다.) idLexer에 대해서는 스크립트 시스템을 다루는 편에서 살펴보겠습니다.


3.7 idEditField 클래스 

    콘솔창에서 글자를 입력받고 커서 이동을 하고 글자 출력까지 담당하는 클래스입니다. 붙여넣기(Ctrl + V) 기능도 여기서 구현합니다. 제일 중요한 기능은 자동 완성(Auto Complete) 기능 입니다. 콘솔창을 열고 sp만 입력한 채로 탭(Tab)키를 누르면 spawnServer가 자동 완성됩니다. 다시 탭키를 누르면 spawn이 완성됩니다. 이번엔 spawn 다 입력하고 탭키를 누르면 spawn으로 불러올 수 있는 몬스터 목록이 나옵니다. 더 정확히 spawn 콘솔명령어가 취할 수 있는 첫번째 인자들을 전부 출력합니다. 
    콘솔 명령어나 콘솔 변수를 등록할 때 제일 마지막 인자로 argCompletion_t 시그내처(signature)의 콜백 함수를 만들어서 넘겨주게 됩니다. 이 함수에서 해야할 일은 사용자가 자동 완성을 요구할 때 현재까지 입력받은 idCmdArgs를 보고 이 콘솔 명령어(혹은 콘솔 변수)가 취할 수 있는 모든 경우의 수에 대해서 문자열을 완성시켜 두번째 인자로 넘어오는 callback 함수를 호출해주는 것입니다. 단지 취할 수 있는 문자열은 넘겨주게 되면 문자열 매칭은 idEditField가 알아서 해줍니다.


4. 따라하기

    Ctrl+Shift+F 키를 눌러 idSessionLocal::Init() 를 찾기 합니다. Session의 Init 함수는 엔진의 초기화가 끝나고 게임에 관련된 초기화를 시작할 때 호출됩니다. 그래서 연습용으로 적절한 위치입니다.
 
4.1 HellWorld 콘솔명령어 추가
    HellWorld를 입력하면 실행될 함수를 작성하고 Init() 함수 바로 밑에서 콘솔 명령어를 등록합니다. 이제 콘솔창에서 HellWorld를 입력하면 다음 처럼 Hell World 메시지가 출력되는 것을 볼 수 있습니다.

HellWorld



< 콘솔창 >


4.2 HellLevel 콘솔변수 추가
    HellWorld를 치면 HellLevel 콘솔변수의 값을 가져와서 출력되는 것을 볼 수 있습니다. 다음 처럼  HellLevel의 값을 변경시킨 후 다시 HellWorld를 실행시켜보면 변경된 값이 나오는 것을 알 수 있습니다.

HellLevel 10 ; HellWorld


< 콘솔창 >


4.3 s_hellLevel 전역변수로 추가
    이번에는 HellLevel을 전역변수 s_hellLevel로 만들어서 사용해보겠습니다. 따로 등록하는 메소드를 호출해주지 않아도 콘솔창에서 HellLevel의 값을 변경할 수 있는 것을 볼 수 있습니다.


4.4 자동완성 기능 추가
    아래 처럼 HellWorld까지만 입력하고 탭(Tab)키를 누르면 함수에서 작성한대로 취할 수 있는 목록이 나오고 계속 탭키를 누르면 항목이 바뀜을 볼 수 있습니다.

HellWorld [탭키]


< 콘솔창 >


5. 마무리

    콘솔은 둠3에서 전역적으로 매우 빈번하게 등장하고 중요하게 사용되는 시스템입니다. 또한 앞으로 우리가 소스를 분석하면서 테스트용으로 작성하는 코드들을 언제든지 원하는 때에 실행시켜보거나 실시간으로 변수값을 조절함으로써 좀 더 효율적으로 분석해볼 수 있습니다.

    효율적으로 컨텐츠를 테스트 하기 위한 치트키와 실시간으로 값을 바꿔보면서 테스트 해보는 것, 언제든지 새로 작성한 함수를 실행시킬 수 있는 방법이 있다는 것은 개발에 큰 도움이 됩니다. 반대로, 개발을 시작할 때 이러한 것들을 준비해야 좀 더 효율적으로 개발 할 수 있다는 것을 알 수 있습니다.

 
반응형
,
Posted by 알 수 없는 사용자

안녕하세요. 미친고양이(이택승)입니다. 광묘아라는 닉네임은 보통 미친고양이라는 닉이 필터링 등의 이유로 사용이 불가능할 때 사용하는 닉네임이고 보통은 미친고양이로 활동하지요.

개발 경력도 짧고 아는 것도 없어 도저히 여기 올리기 민망할 수준의 글들만 올리게 될 것 같아서 죄송하네요. 그래도 지망생들에게는 약간 도움이 될 수 있는 글은 쓸 수 있지 않을까?(없을지도.orz) 싶어 신청하게 되었습니다.

일단 제가 약 2년 동안 해 왔던 온라인 게임 퀘스트에 대한 이야기를 할까 합니다. 처음부터 딱딱한 이야기를 하면 그나마 없는 실력 빨리 들통날 것 같아서 일단은 ‘온라인 게임에서 퀘스트를 어떻게 보여주는가?’에 대한 이야기를 해 볼까 합니다.

온라인 게임, 특히 MMORPG에서 퀘스트를 보여주는 데에는 두 가지 이슈가 있지 않나 생각됩니다.

하나는 카메라를 고정할 것인가? 아니면 별도로 처리하지 않고 방임할 것인가?

다른 하나는 퀘스트 내용을 대화하는 것으로 처리할 것인가? 아니면 서술형으로 보여줄 것인가 하는 점입니다.

그럼 실제로 게임 스크린샷들을 보면서 각 게임들이 위 두 문제를 어떤 방식으로 결정했는지, 왜 그렇게 결정(이건 제 생각입니다만...)했는지 살펴보겠습니다.


우선 퀘스트 위주 게임의 모범적인 사례가 되어 이후 게임들에 영향을 준 와우입니다.

와우는 퀘스트 전환 시 별도로 카메라를 전환하지 않으며 거의 모든 퀘스트를 서술형으로 보여줍니다.

카메라를 전환하지 않고 모든 퀘스트를 서술형으로 처리하기 때문에 퀘스트 하나를 받는 시간이 짧습니다.

퀘스트를 많이 받고 많이 해결하는 것을 스타일로 잡고 있는 와우의 게임 플레이에 어울리는 방식입니다.

다만 이럴 경우 퀘스트 이야기에 대한 몰입도는 상대적으로 떨어지게 됩니다와우의 경우에는 세계관과 다양한 연출게임 내 퀘스트 경험에 대한 고민을 통해 풀어냈지만 같은 방식을 채택한 다른 게임들은 와우처럼 성공적이지 못했습니다.
 


다음은 반대쪽 사례라고 할 수 있는 예를 들어보겠습니다.
예. 다들 알고 계실 블앤소입니다.
블앤소는 퀘스트 시 카메라를 NPC정면으로 고정하며 대화 형식으로 퀘스트를 보여줍니다. 카메라가 고정되는 시간을 통해 게임 플레이 흐름을 약간 끊어 이야기 자체에 주의를 환기시켜 줄 수 있습니다.
또한 대화 방식은 한 번에 보여주는 텍스트의 양이 적고 사람들끼리 정보를 전달하는 방식과 유사하기 때문에 이야기를 전달하기 용이하죠.
다만 퀘스트를 만들기 위한 노력이 더 들고 퀘스트 하나를 받는 시간이 더 걸리게 됩니다. 또한 위에서도 이야기한 것처럼 게임플레이의 연속성이 좀 깨집니다.

지난 테스트에서 아키에이지는 퀘스트를 대화식으로 보여주면서 카메라는 전환하지 않는 방식을 보여줬습니다. 다만 그 대화가 말풍선이고 자동으로 지나가기 때문에 한 번 메시지를 놓치면 퀘스트의 이야기 혹은 힌트를 따라가기 힘들었지요.


마비노기 영웅전의 영웅전의 경우에는 블앤소와 비슷하지만 NPC가 모델링이 아닌 원화다 보니까 카메라가 어쩔 수 없이 고정될 수 밖에 없는 구조지요.

정리해보니 대략 다음과 같은 결론이 나오는 것 같네요.

     장점
 카메라 방임  플레이 연속성 유지 
 퀘스트 대량 소비에 유리 
  고정  이야기 내용에 환기시킬 시간을 줄 수 있음 
텍스트 전달 방식  서술형   퀘스트 대량 소비에 유리
 짧은 시간에 전달 
   대화형  이야기를 전달하는데 유리

그 외에도 어떤 방식인지 직접 체험한 것은 아니지만 최신 온라인 게임 몇 종은 이야기 전달 방식을 아래처럼 전달한다고 합니다.

스타워즈: 구공화국의 기사단 온라인 - 컷신과 선택지. 으으 영어만 아니면 해봤을 텐데..;;
길드워2 - 텍스트로 '우리 마을을 위협하는 몬스터 처치해주세요.'라고 들어봐야 실감도 안나시죠? 저희는 직접 현장을 보여드리겠습니다.

이렇게 누구나 아는 내용으로 이 좋은 블로그를 더럽혀서 죄송합니다.orz
다음에는 mmo의 퀘스트는 보통 어떤 구조를 가지고 있는지, 또 한 번 누구나 아는 내용을 할 것 같습니다.;;

혹시라도 제가 쓴 글에 오류가 있다면 잘못된 지식이 널리 퍼지기 전에 지적해주세요.;; 

반응형
,