본문 바로가기
코스웨어/16년 스마트컨트롤러

2016-10-07_조재찬_ 프로젝트 일지_테트리스 게임 (3)

by 알 수 없는 사용자 2016. 10. 7.
728x90
반응형

<테트리스 게임> 프로젝트

- 게임판 위에서의 이동


작업 환경 : 윈도우 10, Visual Studio, C++ (Win32 Console)

참고 서적 : C프로그래밍 파워 업그레이드 (윤성우 저)


게임판(stage) 그리기


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>
#include <Windows.h>
using namespace std;
 
// 블록이 이동가능한 게임 판의 크기 (경계면 미포함)    
#define  GBOARD_WIDTH    5
#define  GBOARD_HEIGHT   10
 
/* 게임 판을 그릴 기준이 되는 위치 */
#define  GBOARD_ORIGIN_X  4
#define  GBOARD_ORIGIN_Y  2
 
void SetCurrentCursorPos(int x, int y);
void DrawGameBoard(void);
 
int main(void)
{
    DrawGameBoard();
    return 0;
}
 
void SetCurrentCursorPos(int x, int y)    // 커서 위치를 이동시키는 함수
{
    COORD pos = { x, y };
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
 
void DrawGameBoard(void)    // 실제 게임판의 경계면을 그림
{
    int x, y;
 
    /* 시각적인 부분 처리 */
    for (y = 0; y <= GBOARD_HEIGHT; y++)
    {
        SetCurrentCursorPos(GBOARD_ORIGIN_X, GBOARD_ORIGIN_Y + y);
 
        if (y == GBOARD_HEIGHT)    // y좌표가 게임판 높이와 같아지면 ┗ 출력
            printf("┗");
        else
            printf("┃");
    }
 
    for (y = 0; y <= GBOARD_HEIGHT; y++)
    {
        SetCurrentCursorPos(GBOARD_ORIGIN_X + (GBOARD_WIDTH + 1* 2, GBOARD_ORIGIN_Y + y);
 
        if (y == GBOARD_HEIGHT)
            printf("┛");    // y좌표가 게임판 높이와 같아지면 ┛ 출력
        else
            printf("┃");
    }
 
    for (x = 1; x < GBOARD_WIDTH + 1; x++)
    {
        SetCurrentCursorPos(GBOARD_ORIGIN_X + x * 2, GBOARD_ORIGIN_Y + GBOARD_HEIGHT);
        printf("━");    // 특수 문자는 2칸 잡아먹음에 유의
    }
 
    SetCurrentCursorPos(00);
}
cs

DrawGameBoard 함수는 for문을 통해 게임판이 어떻게 그려지는지, 종이에 그려가며 분석해보는 것이 좋다.

게임판을 그릴 기준이 되는 GBOARD_ORIGIN_X, GBOARD_ORIGIN_Y 값을 생각해야하며, 이 것은 게임판 밖의 상하좌우 여백(경계면이 아님!)을 결정한다.


Output : 


(RemoveCursor 함수를 적용하지 않아 커서가 깜박거림)




게임판의 정보를 담기위한 배열


static int gameBoardInfo[GBOARD_HEIGHT+1][GBOARD_WIDTH+2]={0,};


[GBOARD_HEIGHT+1]    // 게임판의 바닥면

[GBOARD_WIDTH+2]    // 게임판의 좌우 경계면




충돌 검사 알고리즘


CollisionDetection (int posX, int posY, char blockModel[][4])


위 함수의 첫번째 두번째 인자는 블록의 이동위치(기준점) 정보를 전달받는다. 그리고 이는 배열 index의 x,y값으로 변환된다. 

세 번째 인자는 이동 및 회전 할 블록 정보를 전달한다.


이동 및 회전할 블록 정보(4x4)와 게임판 배열을 비교하여, 겹치는지 확인하여 충돌 여무를 확인하는 것이다.

(경계면과 블록은 1, 빈 공간은 0로 정하고 값을 비교)



콘솔 출력창 좌표 정보 -> 게임판 배열 정보 변환


콘솔의 x좌표 값은 2씩 증가한다. (특문이 2칸을 잡아먹으므로 오른쪽으로 이동한다면 2씩 증가)

다만 이 때 단순히 블록의 좌표의 x값을 2로 나눠서만은 안되고 게임 판이 그려지는 시작 위치(GBOARD_ORIGIN_X, Y)도 고려해야한다.



배열의 인덱스 값과 콘솔 위치 정보가 일치하지 않기 때문에 이를 위한 좌표간 변환 공식이 필요하다.


1
2
int arrCurX=(PosX-GBOARD_ORIGIN_X) / 2;
int arrCurY=PosY-GBOARD_ORIGIN_Y;
cs


또한 충돌 검사를 위해 블럭의 모든 인덱스를 조회해서, 겹치면 0 (FALSE)를 반환, 겹치지 않으면 1 (TRUE)를 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
    /* 충돌 검사 */
    for (x = 0; x<4; x++)
    {
        for (y = 0; y<4; y++)
        {
            /* Short Circuit Evaluation에 의해 배열 일부만 검사 */
            if (blockModel[y][x] == 1 && gameBoard[arrY + y][arrX + x] == 1)
                return 0;
        }
    }
 
    return 1;
cs

Short Circuit Evaluation은 연산의 최소화를 위한 것으로,

위의 if문의 인자에서 첫번째 조건으로 결과값이 판단된다면, 뒤의 연산은 불필요하므로 생략하게 되는데, 이를 Short Circuit Evaluation이라 한다.



이 충돌 검사(DetectCollision) 알고리즘은 블록의 이동과 회전 모두에 적용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL BlockDown(void)
{
    if (!DetectCollision(curPosX, curPosY + 1, blockModel[GetCurrentBlockIdx()]))
        return FALSE;
 
    DeleteBlock(blockModel[GetCurrentBlockIdx()]);
    curPosY += 1;
 
    SetCurrentCursorPos(curPosX, curPosY);
    ShowBlock(blockModel[GetCurrentBlockIdx()]);
 
    return TRUE;
}
cs


if (!DetectCollision(curPosX, curPosY + 1, blockModel[GetCurrentBlockIdx()]))    // if문 조건의 두 번째 인자는 이동 위치에 따라 다르게 적용하면 된다.



게임 오버 처리 함수


게임 종료 조건 만족시 TRUE를 반환하는 함수

처음 블록이 등장했을 때 충돌한다면 이는 내릴 공간이 없다는 뜻이므로, 게임오버를 뜻한다. 

1
2
3
4
5
6
7
BOOL IsGameOver(void)
{
    if (!DetectCollision(curPosX, curPosY, blockModel[GetCurrentBlockIdx()]))
        return TRUE;
    else
        return FALSE;
}
cs


이 함수는. 다음과 같이 main함수안에서 블록을 내리기전에 호출되어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
    RemoveCursor();
    DrawGameBoard();
 
    while (TRUE)
    {
        InitNewBlockPos(START_CURPOS_X, START_CURPOS_Y);        // 커서 위치를 [ , ] 으로 초기화
        RandomBlock();
 
        if (IsGameOver())
            break;    // 게임의 종료 조건을 만족한다면 break 문에 의해 바깥쪽 while문을 빠져나가게 된다.
 
        ( . . . .)
    }
    SetCurrentCursorPos(1010);
    puts("Game Over");
cs


굳어진 블록의 표현


굳어진 블록의 정보를 게임판(Board)에 추가하는 함수 // 배열을 1로 채움

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void AddCurrentBlockInfoToBoard(void)
{
    int x, y;
 
    int arrCurX;
    int arrCurY;
 
    for (y = 0; y<4; y++)
    {
        for (x = 0; x<4; x++)
        {
            /* 커서 위치 정보를 배열 index 정보로 변경 */
            arrCurX = (curPosX - GBOARD_ORIGIN_X) / 2;
            arrCurY = curPosY - GBOARD_ORIGIN_Y;
 
            if (blockModel[GetCurrentBlockIdx()][y][x] == 1)
            {
                gameBoard[arrCurY + y][arrCurX + x] = 1;
            }
        }
    }
}
cs


main 함수안의 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    while (TRUE)
    {
        InitNewBlockPos(START_CURPOS_X, START_CURPOS_Y);        // 커서 위치를 [ , ] 으로 초기화
        RandomBlock();
 
        if (IsGameOver())
            break;
 
        while (TRUE)
        {
            if (BlockDown() == FALSE)
            {
                AddCurrentBlockInfoToBoard();
                break;
            }
        ( . . . .)
cs

BlockDown함수가 0 (FALSE)를 반환했다는 것은

더 이상 블록을 아래로 이동시킬 수 없음을 뜻한다.


따라서 현재의 블럭을 굳히고 새로운 블럭을 내리게 한다.



스페이스 키로 블록 바로 내리기


space키에 대한 정의 추가

enum key {LEFT=75, RIGHT=77, UP=72, SPACE=32}; // up=회전, space=블록 즉시 내리기


case문 추가

1
2
case SPACE:
                SolidCurrentBlock();
cs


1
2
3
4
void SolidCurrentBlock(void)
{
    while (BlockDown());
}
cs

블록을 바로 내리게 하는 것은 단순하다. while문을 통해 BlockDown함수를 계속 호출하도록 하면
0 (FALSE)를 반환할때까지 반복호출되므로, 즉시 내려가는 것처럼 보이게 된다.




728x90