[App 개발] 맥마메 분석 - 2 키보드 입력
본문
사실 마메의 진짜 핵심은 프로세서 에뮬레이션, 비디오 프로세서, 사운드 프로세서 에뮬레이션 루틴이죠. 한 번 마메 소스를 열어보신 분들이라면 실로 방대한 에뮬레이션 루틴들을 보면서 놀라셨을 것입니다.
그런데, 에뮬레이터를 제작해보고 싶은 분들이 아니고 실제 무엇인가를 만들어보고 싶은 사람들은 거기에 관심이 있지 않죠. 제 글은 그런 분들을 위한 (저를 포함해서...) 글입니다. 게임의 기본 입출력을 어떻게 잡아야 할까 하는 아이디어를 얻고자 하는 노력이죠.
첫 번째 글은 그래서 화면 출력이었고, 오늘은 키보드 입력을 살펴볼 것입니다. 계속해서 진도를 마우스 입력, 조이스틱 입력, 그리고 사운드 출력 등을 다루어 보게 되겠지요. 그러면 게임 제작의 가장 근간이라고 할 수 있는 기본 입출력에 대한 기술적인 내용을 모두 살펴보게 될 것입니다. ^^ 특히 매킨토시 게임을 제작하고자 하는 분들을 위해서 (저를 포함해서... ㅡㅡ;;;) 기록하는 글이고, 아, 이것은 제가 쓰는 글이니까... 틀린 내용도 많은데 다른 곳에 퍼가지는 말아 주세요. 저자를 밝히고 퍼가면 되지 않겠느냐고 하시는 분... 그렇게 되면 틀린 내용과 함께 제 이름도 같이 팔리게 되는... 저를 두 번 죽이는 결과가 됩니다. ㅡㅜ;
2. 키보드 입력
키보드 문제에 있어서만큼은 피씨나 맥이나 거의 비슷한 방법으로 구현되어 있지 않은가 생각됩니다. 일반적인 응용프로그램에서의 키 입력은 키보드 버퍼를 이용하여 윈도우에서는 메세지 큐, 맥에서는 이벤트 큐에 키 입력이 차곡 차곡 쌓여서 그것을 차례대로 처리한다는 개념을 갖고 있습니다. 하지만 게임의 키 입력 방식은 전혀 다르죠.
1) 게임의 키 입력
일반 응용 프로그램에서는 키보드 입력을 놓치면 안됩니다. 워드프로세서를 만드는데 키보드를 10개 중에 하나씩, 혹은 100개 중에 하나씩 잊어먹는 키보드 입력 루틴을 갖고 있다? 그 워드 프로세서는 10원짜리도 안 됩니다.
그러나 게임은 다르죠. 게임 프로그램에 있어서는 가끔씩 총알 버튼을 까먹더라도 크게 지장이 없습니다. 한번 잘 생각해 보세요. 슈팅 게임을 만드는데… 열나게 키보드를 눌러 대는데 10 번 눌렀는데 9번 발사되었다고 “어? 불량인데?” 라고 말할 사람은 별로 없습니다. 왜냐하면 당장 주인공 살기에 바쁘기 때문이죠. 하하… 약간 농담이 섞여 있긴 합니다만… 그만큼 키보드를 버퍼에 넣어야 할 필요는 없는 거죠.
키 입력을 버퍼에 넣으면 곤란한 이유는 사실 이것때문입니다. 총알을 눌렀는데 총알 눌린 메세지가 큐에 있다가 한 1~2초 후에 발사되는 게임이라고 생각해 봅시다. 그 게임 하고 싶을까요?
그러니까, 게임 제작에 있어서 입력 장치의 처리는 그만큼 실시간 처리가 중요합니다. 큐에 넣을 새 없이 바쁘게 돌아가야 하는 거죠.
2) 맥마메에 구현된 키 입력 방식
OS X 에 들어오면서 애플은 기존의 게임 스프라켓을 거의 사문화시킵니다. 그리고 화면 출력은 Quartz 에 담당시키고, 게임 입력은 HID 클래스로 구현합니다. HID 는 Human Interface Device 입니다. 이것은 USB 의 슈퍼셋입니다. 이제 새로 나오는 맥의 모든 입력장치는 USB 를 거치기 때문이죠.
그러나, 카본에서는 아직도 옛날 방식을 지원하고 있습니다. 구 기종 소프트웨어와의 호환성을 배려하는 측면이죠. 근데 재미있는 것은 HID 로 구현하는 것보다 이 옛날 방식이 키보드 처리에는 더 편리하다는 점에 있습니다.
원리는 간단합니다. 키 스테이트 배열을 하나 준비합니다. 배열의 길이는 127개입니다. 말하자면 키보드의 최대 갯수이죠. 이 배열을 일정 시간이 흐를 때마다 업데이트를 시켜주는 것입니다. 키 스테이트 배열에는 플래그가 설정되어 있어서 키보드가 눌려 있으면 1 아니면 0 이런 식으로 들어있는 거죠.
맥마메의 키보드 입력 루틴은 macinput.c 에 구현되어 있습니다. 이것도 역시 override 폴더 내의 macintosh 폴더에 수록되어 있습니다. 키 스테이트 배열을 검사하는 루틴은 아주 간단합니다.
inline Boolean TestKeyPressed(UInt8 inKeycode)
{
return (sKeyState[inKeycode]);
}
sKeyState 이 바로 키 스테이트 배열입니다. TestKeyPressed() 함수는 특정한 키 코드 값을 입력하면 그 키에 대한 현재 상태를 리턴합니다.
3) OS Dependent 키 입력 함수
맥마메는 기존의 마메 위에 매킨토시 쉘을 구성한 것입니다. 따라서 키 입력 루틴을 구현할 때에도 매킨토시용 키 코드를 사용하게 됩니다. osd_is_key_pressed() 함수를 살펴보면 어떻게 키 입력을 처리하고 있는지를 살펴볼 수 있습니다.
앞부분에는 약간 복잡한 if statement 의 숲이 보입니다. 게임 처리를 위한 루틴으로 보입니다. 조이스틱의 pause 가 눌렸으면서 뭐가 눌린 경우 어쩌구… 하는 구문들이기 때문입니다.
그런데, 특이한 것은, 키 스테이트 배열을 업데이트하는 루틴이 보이지 않는다는 점입니다. 처음에 제가 이 소스를 볼 때에는 이 부분이 키 스테이트 배열 업데이트 루틴이라고 생각했거든요.
#if 0
// if a tick has passed, get a new key state
currentTicks = TickCount();
if (currentTicks != sLastTicks)
{
sLastTicks = currentTicks;
GetKeys((SInt32 *)sKeyStates);
}
#endif
그런데 자세히 보니 if 0 이라고 되어서 컴파일 레벨에서 제외되어 있는 상태였습니다. TickCount() 함수와 GetKeys() 함수는 카본 함수이고요, 각각 특정 틱 카운트 값을 읽는 함수, 키 스테이트 배열을 받는 함수입니다.
그렇다면 키 스테이트 배열을 업데이트 시켜주는 루틴은 어디 있을까…
4) 키보드 이벤트 핸들러
맥마메 구현의 약간 특이한 점은, 키 스테이트 배열을 메인 윈도우의 키보드 이벤트를 받아서 수동으로 관리하고 있다는 점입니다. 맥마메 윈도우의 키보드 핸들러 등록이 InitializeKeyboardEvents() 에 구현되어 있습니다. 일반적인 윈도우 이벤트 핸들러 등록과 별반 다를 바가 없습니다.
void InitializeKeyboardEvents (WindowRef inWindow)
{
EventTypeSpec keyboardEventList[] =
{
{ kEventClassKeyboard, kEventRawKeyDown },
{ kEventClassKeyboard, kEventRawKeyUp },
{ kEventClassKeyboard, kEventRawKeyRepeat },
{ kEventClassKeyboard, kEventRawKeyModifiersChanged }
};
// Attach the MAME core keyboard handler to this window
InstallWindowEventHandler(inWindow, NewEventHandlerUPP(keyboardEventHandler), GetEventTypeCount(keyboardEventList), keyboardEventList, 0, NULL);
// Clear the key state array
memset (sKeyState, 0, sizeof (sKeyState));
}
InstallWindowEventHandler() 함수를 이용하여 keyboardEventHandler() 함수를 핸들러로 등록하고 있지요. keyboardEventHandler() 함수도 역시 동일한 파일 내에 구현되어 있습니다.
함수keyboardEventHandler() 에서는 먼저 이벤트의 종류와 전달 인수를 디코딩합니다. 어떤 메세지인지를 알아내야 그 다음 일이 되겠지요? GetEventKind() 함수는 이벤트의 종류를 알아내고, GetEventParameter() 함수는 이벤트에 부가된 인수를 알아냅니다. 키 코드와 캐릭터 코드도 여기서 알게 되는군요.
event_kind = GetEventKind (event);
(void)GetEventParameter (event, kEventParamKeyCode, typeUInt32, NULL, sizeof(keyCode), NULL, &keyCode);
(void)GetEventParameter (event, kEventParamKeyMacCharCodes, typeUInt32, NULL, sizeof(charCode), NULL, &charCode);
(void)GetEventParameter (event, kEventParamKeyModifiers, typeUInt32, NULL, sizeof(modifiers), NULL, &modifiers);
그 다음은 switch() 구문을 이용하여 키 스테이트 배열을 손수 업데이트 해줍니다. 이벤트의 종류가 만일 키가 눌리는 이벤트이거나 계속 눌려진 상태라면 (게임에선 이게 쥐약이죠… ^^) 계속해서 키 스테이트 배열의 특정 키 코드 요소를 1로 만들어 놓습니다. 키가 떨어질 때에는 0 으로 바꿔주는 거죠. 간단한 로직이지만 효과는 대단합니다.
switch (event_kind)
{
case kEventRawKeyDown:
case kEventRawKeyRepeat:
sKeyState[keyCode] = 1;
gLastKey = charCode;
SetModifierState (modifiers);
err = noErr;
break;
case kEventRawKeyUp:
sKeyState[keyCode] = 0;
SetModifierState (modifiers);
err = noErr;
break;
case kEventRawKeyModifiersChanged:
SetModifierState (modifiers);
err = noErr;
break;
}
특이한 부분 한 가지는 SetModifierState() 입니다. 키 입력 이벤트에 있어서 쉬프트 키나 커맨드 키, 컨트롤 키 등은 다른 종류의 이벤트로 처리됩니다. kEventRawKeyModifiersChanged 이벤트죠. 왜냐하면 쉬프트나 커맨드 키 등은 자체 입력으로 의미가 있기보다는 Shift + A 이런 식으로 동시에 눌리는 경우가 더 많기 때문이죠. 하지만, 게임에서는 쉬프트 입력 하나 하나가 의미가 있기 때문에 이것을 디코딩하는 루틴을 따로 구현한 것입니다. 유용한 테크닉이죠?
void SetModifierState (UInt32 inModifiers)
{
static UInt32 lastModifiers;
if (inModifiers == lastModifiers) return;
if (inModifiers & cmdKey) sKeyState[kMacKeyCommand] = 1;
else sKeyState[kMacKeyCommand] = 0;
if (inModifiers & shiftKey) sKeyState[kMacKeyLeftShift] = 1;
else sKeyState[kMacKeyLeftShift] = 0;
if (inModifiers & alphaLock) sKeyState[kMacKeyCapsLock] = 1;
else sKeyState[kMacKeyCapsLock] = 0;
if (inModifiers & optionKey) sKeyState[kMacKeyLeftOption] = 1;
else sKeyState[kMacKeyLeftOption] = 0;
if (inModifiers & controlKey) sKeyState[kMacKeyLeftControl] = 1;
else sKeyState[kMacKeyLeftControl] = 0;
if (inModifiers & rightShiftKey) sKeyState[kMacKeyRightShift] = 1;
else sKeyState[kMacKeyRightShift] = 0;
if (inModifiers & rightOptionKey) sKeyState[kMacKeyRightOption] = 1;
else sKeyState[kMacKeyRightOption] = 0;
if (inModifiers & rightControlKey) sKeyState[kMacKeyRightControl] = 1;
else sKeyState[kMacKeyRightControl] = 0;
lastModifiers = inModifiers;
}
약간 길다 싶은 함수로 Modifier 의 상태를 점검하는 이유는 이런 키들이 Up/Down 에 따라 다른 이벤트로 입력되지 않기 때문입니다. 각각의 상태를 일일이 대조하여 키 스테이트 배열을 등록하는 것이죠. 이렇게 하면 이벤트 큐에서 총알 누른 키 입력이 처리되기를 기다릴 필요가 없이 실시간으로 키보드 입력 상태를 점검할 수 있게 됩니다.
이것은 제 의견으로는 편법에 가깝다고 봅니다. 역시 정석으로는 HID 클래스 핸들러를 구현하는 것이겠죠. HID 사용에 대해서는 조이스틱 루틴을 분석하면서 살펴보게 될 것 같습니다.
최신글이 없습니다.
최신글이 없습니다.
댓글목록 0