눈팅하는 게임개발자 블로그

C++ 테트리스 본문

Project/C++ 테트리스

C++ 테트리스

Palamore 2020. 9. 20. 19:53

깃헙 : github.com/Palamore/Tetris_palamore

 

Palamore/Tetris_palamore

tetris. Contribute to Palamore/Tetris_palamore development by creating an account on GitHub.

github.com

 

우선 게임의 흐름도부터.

각 클래스를 간단히 짚어보자면

Block

테트리스 블록 객체들의 클래스

블록은 시계방향으로 회전(Rotate)할 수 있다.

Game Manager

게임의 전반적인 Context를 관리한다.

현재 쌓여있는 블럭 상황(맵)에 대한 정보, 1줄이 완성되면 없어지는 로직,

Handler가 다음에 사용할 테트리스 블럭 등에 대한 정보들이 있다.

Handler

유저가 직접 조작하는 블럭과 이에 관한 Context들을 관리한다.

말 그대로 테트리스 블럭을 움직이는 Handle에 해당한다.

Game Runner

게임 루프가 돌아가며 유저의 입력을 처리하고

Game Manager와 Handler의 Context들을 받아 서로에게 전달해주거나

게임 그래픽이 출력되기 위해 관련 정보들을 Renderer 객체에 전달해주는 클래스.

Renderer

쌓여있는 블럭 상황과 현재 움직이는 테트리스 블럭에 대한 정보를 받아

CLI 화면에 출력하는 클래스.

-----------------------------------------------------------------------------------------

#include "Block.h"


Block::Block()
{
	srand(time(NULL));
	int random = rand() % 7;
	switch (random) {
	case 0:
		block[1] = 1;	block[5] = 1;	block[9] = 1;	block[13] = 1;
		break;
	case 1:
		block[5] = 1;	block[6] = 1;	block[9] = 1;	block[10] = 1;
		break;
	case 2:
		block[5] = 1;	block[6] = 1;	block[7] = 1;	block[9] = 1;
		break;
	case 3:
		block[4] = 1;	block[5] = 1;	block[6] = 1;	block[10] = 1;
		break;
	case 4:
		block[6] = 1;	block[9] = 1;	block[10] = 1;	block[13] = 1;
		break;
	case 5:
		block[5] = 1;	block[9] = 1;	block[10] = 1;	block[14] = 1;
		break;
	case 6:
		block[5] = 1;	block[8] = 1;	block[9] = 1;	block[10] = 1;
		break;
	default:
		break;
	}
}

int* Block::GetBlockValue()
{
	return block;
}

void Block::Rotate()
{
	int tmp[16];
	memcpy(tmp, block, 16 * sizeof(int));
	block[0] = tmp[12];	block[1] = tmp[8];	block[2] = tmp[4];	block[3] = tmp[0];
	block[4] = tmp[13];	block[5] = tmp[9];	block[6] = tmp[5];	block[7] = tmp[1];
	block[8] = tmp[14];	block[9] = tmp[10];	block[10] = tmp[6];	block[11] = tmp[2];
	block[12] = tmp[15];	block[13] = tmp[11];	block[14] = tmp[7];	block[15] = tmp[3];
}

void Block::RotateReverse()
{
	int tmp[16];
	memcpy(tmp, block, 16 * sizeof(int));
	block[0] = tmp[3];	block[1] = tmp[7];	block[2] = tmp[11];	block[3] = tmp[15];
	block[4] = tmp[2];	block[5] = tmp[6];	block[6] = tmp[10];	block[7] = tmp[14];
	block[8] = tmp[1];	block[9] = tmp[5];	block[10] = tmp[9];	block[11] = tmp[13];
	block[12] = tmp[0];	block[13] = tmp[4];	block[14] = tmp[8];	block[15] = tmp[12];
}

block은 4x4 크기의 컨테이너에 1과 0만으로 블럭을 표현한다.

크기가 16인 int형 배열을 이용하여 표현한다.

해당 인덱스의 값이 1이면 블럭이 존재하는 것이고. 값이 0이면 빈 것이다.

처음 블럭이 생성되었을 때 생성자에서 랜덤으로 7가지의 블럭 모양 중 하나가 결정된다.

회전은 위의 그림을 그대로 90도 회전시켜 각 인덱스의 값을 대입시켜주면 된다.

회전시키면 이렇게 된다.

-----------------------------------------------------------------------------------------

void GameManager::InsertNewBlock()
{
	mBlockContainer.push(make_unique<Block>());
}

void GameManager::PopBlock()
{
	mBlock = move(mBlockContainer.front());
	mBlockContainer.pop();
	InsertNewBlock();
}

unique_ptr<Block> GameManager::GetBlock()
{
	return move(mBlock);
}

블럭을 관리하는 부분의 코드. 클래스 내부 멤버변수로 Block Type의 Queue가 있다.

InsertNewBlock()함수로 멤버변수 큐에 새로운 블럭을 넣고.

PopBlock()함수로 바로 다음에 Handler에게 넘겨줄 블럭을 큐에서 가져와 mBlock 멤버변수에 저장한다.

GetBlock()함수로 갖고있던 블럭 객체를 넘겨준다.

 

void GameManager::ClearLine()
{
	vector<int> v;
	int fullLine[MAP_WIDTH];
	for (int i = 0; i < MAP_WIDTH; i++)
		fullLine[i] = 1;
	for (int i = 0; i < MAP_HEIGHT; i++) {
		if (memcmp(mCurrentMap[i], fullLine, MAP_WIDTH * sizeof(int)) == 0) {
			v.push_back(i);
			mScore += 100;
		}
	}
	DownLines(v);
}

void GameManager::DownLines(vector<int> v)
{
	auto iter = v.begin();
	int size = v.size();
	int empty[MAP_WIDTH];
	memset(empty, 0, MAP_WIDTH * sizeof(int));
	for (int i = 0; i < size; i++) {
		for (int j = (*iter) - 1; j >= 0; j--) {
			memcpy(mCurrentMap[j + 1], mCurrentMap[j], MAP_WIDTH * sizeof(int));
		}
		memcpy(mCurrentMap[0], empty, MAP_WIDTH * sizeof(int));
		iter++;
	}
}

void GameManager::UpdateMap(int x, int y, int blockValue[])
{
	int index = 0;
	for (int i = y - 1; i < y + 3; i++) { // x,y의 최소값은 1이기때문에 인덱스값 보정.
		for (int j = x - 1; j < x + 3; j++) {
			if (blockValue[index])
				mCurrentMap[i][j] = 1;
			index++;
		}
	}
}

int* GameManager::GetMapValue()
{
	return *mCurrentMap; //주소값만 넘어가면 된다.
}

맵을 관리하는 부분의 코드. 클래스 내부 멤버변수로 15 * 10 크기의 맵의 정보를 저장하는 int형 2중배열 변수가 있다.

블럭이 맵에 쌓일 때마다 ClearLine()함수가 실행되어 완성된 줄이 있으면 해당 줄의 인덱스 값을 vector에 담아

DownLines()함수로 보낸다.

DownLines()함수에서는 받아온 인덱스 정보들로 해당 줄 위의 모든 줄을 한칸씩 내려준다.

UpdateMap()함수는 Handler에서 블럭을 쌓을때 받아온 정보들로 실제 맵 정보에 반영한다.

GetMapValue()함수로 가지고 있던 맵 정보를 넘겨준다.

-----------------------------------------------------------------------------------------

void Handler::SetNextBlock(GameManager *GM)  // 새로운 블록 init
{
	mBlock = GM->GetBlock();
	memcpy(mRealtimeMap, GM->GetMapValue(), MAP_WIDTH * MAP_HEIGHT * sizeof(int));
	memcpy(mCurrentMap, GM->GetMapValue(), MAP_WIDTH * MAP_HEIGHT * sizeof(int));
	mBlockPosition.x = 0;
	mBlockPosition.y = 0;
}

bool Handler::CheckBlocking()
{
	int* blockValue = mBlock->GetBlockValue();
	int index = 0;
	for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
		for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
			if (blockValue[index] == 1) {
				if (i == 14) {
					return true;
				}
				if (mCurrentMap[i + 1][j] == 1) {
					return true;
				}
			}
			index++;
		}
	}
	return false;
}

bool Handler::CheckSideBlocking(int direction)
{
	int* blockValue = mBlock->GetBlockValue();
	int index = 0;
	if (direction == 1) { //왼쪽

		for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
			for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
				if (blockValue[index] == 1) {
					if (j == 0) //맵의 가장 왼쪽일 경우
						return true;
					if (mCurrentMap[i][j - 1] == 1) { // 왼쪽에 이미 쌓인 블럭이 있을 경우
						return true;
					}
				}
				index++;
			}
		}
	}
	else { //오른쪽
		for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
			for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
				if (blockValue[index] == 1) {
					if (j == 9)
						return true;
					if (mCurrentMap[i][j + 1] == 1) {
						return true;
					}
				}
				index++;
			}
		}
	}
	return false;
}

void Handler::GetBlockIntoMap()
{
	int* blockValue = mBlock->GetBlockValue();
	int index = 0;
	for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
		for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
			if (blockValue[index] == 1) {
				if (j < 0) {
					mBlockPosition.x++;
					return;
				}
				if (j > 9) {
					mBlockPosition.x--;
					return;
				}
			}
			index++;
		}
	}
}

bool Handler::ValidateRotation()
{
	int* blockValue = mBlock->GetBlockValue();
	int index = 0;
	for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
		for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
			if (blockValue[index] == 1) {
				if (mCurrentMap[i][j] == 1) {
					return false;
				}
			}
			index++;
		}
	}
	return true;
}

핸들러가 다음 블럭을 가져오는 부분과 가져온 블럭을 조작하면서 체크해야 될 부분의 코드.

 

SetNextBlock()함수로 다음 블록을 Game Manager 객체로부터 가져오고.

핸들러가 블럭을 조작하면서 필요한 맵의 정보들을 내부 멤버변수에 저장.

mCurrentMap은 현재 조작중인 블럭을 제외한 쌓여있는 블럭들만의 정보를 저장하고

블럭을 조작하면서 해당 위치에 블럭이 쌓여있는지, 아닌지에 대한 정보를 알기 위해 사용되고.

void Handler::UpdateRealtimeMap(GameManager *GM)  // 매 프레임마다 RealtimeMap에 블록 그래픽 덮어씌우기
{
	memcpy(mRealtimeMap, GM->GetMapValue(), MAP_WIDTH * MAP_HEIGHT * sizeof(int));
	int* blockValue = mBlock->GetBlockValue();
	int index = 0;
	for (int i = mBlockPosition.y; i < mBlockPosition.y + 4; i++) {
		for (int j = mBlockPosition.x; j < mBlockPosition.x + 4; j++) {
			if (blockValue[index] == 1)
				mRealtimeMap[i][j] = 1;
			index++;
		}
	}
}

mRealtimeMap은 쌓여있는 블럭들의 정보와 현재 조작중인 블럭을 포함한 정보를 저장한다.

게임이 화면에 출력될 때 쌓여있는 블럭과 함께 현재 조작하는 블럭도 같이 출력되어야 하기 때문

해당 정보를 저장하기 위해 사용된다.

 

이 외 나머지 함수들은 조작중인 블럭이 맵 끝에 존재하거나, 다른 블럭에 막히거나 

회전했을 때 다른 블럭과 겹치거나 블럭이 맵 밖으로 나가는 경우를 방지하기 위한 Flag 함수이다.

 

void Handler::Rotate()
{
	mBlock->Rotate();   //일단 rotate.
	GetBlockIntoMap(); // 로테이션 후 블록이 맵 밖으로 나갔을 경우
	GetBlockIntoMap(); // 맵 안으로 밀어넣기
	if (!ValidateRotation()) { // rotate 이후 겹치는 블록이 있을 경우 원상태로 복구
		mBlock->RotateReverse();
	}
}

void Handler::MoveLeft()
{
	if (!CheckSideBlocking(1))
		mBlockPosition.x--;
}

void Handler::MoveRight()
{
	if (!CheckSideBlocking(2))
		mBlockPosition.x++;
}

// 블록이 쌓인 후의 로직은 Running에서 down의 리턴값에 따라서 결정.
int Handler::MoveDown()    // down이 리턴하는 값에 따라서 down하느냐 맵을 갱신하느냐 결정.
{
	if (!CheckBlocking()) {
		mBlockPosition.y++;
		return 0;
	}
	else {
		cout << "Blocked !! " << endl;
		return 1;
	}
}

void Handler::Drop()
{
	while (!MoveDown());
}

블럭을 조작하는 함수들.

정확한 로직을 따지자면 블럭 객체 자체는 움직이지 않는다.

mBlockPosition의 x, y좌표만 더하거나 빼주고 해당 좌표에 블럭을 갖다 넣고 조건을 체크하거나 출력할 뿐이다.

 

Rotate()는 회전 후 블럭이 이상한 위치에 있을 경우 이를 보완한다.

맵 밖으로 나갈 경우 맵 안으로 밀어넣고

다른 블럭과 겹치는 경우 반대로 돌려서 원상복귀시킨다.(회전 후 겹치는 경우 아예 회전이 안되는 것과 같음.)

MoveLeft(), MoveRight()는 단순히 블럭을 왼쪽, 오른쪽으로 움직이는데

이미 다른 블럭이 쌓여 있지 않고, 맵의 끝부분이 아니어야 한다.

MoveDown()은 아래로 움직인다. 아래로 움직이지 못하는 경우(블럭이 쌓여야 하는 경우)

1을 리턴하여 게임 루프쪽에서 관련 함수들을 실행할 수 있도록 flag의 역할을 하기도 한다.

Drop()은 간단. MoveDown()이 1을 리턴할 때까지 반복.

-----------------------------------------------------------------------------------------

GameRunner::GameRunner()
	: start(0)
	, end(0)
	, mInput(0)
	, mBlockTouchedFlag(0)
{
	HD.SetNextBlock(&GM);
}

void GameRunner::RenderMap()
{
	HD.UpdateRealtimeMap(&GM);
	RD.GetMapValue(&HD);
	RD.Render();
}

void GameRunner::Run()
{
	double gapTime = 0.0;
	double interval = 0.0;
	while (true) {

		start = GetTickCount();

		if (_kbhit()) {
			GetKeyboardInput();
			RenderMap();
		}
		if (interval > 0.7) {
			interval -= 0.7;
			mBlockTouchedFlag = HD.MoveDown(); //Flag가 1리턴 시 블럭이 쌓임.
			if (mBlockTouchedFlag) { 
				GM.UpdateMap(HD.GetX() + 1, HD.GetY() + 1, HD.GetCurrentBlock().GetBlockValue());
				GM.ClearLine();
				GM.PopBlock();
				HD.SetNextBlock(&GM);
			}
			RenderMap();
		}
		end = GetTickCount();

		gapTime = (end - start) / 1000.0;
		interval += gapTime;
	}
}

void GameRunner::GetKeyboardInput()
{
	mInput = _getch();
	switch (mInput) {
	default:
		break;
	case 72:            // 위 방향키
		HD.Rotate();  
		break;
	case 75:            // 왼쪽 방향키
		HD.MoveLeft();
		break;
	case 77:            // 오른쪽 방향키
		HD.MoveRight();
		break;
	case 80:            // 아래쪽 방향키
		HD.MoveDown();
		break;
	case 32:            // 스페이스 바
		HD.Drop();
		break;
	}
}

 

게임 루프가 돌아가는 Game Runner 클래스 코드.

유저의 입력이 들어왔을 경우에는 입력이 바로 게임에 반영되도록 렌더링해주고.

GetTickCount()함수로 시간을 재서 0.7초마다 블럭이 한칸씩 떨어지도록 한다.

mBlockTouchedFlag 변수로 블럭이 쌓이는지 안쌓이는지 확인(MoveDown함수에서의 플래그 역할)하여

GameManager의 실제 맵에 반영하고, 한줄이 완성된 경우 해당 줄을 지우고.

다음 블럭을 핸들러에 전해주기 위해 PopBlock()함수로 다음 블럭을 준비해둔다.

Game Manager에 준비되어 있는 블럭을 Handler에 SetNextBlock()함수로 가져오고.

다시 게임 루프 반복.

 

RenderMap()함수에서는 Handler 객체가 Game Manager로부터 맵을 받아 mRealtimeMap을 업데이트하고.

Renderer 객체가 Handler에 있는 mRealtimeMap을 가져와서 렌더링한다.

-----------------------------------------------------------------------------------------

void Renderer::GetMapValue(Handler *HD)
{
	memcpy(mRenderInfo, HD->GetRealtimeMapValue(), MAP_WIDTH * MAP_HEIGHT * sizeof(int));
}

void Renderer::Render()
{
	system("cls");
	for (int i = 0; i < 15; i++) {
		for (int j = 0; j < 10; j++) {
			if (mRenderInfo[i][j])
				cout << "■";
			else
				cout << "□";
		}
		cout << endl;
	}
}

 

마지막으로 Renderer 클래스

GetMapValue() 함수로 맵 정보를 가져와 mRenderInfo 내부 멤버변수에 저장하고

Render() 함수로 mRenderInfo 멤버변수를 이용해 게임을 출력한다.