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를 실행시켜보면 변경된 값이 나오는 것을 알 수 있습니다.
HellWorld를 치면 HellLevel 콘솔변수의 값을 가져와서 출력되는 것을 볼 수 있습니다. 다음 처럼 HellLevel의 값을 변경시킨 후 다시 HellWorld를 실행시켜보면 변경된 값이 나오는 것을 알 수 있습니다.
HellLevel 10 ; HellWorld
<
콘솔창 >
4.3 s_hellLevel 전역변수로 추가
이번에는 HellLevel을 전역변수 s_hellLevel로 만들어서 사용해보겠습니다. 따로 등록하는 메소드를 호출해주지 않아도 콘솔창에서 HellLevel의 값을 변경할 수 있는 것을 볼 수 있습니다.
4.4 자동완성 기능 추가
아래 처럼 HellWorld까지만 입력하고 탭(Tab)키를 누르면 함수에서 작성한대로 취할 수 있는 목록이 나오고 계속 탭키를 누르면 항목이 바뀜을 볼 수 있습니다.
HellWorld [탭키]
< 콘솔창 >
5. 마무리
콘솔은 둠3에서 전역적으로 매우 빈번하게 등장하고 중요하게 사용되는 시스템입니다. 또한 앞으로 우리가 소스를 분석하면서 테스트용으로 작성하는 코드들을 언제든지 원하는 때에 실행시켜보거나 실시간으로 변수값을 조절함으로써 좀 더 효율적으로 분석해볼 수 있습니다.
효율적으로 컨텐츠를 테스트 하기 위한 치트키와 실시간으로 값을 바꿔보면서 테스트 해보는 것, 언제든지 새로 작성한 함수를 실행시킬 수 있는 방법이 있다는 것은 개발에 큰 도움이 됩니다. 반대로, 개발을 시작할 때 이러한 것들을 준비해야 좀 더 효율적으로 개발 할 수 있다는 것을 알 수 있습니다.
효율적으로 컨텐츠를 테스트 하기 위한 치트키와 실시간으로 값을 바꿔보면서 테스트 해보는 것, 언제든지 새로 작성한 함수를 실행시킬 수 있는 방법이 있다는 것은 개발에 큰 도움이 됩니다. 반대로, 개발을 시작할 때 이러한 것들을 준비해야 좀 더 효율적으로 개발 할 수 있다는 것을 알 수 있습니다.
반응형
'프로그래밍' 카테고리의 다른 글
FXAA (Fast Approximate Anti-Aliasing) (13) | 2012.01.17 |
---|---|
컴파일 중에 문자열해쉬 만들기....(를 시도해 보자? -_-) (17) | 2012.01.16 |
squish 라이브러리로 DXT 악몽에서 벗어나기! (16) | 2012.01.13 |
Fast Specular 계산 (14) | 2012.01.12 |
Motion Capture 데이터 파싱 & 렌더링 (6) | 2012.01.10 |