소프트웨어 렌더러 만들기 - 15 (키보드 입력)
https://blog.juhwijung.com/17로도 들어올 수 있습니다.
여기까지의 작업내용: https://github.com/hwi-middle/HimchanSoftwareRenderer/tree/5f60ce091d4a69b2071bad751aeb2ca73636d833
키보드 입력을 위한 함수
일단 전공 수업이었던 <윈도우 프로그래밍=""> 과목에서 배운 바에 따르면 키보드 입력은 WndProc에서 WM\_KEYDOWN, WM\_KEYUP 같은 메시지를 통해서 처리할 수 있다고 했다.윈도우>
그러나 내가 만들고 있는 프로그램은 WndProc에서 모든 메시지를 처리하는 구조를 가지고 있지 않다. 그래서 WndProc이 아닌 곳에서 키보드 입력을 검출할 수는 없는지 알아보았다.
그렇게 아래 2개의 함수를 알게 되었다.
- GetKeyState: 메시지 큐를 통해 해당 키의 상태를 조사
- GetAsyncKeyState: 메시지 큐와 상관없이 즉시 해당 키의 상태를 조사
나는 후자를 골라 구현에 적용했다.
HCEngine 모듈 추가

새로운 모듈(=프로젝트) HCEngine을 추가했다. 키보드 입력은 기존 모듈인 HCCore, HCGraphics, HCMath 어디에도 속하지 않기 때문이었다.
하여, HCEngine 모듈을 추가했다. 앞으로 구현될 리소스 관리 같은 영역도 여기서 담당하게 될 것 같다.
키보드 입력 구현을 위한 준비
enum class EKeyCode
{
NONE = 0,
A = 0x41,
B = 0x42,
C = 0x43,
// ...
Y = 0x59,
Z = 0x5A,
F1 = VK_F1,
// ...
F12 = VK_F12,
ESC = VK_ESCAPE,
ALPHA_0 = 0x30,
ALPHA_1 = 0x31,
// ...
ALPHA_9 = 0x39,
NUM_0 = VK_NUMPAD0,
// ...
NUM_9 = VK_NUMPAD9,
SPACE = VK_SPACE,
BACKSPACE = VK_BACK,
TAB = VK_TAB,
ENTER = VK_RETURN,
LEFT_ARROW = VK_LEFT,
RIGHT_ARROW = VK_RIGHT,
UP_ARROW = VK_UP,
DOWN_ARROW = VK_DOWN,
};
먼저 각 주요(?) 키들을 키 코드로 매핑해준다. 특히 알파벳 키는 16진수고 특수 키들은 VK_로 시작하는 가상 키 코드로 매핑되어있는데, 이 부분을 내가 다시 EKeyCode라는 키 코드로 정의하여 구현할 때 이러한 정보를 몰라도 되도록 했다.
enum class EAxis
{
NONE,
VERTICAL,
HORIZONTAL,
AXIS_1,
AXIS_2,
AXIS_3,
AXIS_4,
};
그리고 Axis는 일단 VERTICAL, HORIZONTAL 정도로 만들어두었다. AXIS_1, 2, 3, 4는 혹시나 쓸 일이 생기면 다시 Rename해서 쓸 생각이다.
struct AxisData
{
EKeyCode Positive;
EKeyCode Negative;
EKeyCode AltPositive;
EKeyCode AltNegative;
AxisData(EKeyCode InPositive, EKeyCode InNegative)
: Positive(InPositive), Negative(InNegative), AltPositive(EKeyCode::NONE), AltNegative(EKeyCode::NONE) {}
AxisData(EKeyCode InPositive, EKeyCode InNegative, EKeyCode InAltPositive, EKeyCode InAltNegative)
: Positive(InPositive), Negative(InNegative), AltPositive(InAltPositive), AltNegative(InAltNegative) {}
};
각 Axis는 AxisData를 가지는데, AxisData는 Positive, Negative, AltPositive, AltNegative로 구성된다. 이건 유니티의 (옛날)입력 시스템에서 따왔다.
Input 클래스
class Input
{
public:
bool GetKeyDown(EKeyCode InKey);
bool GetKey(EKeyCode InKey);
bool GetKeyUp(EKeyCode InKey);
float GetAxis(EAxis InAxis);
void Update();
private:
std::unordered_set<EKeyCode> CurrentlyPressedKey;
std::unordered_set<EKeyCode> PreviouslyPressedKey;
static const std::unordered_map<EAxis, AxisData> AxisMap;
bool GetKeyWasDowned(EKeyCode InKey);
};
Input 클래스의 선언은 이렇다. public으로 선언된 함수를 보면 알겠지만 이 부분도 유니티의 함수 이름을 따왔다. 내부적으로는 unordered_set과 unordered_map을 쓰는데, 전자는 현재/이전 프레임에서 눌린 키를 저장하기 위함이고 후자는 Axis과 AxisData를 매핑하기 위함이다.
constexpr int PRESS = 0x8000;
const std::unordered_map<EAxis, AxisData> Input::AxisMap =
{
{
EAxis::HORIZONTAL,
{
EKeyCode::RIGHT_ARROW,
EKeyCode::LEFT_ARROW,
EKeyCode::D,
EKeyCode::A
}
},
{
EAxis::VERTICAL,
{
EKeyCode::UP_ARROW,
EKeyCode::DOWN_ARROW,
EKeyCode::W,
EKeyCode::S
}
}
};
bool Input::GetKeyDown(EKeyCode InKey)
{
int CurrentState = GetAsyncKeyState(static_cast<int>(InKey));
bool bIsDown = (CurrentState & PRESS) != 0;
bool bWasDown = GetKeyWasDowned(InKey);
if (bIsDown)
{
CurrentlyPressedKey.insert(InKey);
}
return bIsDown && !bWasDown;
}
bool Input::GetKey(EKeyCode InKey)
{
int CurrentState = GetAsyncKeyState(static_cast<int>(InKey));
bool bIsDown = (CurrentState & PRESS) != 0;
if (bIsDown)
{
CurrentlyPressedKey.insert(InKey);
}
return bIsDown;
}
bool Input::GetKeyUp(EKeyCode InKey)
{
int CurrentState = GetAsyncKeyState(static_cast<int>(InKey));
bool bIsUp = (CurrentState & PRESS) == 0;
bool bWasDown = GetKeyWasDowned(InKey);
return bIsUp && bWasDown;
}
float Input::GetAxis(EAxis InAxis)
{
const AxisData& AxisData = AxisMap.at(InAxis);
bool bPositive = GetKey(AxisData.Positive) || GetKey(AxisData.AltPositive);
bool bNegative = GetKey(AxisData.Negative) || GetKey(AxisData.AltNegative);
if (bPositive && bNegative)
{
return 0.f;
}
else if (bPositive)
{
return 1.f;
}
else if (bNegative)
{
return -1.f;
}
return 0.f;
}
void Input::Update()
{
PreviouslyPressedKey = CurrentlyPressedKey;
CurrentlyPressedKey.clear();
}
bool Input::GetKeyWasDowned(EKeyCode InKey)
{
return PreviouslyPressedKey.find(InKey) != PreviouslyPressedKey.end();
}
구현은 이렇다. 일단 AxisMap은 HORIZONTAL에 좌우 방향키와 A, D를 매핑했고 VERTICAL에 상하 방향키와 W, S를 매핑했다. 각 함수에 대한 간략한 설명은 리스트 형식으로 덧붙이겠다.
- GetKeyDown: 키가 눌리기 시작했는지 체크한다. 지금 눌렸고, 이전 프레임에서는 안눌렸다면 키가 ‘눌리기 시작한 것’이다.
- GetKey: 키가 눌리고 있는지 체크한다.
- GetKeyUp: 키가 떼어지기 시작했는지 체크한다. 지금 눌리지 않았고, 이전프레임에서는 눌렸다면 키가 ‘떼어지기 시작한 것’이다.
- GetAxis: (Alt)Positive와 (Alt)Negative에 매핑된 키가 눌려있는 지에 따라 Axis 값을 반환한다.
- Update: 현재 눌린 키 목록을 이전 프레임에 눌린 키 목록으로 저장한다. 현재 프레임에서는 키 입력 처리를 마무리하겠다는 의미이므로 PostUpdate 시점에 호출해야한다.
- GetKeyWasDowned: 이전 프레임에서 해당 키가 눌렸는지 조사한다. private 함수이다.
구현 결과
실행영상

키보드 입력에 따라 정점을 이동시켜서 렌더링해주는 방식으로 테스트해보았다. 잘 작동된다!
이제 키 입력 처리도 마무리되었으니 지난 번에 말했던대로 일단 3차원 공간으로 확장해나갈 차례다. 다만 시기상 2학기 졸업작품 작업을 시작할 때가 되어서 시간이 좀 걸릴 것 같기는 하다. 그래도 남는 시간을 활용해서 올해 내로 3D 렌더러를 완성하는 것이 내 목표다.
해낼 수 있다!