-
[8] - KeyboardGraphics 2021. 7. 12. 21:00
Window가 하나 있으면 다양한 기능들을 Encapsulate한 상태로 갖고 있는데, Mouse와 Keyboard의 Input을 관리하는 역할을 수행하는 기능도 포함되어 있습니다.
키보드가 Window Side와 Client(Game Logic 또는 사용자이지만 사실 Game logic이라고 하는 편이 더 명확할 것 같습니다.) 사이에서 다양한 통신을 한다면, Client가 관심을 가지는 것은 어떤 버튼이 눌렸을 지에 관한 Message일 것입니다. 전에도 보았던 것처럼 Window는 대부분의 경우 Event 기반으로 동작을 합니다. Client 측에서 어떤 버튼을 누르면 키보드에 있던 Queue에서 Message를 꺼내오거나 한다는 뜻입니다. 키보드라고 썼지만 이는 어떤 구조를 의미하는 것이지 물리적인 키보드를 의미하는 바가 아닙니다.
키보드라는 Class 또는 Instance를 따로 제작하는 이유는 Window로 하여금 모든 것을 제어하도록 구성해도 안 될 것은 없으나 Keyboard class를 따로 정의하여 붙여주는 편이 Organisation이나 Modularity 측면에서 낫다고 합니다.
Keyboard
키보드는 크게 두 가지의 내용을 포함합니다. State와 Queue입니다. 당연하겠지만 Window를 바라보는 Interface가 있을 것이고 Client를 바라보는 Interface가 또 있을 것입니다.
이렇게 생겼을 겁니다 Window를 바라보는 Interface는 당연히 Window에 의해 사용이 될 것이고 Event들을 잡아서 키보드 Object의 State를 업데이트 해줄 것입니다.
Client를 바라보는 Interface는 Client에 의해 사용이 될 것이며 어떤 키가 눌렸는가?에 대한 Key code를 전달하고 키보드로의 Queue로부터 정보를 꺼내옵니다. 그리고 키보드엔 크게 두 가지 Event가 존재합니다. 하나는 Key event이고 또 하나는 Char event입니다.
Implementation: Client Side
KeyIsPressed 함수는 가장 기본적인 Interface일 것입니다. 내용이 무엇이든 누르는 버튼의 Key Code를 전달해준다는 것은 명확해 보입니다. 반환형은 Boolean으로 해당 key code에 합당한 버튼이 눌렸는가, 그렇기 않은가?에 대한 답을 True or False로 확인할 수 있음을 암시합니다.
ReadKey 함수는 위에서 말했던 것처럼 Event를 Queue로부터 빼오는 함수입니다. KeyIsEmpty는 queue에 残存하는 Event의 여부를 확인하는 함수이며 ReadKey와 묶어서 알아두면 좋겠습니다. FlushKey는 그 Queue를 비우는 함수입니다.
ReadChar 함수는 KeyIsPressed함수와 유사하게 Character를 읽어오는 함수인데 Event를 반환하는 함수가 존재하지 않습니다. 이유는 Character는 Text input을 위해 사용하고 Character의 Stream이기 때문입니다. 그러니 구태여 Event까지 다룰 필요 없이 그냥 Queue에 있는 Character를 빼오기만 하면 됩니다. 그 외 함수들은 모두 직관적이라 큰 어려움은 없고 Flush함수는 Key와 char을 모두 비우는 함수입니다.
Implementation: Window Side
여기서부터는 Public side가 아니라 private side입니다. Client를 위한 것이 아닌 Window를 위한 것이기 때문에 Client는 알 필요가 없기 때문입니다. 그래서 Window는 이 함수들을 알 필요가 있고 Keyboard Class는 Window의 Friend로 선언이 되어 있습니다.
이렇게 되면 다른 모든 Private가 노출이 되는 단점이 있긴 하지만 Architectural 관점에서 보면 결국 Keyboard는 Window의 연장으로 존재하고 있기 때문에 상관이 없다는 변명을 할 수 있겠습니다. 왜? 애초에 Window가 할 일을 Organised 한 상태로 유지하기 위해 분리한 Class이기 때문입니다.
ClearState 는 bitset를 비우기로 합니다.
buffer가 등장을 하는데 여기서 이 버퍼의 최고 사이즈를 16으로 주었습니다. TrimBuffer함수는 만약 Buffer가 16을 넘어가면 버퍼의 크기가 16아래로 내려갈 때까지 비우는 역할을 하기로 합니다.
Key Code
Key code라는 개념이 계속 등장합니다. " Key code MSDN "을 검색하면 Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Docs여기로 연결이 됩니다. 모든 Key 값은 Hexadecimal notation으로 Mapping이 되어 있으며 가장 큰 값이 0xFE(VK_OEM_CLEAR)으로 10진수 254에 해당합니다. 위의 사진을 보시면 static constexpr unsigned int nKeys의 값을 256u으로 설정한 것을 확인할 수 있는데, 바로 이 이유 때문입니다. Key code가 아무리 커도 256bits, 1byte를 넘어가지 않기 때문입니다. 그리고 Bitset은 C++ bitset and its application - GeeksforGeeks여기에 설명이 잘 나와 있었습니다. 256개의 boolean을 하나의 Byte로 묶어서 보관하고 Index를 통해 접근하겠다는 강력한 의지가 느껴집니다.
Event와 char의 Buffer는 FIFO queue를 사용한 것을 확인할 수 있습니다.
이 함수를 이해하기 위해 Bitset에 대해 알아볼 수밖에 없었습니다. (2) C++ 3D DirectX Tutorial [Keyboard / WM_SYSKEYDOWN] - YouTube영상에서 설명하기를 ketstate에 bit를 하나의 Byte로 묶어 Index로 접근하겠다고 했습니다. 그리고 keystates의 최대 크기는 const한 256u이었습니다. 예시를 들어서 이해해봅니다.
0xFE ( VK_OEM_CLEAR ) -> 이 값이 곧 keycode이자 parameter가 됩니다. 이를 다시 index로 활용하면 bitset의 특정 값에 진입할 수 있습니다. 그렇게 되면, 해당 keycode가 true(pressed)인지 false(released)상태인지 알 수 있습니다.
ReadKey함수는 key buffer에 뭔가 있으면 그것을 추출하고 추출된 정보는 Pop합니다. Event e라는 인스턴스가 보이는데 다음과 같이 Keyboard class 안의 inner class로 정의되어 있습니다.
Event라는 inner class에는 Enumeration type으로 정의된 class가 하나 있습니다. Event의 instance는 default constructor가 호출될 시에 Type::Invalid로 설정되어 있다는 점이 눈에 보입니다.
다시 ReadKey함수로 돌아가서, keybuffer에 어떤 event가 남아 있다면 그것을 추출하고 반환하지만 아무것도 없다면 Type::Invalid와 keycode = 0u로 설정된 Event 인스턴스가 반환될 것입니다. 그리고 c++17이상의 경우엔 std::optional을 사용할 수도 있습니다. 그렇게 되면 Default Constructor가 필요 없어질 테니까요.
Window가 사용할 Private member functions OnKeyPressed/OnKeyReleased 두 함수 모두 Keystates와 Keybuffer를 Update합니다. 조립은 해체의 역순이고 해체는 조립의 역순인 것처럼 OnKeyPressed - OnKeyReleased는 서로 반대의 일을 할 것입니다. 맞는 비유인가? 아니면 말도록 합시다.
직관적입니다. 입력한 key code를 key states의 index로 사용해서 true로 표시합니다. 이는 현재 눌러져 있다는 상태를 나타내기로 약속합니다. ( More precisely, it is Being Pressed or currently down. ) 그리고 나면, Buffer에 Event 인스턴스를 하나 생성하여 넣어주도록 합니다. 마지막으로 혹시나 buffer가 Over해버릴 수도 있으니 확인하는 과정까지 진행하도록 합니다. OnKeyReleased? 같습니다.
OnChar는 조금 다를 수 밖에 없습니다. Char는 Character stream이라고 했으니 Event를 요구하지 않습니다. 입력 받은 character를 고스란히 buffer에 넣고 over여부만 확인합니다.
ClearState는 특별한 Schema나 Lemma를 요구하는 것이 아니었습니다. 단순히 Bitset을 초기화하는 과정일 뿐입니다.
TrimBuffer함수는 Char과 Event 모두 보편적으로 사용해야 하니 Template를 활용한 모습입니다. 최대 사이즈보다 buffer가 크면 pop offs해버리는 모습입니다.
Exercise
이제 이 Keyboard는 window의 Public으로 만들어 주면 됩니다. 그렇다면 이 Keyboard의 입력을 감지하는 부분을 어디에 끼워주면 좋을까요? HandleMsg함수에 끼워주면 좋겠습니다. 작은 실험까지 함께 진행해보도록 합니다.
적혀있는 그대로 입니다. wParam에서 읽은 Message를 토대로 Keyboard에 전달합니다.
그리고 Message Pump part에서 Space key를 눌렀을 때, Message Box를 생성하도록 합니다. Debug를하면 다음과 같은 화면이 나오고 space를 누르면 Message Box가 나타납니다.
잘 나오는 군요 그런데, 지금까지 짠 코드의 논리라면, 생성된 대화상자를 제거하고 난 뒤에 다시 Space를 누르지 않아도 대화상자가 또 튀어나와야 합니다. 왜냐하면 Message Pump를 보면 VK_SPACE가 눌러진 순간에 bitset인 ketstate의 VK_SPACE가 True로 바뀌고 Message Box가 생성됩니다. 이렇게 되면 생성된 Message Box로 window의 focus가 넘어갑니다. 즉, 우리가 열심히 따라 만들었던 window는 더 이상 관심을 받지 못하는 상태가 됩니다. 그 사이에 VK_SPACE를 놓게 되면 KeyRelease Message가 원래 돌아가야 했던 Window로 전해지지 못하고 Window는 여전히 VK_SPACE가 눌려있는 상태라고 생각합니다. 그냥 Key Down State에 Stuck합니다. Zombie Key Press라고 해도 좋겠습니다. 그래서, Window의 kill focus에 관한 Message를 처리해주면 이 좀비 상태를 막을 수 있습니다.
Auto Repeat
다루지 않고 넘어간 내용이 하나 있는데 그것은 바로 반복입니다. 키보드를 꾹 누르고 있는 상태를 일컫습니다. Defualt 상태로, 어떤 key를 꾹 누르고 있으면 Window는 같은 WM_KEYDOWN Message를 엄청나게 생산해내기 시작합니다. 딱히 상관은 없으나 어떤 게임에서는 이 상태가 바람직하지 않을 수 있습니다. Keydown에 대한 MSDN의 문서를 참조합니다.
자주 봅니다 wParam은 지금까지 보았던 것처럼 key code의 정보가 있었습니다. 그렇다면 lParam에는 뭐가 있었던 것일까요? 바로 반복에 대한 내용이 포함되어 있었습니다.
이렇게 각각 bit마다 뚜렷한 의미가 있고 Auto Repeat의 의미에 가장 부합하는 bit는 30입니다. 즉, bit 30의 정보를 체크하면 현재 누르고 있는 키가 이전에도 눌렸는지를 확인하는 것이 가능해질 것입니다. 당연한 말이지만, WM_CHAR에 대해 이 기능을 적용할 경우는 극히 드물 것입니다.
hw3d/hw3d at T8-End · planetchili/hw3d (github.com) 모든 것을 여기서 찾을 수 있습니다.
'Graphics' 카테고리의 다른 글
[10] - App class (0) 2021.07.13 [9] - Mouse (0) 2021.07.12 [7] - ICON (0) 2021.07.10 [6] - Error Handling (0) 2021.07.10 [5] - Window Framework (0) 2021.07.10