📚 입력 시스템 시리즈
🚀 들어가며
게임을 만들다 보면 가장 먼저 필요한 기능 중 하나가 입력 처리입니다.
스페이스바를 누르면 점프한다. 방향키를 누르면 캐릭터가 이동한다. ESC를 누르면 게임을 종료한다.
겉으로 보면 단순한 기능처럼 보이지만, 엔진 구조 관점에서 입력 처리는 단순히 “키가 눌렸는지 확인하는 코드”가 아닙니다.
입력은 사용자의 행동을 게임 내부 시스템으로 전달하는 출발점입니다.
이번 글에서는 Windows 환경에서 사용할 수 있는 GetAsyncKeyState를 이용해 가장 기본적인 키 입력 처리 구조를 살펴보겠습니다.
🎯 키 입력은 어디에서 처리해야 할까?
가장 단순하게 생각하면 이렇게 작성할 수 있습니다.
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
Jump();
}
스페이스바가 눌려 있으면 Jump()를 호출하는 구조입니다.
처음에는 이 정도만으로 충분해 보입니다. 하지만 게임이 조금만 커져도 입력 코드가 여기저기 흩어지기 시작합니다.
if (GetAsyncKeyState(VK_LEFT) & 0x8000)
{
MoveLeft();
}
if (GetAsyncKeyState(VK_RIGHT) & 0x8000)
{
MoveRight();
}
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
Jump();
}
if (GetAsyncKeyState(VK_ESCAPE) & 0x8000)
{
QuitGame();
}
이렇게 되면 나중에 키 설정을 바꾸거나, 입력 상태를 저장하거나, 특정 객체에만 입력을 전달해야 할 때 구조가 금방 복잡해집니다.
그래서 게임 엔진에서는 보통 입력을 한 곳에서 모아 관리합니다.
🧠 GetAsyncKeyState란?
GetAsyncKeyState는 Windows API에서 제공하는 키 상태 확인 함수입니다.
SHORT GetAsyncKeyState(int vKey);
특정 키가 현재 눌려 있는지 확인할 수 있습니다.
예를 들어 스페이스바 상태는 이렇게 확인합니다.
#include <Windows.h>
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
// 스페이스바가 눌려 있음
}
여기서 VK_SPACE는 스페이스바를 의미하는 가상 키 코드입니다.
GetAsyncKeyState(VK_LEFT); GetAsyncKeyState(VK_RIGHT); GetAsyncKeyState(VK_ESCAPE);
🔍 반환값에서 0x8000을 확인하는 이유
GetAsyncKeyState는 단순히 true 또는 false를 반환하지 않습니다.
반환 타입은 SHORT입니다.
현재 키가 눌려 있는지 확인할 때는 최상위 비트를 검사합니다.
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
// 현재 스페이스바가 눌려 있음
}
0x8000은 키가 현재 눌려 있는 상태인지 확인하기 위해 사용합니다.
따라서 단순히 아래처럼 쓰기보다는,
if (GetAsyncKeyState(VK_SPACE))
{
}
의도가 명확하게 드러나도록 이렇게 쓰는 편이 좋습니다.
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
}
🎮 가장 단순한 입력 처리 예제
#include <Windows.h>
#include <iostream>
void ProcessInput()
{
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
std::cout << "Space Pressed\n";
}
if (GetAsyncKeyState(VK_ESCAPE) & 0x8000)
{
std::cout << "Escape Pressed\n";
}
}
게임 루프에서는 매 프레임 이 함수를 호출합니다.
while (true)
{
ProcessInput();
Tick();
Draw();
}
흐름은 다음과 같습니다.
게임 루프 반복 ↓ 현재 키 상태 확인 ↓ 입력에 따른 처리 ↓ 게임 상태 업데이트 ↓ 화면 출력
🎯 Polling 방식이란?
지금처럼 매 프레임 키 상태를 직접 확인하는 방식을 Polling이라고 합니다.
매 프레임 ↓ 키 상태를 직접 확인 ↓ 눌렸으면 처리
즉, 이벤트가 자동으로 날아오는 것이 아니라 게임 루프가 직접 물어보는 방식입니다.
지금 스페이스바 눌렸어? 지금 왼쪽 키 눌렸어? 지금 ESC 눌렸어?
✅ Polling 방식의 장점
Polling 방식은 교육용 게임 엔진에서 매우 좋습니다.
- 구조가 단순하다
- 게임 루프와 연결하기 쉽다
- 디버깅이 쉽다
코드만 봐도 무슨 일이 일어나는지 바로 알 수 있습니다.
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
Jump();
}
⚠️ Polling 방식의 한계
하지만 Polling 방식만 그대로 사용하면 문제가 있습니다.
if (GetAsyncKeyState(VK_SPACE) & 0x8000)
{
Jump();
}
이 코드는 스페이스바를 누르고 있는 동안 매 프레임 Jump()를 호출합니다.
스페이스바 누름 ↓ 1프레임: Jump() 2프레임: Jump() 3프레임: Jump() 4프레임: Jump()
한 번만 점프하고 싶은데 계속 점프할 수 있습니다.
이 문제를 해결하려면 “키가 눌려 있는 상태”와 “이번 프레임에 막 눌린 상태”를 구분해야 합니다.
🧩 Pressed, Held, Released 개념
게임 입력에서는 보통 키 상태를 세 가지로 나눕니다.
Pressed : 이번 프레임에 막 눌림 Held : 계속 누르고 있음 Released : 이번 프레임에 막 뗌
예를 들어 스페이스바 점프는 보통 Pressed에서 처리합니다.
스페이스바를 누르는 순간 → 점프 스페이스바를 계속 누르고 있음 → 추가 점프 없음
반면 이동은 Held에서 처리하는 경우가 많습니다.
왼쪽 키를 누르고 있는 동안 → 계속 왼쪽 이동
🧠 현재 상태와 이전 상태 비교하기
핵심 아이디어는 간단합니다.
현재 프레임 키 상태 이전 프레임 키 상태 ↓ 둘을 비교해서 Pressed / Held / Released 판단
예를 들어 현재는 눌려 있고, 이전에는 눌려 있지 않았다면 Pressed입니다.
이전 프레임: 안 눌림 현재 프레임: 눌림 결과: Pressed
현재도 눌려 있고, 이전에도 눌려 있었다면 Held입니다.
이전 프레임: 눌림 현재 프레임: 눌림 결과: Held
이전에는 눌려 있었고, 현재는 눌려 있지 않다면 Released입니다.
이전 프레임: 눌림 현재 프레임: 안 눌림 결과: Released
🔧 간단한 키 상태 저장 구조
struct KeyState
{
bool current = false;
bool previous = false;
};
현재 상태와 이전 상태를 저장합니다.
KeyState spaceKey;
매 프레임 현재 키 상태를 갱신합니다.
void UpdateKeyState(KeyState& keyState, int keyCode)
{
keyState.previous = keyState.current;
keyState.current = (GetAsyncKeyState(keyCode) & 0x8000) != 0;
}
이제 상태 판별 함수를 만들 수 있습니다.
bool IsPressed(const KeyState& keyState)
{
return keyState.current && !keyState.previous;
}
bool IsHeld(const KeyState& keyState)
{
return keyState.current && keyState.previous;
}
bool IsReleased(const KeyState& keyState)
{
return !keyState.current && keyState.previous;
}
사용 예시는 다음과 같습니다.
void ProcessInput()
{
UpdateKeyState(spaceKey, VK_SPACE);
if (IsPressed(spaceKey))
{
std::cout << "Jump\n";
}
if (IsHeld(spaceKey))
{
std::cout << "Holding Space\n";
}
if (IsReleased(spaceKey))
{
std::cout << "Space Released\n";
}
}
이제 스페이스바를 누르는 순간, 누르고 있는 동안, 떼는 순간을 구분할 수 있습니다.
🎮 입력 시스템으로 확장하기
키가 하나만 있다면 위 구조로 충분합니다.
하지만 실제 게임에서는 여러 키를 다룹니다.
왼쪽 오른쪽 위 아래 스페이스 ESC 공격 상호작용
이런 키를 각각 따로 관리하면 코드가 길어집니다.
그래서 입력 시스템을 따로 만들 수 있습니다.
class Input
{
public:
void Update()
{
UpdateKeyState(space, VK_SPACE);
UpdateKeyState(escape, VK_ESCAPE);
}
bool IsSpacePressed() const
{
return IsPressed(space);
}
bool IsEscapePressed() const
{
return IsPressed(escape);
}
private:
KeyState space;
KeyState escape;
};
사용하는 쪽에서는 이렇게 됩니다.
Input input;
while (true)
{
input.Update();
if (input.IsSpacePressed())
{
std::cout << "Jump\n";
}
if (input.IsEscapePressed())
{
break;
}
Tick();
Draw();
}
이제 입력 확인 코드가 한 곳으로 모였습니다.
🔥 Polling에서 Event로 확장하기
Polling 방식은 매 프레임 키 상태를 직접 확인합니다.
하지만 입력 시스템이 어느 정도 정리되면 다음 단계로 확장할 수 있습니다.
키 상태 확인 ↓ Pressed 감지 ↓ 등록된 함수 호출
즉, 입력 시스템 내부는 Polling으로 동작하지만, 외부에는 Event처럼 보이게 만들 수 있습니다.
내부 구현: Polling 외부 사용: Event
예를 들어 이런 구조입니다.
if (input.IsSpacePressed())
{
OnJump.Broadcast();
}
여기서 OnJump는 앞서 다룬 델리게이트 시스템으로 연결할 수 있습니다.
GetAsyncKeyState ↓ Input System ↓ Pressed / Released 판별 ↓ Delegate Broadcast ↓ Actor의 함수 호출
이 흐름이 바로 엔진식 입력 처리 구조입니다.
🎯 핵심 정리
GetAsyncKeyState로 키 상태를 확인할 수 있다- Polling 방식은 매 프레임 입력을 검사하는 구조이다
- 현재 상태와 이전 상태를 비교하면 Pressed / Held / Released를 구분할 수 있다
- 입력 상태를 한 곳에서 관리하면 엔진 구조로 확장하기 쉽다
- Polling 기반 입력도 외부에는 이벤트 시스템처럼 제공할 수 있다
🎯 다음 글 예고
다음 글에서는 이번 구조를 조금 더 발전시켜서, 여러 키를 관리하는 입력 시스템을 만들어보겠습니다.
단순히 VK_SPACE만 확인하는 것이 아니라, 키 상태를 배열 또는 맵으로 관리하고, 게임 로직에서 더 편하게 입력을 조회할 수 있는 구조로 확장해보겠습니다.
🎮 마무리
입력 처리는 단순한 키 확인 코드처럼 보이지만, 게임 엔진에서는 매우 중요한 시스템입니다.
입력은 사용자 행동이 게임 세계로 들어오는 입구입니다.
따라서 입력을 어떻게 읽고, 저장하고, 전달할 것인지는 게임 구조 전체에 영향을 줍니다.
이 구조를 이해하면 게임 엔진의 입력 시스템과 이벤트 흐름이 어떻게 연결되는지 보이기 시작합니다.
🚀 한 단계 더 나아가고 싶다면
입력 시스템은 게임 루프, 델리게이트, Actor 구조와 자연스럽게 연결됩니다.
이 구조를 직접 구현해보면 게임 엔진이 어떻게 사용자 입력을 게임 객체의 동작으로 연결하는지 이해할 수 있습니다.
단순히 사용하는 것을 넘어서,
엔진이 어떻게 동작하는지 이해하는 데 초점을 맞춘 내용입니다.