Study/C++

[C++/Console] 테트리스 만들어 보기 - 7 (자동 하강, 블럭 회전, 라인 클리어)

2023. 4. 17. 19:54
목차
  1. 자동 하강
  2. 블럭 회전 (스페이스 바 입력 시)
  3. 라인 클리어
  4. 현재 까지의 진행 프로젝트 다운로드
 

[C++/Console] 테트리스 만들어 보기 - 6 (바닥 충돌, 블럭 랜덤 생성)

[C++/Console] 테트리스 만들어 보기 - 5 (충돌 판정 - 벽, 블럭) [C++/Console] 테트리스 만들어 보기 - 4 (플레이어(블럭) 움직임) [C++/Console] 테트리스 만들어 보기 - 3 (화면 출력에 대한 고찰) [C++/Console]

eskeptor.tistory.com

지난 시간에는 바닥 충돌, 블럭 랜덤 생성에 대해서 알아보았습니다.

오늘은 시간이 지남에 따라 자동으로 블럭이 하강하고, 스페이스바를 눌렀을 때 블럭이 회전되며, 하나의 라인을 형성하면 해당 라인을 클리어 후 점수가 올라가는 점수 시스템에 대해서 알아보겠습니다.

 

자동 하강

테트리스는 지정된 시간이 지나면 블럭이 아래로 하강하는 시스템을 가지고 있습니다.

이것을 구현하기 위해서는 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 함수에 라인 클리어 기능을 넣습니다.

 

그리고 실행해봅니다.

이렇게 라인 클리어까지 잘 되는 것을 확인할 수 있습니다.

 

다음 시간이 아마 마지막이 될 것입니다.

다음 시간에는 게임 오버, 재시작, 점수화면 표시에 대해서 알아보겠습니다.

 

현재 까지의 진행 프로젝트 다운로드

TetrisCpp_7편.7z
0.01MB

저작자표시 비영리 변경금지 (새창열림)

'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
  • 자동 하강
  • 블럭 회전 (스페이스 바 입력 시)
  • 라인 클리어
  • 현재 까지의 진행 프로젝트 다운로드
'Study/C++' 카테고리의 다른 글
  • [C++11] 이동 생성자(Move Constructors)와 이동 할당 연산자(Move Assignment Operators)
  • [C++11] unordered_map
  • [C++/Console] 테트리스 만들어 보기 - 6 (바닥 충돌, 블럭 랜덤 생성)
  • [C++/Console] 테트리스 만들어 보기 - 5 (충돌 판정 - 벽, 블럭)
Eskeptor
Eskeptor
Eskeptor
Hello World
Eskeptor
전체
오늘
어제
  • 분류 전체보기 (138)
    • Computer (5)
      • Linux (1)
      • Hardware (2)
      • Software (0)
      • Tips (1)
      • Website (0)
    • Mobile (1)
      • Application (1)
    • Study (108)
      • Android (9)
      • C언어 (45)
      • C++ (17)
      • Unity 5(유니티5) (11)
      • Qt 프로그래밍 (2)
      • MFC (12)
      • C#, Winform (12)
    • My World (24)
      • OpenPad(Android) (12)
      • 한글 패치 (1)
      • C#으로 만든 귀요미들 (5)
      • MFC로 만든 귀요미들 (6)
    • Life Goes On (0)
      • Hip Hop (0)

블로그 메뉴

  • 홈
  • 태그
  • 미디어로그
  • 위치로그
  • 방명록

공지사항

인기 글

태그

  • 포인터
  • C#
  • Android
  • 자료구조
  • Tetris
  • 알고리즘
  • 강의
  • 기초
  • C++
  • 메모장
  • 배열
  • 유니티
  • c++11
  • 만들기
  • 기본
  • 오픈패드
  • 초보
  • 슈팅게임
  • 프로그래밍
  • Java
  • 자바
  • 안드로이드
  • openpad
  • 왕초보
  • C언어
  • MFC
  • 비행기
  • 테트리스
  • 강좌
  • Unity

최근 댓글

최근 글

hELLO · Designed By 정상우.
Eskeptor
[C++/Console] 테트리스 만들어 보기 - 7 (자동 하강, 블럭 회전, 라인 클리어)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.