지난 시간에는 바닥 충돌, 블럭 랜덤 생성에 대해서 알아보았습니다.
오늘은 시간이 지남에 따라 자동으로 블럭이 하강하고, 스페이스바를 눌렀을 때 블럭이 회전되며, 하나의 라인을 형성하면 해당 라인을 클리어 후 점수가 올라가는 점수 시스템에 대해서 알아보겠습니다.
자동 하강
테트리스는 지정된 시간이 지나면 블럭이 아래로 하강하는 시스템을 가지고 있습니다.
이것을 구현하기 위해서는 clock 함수
를 사용하여야 합니다.
#include <ctime> // 또는 #include <time.h>
clock_t clock(void); // 현재 시간을 반환
// clock_t는 실제로 long형
하지만 clock 함수
는 실제로는 엄청 정밀하지는 않습니다.
만약 본인이 다른 프로젝트에서 정말 정밀하게 시간을 제어해야 한다면 QueryPerformanceCounter 함수
를 사용하셔야 합니다.
(이 테트리스 프로젝트에서는 엄청 정밀하게 하지 않아도 되어 clock 함수
를 사용합니다.)
이 clock 함수
를 이용하여 마지막으로 방향키가 움직였을 때를 기준으로 1000ms를 카운트하여 아래로 한 칸씩 내려오도록 할 것입니다.
// Console Structure
struct stConsole
{
// 생략 ..
// Clock
clock_t timeStart; // 마지막 입력지점의 시간
stConsole()
: hConsole(nullptr), hBuffer{ nullptr, }, nCurBuffer(0)
, rdGen(rdDevice()), rdBlockDist(0, 6), rdDirDist(CPlayer::eDirection::Dir0, CPlayer::eDirection::Dir270)
, timeStart(clock())
{}
};
먼저 stConsole 구조체
에 timeStart 변수
를 하나 선언합니다.
원래 clock_t
는 ctime
혹은 time.h
헤더를 포함해야 하나 <random>
을 포함하면 같이 포함됩니다.
void InputKey()
{
int nKey = 0;
if (_kbhit() > 0)
{
nKey = _getch();
switch (nKey)
{
// 생략 ...
case eKeyCode::KEY_DOWN:
{
if (IsMoveAvailable(0, 1))
{
g_player.AddPosition(0, 1);
g_console.timeStart = clock(); // <-- 키 입력 기준 시간 저장
}
break;
}
// 생략 ...
}
}
}
이제 InputKey 함수
의 KEY_DOWN
케이스에서 아래쪽으로 움직일 수 있을 때 시간을 저장하도록 합니다.
void CheckBottom()
{
// 마지막 입력 시간에서 현재 시간을 뺌 = 진행된 시간(ms)
clock_t ctTimeDiff = clock() - g_console.timeStart;
// 1000ms가 지나지 않았다면 바로 return
if (ctTimeDiff < 1000)
return;
// 1000ms 가 지났으니 현재 시간을 다시 저장
g_console.timeStart = clock();
if (IsMoveAvailable(0, 1))
{
// 아래로 한 칸 이동
g_player.AddPosition(0, 1);
return;
}
memcpy_s(g_nArrMapBackup, sizeof(int) * MAP_WIDTH * MAP_HEIGHT, g_nArrMap, sizeof(int) * MAP_WIDTH * MAP_HEIGHT);
g_player.SetPosition(START_POS_X, START_POS_Y);
g_player.SetBlock(RandomBlock());
g_player.SetDirection((CPlayer::eDirection)RamdomDirection());
g_prevPlayerData = g_player;
}
이제 CheckBottom 함수
에 시간을 계산하여 해당 시간이 1000ms가 넘었을 때 자동으로 한 칸 내려오도록 함수를 변경합니다.
여기까지 수정 후 테트리스를 실행해봅시다.
이제 시간이 지났을 때 자동으로 아래로 한 칸씩 내려오는 것을 볼 수 있습니다.
또한 임의로 한 칸을 움직였을 때도 키 입력이 끝난 이후로 일정시간 이후 다시 한 칸씩 내려오는 것을 볼 수 있습니다.
블럭 회전 (스페이스 바 입력 시)
이제 스페이스 바를 눌렀을 때 블럭이 회전하도록 하여봅시다.
먼저 블럭이 회전이 가능한 지 확인하는 함수를 먼저 만들어봅시다.
bool IsRotateAvailable()
{
// 현재 블럭을 다음 방향으로 돌렸을 때의 블럭 모양
int* pBlock = GetRotateBlock(g_player.GetBlock(), g_player.GetNextDirection());
// 돌린 블럭이 벽에 부딛히지 않는지 확인
return !IsCollision(pBlock, g_player.GetCursor());
}
이제 InputKey 함수
에 블럭 회전 기능을 넣어 봅시다.
void InputKey()
{
int nKey = 0;
if (_kbhit() > 0)
{
nKey = _getch();
switch (nKey)
{
// 생략 ...
case eKeyCode::KEY_SPACE:
{
// 블럭 회전이 가능한 경우
if (IsRotateAvailable())
// 블럭을 회전 시킴
g_player.SetNextDirection();
break;
}
// 생략 ...
}
}
}
이제 실행하여봅시다.
라인 클리어
라인 클리어의 경우에 실제로 한 줄이 되는 라인을 지우는 것이 아닌 해당 줄의 정보를 바로 윗줄의 정보로 덮어쓰기 하는 방식으로 진행합니다.
위의 그림처럼 라인이 채워진 부분을 기준으로 위에서 한 칸씩 덮어써지는 느낌으로 구현합니다.
라인 클리어 확인 시점을 블럭이 바닥에 닿아서 다음 블럭으로 넘어가기 전에 테트리스 맵 배열을 탐색하여 라인이 이루어졌는지 확인하도록 합니다.
bool CheckFillLine()
{
// 현재 플레이어(블럭)의 좌표
COORD curPos = g_player.GetCursor();
// 라인 클리어를 해야하는지 유무
bool bFill = true;
// 라인 클리어를 시킬 배열의 사이즈
int nSize = 0;
// 라인 클리어를 해야하는지 유무
bool bLineCleared = false;
// 한 블럭은 4x4의 크기임으로 Y좌표로 +4까지만 확인
for (int nY = curPos.Y; nY < curPos.Y + 4; ++nY)
{
bFill = true;
for (int nX = 1; nX < MAP_WIDTH; ++nX)
{
// 현재 라인에서 빈 칸이 있다면 해당 라인은 클리어될 수 없음
if (g_nArrMapBackup[nY][nX] == 0)
{
bFill = false;
break;
}
}
// 라인 클리어를 해야 한다면
if (bFill &&
nY < MAP_HEIGHT - 1)
{
nSize = sizeof(int) * MAP_WIDTH * (nY - 1);
// 이전의 라인 정보를 다음 라인 정보에 옮김 (즉, 현재 라인이 지워지고 윗 라인이 아래로 내려오는 효과)
memcpy_s(g_nArrMapBackup[2], nSize, g_nArrMapBackup[1], nSize);
bLineCleared = true;
}
}
return bLineCleared;
}
위와 같이 라인 클리어를 하는 함수를 만듭니다.
void CheckBottom()
{
// 마지막 입력 시간에서 현재 시간을 뺌 = 진행된 시간(ms)
clock_t ctTimeDiff = clock() - g_console.timeStart;
// 1000ms가 지나지 않았다면 바로 return
if (ctTimeDiff < 1000)
return;
// 1000ms 가 지났으니 현재 시간을 다시 저장
g_console.timeStart = clock();
if (IsMoveAvailable(0, 1))
{
// Y Move
g_player.AddPosition(0, 1);
return;
}
memcpy_s(g_nArrMapBackup, sizeof(int) * MAP_WIDTH * MAP_HEIGHT, g_nArrMap, sizeof(int) * MAP_WIDTH * MAP_HEIGHT);
// 라인 클리어 기능 추가
if (CheckFillLine())
{
g_player.AddGameScore(1);
memcpy_s(g_nArrMap, sizeof(int) * MAP_WIDTH * MAP_HEIGHT, g_nArrMapBackup, sizeof(int) * MAP_WIDTH * MAP_HEIGHT);
}
g_player.SetPosition(START_POS_X, START_POS_Y);
g_player.SetBlock(RandomBlock());
g_player.SetDirection((CPlayer::eDirection)RamdomDirection());
g_prevPlayerData = g_player;
}
그리고 CheckBottom 함수에 라인 클리어 기능을 넣습니다.
그리고 실행해봅니다.
이렇게 라인 클리어까지 잘 되는 것을 확인할 수 있습니다.
다음 시간이 아마 마지막이 될 것입니다.
다음 시간에는 게임 오버, 재시작, 점수화면 표시에 대해서 알아보겠습니다.
현재 까지의 진행 프로젝트 다운로드
'Study > C++' 카테고리의 다른 글
[C++11] 이동 생성자(Move Constructors)와 이동 할당 연산자(Move Assignment Operators) (0) | 2024.12.01 |
---|---|
[C++11] unordered_map (0) | 2023.03.20 |
[C++/Console] 테트리스 만들어 보기 - 6 (바닥 충돌, 블럭 랜덤 생성) (0) | 2023.03.06 |
[C++/Console] 테트리스 만들어 보기 - 5 (충돌 판정 - 벽, 블럭) (0) | 2023.02.20 |
[C++/Console] 테트리스 만들어 보기 - 4 (플레이어(블럭) 움직임) (0) | 2023.02.09 |