지난 시간에는 기본적인 소개와 화면 갱신에 관련하여 알아보았습니다.
오늘은 키보드 입력과 블럭에 대해서 알아보도록 하겠습니다.
윈도우 기반 콘솔에서 C/C++로 키보드 입력 받기
이 방식은 Windows 운영체제의 명령 프롬프트에서 C/C++로 키보드를 입력받는 방식입니다.
Linux 운영체제의 Terminal에서는 사용할 수 없습니다.
여기서 사용하는 conio.h에 정의된 함수들은 비표준 함수입니다.
그러므로 _kbhit 함수와 _getch 함수도 비표준 함수입니다.
다른 프로젝트에서는 가급적 사용하지 않는 것이 좋습니다.
(윈도우 기반의 콘솔 프로젝트가 아닌 이상 사용하지 않는 것이 좋습니다.)
#include <conio.h>
// 키보드가 눌렸을 때 반응하는 함수
int _kbhit(void);
// 키보드로 입력된 문자를 반환하는 함수
int _getch(void);
우리는 콘솔 입출력 함수에서 위의 두 가지 함수를 사용할 것입니다.
(위의 두 함수는 CRT(C Runtime Library) 입니다.)
void InputKey()
{
int nKey = 0;
if (_kbhit() > 0)
{
nKey = _getch(); // <-- 여기에서 중단점 설정!!
}
}
일단 키를 입력받은 함수를 위와 같이 만들고
int main(void)
{
InitGame();
while (true)
{
InputKey(); // 키 입력 확인
ClearScreen();
BufferFlip();
Sleep(1);
}
DestroyGame();
return 0;
}
이렇게 어제 만든 메인 함수에 넣은 후 디버그 모드
로 실행해봅시다.
실행 후 키보드 입력을 하게 되면 입력한 키의 키값(Key Code)을 알 수 있게 되는데
키보드 입력과 동시에 우리가 설정한 중단점에서 멈추게 됩니다.
여기서 다음 줄로 이동(F11)을 하게 되면 우리가 입력한 키의 키값이 nKey
에 저장되는데
값을 확인해보면 입력된 값은 97이라는 것을 알 수 있습니다.
(저는 a를 입력하였습니다.)
이번에는 다른 키를 입력해보겠습니다.
65가 나왔습니다.
(저는 A를 입력하였습니다.)
여기서 알 수 있는 것은 _getch 함수
로 반환 되는 값은 키보드 입력 값의 ASCII 코드
라는 것입니다.
그럼 방향키의 키값을 알아봅시다.
왼쪽 방향키를 누르니 총 2번에 걸쳐서 값이 들어왔습니다.
(224, 75)
오른쪽 방향키도 똑같이 총 2번에 걸쳐서 값이 들어왔습니다.
(224, 77)
그 이유는 MSDN에서도 나와있는데
한글 번역이 조금 이상한데
원문은 이렇습니다.
Function Key나 방향키를 읽을 때는 두 번에 걸쳐서 값이 오는데 첫 번째 값은 0이나 0xE0(224), 그리고 나서 실질적인 키값이 오게 됩니다.
이렇게 해서 알아낸 키값을 토대로 열거형(enum)
을 만듭니다.
// Key Code
enum eKeyCode
{
KEY_UP = 72, // 방향키 ↑
KEY_DOWN = 80, // 방향키 ↓
KEY_LEFT = 75, // 방향키 ←
KEY_RIGHT = 77, // 방향키 →
KEY_SPACE = 32, // 스페이스바
KEY_R = 114, // R키
};
저는 전역으로 선언하였습니다.
이제 이 키값을 기반으로 InputKey 함수
를 수정해봅시다.
void InputKey()
{
int nKey = 0;
// 키입력이 감지되었을 때
if (_kbhit() > 0)
{
// 입력된 키를 받아온다.
nKey = _getch();
switch (nKey)
{
case eKeyCode::KEY_UP: // 방향키 위를 눌렀을 때
{
break;
}
case eKeyCode::KEY_DOWN: // 방향키 아래를 눌렀을 때
{
break;
}
case eKeyCode::KEY_LEFT: // 방향키 왼쪽을 눌렀을 때
{
break;
}
case eKeyCode::KEY_RIGHT: // 방향키 오른쪽을 눌렀을 때
{
break;
}
case eKeyCode::KEY_SPACE: // 스페이스바를 눌렀을 때
{
break;
}
case eKeyCode::KEY_R: // R키를 눌렀을 때
{
break;
}
}
}
}
해당 키를 눌렀을 때의 동작은 추후에 계속 입력하도록 하겠습니다.
테트리스의 블럭
테트리스에는 공식적(The Tetris Company)으로 총 7가지 타입의 블럭이 존재합니다.
왼쪽 위부터 I, J, L, O, S, T, Z로 표현합니다.
이 블럭은 총 4번 회전을 하게 됩니다.
이 블럭들을 각각 일차원 배열로 만들어서 선언 후 하나로 뭉쳐서 이차원 배열로 관리할 것입니다.
// Block Data
// - BLOCK_WIDTH = 4
// - BLOCK_HEIGHT = 4
const int BLOCKS[][BLOCK_WIDTH * BLOCK_HEIGHT] =
{
{ 0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0 }, // I
{ 0,0,0,0,0,0,2,0,0,0,2,0,0,2,2,0 }, // J
{ 0,0,0,0,0,2,0,0,0,2,0,0,0,2,2,0 }, // L
{ 0,0,0,0,0,2,2,0,0,2,2,0,0,0,0,0 }, // O
{ 0,0,0,0,0,2,0,0,0,2,2,0,0,0,2,0 }, // S
{ 0,0,0,0,0,2,0,0,2,2,2,0,0,0,0,0 }, // T
{ 0,0,0,0,0,0,2,0,0,2,2,0,0,2,0,0 }, // Z
};
이렇게 각 도형들이 일자로 선언되어 있어 모양이 잘 보이지 않지만 4개씩 끊어서 보면
{ 0,0,0,0,
0,2,0,0,
0,2,2,0,
0,0,2,0 }, // S
이렇게 도형으로 보이죠.
실제로 화면에 그릴 때 4개씩 끊어서 그릴 것입니다.
테트리스 블럭의 회전
위에서 우리는 블럭을 배열에 선언하였습니다.
그런데 테트리스의 블럭은 4번 회전한다고 했습니다.
하지만 각각의 회전한 모양을 따로 선언을 하지 않았습니다.
왜냐하면 우리는 특정 알고리즘을 이용하여 저 일차원 배열을 실시간으로 회전시킬 것이기 때문이죠.
기존의 도형(왼쪽)이 90도를 회전하게 되면 오른쪽과 같아집니다.
우리가 선언한 도형은 1차원 배열이기때문에 1차원적으로 보게 되면
이렇게 됩니다.
이것을 코드로 구현해보면
// 회전횟수 = 1 -> 90도 회전
for (int nRot = 0; nRot < 회전횟수; ++nRot)
{
// 회전한 블럭을 저장할 임시 배열
int nTemps[BLOCK_HEIGHT * BLOCK_WIDTH] = { 0, };
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
// 현재 블럭의 회전한 모양을 임시 배열에 저장
nTemps[(nX * BLOCK_WIDTH) + (BLOCK_HEIGHT - nY - 1)] = g_pCurBlock[(nY * BLOCK_HEIGHT) + nX];
}
}
// 회전된 블럭을 현재 배열로 복사
memcpy_s(g_pCurBlock, nMemSize, nTemps, nMemSize);
}
위와 같습니다.
실제로 변경되는지 디버깅으로 확인해보겠습니다.
(아직 도형 그리는 것을 하지 않았기 때문이죠.)
int main(void)
{
InitGame();
// 현재 블럭을 설정하기 위한 구문들
int nCurBlock[BLOCK_WIDTH * BLOCK_HEIGHT] = { 0, };
int nMemSize = sizeof(int) * BLOCK_HEIGHT * BLOCK_WIDTH;
memcpy_s(nCurBlock, nMemSize, BLOCKS[1], nMemSize); // 1번 블럭은 J블럭
printf(""); // <-- 여기에 중단점 설정!!
// 90도 회전
for (int nRot = 0; nRot < 1; ++nRot)
{
int nTemps[BLOCK_HEIGHT * BLOCK_WIDTH] = { 0, };
for (int nY = 0; nY < BLOCK_HEIGHT; ++nY)
{
for (int nX = 0; nX < BLOCK_WIDTH; ++nX)
{
nTemps[(nX * BLOCK_WIDTH) + (BLOCK_HEIGHT - nY - 1)] = nCurBlock[(nY * BLOCK_HEIGHT) + nX];
}
}
memcpy_s(nCurBlock, nMemSize, nTemps, nMemSize);
}
printf(""); // <-- 여기에 중단점 설정!!
while (true)
{
InputKey();
ClearScreen();
BufferFlip();
Sleep(1);
}
DestroyGame();
return 0;
}
위의 코드에 총 2군데에 중단점이 찍혀있습니다.
회전하기 전에 중단점이 있고 회전한 후에 중단점이 있습니다.
코드를 실행해보겠습니다.
일단 회전하기 전에는 배열은 위와 같습니다.
이것을 그림으로 그려보면
현재는 이런 모양입니다.
이 도형을 90도 회전을 시키게 되면
배열이 이렇게 변하였습니다.
이것을 그림으로 그려보면
이렇게 도형이 90도 회전한 것을 확인할 수 있습니다.
다음 시간에는 테트리스의 도형과 프레임을 어떻게 그릴것인가에 대해서 이야기해보겠습니다.
'Study > C++' 카테고리의 다른 글
[C++/Console] 테트리스 만들어 보기 - 4 (플레이어(블럭) 움직임) (0) | 2023.02.09 |
---|---|
[C++/Console] 테트리스 만들어 보기 - 3 (화면 출력에 대한 고찰) (0) | 2023.02.02 |
[C++/Console] 테트리스 만들어 보기 - 1 (간단 소개 및 더블 버퍼링) (1) | 2023.01.28 |
[C++] ImGui - C++용 GUI Library (0) | 2023.01.07 |
[C++] 자료구조 Queue(큐) 만들어 보기 (0) | 2022.09.22 |