'게임 개발/프로그래밍'에 해당되는 글 139건
- 2010/02/14 게임용 라이팅(Lighting) 미들웨어(Middleware) (4)
- 2010/02/14 가시성 최적화 미들웨어(Middleware) (6)
- 2010/01/27 게임 엔진은 게임 개발의 결과물이어야 합니다 (4)
- 2010/01/19 컴퓨터 그래픽스에서 앨비도우(Albedo)의 뜻 (2)
- 2010/01/17 C++에서 비트 연산자를 쓸 때엔 우선순위를 항상 생각할 것
- 2010/01/17 Visual C++ (Visual C++) 창의 레이아웃을 원래대로 돌리기
- 2010/01/09 마우스 휠 처리
- 2010/01/04 소수 구하기
- 2010/01/03 IME에서 조합 중인 글자를 강제로 완성시키기
- 2010/01/02 mbstowcs나 wcstombs가 올바르게 동작하게 하려면, setlocale 설정이 필수
- 2009/12/24 윈도에서 디버거에 연결돼 있는지 확인하기 (2)
- 2009/12/05 게임 그래픽스에서 정적 조명과 동적 조명
- 2009/12/05 게임 그래픽스 프로그래밍에서 베이킹(Baking)의 뜻 (2)
- 2009/11/20 유용한 비교 및 병합 도구, Perforce Visual Merge Tool(P4Merge)
- 2009/11/05 파일 접근 실패 오류 추적하기
- 2009/11/01 Image-Based Lighting (IBL) (2)
- 2009/10/11 더블 버퍼링(Double Buffering)을 이용해 스레드 동기화 줄이기 (2)
- 2009/10/07 "프로시저 시작 지점 ...을(를) DLL ...에서 찾을 수 없습니다."라는 오류를 해결하는 방법 (6)
- 2009/10/07 윈도 GUI 프로그램에서 콘솔 창 띄우기
- 2009/09/19 유닛 테스트(Unit Test)의 유용성에 대한 의문 (10)
- 2009/09/12 불필요한 헤더 파일 포함(#include)을 지속적으로 제거하기 (2)
- 2009/09/07 비디오 메모리 용량 구하기
- 2009/09/02 빌드 서버 구축하기 (4)
- 2009/09/01 게임에 사용하는 DirectX 배포 방법
- 2009/08/08 R6025 - Pure Virtual Function Call 오류 잡기
- 2009/08/04 IME 비활성화하기
- 2009/08/02 원인을 찾기 어려운 버그의 디버깅 방법
- 2009/08/01 링크 시간을 줄이려면 증분 링크를 사용하세요 (2)
- 2009/07/04 객체를 재활용하지 말고, 소멸 후 다시 생성해서 사용할 것
- 2009/06/14 불필요한 Indirection을 없앨 것 (2)
실제로 사용해 보진 않아서 잘 모르지만, 대략 다음과 같은 것이 있나 봅니다.
Beast와 관련된 설명 자료는 The Unique Lighting of Mirrors Edge와 Beauty from the Beast: Using Global Illumination to Enhance the Visual Quality of Your Game에서 더 볼 수 있습니다.
Lightsprint는 실시간 global illumination을 지원하는군요. 성능이 얼마나 나올지는 모르겠지만, 독특한 미들웨어네요. 조명 전처리 때문에 생기는 레벨 디자인 확인 지연을 없앨 수 있다는 점에서는 가치가 있겠습니다.
참고로, Image-Based Lighting도 좀 더 섬세한 정적 조명을 표현하는 방법입니다. 하지만, 그래픽 아티스트가 이미지를 일일이 뽑아 내야 한다는 단점이 있습니다. 맥스 스크립트 등을 활용하면 좀 더 쉬워질 수는 있지만, 그렇다 해도 자동화가 상당히 어렵습니다. 그래서 이런 미들웨어를 활용해 정적 조명 전처리를 완전히 자동화하는 게 생산성 측면에서 더 나은 것 같습니다.
Umbra Software라는 곳에서 가시성 최적화 미들웨어를 판매하고 있네요. 비슷한 미들웨어가 더 있을 것 같은데, 찾아 보진 않았습니다.
여유 자금이 있다면 이런 미들웨어를 구입해서 가시성 최적화 문제를 해결하는 게 좋은 것 같습니다. 레벨 디자인 단계에서부터 레벨 디자이너가 가시성 최적화를 고민하는 것도 한 가지 방법일 것입니다. 하지만, 게임 개발자는 사용자에게 즐거움을 줄 만한 창의적인 일을 해야 하는 것이고, 기술적인 문제에 대한 고민에 노력을 많이 투자하는 것은 좀 아깝습니다.
엔진을 만들 때, 많이 실수하는 부분이 있습니다. 그건 바로 엔진을 먼저 만들고, 엔진을 이용해 게임을 만드는 것입니다. 기본적인 것을 먼저 구현하고 그 위에 부가적인 요소를 올리는 게 처음엔 맞는 것처럼 보입니다. 하지만, 실제로 작업해 보면 반대라는 것을 알게 됩니다.
게임을 만들 때엔 다른 것은 생각하지 말고, 게임을 만드는 데에만 집중해야 합니다. 게임을 만들고 나면 자연스럽게 재활용 가능한 기본적인 부분이 보이고, 그걸 따로 정리하면 엔진이 되는 것입니다. 그런데 하나의 프로젝트만으로는 어디까지가 엔진이 돼야 할지가 명확하지 않습니다. 여러 게임을 개발해 보면 각 게임의 컨텐츠와 무관한 코드를 좀 더 명확히 알 수 있게 됩니다.
엔진을 개발하고 싶다면, 게임을 먼저 개발해야 합니다. 그게 훌륭한 엔진을 만드는 방법입니다.
앨비도우는 반사율인데, 일반적으로 분산광에 대한 반사율을 말합니다. 그래서 분산 텍스쳐 (diffuse texture)를 앨비도우라고도 부릅니다. 그런데 때로는 반사광에 대한 반사율도 specular albedo라고 부를 때가 있어서, 혼란스럽습니다. 용어는 되도록 하나로 통일되면 좋겠습니다.
C++을 어렵게 느껴지게 하는 것 중 하나로 연산자 우선순위가 있습니다. C++ 연산자 우선순위는 어떤 자연스러운 규칙에 의해 이해할 수 있는 게 아닙니다. 언어 제작자가 자신이 좋아하는 방식대로 정해 놓았을 뿐이고, 언어를 배우는 사람은 이해가 안 돼도 따라야 하는 규칙일 뿐입니다.
특히 비트 연산자의 우선순위는 상당히 의문스럽습니다. <<나 >>와 같은 비트 연산자는 ==, <=, >=, 그리고 !=와 같은 비교 연산자보다도 우선순위가 낮습니다. 그래서 비트 연산자를 괄호로 묶어 주지 않으면, 예상과 전혀 다른 동작을 할 때가 잦습니다.
비트 연산자가 있는 코드가 제대로 동작하지 않는다면, 연산자 우선순위를 꼭 확인해 봐야 합니다.
비쥬얼 C++을 사용하다 보면, 의도하지 않게 창 레이아웃이 배치돼 버릴 때가 있습니다. 그리고 원래 상태로 복구하려고 해도 잘 안 돼서 고생하게 됩니다. 그럴 때엔 메뉴에서 Window의 Reset Window Layout을 선택하면 됩니다.
윈도 메시지 처리 함수에서 다음처럼 처리하면 됩니다.
case WM_MOUSEWHEEL:POINT window_position_in_screen = {};ClientToScreen(window_handle, &window_position_in_screen);UINT wheel_scroll_line;SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheel_scroll_line, 0);POINT cursor_position_in_client ={GET_X_LPARAM(l_parameter) - window_position_in_screen.x,GET_Y_LPARAM(l_parameter) - window_position_in_screen.y};int wheel_delta = GET_WHEEL_DELTA_WPARAM(w_parameter) / WHEEL_DELTA * wheel_scroll_line;...break;
소수를 실무에서 구해야 할 일은 별로 없습니다. 하지만, 면접 볼 때 관련된 질문을 받을 때가 있습니다. 참고로, 저는 이렇게 실전엔 별로 유용하지 않은 이론적인 질문을 하는 면접을 상당히 싫어합니다.
소수를 구하려면 소수의 성질을 이용하면 됩니다. 어떤 수 n이 소수인지 판별하려면 다음처럼 하면 됩니다. n을 2부터 n - 1까지 하나씩 나누어서 나머지가 0일 때가 한 번도 없으면, n은 소수가 됩니다. 소스 코드는 아래와 같습니다.
bool is_prime_number(unsigned int number){if (number < 2)return false;for (unsigned int divisor = 2; divisor < number; ++divisor)if (number % divisor == 0)return false;return true;}
IME를 처리하다 보면, 조합 중인 글자를 강제로 완성시켜야 할 때가 있습니다. 예를 들면, 글자 조합 도중에 버튼을 누르는데 조합이 완료된 텍스트를 일반적인 방법으로는 도저히 얻어 낼 수 없을 때입니다. 이럴 때엔 다음처럼 조합 중인 글자를 강제로 완성시키게 하면 됩니다. 하지만 그다지 권장할 만한 방법은 아니므로, 되도록 이런 처리가 필요없도록 하는 게 더 좋습니다.
case WM_LBUTTONDOWN:HIMC ime_context = ImmGetContext(window_handle);if (ime_context != NULL){if (ImmGetCompositionString(ime_context, GCS_COMPSTR, NULL, 0) > 0)ImmNotifyIME(ime_context, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);ImmReleaseContext(window_handle, ime_context);}break;
문자열 코드 변환에 사용되는 함수 중에서 코드 페이지를 인자로 받는 MultiByteToWideChar 함수나 WideCharToMultiByte 함수와 달리, mbstowcs 함수나 wcstombs 함수는 코드 페이지를 인자로 받지 않습니다. 그래서, 미리 코드 페이지 설정을 해 두어야 합니다. 실제로 mbstowcs 함수나 wcstombs 함수의 MSDN 도움말에도 지역 설정이 돼 있어야 한다고 명시돼 있지만, 구체적으로 setlocale 함수를 호출해야 한다고는 명시돼 있지 않아서 좀 혼란스럽습니다.
그런데, 외국에서 만든 일부 소프트웨어는 이런 처리를 하지 않습니다. 그래서 영어가 아닌 문자열을 제대로 인식하지 못할 때가 있는데, 이럴 때엔 당황하지 말고 코드 변환 함수 호출 전에 setlocale(LC_ALL, "")라고 해 주면, 현재 운영체제의 지역 설정에 따라 정상적으로 동작합니다.
IsDebuggerPresent 함수를 이용하면 됩니다. 하지만, 특별히 사용할 일은 거의 없는 듯합니다.
게임 그래픽스에선 조명이 크게 두 가지 종류로 나뉘어집니다. 하나는 정적 조명이고, 다른 하나는 동적 조명입니다. 정적 조명은 실시간으로 속성 변경이 불가능하고 동적 객체로부터 영향을 받지 못합니다. 그래서 사실감이 떨어지므로, 최근엔 정적 조명보다 동적 조명을 지향하는 추세입니다.
정적 조명은 속성이 정적인 조명입니다. 여기서 속성이라 하면 조명의 종류, 위치, 방향, 색깔, 그리고 강도 등이 됩니다. 정적 조명은 동적인 객체(대표적으로 캐릭터)에 독립적(동적인 객체로부터 영향을 받지 않는)입니다. 정적 조명은 동적인 객체로부터 영향을 받지 않지만, 동적인 객체에 영향을 줄 수는 있습니다. 정적 조명은 실시간 계산이 필요없습니다. 그래서 대개 결과를 미리 구해 놓으므로, 복잡한 조명을 구현하더라도 게임 성능엔 거의 영향을 주지 않습니다. 정적 조명은 라이트 그리드(light grid), 라이트맵(lightmap), vertex color, ambient cube, irradiance volume, spherical harmonics environment light probes, 그리고 이미지-베이스드 라이팅(image-based lighting) 등의 기법으로 활용됩니다.
동적 조명은 속성이 동적인 조명입니다. 동적 조명은 동적인 객체의 영향을 받기도 해서, 동적인 객체의 형태가 변하면 그것의 영향을 받는 동적 조명의 결과도 달라질 수 있습니다. 동적 조명은 실시간으로 계산되므로, 조명 구현이 복잡하거나 많은 수의 동적 조명을 사용하면 게임 성능을 크게 떨어트립니다. 그런데 최근엔 deferred rendering과 그 유사한 기법의 등장으로, 상당히 많은 수의 동적 조명을 활용하는 것도 가능해졌습니다. 동적 조명은 크게 두 가지 종류로 나뉘어집니다. 첫 번째는 동적 객체에만 영향을 주는 것입니다. 두 번째는 정적 객체에도 영향을 주는 것인데, 이것은 부하가 당연히 더 큽니다. 대표적인 예로 태양광이 있습니다.
참고:
게임 그래픽스에 등장하는 용어 중에 베이킹이라는 게 있습니다. 일반적으로 베이킹은 빵 같은 걸 굽는 것을 말하는데, 그래픽스에선 여러 구성 요소에 어떤 처리를 해서 하나의 완성품을 만드는 것을 뜻하는 듯합니다. 특히 그 작업은 실시간으로 진행되지 않고 미리 해 두는 때가 잦습니다.
대표적으로, 복잡한 연산을 수행한 여러 결과를 조합해서 텍스쳐로 미리 만들어 두거나, 소프트웨어 인스턴싱을 구현하기 위해서 메쉬 여러 개를 미리 하나의 메쉬로 조합하는 것이 베이킹에 해당합니다.
문서 파일의 비교(diff) 및 병합(merge)에 사용할 수 있는 도구는 많습니다. TortoiseSVN 사용자라면 기본적으로 TortoiseMerge를 사용하게 돼 있는데, 그 도구도 괜찮지만 찾아 보면 자신에게 더 잘 맞는 도구가 있습니다. 유명한 비교 및 병합 도구로는 Beyond Compare, Araxis Merge, WinMerge,그리고 KDiff3 등이 있습니다.
P4Merge는 Perforce에 포함된 도구입니다. P4Merge는 다른 비교 및 병합 도구에 비해 많이 사용되는 것 같진 않지만, 4개의 문서를 동시에 비교할 수 있는 기능을 제공하고 깔끔한 사용자 인터페이스까지 갖춰서 좋습니다. 게다가 Perforce와 달리, 무료로 사용할 수 있습니다.
P4Merge의 큰 단점은 편집이 쉽지 않다는 것입니다. 병합 도중에 충돌이 나면 TortoiseMerge에서는 클릭 한 번으로 두 파일 중 어느 파일의 것을 선택할지 정할 수 있어서 편합니다. 하지만, P4Merge에서는 각 파일의 내용을 순서대로 합쳐 버리기 때문에, 필요 없는 부분을 사용자가 일일이 제거해야 합니다.
TortoiseSVN Settings의 External Programs의 Diff Viewer의 Configure the program used for comparing different revisions of files에서 TortoiseMerge 대신에 External을 선택하고, '...\P4Merge.exe %base %mine'라고 입력합니다. ('...'엔 P4Merge의 경로를 넣으면 됩니다.)
TortoiseSVN Settings의 External Programs의 Merge Tool의 Configure the program used to resolve conflicted files에서 TortoiseMerge 대신에 External을 선택하고, '...\P4Merge.exe %base %theirs %mine %merged'라고 입력합니다. ('...'엔 P4Merge의 경로를 넣으면 됩니다.)
얼마 전에 게임 엔진을 새 버전으로 교체하는 작업을 했습니다. 그런데, D3DXAssembleShader 함수가 HRESULT 0x8007007e 오류를 되돌리면서 실행되지 않는 것이었습니다. 디버거 창에는 '지정된 모듈을 찾을 수 없습니다.'라고만 나오니까 어떻게 해야 하는지 알 수 없었습니다. D3DXAssembleShader 함수 인자를 설정하면 추가적인 메시지를 얻을 수 있다고 돼 있지만, 소스 코드를 수정하기는 어려운 상황이었습니다.
지푸라기라도 찾자는 마음으로 구글을 열심히 검색하다가, 어떤 파일을 접근하다가 실패하는지 찾으려면 Process Explorer를 활용하면 된다는 글을 우연히 발견했습니다. 그래서 그 프로그램을 띄워 놓고 에러 날 때의 파일 접근을 조사해 보았습니다. 그랬더니 d3dcompiler_42.dll이라는 파일을 찾다가 실패하는 것임을 알았습니다. 그래서 DirectX 2009년 8월 버전을 설치하니까, 그 DLL에 접근이 잘 되네요.
아... 역시 디버깅은 어렵습니다.
이미지-베이스드 라이팅을 요즘에 좀 조사했는데, 아직도 잘 모르겠습니다.
IBL로 반사광(specular)을 얻는 방식은 cube-mapped reflection과 차이가 없는 듯합니다. 카메라에서 물체 표면을 향하는 벡터, 물체 표면 법선 벡터, 그리고 반사 벡터를 이용해서 큐브 맵 텍셀을 얻어 오면 됩니다. Cube-mapped reflection을 쓰면 해당 큐브 맵이 텍스쳐로 사용되고, IBL로 반사광을 얻으면 해당 큐브 맵이 반사광 조명으로 사용된다는 차이뿐인 듯합니다. 하지만 정확한 차이점을 모르겠네요.
재미있는 것은 IBL을 이용해서 분산광(diffuse)을 실시간으로 얻을 수 있다는 것입니다. 굉장히 많은 수의 정적 조명이나 global illumination 효과를 움직이는 객체에 적용하고 싶은 게임에서는 쓸모가 있을 수도 있겠습니다. 예를 들어, 밝게 빛나는 파란 건물 앞에 캐릭터를 세우면 캐릭터 앞이 살짝 파래지는 효과를 낼 수 있습니다. 하지만 저는 굳이 이렇게까지 할 필요가 있을까란 생각이 듭니다. 많은 수의 분산광 조명을 활용하려면 deferred shading을 활용하는 게 나을 듯하고, global illumination은 별로 티도 안 나니 큰 이득이 없어 보입니다.
IBL로 분산광을 실시간으로 얻는 과정을 제가 이해하고 생각한 대로 정리하면 다음과 같습니다.
우선 전처리 과정이 필요합니다. 월드의 몇몇 지점에서 큐브 맵을 만듭니다. 그 다음, 각 큐브의 중앙으로부터 모든 방향에 대한 분산광을 계산해서 저장합니다. 각 방향의 분산광은 그 방향과 큐브 맵이 이루는 각도에 따라 큐브 맵 색상에 가중치를 곱한 것을 모두 더해서 평균을 구하면 됩니다. 좀 더 빠르게 하려면 HDRI로 큐브 맵의 빛 강도를 미리 추출해서 강도가 센 텍셀 위주로만 계산할 수도 있고, 특정 방향의 큐브 맵 텍셀 몇 개만 추출해서 계산할 수도 있습니다. 만약 global illumination을 적용해서 렌더링할 수 있으면, 렌더링한 것을 그대로 큐브 맵으로 써도 됩니다.
전처리가 끝났으면 전처리된 텍스쳐의 정보, 그리고 정점이나 픽셀의 법선을 이용해서 해당 방향에 맞는 텍셀을 얻으면 됩니다. 객체가 큐브 안에 있을 때엔 그 큐브의 맵을 이용하면 되고, 그렇지 않으면 가장 가까운 큐브의 맵을 이용하면 될 것입니다. 또는 주변 큐브와의 거리에 따라 가중치를 둬서 여러 큐브의 맵을 혼합해서 사용하면 좀 더 정확하겠습니다.
추가적으로, 큐브 맵 안에서의 좌표에 따라 좀 더 정확한 큐브 맵 텍스쳐의 좌표를 얻어 오도록 개량할 수도 있습니다.
다음 문서를 읽어 보면 도움이 될 것입니다.
적어 놓고 보니, 그림이 없어서 이해가 쉽지 않겠네요. 나중에 그림을 추가해야겠습니다.
Designing & Optimizing Software for N Cores을 보다 보니까, 더블 버퍼링을 이용해서 스레드 동기화를 줄이는 것이 소개돼 있었습니다. 더블 버퍼링의 개념은 그래픽스 프로그래머에겐 익숙한 개념인데, 이걸 스레드 동기화에도 사용할 수 있다는 게 신선하게 느껴졌습니다. 그 문서엔 구현이 구체적으로 나와 있지 않았는데, 어떻게 구현하면 될 것인지 그냥 제 나름대로 생각해 보았습니다.
더블 버퍼링엔 두 개의 버퍼가 필요합니다. 하나는 읽기용이고, 다른 하나는 쓰기용입니다. 외부로부터 요청을 받아 어떤 일을 하는 예제를 만들어 보겠습니다.
class work_type{};class worker_type{public:worker_type():my_current_work(&my_work[0]),my_next_work(&my_work[1]){}void add_work(const work_type& work){my_next_work->push(work);}private:typedef std::queue<work_type> work_queue_type;void do_work(){for (work_queue_type::iterator work_iterator = my_current_work->begin(); work_iterator != my_current_work->end(); ++work_iterator){// 각 작업을 합니다.}my_current_work.clear();std::swap(my_current_work, my_next_work);}work_queue_type my_work[2];work_queue_type* my_current_work;work_queue_type* my_next_work;};
대략 위와 같은 형태가 될 것 같습니다. 외부에선 항상 add_work로 다음 버퍼에만 접근해서 작업을 추가하므로, 이 객체가 do_work로 현재 버퍼를 이용해 자신의 일을 처리하고 있는 데엔 영향을 주지 않습니다. 따라서 위와 같이 하면, 큐에 작업을 추가할 때마다 크리티컬 섹션 등으로 스레드 동기화하던 부하를 없앨 수 있습니다. 그런데 참고로, 버퍼 교환엔 동기화가 필요할 수도 있습니다.
DLL을 사용하는 프로그램을 실행시켰는데, "프로시저 시작 지점 ...을(를) DLL ...에서 찾을 수 없습니다."라고 표시하는 메시지 박스가 뜨면서 실행이 안 될 때가 있습니다. 이럴 때 원인은 대개 해당 DLL의 문제입니다. 하지만 가끔, 해당 DLL에 접근하는 다른 DLL이나 EXE가 잘못된 것이어서 그런 오류가 나는 때도 있습니다. 따라서 모든 DLL이 정상적인 것인지 확인해야 합니다.
콘솔 프로그램이 아니지만 콘솔 창을 띄워서 실시간으로 어떤 값을 확인하고 싶을 때가 있습니다. 이럴 때엔 다음처럼 처리하면 됩니다.
// 콘솔 창을 생성합니다.AllocConsole();// 콘솔 창에 텍스트를 출력합니다.DWORD number_of_characters_written;string output_text;WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), output_text.c_str(), output_text.size(), &number_of_characters_written, NULL);// 콘솔 창을 해제합니다.FreeConsole();
애자일 방법론(Agile methodology), 특히 익스트림 프로그래밍(Extreme Programming)에서는 유닛 테스트를 상당히 강조합니다. 그런데, 제 생각은 좀 다릅니다. 유닛 테스트에 들이는 노력 대비 효과가 큰 것인지 의문스럽습니다.
모든 기능에 대한 유닛 테스트를 만드려면, 테스트 케이스를 엄청나게 많이 만들어야 합니다. 뿐만 아니라, 다른 클래스의 함수를 호출하고 있을 때에도 유닛 테스트를 작성할 수 있도록 Mock 객체 등의 개념을 도입해야 합니다.
유닛 테스트를 작성하면, 다른 일에 사용될 수 있었던, 프로그래머의 많은 시간이 유닛 테스트 작성에 할애됩니다. 프로그래머의 시간이 부족하면 결국 프로그래머를 더 충원해야 하고, 프로그래머 인원이 증가하면서 관리가 어려워지며 의사 소통 부담도 커질 뿐만 아니라, 다른 파트에 할당될 수 있었던 인원을 소모하는 결과까지 낳습니다.
하지만, 이런 대가를 치루면서 유닛 테스트를 만든다고 해도, 코드의 안전성을 다음과 같은 이유 때문에 100% 확신할 수도 없습니다. 유닛 테스트는 기본적으로 하나의 클래스만을 대상으로 테스트하는 것이라서, 클래스 간의 상호 작용 과정에서 발생하는 문제를 찾을 수 없습니다. 그리고 모든 입력 값에 대한 테스트 케이스를 테스트한다는 것도 불가능합니다. 또, 스레드나 GUI와 관련된 코드는 유닛 테스트를 작성하기 어렵습니다.
게다가, 게임은 가상 세계라서 문제가 생긴다고 해도 여파가 크지 않으며, 클라이언트는 더 그렇습니다. 유닛 테스트가 효과를 발휘하는 곳은 사소한 버그라도 심각한 영향을 끼칠 수 있는 곳입니다. 예를 들면, 컴파일러, 운영체제, 의료, 금융, 인사 관리, 급여 관리, 군사, 그리고 우주 항공 등에 쓰이는 소프트웨어 중에서도 핵심 코드입니다.
이 글을 보면, 프로그래밍계에서 유명한 Joel Spolsky라는 분도 유닛 테스트에 대해 부정적인 듯합니다. 물론, 저는 Joel Spolsky라는 분의 다른 글에는 동의하지 않는 부분이 많이 있지만, 그 글엔 어느 정도 생각이 일치합니다.
우리는 품질 좋은 소프트웨어를 만들어야 하지만, 노력 대비 효과가 크지 않은 곳에까지 품질 향상을 위해 노력하는 건 낭비입니다.
헤더 파일을 불필요하게 포함하는 것은 빌드를 느려지게 하는 중요한 이유입니다. 그리고 이 문제를 해결하려면, 전방 선언과 Pimpl idiom(Opaque pointer - Wikipedia, the free encyclopedia, GotW #24: Compilation Firewalls, Pimpl Idiom, Dr. Dobb's | Making Pimpl Easy | January 25, 2008)을 활용하면 됩니다. 다음은 그 해결책을 사용한 이후의 유지보수와 관련된 얘기입니다.
모든 소스 파일에서 불필요한 #include를 지우는 것은 시간이 너무 오래 걸립니다. 그래서, 먼저 정리할 파일을 추려야 합니다. 방법을 간단히 생각해 보면, 가장 많은 #include를 포함하고 있는 클래스의 .hpp와 .cpp부터 정리하면 될 것입니다. #include 구문이 파일에 몇 개 들어있는지 세는 프로그램을 직접 작성해도 되고, 좋은 문자열 처리 도구가 있으면 그걸 써도 됩니다. 비쥬얼 스튜디오의 프로젝트 옵션에 보면 showIncludes라는 게 있는데, 그걸 활용해도 되겠습니다.
#include를 많이 갖고 있는 파일을 추려 냈으면, 한 파일씩 그 파일에서 #include를 하나씩 제거하고 컴파일합니다. 오류가 나면 그 #include를 되살리고, 그렇지 않으면 다음 #include를 제거하는 식으로 계속합니다. 모든 #include를 확인했으면, 이제 그 클래스 파일은 꼭 필요한 헤더 파일만 포함한 파일이 됩니다. 이런 식으로 각 파일을 처리합니다.
이 정리 작업 주기는 세 달에 한 번 정도가 적당할 듯합니다. 참고로, 최근에 정리한 파일을 또 정리하는 것은 의미가 별로 없습니다. 그래서, 정리한 지 오래되고 #include 수가 많은 파일을 정리하는 게 좋습니다.
NVIDIA의 PerfHUD라는 프로그램을 사용하면 오른쪽 아래에 비디오 메모리 정보가 나오는데, 가끔 정보가 나오지 않을 때가 있습니다.
다른 방법으로는 DirectX SDK의 DirectX Caps Viewer라는 프로그램을 이용하면 됩니다. DirectDraw Devices라는 항목 아래의 주 디스플레이 드라이버에서 Memory를 보면 Video, Video (local), Video (non-local), Texture 등의 메모리 정보가 나옵니다. 참고로, 메모리 정보가 실시간으로 갱신되지 않으므로, 현재 시점의 메모리 정보를 확인하려면 다른 트리의 노드를 선택했다가 다시 메모리 정보 노드를 선택해야 합니다.
프로그래밍을 이용해 비디오 메모리 양을 구하려면, DirectX SDK의 VideoMemory Sample에 소개된 방법을 사용하면 됩니다. DirectDraw를 이용해 비디오 메모리의 용량을 구하는 방법의 소스 코드는 다음과 같습니다.
다른 방법으로는 DirectX SDK의 DirectX Caps Viewer라는 프로그램을 이용하면 됩니다. DirectDraw Devices라는 항목 아래의 주 디스플레이 드라이버에서 Memory를 보면 Video, Video (local), Video (non-local), Texture 등의 메모리 정보가 나옵니다. 참고로, 메모리 정보가 실시간으로 갱신되지 않으므로, 현재 시점의 메모리 정보를 확인하려면 다른 트리의 노드를 선택했다가 다시 메모리 정보 노드를 선택해야 합니다.
프로그래밍을 이용해 비디오 메모리 양을 구하려면, DirectX SDK의 VideoMemory Sample에 소개된 방법을 사용하면 됩니다. DirectDraw를 이용해 비디오 메모리의 용량을 구하는 방법의 소스 코드는 다음과 같습니다.
LPDIRECTDRAW7 direct_draw;HRESULT hresult = DirectDrawCreateEx(NULL, (VOID**)&direct_draw, IID_IDirectDraw7, NULL);if (FAILED(hresult))throw "";DDSCAPS2 ddscaps2 = {};ddscaps2.dwCaps = DDSCAPS_VIDEOMEMORY | DDSCAPS_LOCALVIDMEM;DWORD total_local_video_memory;DWORD free_local_video_memory;hresult = direct_draw->GetAvailableVidMem(&ddscaps2, &total_local_video_memory, &free_local_video_memory);if (FAILED(hresult))throw "";ddscaps2.dwCaps = DDSCAPS_VIDEOMEMORY | DDSCAPS_NONLOCALVIDMEM;DWORD total_nonlocal_video_memory;DWORD free_nonlocal_video_memory;hresult = direct_draw->GetAvailableVidMem(&ddscaps2, &total_nonlocal_video_memory, &free_nonlocal_video_memory);if (FAILED(hresult))throw "";direct_draw->Release();total_video_memory = total_local_video_memory + total_nonlocal_video_memory;
윈도 환경에서 빌드 서버 구축은 크루즈컨트롤닷넷(CruiseControl.NET)이라는 프로그램을 이용하면 편합니다.
참고로, "윈도우 프로젝트 필수 유틸리티 Subversion, Trac, CruiseControl.NET"라는 책을 보면, 여러 가지 설정을 하는 방법이 나와 있습니다. 그런데 설정을 그렇게 많이 할 필요는 없고, 꼭 필요한 것만 하면 됩니다.
필수적인 것은 크루즈컨트롤닷넷과 CCTray를 설치하는 것인데, 이건 다운로드 받고 설명서대로 설정하면 되니 크게 어려운 게 없습니다.
또, 거의 필수적인 일은 빌드에 버전을 포함시키는 것입니다. 이렇게 해 두면 해당 빌드가 어떤 버전인지 쉽게 알 수 있어서, 문제가 발생했을 때 빨리 대처할 수 있습니다. 버전은 "C:\Program Files\CruiseControl.NET\server" 아래에 .state 파일로 저장된다고 합니다. 해당 파일을 읽어서 적당한 형태로 가공해 빌드에 포함시키면 됩니다. 빌드 버전 갱신은 실행 파일의 리소스에 버전을 등록시켜도 되는데, 그냥 텍스트 파일을 하나 만들어서 처리하는 게 더 편할 것입니다.
추가적으로 생각해 볼 것 중에 제일 유용한 것은 서브버전(Subversion)과 심볼 파일을 연동하는 것입니다. 이렇게 하면 덤프 파일만 있어도, 해당하는 PDB와 소스를 찾을 필요 없이 바로 디버깅할 수 있으니 편합니다. 연동을 하려면, Debugging Tools for Windows를 설치하고 경로 추가를 해야 합니다. 즉, 시작 메뉴의 제어판의 시스템의 고급 탭의 환경 변수의 시스템 변수의 Path를 선택하고 편집을 눌러서 "C:\Program Files\Debugging Tools for Windows\sdk\srcsrv"를 추가합니다. 그리고 비쥬얼 스튜디오의 옵션에서도 심볼 서버를 사용하도록 설정해야 합니다. 비쥬얼 스튜디오의 Tools의 Options의 Debugging에서 Enable source server support와 그 하위 항목에 체크합니다. Symbols에서 Symbol file (.pdb) locations에 심볼의 네트워크 경로를 적고, Cache symbols from symbol servers to this directory에 적당한 디렉토리를 지정하면 됩니다.
그 외에 더 생각해 볼 것으로는 콘피그(debug, release 등)별 빌드 기능, 패키징 기능, 배포 전 바이러스 검사, 소스코드 정적 분석 (Cppcheck 등), 자동 테스트 등을 빌드 과정에 포함시키는 것입니다. 또 빌드가 실패하면 메신저나 이메일을 이용해서 즉시 알 수 있게 하는 것도 좋습니다.
다음은 크루즈컨트롤닷넷에 쓰일 설정 파일의 예입니다. 지속 빌드와 일일 빌드를 나눠서, 각각이 수행되도록 하되, 우선순위를 다르게 해서 동시에 두 가지 빌드가 수행되지 않도록 막았습니다. 지속 빌드는 통합이 잘 됐는지만 확인하는 용도이므로, 최대한 빨리 빌드가 끝나도록 했습니다. 반면에 일일 빌드는 실제 제품으로 쓸 수 있도록 빌드 전체 과정을 모두 거치도록 했습니다. 그리고 레이블은 서브버전의 리비전과 일치시키는 게 관리하기 편하므로 그렇게 했습니다.
<?xml version="1.0" encoding="utf-8"?><cruisecontrol xmlns:cb="urn:ccnet.config.builder"><project name="My Project - Continuous Build" queue="My Project" queuePriority="1"><triggers><intervalTrigger /></triggers><!-- 레이블을 서브버전 리비전과 일치시킴 --><labeller type="lastChangeLabeller"><prefix>0.0.0.</prefix></labeller><!-- 서브버전 저장소 주소 및 작업 디렉터리 설정 --><sourcecontrol type="svn"><trunkUrl>http://my_project/trunk</trunkUrl><workingDirectory>D:\my_project\trunk</workingDirectory><timeout units="hours">1</timeout></sourcecontrol><tasks><!-- 비쥬얼 스튜디오 2005 빌드 설정 --><devenv><solutionfile>D:\my_project\trunk\source\my_project.sln</solutionfile><configuration>Release</configuration><buildTimeoutSeconds>3600</buildTimeoutSeconds></devenv></tasks></project><project name="My Project - Daily Build" queue="My Project" queuePriority="2"><triggers><scheduleTrigger time="6:00" /></triggers><!-- 레이블을 서브버전 리비전과 일치시킴 --><labeller type="lastChangeLabeller"><prefix>0.0.0.</prefix></labeller><!-- 서브버전 저장소 주소 및 작업 디렉터리 설정 --><sourcecontrol type="svn"><trunkUrl>http://my_project/trunk</trunkUrl><workingDirectory>D:\my_project\trunk</workingDirectory><timeout units="hours">1</timeout><revert>true</revert></sourcecontrol><tasks><!-- TODO: 버전 자동 업데이트 필요 --><!-- 비쥬얼 스튜디오 2005 빌드 설정 --><devenv><solutionfile>D:\my_project\trunk\source\my_project.sln</solutionfile><configuration>Release</configuration><buildTimeoutSeconds>3600</buildTimeoutSeconds></devenv><!-- PDB 파일에 서브버전 저장소 정보를 인덱싱하고, 압축한 뒤 심볼 서버에 등록 --><exec><!-- TODO: svnindex.cmd와 symstore.exe 호출 필요 --></exec><!-- TODO: 빌드된 결과물을 저장소로 커밋 처리 필요--></tasks></project></cruisecontrol>
Installing DirectX with DirectSetup라는 글에 잘 설명돼 있네요. DirectX SDK에도 포함된 내용입니다.
이 오류에 대한 설명은 Description of the R6025 run-time error in Visual C++를 보시면 됩니다. 이 오류는 순수 가상 함수를 호출하려고 할 때 발생하는 오류인데, 일반적인 방법으로는 예외 처리가 안 됩니다. 이걸 해결하는 간단한 방법은 _set_purecall_handler 함수로 예외 처리 함수를 지정하는 것입니다. 다음은 실제 사용 예입니다.
void handle_pure_virtual_function_call(){RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, NULL);}int main(int argc, char* argv[]){_set_purecall_handler(handle_pure_virtual_function_call);return 0;}
게임에서 한/영 키를 누르고 나면, IME 때문에 WM_KEYDOWN 등의 메시지가 다르게 처리돼서 원래 의도대로 키가 작동하지 않는 문제가 생깁니다. 이걸 해결하는 간단한 방법은 평소엔 IME와 윈도우 핸들의 연결을 끊어두고, 한글을 입력 받아야 할 때만 연결하는 것입니다. 이에 대한 힌트는 DirectX SDK의 DXUT에서 IME 관련 소스 코드를 보면 얻을 수 있습니다. 대략 다음 소스 코드처럼 처리하면 됩니다.
HWND global_window_handle;HIMC global_ime_context;void save_ime_context(){global_ime_context = ImmGetContext(global_window_handle);ImmReleaseContext(global_window_handle, global_ime_context);}void set_ime_enablement(bool enablement){ImmAssociateContext(global_window_handle, enablement ? global_ime_context : NULL);}
프로그래밍을 하다 보면 다양한 버그를 접하게 됩니다. 문제가 발생한 시점에 문제가 바로 터져서 디버깅이 가능한 버그는 해결이 그다지 어렵지 않습니다. 무엇 때문에 문제가 발생하는지 쉽게 알기 어려운 버그가 정말 까다로운 버그입니다.
예를 들어, 버그가 발생한 곳에서 프로그램이 뻗지 않고 문제 지점을 한참 지나쳐서 엉뚱한 곳에서 터지는 버그는 상당히 당황스럽습니다. 이럴 때엔 콜 스택이나 메모리 조사 기능 등 현 시점의 상태를 조사하는 디버거의 각종 기능이 거의 무의미합니다. 왜냐하면 그 상태에서는 문제를 이미 지나친 후이기 때문입니다. 이럴 때엔 시야를 넓혀서 프로그램의 전체적인 흐름을 파악하고 원인이 될 만한 곳을 추측해야 문제를 빨리 해결할 수 있습니다. 즉, 어떤 함수를 호출하고 나면 버그가 발생하는지 조사해야 합니다.
원인을 알 수 없는 버그를 빨리 찾는 유용한 방법으로 이분법이 있습니다. 만약 몇몇 최근 리비전에서만 버그가 발생한다면, 버그가 없다고 보고된 가장 최근 리비전과 버그가 있다고 보고된 가장 오래된 리비전 사이에서 버그가 발생한 리비전을 이분 탐색하면 어느 리비전부터 버그가 생겼는지 제일 빠르게 찾을 수 있습니다. 리비전 이분 탐색은 자주 사용되므로, 가능하면 하나의 리비전은 최소 단위로 갱신해야 문제를 나중에 쉽게 찾을 수 있습니다. 그리고 이분법은 수백 줄 이상의 코드 중에서 어느 코드가 문제를 일으키는지 도저히 알 수 없는, 악몽 같은 디버깅 상황에도 응용할 수 있습니다.
참고로, 각종 CRT 메모리 검사 함수를 이용하면 버그를 좀 더 편하게 찾을 수도 있는데, 저는 그 함수를 자주 사용해 보지 않아서 잘 모릅니다.
증분 링크(incremental linking)는 링크 시간을 줄이는 좋은 도구입니다. 특히 파일 한 두 개만 살짝 고치고 링크할 때 최상의 효과를 얻을 수 있습니다.
비주얼 C++(Visual C++)에서 프로젝트를 생성하면, 디버그 버전에서는 이 옵션이 기본적으로 켜져 있으므로 따로 신경쓰지 않아도 됩니다. 릴리즈 버전에서는 이 옵션이 꺼져 있는데, 만약 릴리즈 버전으로 자주 빌드해야 한다면 이 옵션을 켜는 것도 생각해 보면 좋습니다. 이 옵션을 켜면 링크 시간을 줄이는 장점이 있지만 파일 크기가 커지는 단점도 있으니, 장단점을 잘 따져 보아야 합니다.
참고로, 증분 링크는 일부 안티바이러스 프로그램과 충돌을 일으켜서 동작하지 않을 때가 있습니다. 그럴 때엔 안티바이러스 프로그램의 설정을 바꿔야 합니다.
많은 게임 프로그래머가 최적화에 너무 집착하는 경향이 있습니다. 그 대표적인 예가 객체를 소멸시키고 생성하는 것에 거부감을 갖는 것입니다. 객체를 소멸시키고 생성하는 부하가 너무 크다면, 객체를 당연히 재활용해야 합니다. 하지만, 그렇지 않은 곳에서까지 그 방법을 택하는 것은 프로그램을 필요 이상으로 복잡하게 만들어서 좋지 않습니다.
객체를 재활용하려면 객체의 상태를 초기화하는 함수를 만들어야 합니다. 즉, 소멸자와 생성자가 하는 일을 거의 그대로 하는 함수를 만들어야 하는 것입니다. 그러면 대개 다음처럼 진행됩니다.
참고로, reset 함수 안에서는 불필요한 일을 좀 더 줄이기 위해서, 변형된 finalize와 initialize를 호출할 수도 있습니다. 하지만, 기본적인 틀은 위의 코드에서 크게 벗어나지 않습니다.object_type::object_type()
{
initialize();
}object_type::~object_type()
{
finalize();
}void object_type::initialize()
{
...
m_valid = true;
}void object_type::finalize()
{
...
m_valid = false;
}void object_type::reset()
{
finalize();
initialize();
}
위 방식이 특히 비효율적일 때는 생성자와 소멸자를 활용하는, 스마트 포인터 등의 멤버 변수를 갖고 있을 때입니다. 생성자 안에서는 멤버 변수의 생성자가 자동으로 불리고, 소멸자 안에서도 멤버 변수의 소멸자가 자동으로 불립니다. 그래서 사용자는 복잡한 생성 소멸 처리를 따로 할 필요가 없습니다. 하지만 객체 재활용 방식을 사용하면, reset 함수에서 초기화 처리를 별도로 해 줘야 합니다.
게다가, m_valid = true처럼 객체가 유효한 상태인지 기억해 둬야 하며, 외부에서 그 객체를 접근할 때에도 유효한 상태인지 확인해야 해서 실수의 여지가 많아집니다. 뿐만 아니라, 어떤 객체는 위의 방식을 쓰고 어떤 객체는 일반적인 방식을 쓴다면, 코드는 더 난해해집니다.
생성 소멸 문제를 쉽게 해결하려면, 객체를 재활용하려고 하지 말고 생성자와 소멸자를 적극적으로 활용하는 것이 좋습니다.
생성 소멸 문제를 쉽게 해결하려면, 객체를 재활용하려고 하지 말고 생성자와 소멸자를 적극적으로 활용하는 것이 좋습니다.
소프트웨어 개발에서 많은 부분이 indirection을 통해 해결됩니다. 그런데 때로는 indirection을 지나치게 남용해서, 프로그래밍을 더 복잡하게 할 때가 많습니다.
이렇게 indirection이 불필요하게 등장하는 주된 이유는, 프로그래머가 상황을 미리 대비하려고 하거나 좀 더 우아한 설계를 추구하기 때문입니다. 지금 할 수 있는 일만 잘 할 수 있게 코드를 최소한으로 짜 놓으면, 군더더기 indirection 코드는 생기지 않습니다.
간단한 예를 들어 보겠습니다. A 클래스가 B 클래스의 함수를 호출하는데, 호출 전에 B의 정보에 따라 어떤 처리를 해야 할 때가 가끔 있습니다. 그 처리를 A에서 직접 하는 게 맘에 들지 않는 프로그래머라면, A와 B 사이에 B의 관리자 역할을 하는 C 클래스를 만들어 A가 C를 거쳐 B에 명령을 내리려고 할 것입니다. 즉 퍼사드 패턴(facade pattern)을 도입하는 것입니다. 그러나 C가 실제로 하는 일이 거의 없다면, C는 단지 명령을 전달하는 역할을 할 뿐이고 B에 기능을 추가할 때마다 C도 고쳐야 하는 불편함만 따릅니다. 이럴 때엔 그냥 A에서 C의 역할을 하는 게 코드 양을 줄일 수 있습니다.
문제는 가능한 한 쉽게 해결해야 합니다. 따라서 indirection은 꼭 필요할 때만 사용해야 합니다.
이렇게 indirection이 불필요하게 등장하는 주된 이유는, 프로그래머가 상황을 미리 대비하려고 하거나 좀 더 우아한 설계를 추구하기 때문입니다. 지금 할 수 있는 일만 잘 할 수 있게 코드를 최소한으로 짜 놓으면, 군더더기 indirection 코드는 생기지 않습니다.
간단한 예를 들어 보겠습니다. A 클래스가 B 클래스의 함수를 호출하는데, 호출 전에 B의 정보에 따라 어떤 처리를 해야 할 때가 가끔 있습니다. 그 처리를 A에서 직접 하는 게 맘에 들지 않는 프로그래머라면, A와 B 사이에 B의 관리자 역할을 하는 C 클래스를 만들어 A가 C를 거쳐 B에 명령을 내리려고 할 것입니다. 즉 퍼사드 패턴(facade pattern)을 도입하는 것입니다. 그러나 C가 실제로 하는 일이 거의 없다면, C는 단지 명령을 전달하는 역할을 할 뿐이고 B에 기능을 추가할 때마다 C도 고쳐야 하는 불편함만 따릅니다. 이럴 때엔 그냥 A에서 C의 역할을 하는 게 코드 양을 줄일 수 있습니다.
문제는 가능한 한 쉽게 해결해야 합니다. 따라서 indirection은 꼭 필요할 때만 사용해야 합니다.

댓글을 달아 주세요
관리자만 볼 수 있는 댓글입니다.
그렇군요. 좋은 도구를 쓴다고 해도 기본이 돼 있지 않으면, 효과를 크게 얻을 수 없나 봅니다. 그런데 라이트매스는 언리얼에서만 쓸 수 있는 것이라서, 미들웨어라고 부르긴 어렵네요.
비스트랑 라이트매스랑 사용법과 효과가 거의 같아서요..
링크는 diffuse텍스쳐 재작시 gi를 사용할 경우 고려해야 한다는 부분이라서 적었어요.. ^^
"콘트라스트가 높거나 어두운 diffuse 텍스처가 조명을 알아보기 어렵게 하는 반면, 낮은 콘트라스트와 중간 범위의 diffuse 텍스처는 조명의 디테일이 나타나도록 합니다. "
프로젝트 초기에 저런 미들웨어 도입하면 좀더 다른 느낌의 라이팅을 줄수 있을거 같습니다.
네.... 아무래도 조명의 다양한 값을 잘 살리려면 텍스쳐가 그에 맞게 제작돼야겠네요.