테트리스 개발 4일차

나묘쿠·2022년 1월 11일
0

테트리스

목록 보기
4/8

저번에 코드를 정리한다고 정리를 했는데 여전히 기존 코드에서 수정할 것들이 계속 나와 새로 코드를 짤 때마다 수정해주고 있다.
먼저, 회전과 이동 함수를 x좌표 기반에서 gameblock이라는 2차원 배열을 새로 만들어 acblock 주변좌표에 블록이 있는지를 검사하는 방식으로 바뀌었다. 이동은 완전히 구현해냈지만, 회전은 단순히 코드를 고치는 문제가 아니라 어떻게 회전을 시킬지 게임 룰의 문제라 알고리즘을 아예 바꾸어야 해서 조금 더 걸릴 것 같다.

일단은, 현재 블록 다음에 나올 블록을 5개까지 보여주는 기능을 구현했고, 블록이 놓여질 시, 그 자리에 고정을 시켜 다음 블록을 내보내고 다시 다음에 나올 블록을 보여주는 식으로 구현했다. 순서는 7개의 블록이 중복이 없이 랜덤으로 나오며, 한 세트(7개)가 지날 때 마다 초기화된다.

먼저, 다음에 나올 블록을 미리 보여주기 위해선, 기존에 acblock에 있던 랜덤생성 기능을 따로 빼서 newblockrand() 라는 함수에 넣어주었다. 이때 한 세트동안 중복이 있으면 안 되는 기능을 구현하느라 꽤나 애먹었다.

먼저 currentnum=0; 을 전역변수로 선언해주어, 현재까지 출력된 블록의 개수를 표시해주고, blockset[7] 배열도 전역변수로 선언해줘서 blockset[currentnum] 와 같은 식으로 사용하여 현재까지 어떠한 블록이 사용되었는지 저장할 것이다.

int newblockrand(void)		//블록의 종류가 중첩되지 않게 랜덤생성 
{
	int i=0;
	
	if(currentnum==7)
			currentnum=0;
			
	while(1)	//중복이 아닌 블록이 나올 때 까지
	{	
		if(currentnum!=0)		//첫번째 블록이 아닐 경우 
		{
			blockset[currentnum]=rand()%7;
			
			for(i=0; i<currentnum; i++)		//이번 세트에서 중첩이 있는지 확인 
			{
				if(blockset[i]==blockset[currentnum])
					break;
			}
			
			if(i==currentnum)		//없으면 탈출 
				break;
		}
		
		else		//첫번째 블록일 경우 
		{
			blockset[currentnum]=rand()%7;
			break;
		}
	}
	
	return blockset[currentnum++];		//현재 순서의 블록 출력 후 currentnum+1 
}

이렇게 블록을 랜덤으로 지정하는 함수를 만들었다. 그 다음엔 출력될 블록을 미리 보여주는 changeac 함수를 만들 것이다. 이 함수에서 기존acblock이 멈춰서 다음 acblock으로 바꾸는 것과, 랜덤으로 블록을 지정해서 새로 넣는 것, 그리고 화면 옆에 5개의 블록을 출력하는 것 까지 할 것이다. 먼저 int blocksetprint[5][4][4]; 이라는 3차원 배열을 지정해준다. 뒤의 1, 2차원은 블록의 모양이 저장되는 곳이고 가장 앞 3차원의 [5]는 미리 보여줄 5개의 블록이 순서대로 들어갈 공간이다.

먼저, 미리 보여줄 블록을 출력하는 함수부터 만들 것이다. 이를 그대로 changeac에 넣는다면 함수 하나가 너무 많은 기능을 하고 가독성이 떨어지게 된다.

void blockprint(void)		//대기중인 블록 출력 
{
	x=40;
	y=10;		
	printsq(blocksetprint[0]);		//첫번째 대기 
	
	y=15;
	printsq(blocksetprint[1]);
	
	y=20;
	printsq(blocksetprint[2]);
	
	y=25;
	printsq(blocksetprint[3]);
	
	y=30;	
	printsq(blocksetprint[4]);		//다섯번째 대기 
}

좌표는 게임이 이루어지는 공간에서 겹치지 않는 적당한 곳에 적당한 간격을 두는 곳으로 정했다. 이때 블록이 출력되려면 잔상을 지우는 기능도 있어야 하므로 잔상을 지우는 함수도 rvprintsq를 사용하여 만들어준다.

void rvblockprint(void)		//대기중인 블록 출력 
{
	x=40;
	y=10;
	rvprintsq(blocksetprint[0]);		//첫번째 대기 
	
	y=15;
	rvprintsq(blocksetprint[1]);
	
	y=20;
	rvprintsq(blocksetprint[2]);
	
	y=25;
	rvprintsq(blocksetprint[3]);
	
	y=30;
	rvprintsq(blocksetprint[4]);		//다섯번째 대기 
}

다음은 changeac을 코딩해준다. 그냥 순서대로 blocksetprint[4]부터 [0]까지 하나하나 옮겨주면 된다. blocksetprint[0]의 값은 acblock으로 옮긴다.

void changeac(int num)		//대기중인 블록 한칸씩 앞으로 당기기 
{
	int i, j;
	
	rvblockprint();		//잔상제거 
	
	for(i=0; i<4; i++) 
	{	
		for(j=0; j<4; j++)
		{
			acblock[i][j]=blocksetprint[0][i][j];
			blocksetprint[0][i][j]=blocksetprint[1][i][j];
			blocksetprint[1][i][j]=blocksetprint[2][i][j];
			blocksetprint[2][i][j]=blocksetprint[3][i][j];
			blocksetprint[3][i][j]=blocksetprint[4][i][j];
			blocksetprint[4][i][j]=block[num][i][j];
		}
	}
	
	blockprint();		//블록출력 
}

이때 num은 랜덤으로 지정된 블록이 전달될 공간이다.

이제 main 함수에서 acblock이 처음으로 움직이기 전 부분에

	changeac(newblockrand());		//처음 대기 블록 5개와 첫 활성화 될 블록 지정 
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());

이렇게 여섯번 사용하여 blocksetprint와 acblock의 자리를 모두 채워준다.

자. 이제 첫 acblock이 멈춘 다음에 대기중인 블록이 acblock이 되어 내려올 준비는 되었다. 이제 acblock이 작동을 멈추어야 할 조건에서 멈추는 함수를 만들 것이다. 이때 기존 블록들과 내려오는 acblock이 겹치면 안 되니
int gameblock[21][12]; //게임화면에 블록이 있으면 1, 없으면 0

을 전역변수로 선언해주어 작동을 멈춘 블록이 있는 자리를 1로 저장하는 함수도 만들 것이다. 게임블록의 테두리들을 모두 1로 지정해주자.

void gameblockset(void)		//윗줄 제외 경계선 값 모두 1로 세팅 
{
	int i;
	
	for(i=0; i<12; i++)
		gameblock[20][i]=1;
		
	for(i=0; i<21; i++)
	{
		gameblock[i][0]=1;
		gameblock[i][11]=1;
	}
		
}

아까 얘기했듯이 내가 저번에 만든 이동함수는 x좌표에 기반하여 게임화면의 양쪽 끝 벽과의 간섭만 확인할 수 있는 함수였다. 이제 그 함수를 acblock이 이동하는 좌표와 gameblock이 간섭 하는지 확인하는 함수로 고칠 것이다.

int blockcheckmoveL(void)		//좌로 이동시 간섭체크 
{
	int i, j;
	
	for(i=0; i<4; i++)
	{
		for(j=0; j<4; j++)
		{
			if(acblock[j][i]==1&&gameblock[y-11+j][(x-10)/2-1+i]==1)
				return 1; //간섭시 1 반환
		}
	}
}

int blockcheckmoveR(void)		//우로 이동시 간섭체크 
{
	int i, j;
	
	for(i=3; i>=0; i--)
	{
		for(j=0; j<4; j++)
		{
			if(acblock[j][i]==1&&gameblock[y-11+j][(x-10)/2+1+i]==1)
				return 1;
		}
	}
}

int blockcheckdown(void) //하강시 간섭체크
{
	int i, j;
	
	for(i=3; i>=0; i--)
	{
		for(j=0; j<4; j++)
		{
			if(acblock[i][j]==1&&gameblock[y-10+i][(x-10)/2+j]==1)
				return 1;
		}
	}
	
	return 0;
}

주의해야 할 부분은
if(acblock[i][j]==1&&gameblock[y-10+i][(x-10)/2+j]==1) 이다. 잘 보면 알겠지만 좌, 우, 하단 이동시에 비교되는 gameblock의 x, y 좌표의 세팅이 다르다. 각각 acblock의 칸에서 gameblock의 한 칸 바깥 칸을 비교해야 하기 때문이다. 이거 생각해낼 땐 정말 오래걸렸던 것 같은데 막상 해보니 별거 없다. 역시 뭐든지 해보고 생각하기 나름... 뭐 어쨌던간에 이렇게 이동시 간섭을 체크하는 함수를 만들고

void moveL(void)		//좌로 이동 
{
	if(blockcheckmoveL()!=1)		//간섭 없을 시 
	{
		rvprintsq(acblock);		//잔상 제거 
		x-=2;		//좌표 이동 
		printsq(acblock);		//블록 출력 
	}
}

void moveR(void)		//우로 이동 
{
	if(blockcheckmoveR()!=1)		//간섭 없을 시 
	{
		rvprintsq(acblock);		//잔상 제거 
		x+=2;		//좌표 이동 
		printsq(acblock);		//블록 출력 
	}
}

int moveD(void)		//하단으로 이동 
{	
	if(blockcheckdown()!=1)		//간섭 없을 시
	{
		rvprintsq(acblock);		//잔상 제거 
		y+=1;
		printsq(acblock);		//블록 출력  
		return 0;
	}
		
	else		//간섭 시
		return 1;	
}

이렇게 이동하는 함수를 고쳐준다. 이때 하단으로 이동할 때만 간섭시 1을 반환해주는 이유는 좌우 이동시에 간섭하면 이동을 하지 않으면 그만이지만 테트리스는 하단에서 간섭이 되면 블록이 그 자리에 고정되어야 하기 때문이다. 이렇게 만들었으니 moveac 함수도 고쳐주어야 한다. 하단으로 이동하는 함수를 만들었고, 잔상의 제거와 블록의 출력을 이동시키는 함수에서 처리해도록 했으니 다음과 같이 바꾸어 준다.

void moveac(void)		//활성화된 블록의 좌표의 이동 
{
	int i;
	x=18;
	y=11;
	
	while(1)	// 무한반복
	{	 
		for(i=0; i<100; i++)	//100*10 밀리세컨트 = 1초
		{
			if(kbhit())	//키보드 입력이 있다면
			{
				if(kb()==1);	
					continue;	//밑으로 이동할 때 지연시간 초기화(0.5초 지났을 때 밑으로 하강시 다시 1초 기다려야 1칸 하강)
			}
				
			Sleep(10);
		}
		
		if(moveD()==1)		//1초마다 한 칸씩 밑으로 내려감
			return;
	}
}

이렇게 고정하라는 신호를 받으면 moveac 함수가 종료되도록 했다. 자 이제 고정하시오! 하고 신호를 보내는 것 까지 만들었으니 신호를 받았을 때 고정하는 함수를 만들어야 한다. acblock의 현재 좌표를 기반으로 gameblock에 올바른 좌표에 저장해주자.

void setgameblock(void)
{
	int i, j;
	
	for(i=0; i<4; i++)
	{
		for(j=0; j<4; j++)
		{
			if(acblock[i][j]==1)
				gameblock[y-11+i][(x-10)/2+j]=1;
		}
	}	
}

이제 다 했다. 메인 함수를 고쳐주자. 기존에는 moveac 안에서만 계속 돌아가도록 만들었지만 이젠 moveac이 종료되고 종료되면 블록을 고정시키고, 대기중이던 블록들을 한 칸씩 앞으로 당기는 것까지 해주어야 한다. 그리고 작업이 끝나면 다시 moveac을 실행해야 한다. while(1)안에 묶어주면 딱이다. 아까 만들었던 changeac초기설정까지 같이 써주면


	changeac(newblockrand());		//처음 대기 블록 5개와 첫 활성화 될 블록 지정 
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());
	changeac(newblockrand());
    
	while(1)
	{
		moveac();
		setgameblock();
		changeac(newblockrand());
	}

와 같이 수정해주면 된다. 그럼 작업은 일단락 됐다.
.
.
.
.
.
.

근 일주일 정말 열심히 했다. 하. 사실 회전하는 것 까지 수정하고 자려고 했는데, 아까 말했듯이 단순히 회전하고 "어? 겹치네? 그럼 옆으로 좀 가" 의 문제가 아니라 게임 룰 문제이기 때문에 게임을 어떻게 설계할지부터 다시 생각해봐야했다. 그리고 블로그를 처음 시작할 때에는 그냥 글쓰기도 겸사겸사 연습하는 목적으로 했는데 개발일지 특성상 기록의 목적이 강하다 보니 잘 되지 않는다. 글에 조금 더 신경을 쓰고 싶지만 그럴 여유가 있다면 코드를 조금 더 손보는게 낫지 않나...싶다. 시간이 늦었으니 일단 말을 줄이고 자러 가야겠다. 코드를 다 만들고 나면 최종파이널정리완전판.ver 같은 정리글을 올리지 않을까 싶다. 내 성격상 마무리 작업이 꼭 필요하고 그래야 블로그를 쓰기 시작한 본 목적을 달성할 수 있을 것 같아서인데 또 귀찮아지면 어떻게 될지 모른다. 오늘은 여기까지.

profile
사고의 흐름을 따라가 보자.

0개의 댓글