티스토리 뷰

0. Flow chart

이런 개발 쪽에서 가장 중요한 Flow chart(순서도)부터 정의해봅시다.

 

Fig1. 순서도

진~~~ 짜 핵심적인 부분만 간단하게 하면 위의 순서도와 같다.

 

이거보다 더 단순하게 하기는 어려울 것 같다. 물론 저 순서도에서 박스 하나하나 마다 안에 코드는 많지만 크~게 보자면 위의 순서도와 같겠다.

 

 

전체 코드를 설명하기보다는 이 전 포스팅의

https://kibbomi.tistory.com/161

 

[C++ / 2인용] 테트리스 개발 ( Class 부분 )

학부 2학년 1학기 때 만들었던 테트리스.. 몇 년이 지나 이제야 올려봅니다. 가장 처음, 어떤 기능을 가진 Class들을 정의할 것인지 봐야 한다. GUI는 적용하지 않고 Console환경에서 실행할 것이다. Cl

kibbomi.tistory.com

각 Class부분의 역할을 생각하고

크게

① Key Handler 부분 ( 키 입력 처리하는 부분 )

② Normal step - Next step 부분 ( 키 입력이 없을 시 자동으로 실행되는 부분 )

③ 그 외 ( 다음 블록 생성, 가득 찬 블록 삭제, 등등..)

3가지로 분류하여 차례대로 들어가보자. (DFS처럼 코드를 살펴보자.)

 

1. Key Handler

Tetris class의 멤버함수인 KeyHandler(int key)는 Key입력이 있을 경우 그 Key값을 매개변수로 받아 적절한 Action을 취하는 함수이다.

 

Fig2. KeyHandler 함수가 들어있는 부분

키 입력이 있다면 어떻게 처리를 해주어야할까? Fig2의 빨간 박스 부분을 깊게 분석해보자.

예를 들어서 key를 눌렀다고 해보자

 

그러면 (1p만 실행했을 때의 기준)

① 1p의 Tetris가 실행되고 있는 동안,

② key의 입력이 있다면,

③ Controller class의 GetKey함수를 이용해서 을 파싱하고

④ KeyHandler에게 넘겨준다.

KeyHandler의 코드는 다음과 같다.

bool Tetris::KeyHandler(int key)
{
	if (key == controller_.Getkey_esc())
	{
		running_ = false;
		return true;
	}
	else if (key == controller_.Getkey_right())
	{
		cur_tetromino_->MoveRight(reference_pos_);
		return true;
	}
	else if (key == controller_.Getkey_left())
	{
		cur_tetromino_->MoveLeft(reference_pos_);
		return true;
	}
	else if (key == controller_.Getkey_rotate())
	{
		cur_tetromino_->Rotate(reference_pos_);
		return true;
	}
	else if (key == controller_.Getkey_down())
	{
		cur_tetromino_->MoveDown(reference_pos_);
		return true;
	}
	else if (key == controller_.Getkey_savetetromino())
	{
		if (Option::opt_save) {
			SaveTetromino();
			return true;
		}
	}
	else if (key == controller_.Getkey_space()) 
	{
		cur_tetromino_->GoBottom(reference_pos_);
		PrepareNextStep();
		return true;
	}
	return false;
}

그냥 우리가 테트리스뿐만 아니라 다른 게임을 할 때와 같이 똑같이 생각하면 된다.

esc를 누르면 게임을 종료하는 것이니 tetris객체의 running을 false로 한다.

왼쪽, 밑, 오른쪽은 현재 테트로미노(블록)를 한 칸 이동한다. 

위는 회전 블록 저장하는 key나 현재 블록을 맨 밑으로 내리는 키를 입력한 경우에는 또 그에 맞게 처리해준다.

 

자 그럼 여기서 KeyHandler가 어떻게 Key를 입력하는지 알았으니 블록이 한 칸 이동할 때 어떤 과정이 있나 보자.

 

 

2. Block Move

또 예를 들어서, 라는 Key가 들어왔고 파싱까지 완료해서 cur_tetromino -> MoveLeft(reference_pos_);를 실행시켰다고 생각해보자.

cur_tetromino는 현재 블록을 가리키는 Tetromino*형 변수이다.

reference_pos_는 화면과 배열의 차이를 보정하기 위해 즉, 실제 콘솔 화면 좌표와 데이터를 관리하는 배열 사이의 차이를 보정하기 위한 Point형 변수이다.

 

그럼 어떤 절차가 있어야 할까?

① 움직일 Block의 중심, 기준이 되는 한 점의 x좌표를 -1 한다. (왼쪽이니까 -1, 즉 왼쪽으로 한 칸 움직인다는 의미.)

② 움직인 점이 유효한 위치인가? ( 벽안으로 들어갔다던가, 다른 블록이 이미 존재하는 자리인지,, 등)

    ②-1 (YES) 유효하다면, 예전 위치의 블록들을 지우고,  현재 위치에 블록을 그린다.

    ②-2 (NO) 유효한 자리가 아니라면 다시 원래대로 돌리기 위해 x좌표를 +1 한다.

 

이러한 방식으로 왼쪽, 오른쪽, 밑, 회전이 동작한다.

일단 해보고, 유효하면 진행, 유효하지 않으면 원래대로. 와 같은 방식이다.

 

유효한 위치를 검증하는 방법은 뭐 x좌표를 통해서 간단히 구현할 수 있을 것이다. 범위 안쪽에 있으면 true 아니면 false와 같은 간단한 방법.

 

여기서 또 생기는 궁금증,

블록들을 지우고 그리는 건 어떻게 할까?이다.

 

2-1 Block Draw & Erase

블록을 그리고 지우는 것은 똑같은 방법으로 하면 된다. 블록을 그려주느냐, 공백을 그려주느냐 그 차이이기 때문이다.

자, 그러면 어떻게 블록을 그릴까? 

아까 위에서 말했듯이, 각 테트로미노 형태마다 CenterPos라는 좌표가 있었다.

 

Point g_tetromino_pattern[7][4][4] =
{
	{ { Point(0, 1), Point(0, 0), Point(0, -1), Point(0, -2) },{ Point(-2, 0), Point(-1, 0), Point(0, 0), Point(1, 0) },
	{ Point(0, 1), Point(0, 0), Point(0, -1), Point(0, -2) },{ Point(-2, 0), Point(-1, 0), Point(0, 0), Point(1, 0) } },  // I
	{ { Point(0, 1), Point(0, 0), Point(0, -1), Point(-1, -1) },{ Point(-1, 0), Point(0, 0), Point(1, 0), Point(-1, 1) },
	{ Point(0, 1), Point(0, 0), Point(1, 1), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(1, 0), Point(1, -1) } },  // J
	{ { Point(-1, 1), Point(0, 1), Point(0, 0), Point(0, -1) },{ Point(1, 1), Point(-1, 0), Point(0, 0), Point(1, 0) },
	{ Point(0, 1), Point(0, 0), Point(0, -1), Point(1, -1) },{ Point(-1, 0), Point(0, 0), Point(1, 0), Point(-1, -1) } }, // L
	{ { Point(-1, 0), Point(0, 0), Point(-1, -1), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(-1, -1), Point(0, -1) },
	{ Point(-1, 0), Point(0, 0), Point(-1, -1), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(-1, -1), Point(0, -1) } },  // O
	{ { Point(0, 1), Point(0, 0), Point(1, 0), Point(1, -1) },{ Point(0, 0), Point(1, 0), Point(-1, -1), Point(0, -1) },
	{ Point(0, 1), Point(0, 0), Point(1, 0), Point(1, -1) },{ Point(0, 0), Point(1, 0), Point(-1, -1), Point(0, -1) } },  // S
	{ { Point(0, 1), Point(-1, 0), Point(0, 0), Point(0, -1) },{ Point(0, 1), Point(-1, 0), Point(0, 0), Point(1, 0) },
	{ Point(0, 1), Point(0, 0), Point(1, 0), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(1, 0), Point(0, -1) } },  // T
	{ { Point(1, 1), Point(0, 0), Point(1, 0), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, -1) },
	{ Point(1, 1), Point(0, 0), Point(1, 0), Point(0, -1) },{ Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, -1) } }  // Z
};

 위의 코드와 같이, 이 CenterPos를 중심으로 각 블록들에 대해서 상대적인 위치를 미리 정의해놓았다.

예를 들어서 ㅁ 모양의 왼쪽 위 부분을 CenterPos라고 하면, 왼쪽 위에서부터 시계방향으로

(0,0) (0,1)

(1,0) (1,1)

과 같은 형태로 나타낼 수 있다. 그래서 CenterPos만 안다면 그냥 반복문으로 그려주기만 하면 된다.

 

void Tetromino::Erase(Point reference_pos)
{
	for (int i = 0; i < 4; i++)
	{
		if ((center_pos_ + g_tetromino_pattern[type_][rotate_][i]).GetY() >= reference_pos.GetY() + 0 && (center_pos_ + g_tetromino_pattern[type_][rotate_][i]).GetY()<reference_pos.GetY() + 20)
		{
			Point::GotoXY(reference_pos + Point::GetScrPosFromCurPos(center_pos_ + g_tetromino_pattern[type_][rotate_][i]));//
			cout << "  ";
		}
	}
}

 

그래서 위의 코드와 같이 center_pos_를 중심으로 상대적인 위치에 접근하고, 상대적인 위치를 매번 더해서 공백을 그리면서 Erase를 구현한다.

Draw와 같은 경우는 블록 Type에 맞춰서 색깔을 정하고 블록을 그리고 색깔을 다시 Default로 설정하는 부분만 다르다.

 

 

 

2. Normal step

자 그러면 이제 Key가 들어왔을 때 Key에 알맞게 Action 하는 것 까지 구현했다.

이제 normal step 쪽으로 들어가 보자.

 

Fig3. Normal step

Normal step부분의 순서도는 위 Fig3. 과 같다.

담당하는 일은 Key입력이 없을 경우 어떠한 일을 처리해준다.

여기서 어떠한 일은, 일정 시간이 경과하면 블록을 1칸 내려주는 것이다.

 

처음에 구상했던 대로, Key입력이 있다면 그에 따른 Action을 바로 해주면 된다. 하지만 Key입력이 없다면 보통의 테트리스 게임은 어떻게 플레이될까? 생각해보면 그냥 일정 시간이 되면 한 칸 내려오는 동작을 취한다. 그게 바로 이 Normal step에서 처리해줄 것이다.

 

void Tetris::RunStep(void)
{
	if (_kbhit())
	{
		int key = Controller::GetKey();
		KeyHandler(key);
	}
	else
	{
		NormalStep();
	}
}

1인용 테트리스를 실행할 경우 Tetris.Run() -> RunStep()을 실행하여 도달한다.

위의 코드는 RunStep()을 구현한 코드이다.

 

① Key입력이 있다면

    ①-1 (Yes) Key Handler에게 넘겨주기.

    ①-2 (No) NormalStep을 실행

정말 간단하다.

 

이제 NormalStep을 보자.

void Tetris::NormalStep(void)
{
	if (GetDiffTime() >= falling_speed_)
	{
		if (cur_tetromino_->MoveDown(reference_pos_))
		{
			start_time_ = clock();
		}
		else
		{	// 끝까지 내려왔음. 후처리
			PrepareNextStep();
		}
	}
}

Normal Step도 간단하다. 블록이 밑으로 한 칸 자연적으로 떨어지는 것을 구현하기 위해서는 Clock변수를 하나 갖고 있어야 한다. 즉 1 플레이어당 1개의 타이머를 갖고 있어야 한다는 의미이다.

 

그럼 이 타이머가 언제 갱신되어야 할까?

생성자에서 처음에 갱신한다. 이때 첫 번째 블록이 생성될 것이다.

그다음, 첫 번째 if문 안의 GetDiffTime을 실행하여 현재 Clock - StartClock을 구하고 그게 사전에 정의한 Falling_speed_보다 크면(이상이면) 블록을 한 칸 내린다. 

여기서, 한 칸 내렸을 때 유효한 칸인 경우 다시 타이머를 가동하여 Key입력을 기다리거나 다시 Normalstep을 실행하게 된다.

 

블록이 밑으로 한 칸 움직이면 바로 타이머를 시작한다.

그럼 한 칸을 내려갈 수 없는 경우에는 어떤 처리를 해주는지 살펴보자.

이 부분이 테트리스 구현의 마지막이 될 것이다.

 

 

3. Next(Prepare) step

Next step은 Normal step을 실행하고 나서  종료하는 조건인지, 다시 원래대로 돌아가는지 처리하는 단계라고 보면 되겠다.

 

Fig 4. Prepare Next step

Nextstep은 블록이 Normal step에 의해 한 칸 내려갈 수 없을 경우 뒷 처리를 해주도록 했다.

여기서 블록이 내려갈 수 없는 두 가지 경우에 대해서 살펴보자.

 

① 새로운 블록이 생성되는 자리에 이미 블록이 쌓여있는 경우. ( Game over )

② 그냥 쌓여있는 블록의 위에 놓으려고 하는 경우 ( 일반적인 경우 )

2가지로 구분 지을 수 있겠다.

 

일단 ②라고 가정하고 테트리스 맵을 관리하는 Board 배열에 값을 삽입하자. 0은 비어있는 거고 Type을 넣어주면 블록이 존재하는 것.

블록을 쌓았다고 가정하고 가득 찬 줄이 있는지 확인하자. 배열을 훑어보고 모든 칸이 가득 차 있다면 지워야 할 대상이 된다.

int Board::CheckLineFull(Point reference_pos)
{
	int count = 0, erased_line = 0, exp_score = 2;//exp_score은 여러줄을 동시에 깼을때 추가될 점수의 배율
	int line = 0;

	int sero, garo, temp_garo;
	for (sero = 0; sero < 19; sero++) //줄
	{
		for (garo = 0; garo < 10; garo++)//칸
		{
			if (board_[garo][sero] == EMPTY)	//sero줄이 가득차있지 않다면
				break;
		}

		//sero번째줄이 가득차있다면..
		if (garo == 10)
		{

			PlaySound(TEXT("block.wav"), NULL, SND_ASYNC);	//블럭 삭제될때 사운드.

			for (int k = 0; k < 10; k++)//garo랑같은 기능을 하는 변수k
			{
				for (line = 0; line + sero <19; line++)//i번째 줄 위로 다 한칸식 내려야 하므로 변수 line 사용
				{
					board_[k][line + sero] = board_[k][line + sero + 1];

					if (board_[k][line + sero + 1] != EMPTY)	//위에칸이 빈칸이아니라면 그려라
					{
						Point::GotoXY(reference_pos + Point::GetScrPosFromCurPos(Point(k, sero + line)));
						if (board_[k][line + sero] == 0) SHY_BLUE
						else if (board_[k][line + sero] == 1) BLUE
						else if (board_[k][line + sero] == 2) GOLD
						else if (board_[k][line + sero] == 3) GREEN
						else if (board_[k][line + sero] == 4) HIGH_GREEN
						else if (board_[k][line + sero] == 5) PURPLE
						else if (board_[k][line + sero] == 6) RED
							cout << "■";
						BLACK
					}
					else
					{
						Point::GotoXY(reference_pos + Point::GetScrPosFromCurPos(Point(k, sero + line)));
						cout << "  ";

					}
				}
			}
			count = 100;	//한줄이라도 지우게된다면 count는 100점으로 초기화
			erased_line++;//한번에 지워진 줄의 개수
						  //2줄이상 지울때 필요함

			for (temp_garo = 0; temp_garo < 10; temp_garo++)
			{
				if (board_[garo][sero + 1] == EMPTY)
					break;
			}
			if (temp_garo == 10)
				sero--;			//맨밑칸이 안지워지기 때문에 sero--해줌
		}
	}

	//여러줄을 지웠을때 보너스점수
	if (erased_line != 1)
		for (int temp = 0; temp < erased_line; temp++)
			count *= exp_score;

	return count;
}

블록을 지울 때 크게 어렵지 않다. 그냥 가로줄을 한 번 탐색해서 가득 찼다면 한 칸씩 내려주면 된다.

복사하고 출력하는 시간을 줄이고 싶으면 배열을 모두 처리해주고 마지막에 한 번만 그리는 것도 괜찮지만, 가로 세로 수가 그렇게 크지 않아서 괜찮을 것 같다.

 

자 이렇게 블록을 지우고 지운 줄의 개수를 반환받아서 스코어를 갱신시켜주자.

 

그다음, 현재의 테트로미노(블록)가 끝날 조건인지 체크해봐야 한다.

(위 의 ① 새로운 블록이 생성되는 자리(줄)에 이미 블록이 쌓여있는 경우. ( Game over ) 조건이다.)

bool Tetromino::CheckEndCondition(void)
{
	for (int i = 0; i < 9; i++)
		if (board_->Board::GetState(Point(i, 19)) != EMPTY)	//해당좌표가 비어있지않다면
			return true;	//끝날조건이다
		
	return false;
}

결과적으로 봤을 때, 여기까지 오려면

일단 더 이상 내려갈 수 없기 때문에 Next step으로 왔다.

그래서 일단 그 자리에 놓고,

가득 찬 줄을 지웠다.

끝날 조건인지 체크한다. ←

위와 같은 과정을 거쳐서 와야 한다.

 

끝날 조건인지 체크는 위의 코드처럼 '가장 맨 윗줄에 블록이 놓여 있다면 종료'로 정의했다.

 

Ranking은 단순 파일 입출력이기 때문에 생략하고,

종료단계로 간다.

종료하면 다시 Main화면 즉, 처음 시작화면으로 돌아간다.

 

그럼 끝날 조건이 아닐 때를 한 번 보자.

//끝날조건이 아니면
next_tetromino_->Tetromino::Erase(reference_pos_);
GetCurTetrominoFromNext();
cur_tetromino_->Draw(reference_pos_);
cur_tetromino_->ShadowDraw(reference_pos_, cur_tetromino_->Shadowtetromino(reference_pos_));
GenerateNextTetromino();
next_tetromino_->Draw(reference_pos_);
start_time_ = clock();

위와 같겠다.

① Next 테트로미노를 지우고, 현재 테트로미노에 대입.

② 현재 테트로미노를 그림. (Shadow는 추가 옵션이기 때문에 생략)

③ Next 테트로미노를 생성.

④ Next 테트로미노를 그린다. 

⑤ 타이머 on.

위 다섯 단계로 한다.

이 단계가 종료되면 다시 Key입력을 받거나, Normal step단계로 가서 반복하게 된다.

 

이러한 단계로 진행하게 된다.

Flow chart를 보면 그렇게 예외? 도 없고 크게 어려운 부분이 없다.

키 입력이 있으면 처리해주고 없으면 한 칸 내리는 것?

 

 

4. Main

main에서는 화면을 띄워주고,  1p를 할 것인지 2p를 할것인지 or 옵션을 수정할 건지 묻는다.

그리고 1p라면 Tetris객체를 1개 만들고. Run() 멤버 함수를 실행하고

2p라면 Tetris 객체를 2개 만들고 따로 실행시켜준다.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <conio.h>
using namespace std;
#include "Tetris.h"
#include "Option.h"

int main()
{
	srand(time(NULL));		// 난수 발생기 초기화
	int input;
	Option opt_;
	
	while (1)
	{
		input = opt_.show_main();

		if (input == 1)         // 1인용 
		{
			system("cls");
			Tetris tetris(Point(0, 0));
			tetris.Run();
		}
		else if (input == 2)              // 2인용
		{
			system("cls");
			Tetris tetris1(Point(0, 0));
			//Tetris tetris2(Point(38, 0), KEY_ESC,KEY_RIGHT,KEY_LEFT,KEY_UP,KEY_DOWN,'g','h',2);
			Tetris tetris2(Point(38, 0), KEY_ESC, '6', '4', '8', '5', 'g', 'h', 2);
			while (tetris1.IsRunning() || tetris2.IsRunning())
			{
				bool key1 = false;
				bool key2 = false;

				if (_kbhit())    // 키 입력이 있다면 
				{
					int key = Controller::GetKey();
					if (tetris1.IsRunning())
						key1 = tetris1.KeyHandler(key);
					if (tetris2.IsRunning() && !key1)
						key2 = tetris2.KeyHandler(key);
				}

				if (tetris1.IsRunning() && !key1)
					tetris1.NormalStep();
				if (tetris2.IsRunning() && !key2)
					tetris2.NormalStep();
			}
		}
		else if (input == 3)
		{
			system("cls");
			Score::ReadRanking();
			_getch();
			system("cls");
		}
		else if (input == 4)
		{
			opt_.show_option();
		}
		else
		{
			break;
			//return 0;
		}
	}


	return 0;
}

 

5. 실행 화면

초기화면

처음 시작화면이 디자인적으로 그렇게 예쁘지는 않다...

그래도 방향키로 커서 움직이는 것이나 이런 거는 구현해 두었다.

 

선택하고 싶다면 엔터 or space를 누르면 된다.

Option선택

밑 방향키를 3번 눌러서 Option을 가리키고 있는 상태

option화면

엔터를 누르고 들어가면 다음과 같이 떨어지는 곳을 미리 가르쳐주는 Ghost block 기능을 ON/OFF 할 수 있다.

다른 기능들도 마찬가지. (왼쪽 오른쪽 키를 눌러 변경하거나 , space를 눌러 변경할 수 있다.)

떨어지는 속도도 조절할 수 있다.

 

 

실행화면

밑의 검은색 블록이 떨어질 곳을 가르쳐 주는 기능을 on 했을 경우에 나타나는 블록이다.

c버튼을 누르면 현재 떨어지고 있는 블록과, 저기 옆에 저장되어있는 블록이 교환된다.

 

종료 화면

종료된 화면이다.

Y, y를 입력하면 스코어를 이름과 함께 저장할 수 있다.

 

 

 

2p

이번엔 2인용 화면을 나타내었다.

 

 

1p 게임오버

한 명의 플레이어가 게임오버되어도 계속 진행할 수 있다.

 

 


추가적으로...

조금의 안정성이나 이런 부분을 수정하고 싶다.. 

코드는 공유하니 마음껏 변경해서 자기만의 테트리스를 만들어보시거나, 이런 메커니즘으로 돌아간다는 것만 캐치하고 처음부터 개발하셔도 좋을듯합니다.

 

만약 추가적인 기능을 더 넣는다면, 아이템을 사용할 수 있는 것과 소켓 프로그래밍으로 다른 컴퓨터 사람과  같이 하는 기능을 만들면 좋겠다는 점?

뭐 맵 상태 위주로 보내주면 될 것 같으니..

 

추가적인 기능을 넣는다면 C++ 이 아닌 자바나 C#으로 만들 것 같습니다. 다 똑같은 객체지향이니 코드도 거의 활용할 수 있을듯하고...

 

Code

https://github.com/Kibbomi/Tetris

 

 

 

 

댓글
«   2024/05   »
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
Total
Today
Yesterday