Window 환경에서 C++과 Console창을 이용하여 테트리스를 만들어 봅시다.
명령 프롬프트의 환경설정
테트리스에서 사용하는 특수 문자(벽돌과 같은)는 명령 프롬프트의 폰트 설정에 많은 영향을 받습니다.
그러므로 명령 프롬프트의 폰트는 "굴림"을 사용 해주세요.
테트리스의 소스 코드
현재 이 테트리스의 코드는 깃허브에 공개되어 있습니다.
테트리스 매커니즘
이 프로그램은 다음과 같은 매커니즘을 갖습니다.
- 상단에서 블럭이 하단으로 내려온다.
- 도형은 총 7개가 있다.
- 특정 키를 눌렀을 때 도형이 회전한다.(4가지 방향으로 회전)
- 바닥 또는 도형에 닿으면 다음 도형으로 넘어간다.
- 도형을 맞추어 일자가 되면 제거되고 나머지 블럭은 아래로 내려온다.
테트리스 메인 함수 설계
메인 함수에서는 게임이 종료 될 때 까지 While문
을 이용하여 게임을 진행 할 것입니다.
While문
안에는 키 입력, 플레이어(도형)의 위치 계산, 바닥에 닿았는지 확인, 그리는 기능을 넣을 것입니다.
int main(void)
{
InitGame(); // 게임 초기화 (게임 설정 및 콘솔 설정)
while (true)
{
InputKey(); // 키입력
CalcPlayer(); // 플레이어(도형)의 위치 계산
CheckBottom(); // 플레이어가 바닥 또는 도형에 닿았는지 확인
Render(3, 1); // 플레이어 및 프레임 그리기
ClearScreen(); // 화면 클리어
BufferFlip(); // 화면 버퍼 전환 (Double Buffer)
Sleep(1);
}
DestroyGame(); // 게임 제거 (메모리 해제)
return 0;
}
위와 같이 작성을 할 것입니다.
테트리스에서 사용할 콘솔 관련 기능
윈도우 콘솔에서 무언가를 출력한 후 갱신을 하기 위해서는 화면을 클리어 한 후 다시 그리는 과정을 해주어야 합니다.
그렇지 않으면 이전에 출력했던 내용이 그대로 남아있기 때문이죠.
일반적으로 콘솔에서 화면을 클리어 할 때
#include <Windows.h>
system("cls");
이런 방식을 많이 사용하게 되는데 system("cls");
는 콘솔 전체를 한 번 훑어내는 동작을 하기 때문에 시스템 소모값도 크다는 단점이 있습니다.
그 보다 더 큰 문제는 이 방법을 이용하여 화면을 클리어 할 때 Flickering 현상
이 일어납니다.
이 현상을 Flickering
이라고 하는데 이 현상을 없애기 위해서는 콘솔 화면 버퍼 2개를 이용하여 더블 버퍼링
을 구현하여 사용해야 합니다.
// Rectangle 구조체
struct stRect
{
int nWidth;
int nHeight;
};
// 콘솔 관련 설정 값을 가지고 있을 구조체
struct stConsole
{
// Console Handler
HANDLE hConsole;
// Console Rect Data
stRect rtConsole;
// Console Buffer Handler
HANDLE hBuffer[2];
// Current Console Buffer Index
int nCurBuffer;
stConsole()
: hConsole(nullptr), hBuffer{ nullptr, }, nCurBuffer(0)
{}
};
// 전역 변수
stConsole console;
먼저 위와 같이 구조체를 하나 만들고 InitGame
이라는 함수를 하나 만들어서 해당 함수에서 콘솔 관련 설정을 하도록 합니다.
void InitGame(bool bInitConsole = true)
{
// Initialize Console Data
if (bInitConsole)
{
// 현재 콘솔의 핸들을 받아옵니다.
console.hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 현재 콘솔 버퍼의 인덱스를 0으로 초기화
console.nCurBuffer = 0;
// 콘솔 관련 설정
CONSOLE_CURSOR_INFO consoleCursor{ 1, FALSE }; // 콘솔의 커서 깜빡임을 제거합니다.
CONSOLE_SCREEN_BUFFER_INFO consoleInfo{ 0, };
GetConsoleScreenBufferInfo(console.hConsole, &consoleInfo);
consoleInfo.dwSize.X = 40; // 콘솔의 Width
consoleInfo.dwSize.Y = 30; // 콘솔의 Height
// 콘솔의 크기를 다시 계산 (나중에 그림 그릴때 사용)
console.rtConsole.nWidth = consoleInfo.srWindow.Right - consoleInfo.srWindow.Left;
console.rtConsole.nHeight = consoleInfo.srWindow.Bottom - consoleInfo.srWindow.Top;
// 콘솔의 첫번째 화면 버퍼 생성
console.hBuffer[0] = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleScreenBufferSize(console.hBuffer[0], consoleInfo.dwSize); // 화면 버퍼 크기 설정
SetConsoleWindowInfo(console.hBuffer[0], TRUE, &consoleInfo.srWindow); // 콘솔 설정
SetConsoleCursorInfo(console.hBuffer[0], &consoleCursor); // 콘솔의 커서 설정
// 콘솔의 두번째 화면 버퍼 생성
console.hBuffer[1] = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleScreenBufferSize(console.hBuffer[1], consoleInfo.dwSize);
SetConsoleWindowInfo(console.hBuffer[1], TRUE, &consoleInfo.srWindow);
SetConsoleCursorInfo(console.hBuffer[1], &consoleCursor);
}
}
이렇게 하여 콘솔 설정 및 화면 버퍼를 만들었습니다.
그리고 이 콘솔 버퍼를 해제하는 함수도 만들어야합니다.
void DestroyGame()
{
if (console.hBuffer[0] != nullptr)
{
CloseHandle(console.hBuffer[0]);
}
if (console.hBuffer[1] != nullptr)
{
CloseHandle(console.hBuffer[1]);
}
}
콘솔 화면 버퍼는 CloseHandle
을 이용하여 메모리를 해제해주어야 합니다.
이제 이것을 이용하여 화면을 Clear하는 함수를 만들어 봅시다.
// 화면 클리어
void ClearScreen()
{
COORD pos{ 0, };
DWORD dwWritten = 0;
unsigned size = console.rtConsole.nWidth * console.rtConsole.nHeight;
// 콘솔 화면 전체를 띄어쓰기를 넣어 빈 화면처럼 만듭니다.
FillConsoleOutputCharacter(console.hConsole, ' ', size, pos, &dwWritten);
SetConsoleCursorPosition(console.hConsole, pos);
}
// 버퍼 스왑
void BufferFlip()
{
// 화면 버퍼 설정
SetConsoleActiveScreenBuffer(console.hBuffer[console.nCurBuffer]);
// 화면 버퍼 인덱스를 교체
console.nCurBuffer = console.nCurBuffer ? 0 : 1;
}
매커니즘은 이렇습니다.
먼저 ClearScreen
을 이용하여 화면을 깨끗하게 만든 뒤 화면 버퍼를 스왑하여 부드럽게 화면이 전환되도록 합니다.
이제 메인 함수
를 작성해봅시다.
int main(void)
{
InitGame();
char chBuf[256] = { 0, };
COORD coord{ 0,0 };
DWORD dw = 0;
while (true)
{
// Flickering Test 출력
memset(chBuf, 0, sizeof(chBuf));
int nLen = sprintf_s(chBuf, sizeof(chBuf), "Flickering Test");
SetConsoleCursorPosition(console.hBuffer[console.nCurBuffer], coord);
WriteFile(console.hBuffer[console.nCurBuffer], chBuf, nLen, &dw, NULL);
ClearScreen();
BufferFlip();
Sleep(1);
}
DestroyGame();
return 0;
}
이렇게 한 후 실행하여 봅시다.
이렇게 Flickering 현상
이 일어나지 않고 계속 갱신되는 것을 볼 수 있습니다.
이 방식을 사용하면 Flickering 현상
은 없어지나 기존 방식으로는 콘솔 화면에 출력을 할 수 없고
// 출력할 위치를 콘솔 화면 버퍼에 지정
SetConsoleCursorPosition(console.hBuffer[console.nCurBuffer], coord);
// 콘솔 화면 버퍼에 출력
WriteFile(console.hBuffer[console.nCurBuffer], chBuf, nLen, &dw, NULL);
이와 같은 방식으로 출력을 해야하는 번거로움은 있습니다.
다음 시간에는 키보드 입력 및 도형 출력에 관한 글로 뵙겠습니다.
'Study > C++' 카테고리의 다른 글
[C++/Console] 테트리스 만들어 보기 - 3 (화면 출력에 대한 고찰) (0) | 2023.02.02 |
---|---|
[C++/Console] 테트리스 만들어 보기 - 2 (키보드 입력 및 블럭) (0) | 2023.01.29 |
[C++] ImGui - C++용 GUI Library (0) | 2023.01.07 |
[C++] 자료구조 Queue(큐) 만들어 보기 (0) | 2022.09.22 |
[C++11] 스마트 포인터3 (weak_ptr) (0) | 2022.09.21 |