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

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

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

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

- 키 입력을 통한 이동과 회전


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

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


게임 구현에 필요한 콘솔 I/O


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <conio.h>
using namespace std;
 
int main(void)
{
    while (true)
    {
        while (!_kbhit())
        {
            puts("키 입력");
            for (int i = 0; i < 25000; i++)
                for (int j = 0; j < 25000; j++)
                    ;    // 하는 일 없음
        }        
        cout << "입력된 키의 아스키 코드 : " << _getch() << endl;
    }    
    return 0;
}
cs


9행 : kbhit 함수는 conio.h에 선언된 콘솔 I/O 함수

키보드가 눌렸다면 0이 아닌 값 (True), 눌려지지 않았다면 0 (FALSE) 반환


12-14행 : 실행 흐름을 낮추기 위한 코드로 pc에 따라 실행속도가 달라질 수 있음


16행 : _getch 함수도 conio.h에 선언된 콘솔 I/O 함수

입력된 키의 아스키 코드 값을 반환하며, 데이터 입력을 위해 Enter 키 입력이 필요없다. 또한 입력한 키의 내용을 모니터로 출력하지도 않음


Output : 

키 입력

입력된 키의 아스키 코드 : 104

키 입력

입력된 키의 아스키 코드 : 106

키 입력

입력된 키의 아스키 코드 : 224

입력된 키의 아스키 코드 : 75

키 입력

입력된 키의 아스키 코드 : 224

입력된 키의 아스키 코드 : 72

키 입력

입력된 키의 아스키 코드 : 0

입력된 키의 아스키 코드 : 59

키 입력

입력된 키의 아스키 코드 : 0

입력된 키의 아스키 코드 : 59

키 입력

키 입력


위에서 출력결과를 보면 일반문자는 아스키코드가 한번에 출력되지만

특정 키는 아스키 코드가 두번출력된 걸 볼 수 있다. 이를 통해 화살표 문자와 특수 문자(F1~ 등)같은 경우 두 번에 걸쳐서 읽음을 알 수 있다. 



블럭을 좌우로 이동 시키기 위한 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void BlockMoveLeft(void)
{
    DeleteBlock(blockModel[GetCurBlockIdx()]);
    curPosX -= 2;    // 왼쪽 이동(■는 2칸 차지)
 
    SetCurrentCursorPos(curPosX, curPosY);
    ShowBlock(blockModel[GetCurBlockIdx()]);
}
 
void BlockMoveRight(void)
{
    DeleteBlock(blockModel[GetCurBlockIdx()]);
    curPosX += 2;    // 오른쪽 이동(■는 2칸 차지)
 
    SetCurrentCursorPos(curPosX, curPosY);
    ShowBlock(blockModel[GetCurBlockIdx()]);
}
cs

블럭의 회전을 위한 함수 (이를 위해 GetCurBlockIdx 함수의 변경이 있다)
1
2
3
4
5
6
7
8
9
10
11
12
void BlockRotate(void)    // 배열 idx값을 1->2->3->0..으로 순차적 변경하도록    
{    
    int nextRotate;
    DeleteBlock(blockModel[GetCurBlockIdx()]);    
    
    nextRotate = rotation + 1;    // 전역변수 int rotation
    nextRotate = nextRotate % 4;    
    rotation = nextRotate;
 
    SetCurrentCursorPos(curPosX, curPosY);
    ShowBlock(blockModel[GetCurBlockIdx()]);
}
cs
1을 4로 나눈 나머지는 1... 따라서 idx[0]
2로 4로 나눈 나머지는 2... 이런식으로 배열의 idx값을 순차적으로 변경한다.

GetCurBlockIdx 함수는 다음과 같이 변경된다.

1
2
3
4
int GetCurBlockIdx(void)
{
    return CurBlockModel + rotation;
}
cs



키 입력을 받고 입력 결과를 처리하기 위한 함수 ( #define delay 20 )

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
#define KEY_SENSITIVE 100   // 키 입력감도
 
void KeyInput(void)
{
    int key;
    for (int i = 0; i < KEY_SENSITIVE; i++)
    {
        if (_kbhit() != 0)
        {
            key = _getch();
 
            switch (key)
            {
            case LEFT:
                BlockMoveLeft();
                break;
            case RIGHT:
                BlockMoveRight();
                break;
            case UP:
                BlockRotate();
            }            
        }
         if(i % keyDelayRate == 0)
        Sleep(delay);
    }
}
cs

위 함수에서 중요한 것은 main함수에 있던 Sleep함수 호출이, 키 입력 함수안에서 호출되도록 변경되었단 것이다.

또한 정의된 KEY_SENSITIVE는 for문안에서 loop할동안 몇번의 입력을 받을 수 있을지를 결정한다.


24행에서는 키 입력 사이사이에 delay를 주기위한 값이다. (시스템 함수 sleep호출) 

keyDelayRate를 나눈 값만큼 쪼개서 delay를 주기 때문에 게이머가 키 입력시 간격을 크게 느끼지 못하게 된다.



1
2
3
4
5
6
7
void InitKeyDelayRate(int rate)
{
    if (rate<1)        
    return;
 
    keyDelayRate = rate;
}
cs

물론 변수를 선언하고 특정값으로 초기화할수도 있지만 따로 함수를 만드는 것이 좋다. InitKeyDelayRate함수는 keyDelayRate를 초기화하기 위한 함수로 main함수에서 가장 먼저 호출되는 함수이다. (게임의 초기 속도를 결정)

if문의 조건과 수행문은 임의로 수정해서 값을 제한할 수도 있겠다.


아래와 같은 main함수의 변경은 좋지 못한 방법이다. ( #define delay 100, KeyInput 함수안에서 Sleep함수 호출이 없다고 가정 )

1
2
3
4
5
6
while (TRUE)
    {
        BlockDown();
        KeyInput();
        sleep(delay);
    }
cs


이러한 코드는 다음과 같은 문제를 발생시킨다.

 s

 d

 k

 s

 d

 k

s


d: 블록 한칸 이동 시간

k : 하나의 키 입력 처리시간

s : 한번의 Sleep함수 호출로 스레드가 일시 중단되는 시간


즉, Sleep 함수 호출로 일시 중단된 시간동안 키보드 입력이 반영되지 않는 것이다.

그렇다고해서 Sleep의 delay를 짧게하면 블록이 내려가는 시간도 빨라지게 된다.


따라서 KeyInput 함수안에서 Sleep함수 호출을 하도록 변경하고, main 함수를 다음과 같이 변경하는 것이 좋다.


while (TRUE)

{

BlockDown();

KeyInput();

}


delay시간을 짧게 정의하는 대신 빈번하게 호출되게 함으로서, 블록의 적절한 이동시간과 키보드의 빠른 입력반영이 가능하도록 한 것이다.


Output :




- 키 입력을 통한 이동과 회전


게임판 그리기


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 : 


728x90