본문 바로가기
C언어

[C언어] 테트리스 게임 만드는 방법

by 호일이 2020. 5. 15.

콘솔창에서 간단한 테트리스를 만들어봅시다.

 

콘솔창에서 게임을 만들 때 가장 중요한 것이 있습니다.

 

1. 원하는 좌표에 출력을 할 수 있어야 합니다.

2. 화면을 깨끗이 지울 수 있어야 합니다.

3. 키보드 입력을 받아야 합니다.

 

1, 2는 window.h을 Include해야 합니다.

 

1. SetConsoleCursorPosition(콘솔창의 핸들, 좌표);

그냥 쓰면 너무 길기도 하고 콘솔창의 핸들을 얻는 과정이 귀찮기 때문에 보통 함수를 정의해서 사용합니다.

void gotoxy(int x, int y) {
	COORD pos;
	pos.X = x;
	pos.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

첫번째 인자로 콘솔창의 핸들을 넣어줘야 합니다.

GetStdHandle(콘솔의 번호)로 핸들을 알 수 있습니다.

 

두번째 인자는 좌표값입니다.

COORD라는 자료형을 사용합니다.

typedef struct _COORD {
    SHORT X;
    SHORT Y;
} COORD, *PCOORD;

정의는 위와 같이 간단하게 되어있습니다.

단순히 X, Y만 가지는 구조체입니다.

 

COORD도 windows.h를 포함하면 쓸 수 있습니다.

 

사용 예)

gotoxy(10,5);

->콘솔의 커서를 좌표(10, 5)로 이동한다

 

2. system("cls");

콘솔 화면을 지워주지만 한가지 단점은 실행에 시간이 약간 걸리므로 게임에 어울리지 않습니다.

실행이 빠른 스크린 클리어 방법을 찾아봤으나 결국 못찾았습니다..

예제용으로 만드는게 목적이었기 때문에 어쩔 수 없이 그냥 사용하기로 했습니다.

 

3. _kbhit(), _getch()

_kbhit는 입력을 감지하는 함수이고 conio.h을 Include해야 합니다.

_kbhit로 입력을 감지할 때마다 _getch함수로 입력을 받게 했습니다.

밑에서 사용법에 대해 설명하겠습니다.

 

 

테트리스 소스코드

#include <stdio.h>
#include <windows.h>
#include <conio.h>
#include <time.h> 

clock_t startDropT, endT, startGroundT;
int x = 8, y = 0;
RECT blockSize;
int blockForm;
int blockRotation = 0;
int key;

int block[7][4][4][4] = {
	{ // T모양 블럭
		{
			{0,0,0,0},
			{0,1,0,0},
			{1,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,0,0},
			{0,1,1,0},
			{0,1,0,0}
		},
		{
			{0,0,0,0},
			{0,0,0,0},
			{1,1,1,0},
			{0,1,0,0}
		},
		{
			{0,0,0,0},
			{0,1,0,0},
			{1,1,0,0},
			{0,1,0,0}
		}
	},
	{    // 번개 블럭
		{
			{0,0,0,0},
			{0,1,1,0},
			{1,1,0,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{1,0,0,0},
			{1,1,0,0},
			{0,1,0,0}
		},
		{
			{0,0,0,0},
			{0,1,1,0},
			{1,1,0,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{1,0,0,0},
			{1,1,0,0},
			{0,1,0,0}
		}
	},
	{   // 번개 블럭 반대
		{   
			{0,0,0,0},
			{1,1,0,0},
			{0,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,0,0},
			{1,1,0,0},
			{1,0,0,0}
		},
		{
			{0,0,0,0},
			{1,1,0,0},
			{0,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,0,0},
			{1,1,0,0},
			{1,0,0,0}
		}
	},
	{   // 1자형 블럭
		{
			{0,1,0,0},
			{0,1,0,0},
			{0,1,0,0},
			{0,1,0,0}
		},
		{
			{0,0,0,0},
			{0,0,0,0},
			{1,1,1,1},
			{0,0,0,0}
		},
		{
			{0,1,0,0},
			{0,1,0,0},
			{0,1,0,0},
			{0,1,0,0}
		},
		{
			{0,0,0,0},
			{0,0,0,0},
			{1,1,1,1},
			{0,0,0,0}
		}
	},
	{   // L자형 블럭
		{
			{0,0,0,0},
			{1,0,0,0},
			{1,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{1,1,0,0},
			{1,0,0,0},
			{1,0,0,0}
		},
		{
			{0,0,0,0},
			{1,1,1,0},
			{0,0,1,0},
			{0,0,0,0}
		},
		{
			{0,1,0,0},
			{0,1,0,0},
			{1,1,0,0},
			{0,0,0,0}
		}
	},
	{   // L자형 블럭 반대
		{
			{0,0,0,0},
			{0,0,1,0},
			{1,1,1,0},
			{0,0,0,0}
		},
		{
			{1,0,0,0},
			{1,0,0,0},
			{1,1,0,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{1,1,1,0},
			{1,0,0,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{1,1,0,0},
			{0,1,0,0},
			{0,1,0,0}
		}
	},
	{   // 네모 블럭
		{
			{0,0,0,0},
			{0,1,1,0},
			{0,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,1,0},
			{0,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,1,0},
			{0,1,1,0},
			{0,0,0,0}
		},
		{
			{0,0,0,0},
			{0,1,1,0},
			{0,1,1,0},
			{0,0,0,0}
		}
	}
};

int space[15 + 1][10 + 2] = {  // 세로 15+1(아래벽)칸, 가로 10+2(양쪽 벽)칸  
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,0,0,0,0,0,0,0,0,0,0,1},
	{1,1,1,1,1,1,1,1,1,1,1,1}
};

void Init();
void gotoxy(int x, int y);
void CreateRandomForm();
bool CheckCrash(int x, int y);
void DropBlock();
void BlockToGround();
void RemoveLine();
void DrawMap();
void DrawBlock();
void InputKey();

int main() {
	Init();
	startDropT = clock();
	CreateRandomForm();

	while (true) {
		DrawMap();
		DrawBlock();
		DropBlock();
		BlockToGround();
		RemoveLine();
		InputKey();
	}
	return 0;
}

void Init() {
	CONSOLE_CURSOR_INFO cursorInfo;
	cursorInfo.bVisible = 0;
	cursorInfo.dwSize = 1;
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo);
	srand(time(NULL));
}
void gotoxy(int x, int y) {
	COORD pos;
	pos.X = x;
	pos.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
void CreateRandomForm() {
	blockForm = rand() % 7;
}
bool CheckCrash(int x, int y) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (block[blockForm][blockRotation][i][j] == 1) {
				int t = space[i + y][j + x / 2];
				if (t == 1 || t == 2) { // 벽일 때, 블럭일 때
					return true;
				}
			}
		}
	}
	return false;
}
void DropBlock() {
	endT = clock();
	if ((float)(endT - startDropT) >= 800) {
		if (CheckCrash(x, y + 1) == true) return;
		y++;
		startDropT = clock();
		startGroundT = clock();
		system("cls");
	}
}
void BlockToGround() {
	if (CheckCrash(x, y + 1) == true) {
		if ((float)(endT - startGroundT) > 1500) {
			// 현재 블록 저장
			for (int i = 0; i < 4; i++) {
				for (int j = 0; j < 4; j++) {
					if (block[blockForm][blockRotation][i][j] == 1) {
						space[i + y][j + x / 2] = 2;
					}
				}
			}
			x = 8;
			y = 0;
			CreateRandomForm();
		}
	}
}
void RemoveLine() {
	for (int i = 15; i >= 0; i--) { // 벽라인 제외한 값
		int cnt = 0;
		for (int j = 1; j < 11; j++) { // 
			if (space[i][j] == 2) {
				cnt++;
			}
		}
		if (cnt >= 10) { // 벽돌이 다 차있다면
			for (int j = 0; i - j >= 0; j++) {
				for (int x = 1; x < 11; x++) {
					if (i - j - 1 >= 0)
						space[i - j][x] = space[i - j - 1][x];
					else      // 천장이면 0저장
						space[i - j][x] = 0;
				}
			}
		}
	}
}
void DrawMap() {
	gotoxy(0, 0);
	for (int i = 0; i < 16; i++) {
		for (int j = 0; j < 12; j++) {
			if (space[i][j] == 1) {
				gotoxy(j * 2, i);
				printf("□");
			}
			else if (space[i][j] == 2) {
				gotoxy(j * 2, i);
				printf("■");
			}
		}
	}
}
void DrawBlock() {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (block[blockForm][blockRotation][i][j] == 1) {
				gotoxy(x + j * 2, y + i);
				printf("■");
			}
		}
	}
}
void InputKey() {
	if (_kbhit()) {
		key = _getch();
		switch (key) {
		case 32: // space
			blockRotation++;
			if (blockRotation >= 4) blockRotation = 0;
			startGroundT = clock();
			break;
		case 75: // left
			if (CheckCrash(x - 2, y) == false) {
				x -= 2;
				startGroundT = clock();
			}
			break;
		case 77: // right
			if (CheckCrash(x + 2, y) == false) {
				x += 2;
				startGroundT = clock();
			}
			break;
		case 80: // down
			if (CheckCrash(x, y + 1) == false)
				y++;
			break;
		}
		system("cls");
	}
}

블럭 모양 코드를 제외하면 코드가 많지는 않습니다.

간략하게 만들었으니 천천히 봐주시기 바랍니다.

 

사용자 정의 함수 부분

void Init();
void gotoxy(int x, int y);
void CreateRandomForm();
bool CheckCrash(int x, int y);
void DropBlock();
void BlockToGround();
void RemoveLine();
void DrawMap();
void DrawBlock();
void InputKey();

총 10개의 함수를 정의해서 사용했습니다.

 

1. Init()

void Init() {
	CONSOLE_CURSOR_INFO cursorInfo;
	cursorInfo.bVisible = 0;
	cursorInfo.dwSize = 1;
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo);
	srand(time(NULL));
}

콘솔창은 항상 하얀색 커서가 깜빡입니다.

이것을 없애주기 위해 SetConsoleCursorInfo함수를 썼습니다.

 

srand에 time(NULL)을 줘야 rand()가 고정적으로 나오지 않습니다.

2. gotoxy(int x, int y)

void gotoxy(int x, int y) {
	COORD pos;
	pos.X = x;
	pos.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

위에서 설명했으니 넘어가겠습니다.

 

3.  CreateRandomForm()

void CreateRandomForm() {
	blockForm = rand() % 7;
}

블럭이 내려올 때마다 랜덤으로 바뀌게 합니다.

rand()%7을 해주면 0~6의 랜덤한 값이 나오게 되는 것을 이용했습니다.

 

4. CheckCrash(int x, int y)

bool CheckCrash(int x, int y) {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (block[blockForm][blockRotation][i][j] == 1) {
				int t = space[i + y][j + x / 2];
				if (t == 1 || t == 2) { // 벽일 때, 블럭일 때
					return true;
				}
			}
		}
	}
	return false;
}

충돌을 감지하는 함수입니다.

매개변수에 위치값을 넣고 블럭이 그 위치에 있을 때 충돌이 되는가를 검사합니다.

하나라도 겹치는 것이 있다면 true을 반환합니다.

x좌표는 2당 1칸의 크기를 가지기 때문에 y는 그대로 쓰고 x는 x/2로 했습니다.

 

5. DropBlock()

void DropBlock() {
	endT = clock();
	if ((float)(endT - startDropT) >= 800) {
		if (CheckCrash(x, y + 1) == true) return;
		y++;
		startDropT = clock();
		startGroundT = clock();
		system("cls");
	}
}

0.8초마다 블럭을 한칸씩 밑으로 내리는 함수입니다.

 

6. BlockToGround()

void BlockToGround() {
	if (CheckCrash(x, y + 1) == true) {
		if ((float)(endT - startGroundT) > 1500) {
			// 현재 블록 저장
			for (int i = 0; i < 4; i++) {
				for (int j = 0; j < 4; j++) {
					if (block[blockForm][blockRotation][i][j] == 1) {
						space[i + y][j + x / 2] = 2;
					}
				}
			}
			x = 8;
			y = 0;
			CreateRandomForm();
		}
	}
}

블럭이 1.5초동안 땅에 닿아있을 때 아무 동작이 없다면 땅으로 변하게 합니다.

그리고 랜덤한 블럭을 만들고 위로 올립니다.

 

7. RemoveLine()

void RemoveLine() {
	for (int i = 15; i >= 0; i--) { // 벽라인 제외한 값
		int cnt = 0;
		for (int j = 1; j < 11; j++) { // 
			if (space[i][j] == 2) {
				cnt++;
			}
		}
		if (cnt >= 10) { // 벽돌이 다 차있다면
			for (int j = 0; i - j >= 0; j++) {
				for (int x = 1; x < 11; x++) {
					if (i - j - 1 >= 0)
						space[i - j][x] = space[i - j - 1][x];
					else      // 천장이면 0저장
						space[i - j][x] = 0;
				}
			}
		}
	}
}

1줄이 되었다면 블럭을 제거합니다.

그 줄에서부터 시작해서 한칸씩 다 땡깁니다.

 

int j = 0; i - j >= 0; j++

그 줄에서부터 시작해서 위로 올라가면서 한칸씩 내리게 하는 코드입니다.

int x = 1; x < 11; x++

x=0, x=11 은 벽입니다.

if(i-j-1>=0) 천장이 아닐 때 입니다.

else 천장이면 더이상 블럭이 없으니 당겨올 수 없습니다. 따라서 0을 저장힙니다.

 

8. DrawMap()

void DrawMap() {
	gotoxy(0, 0);
	for (int i = 0; i < 16; i++) {
		for (int j = 0; j < 12; j++) {
			if (space[i][j] == 1) {
				gotoxy(j * 2, i);
				printf("□");
			}
			else if (space[i][j] == 2) {
				gotoxy(j * 2, i);
				printf("■");
			}
		}
	}
}

맵의 형태와 쌓인 블럭을 그립니다.

 

9. DrawBlock()

void DrawBlock() {
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++) {
			if (block[blockForm][blockRotation][i][j] == 1) {
				gotoxy(x + j * 2, y + i);
				printf("■");
			}
		}
	}
}

현재 블럭을 그립니다.

block[7][4][4][4] 4차원 배열로 만들었습니다.

 

[7] : 7개의 블럭

[4] : 4개의 회전모양

[4] : 세로 모양

[4] : 가로 모양

 

[7] 부분에 랜덤으로 숫자를 주면 랜덤한 블럭이 나타나게 됩니다.

[4] 회전키를 누를 때마다 증감해주면 모양이 회전하게 됩니다.

10. InputKey()

void InputKey() {
	if (_kbhit()) {
		key = _getch();
		switch (key) {
		case 32: // space
			blockRotation++;
			if (blockRotation >= 4) blockRotation = 0;
			startGroundT = clock();
			break;
		case 75: // left
			if (CheckCrash(x - 2, y) == false) {
				x -= 2;
				startGroundT = clock();
			}
			break;
		case 77: // right
			if (CheckCrash(x + 2, y) == false) {
				x += 2;
				startGroundT = clock();
			}
			break;
		case 80: // down
			if (CheckCrash(x, y + 1) == false)
				y++;
			break;
		}
		system("cls");
	}
}

_kbhit로 입력을 감지하고 _getch로 입력을 받습니다.

_getch만 쓰면 입력받을 때까지 계속 대기하게 됩니다.

모양 바꾸기, 왼쪽, 오른쪽을 이동할 때마다 StartGroundT = clock()을 하는 이유는

startGroundT의 시간을 갱신해서 블럭이 땅으로 변하지 않게 하기 위해서 입니다.

만약 저 코드가 없다면 아무리 블럭을 이동, 모양을 바꿔도 땅에 닿으면 바로 다음 블럭이 나올 것입니다.

 

system("cls")는 실행하는데 시간이 약간 걸리므로 사용을 최소화하기 위해

키를 누를 때나 땅에 떨어질 때만 실행되게 했습니다.

 

메인 부분

int main() {
	Init();
	startDropT = clock();
	CreateRandomForm();

	while (true) {
		DrawMap();
		DrawBlock();
		DropBlock();
		BlockToGround();
		RemoveLine();
		InputKey();
	}
	return 0;
}

 

이 테트리스는 문제없이 실행되나 블럭이 회전할 때 벽이나 땅과 겹치는 버그가 있습니다.

테트리스의 구조에 대해 설명하기 위해 최대한 간단하게 만들다보니 예외처리를 하지 않았습니다.

 

회전시 겹치는 버그를 수정해보는 것도 좋은 공부가 될 테니 시간이 있다면 해보시기 바랍니다.

 

 

 

읽어주셔서 감사합니다.

 

 

Tetris.exe
0.01MB

 

반응형

댓글