테트리스 개발 2일차

나묘쿠·2021년 12월 30일
0

테트리스

목록 보기
2/8

중간에 긴 텀이 있었다. 동아리라던가 대외활동이라던가 뭐 이것저것 일이 있어 바빴지만 사실 그냥 놀았다. 모두 게으른 내 탓이다. 어쨌던간에 손에 영 잡히지 않는 테트리스 개발을 집 인터넷이 끊겨 할 게 없어 심심해 하던 차에 다시 키보드를 잡아보았다. 저번엔 게임의 베이스를 만드는 작업만 하였고 이번엔 플레이어가 움직이는 블록과 그 주변기능을 만들 것이다.

int acblock[4][4];	//활성화 된 블록

먼저 acblock 배열을 선언해주고 7개의 테트리미노 중 하나를 무작위로 끌어와 이 배열에 저장을 할 것이다. 이는 플레이어가 조종할 수 있는 플레이어블 블록이 될 것이다. 이를 위해 무작위 함수인 rand함수를 사용해야 하는데 그에 앞서 제일 먼저 헤더에 <stdlib.h>과 <time.h>를 선언해주자. <stdlib.h>는 rand함수가 포함되어있는 헤더이고, <time.h>는 time과 같이 말 그대로 시간에 관련된 함수들이 포함되어있는 헤더이다.

rand함수 사용에 time함수가 필요한 이유는 rand함수는 특정 시드값을 바탕으로 무작위 수열을 생성하는데, 이 시드값은 프로그램이 디버깅 될 때 딱 한 번 생성이 되고 다시 바뀌지 않는다. 다시 말해, 프로그램을 껐다 켜도 항상 같은 수열이 생성이 된다는 얘기이다. 이를 방지하기 위해 프로그램이 실행 될 때마다 시드값을 바꿔줘야 하는데 이때 시드값으로 사용하기 가장 좋은 값이 현재 시간의 값이기 때문에 이를 불러오기 위해 <time.h>를 선언해 주는 것이다.

srand(time(NULL));		//시간에서 시드값 끌어오기

일단 메인 함수 안에 이렇게 선언해 시드값을 생성해주고

void acran(void)
{
	int i, j;
	int num;
	
	num=rand()%7;	//블록 종류가 7개이기 때문에
	
	for(i=0; i<4; i++)
	{
		for(j=0; j<4; j++)
		{
			acblock[i][j]=block[num][0][i][j];
		}
	}
}

acran 함수를 만들어주어 acblock 배열에 테트리미노 하나를 랜덤으로 불러오도록 하였다.

그 다음엔 블록이 화면에 나타나도록 출력을 해야한다. 먼저, 블록이 처음 출력되는 좌표는 블록의 가로좌표가 x좌표 8칸을 차지한다는 것을 고려해 (16, 10)으로 정했다.

int x=16; 
int y=10;

그리고 전역변수를 다음과 같이 헤더 밑에 선언해준다. 여러 함수를 넘나들며 사용할 것이기 때문이다. 그 다음에 블록을 출력하는 함수를 넣어주자.

void printsq(int arr[4][4])
{
	int i, j;
	
	for(i=0; i<4; i++)
	{
		gotoxy(x, y+i);
		
		for(j=0; j<4; j++)
		{
			if(arr[i][j]==1)
				printf("▣");
	
			else if(arr[i][j]==0)
				printf("  ");	//스페이스 두 칸
		}
	}
}

하지만 이렇게 코드를 짜면 후에 기존에 있던 블록들이 저 새로 나오는 블록의 공백 두 칸에 덮어씌워져 보이지 않는 경우가 생기게 된다.

void printsq(int arr[4][4])
{
	int i, j;
	
	for(i=0; i<4; i++)
	{
		gotoxy(x, y+i);
		
		for(j=0; j<4; j++)
		{
			if(arr[i][j]==1)
				printf("▣");
	
			else if(arr[i][j]==0)
				gotoxy(x+2*(j+1), y+i);
		}
	}
}

그래서 이렇게 수정해주어 블록이 없는 빈 공간에 공백을 출력하며 지워나가는 것이 아닌 gotoxy를 이용해 커서가 블록이 출력될 부분으로 뛰어넘는 방식이기 때문에 기존에 있던 블록이 덮어씌워지는 것을 막을 수 있다. 그리고 블록이 계속 출력만 하며 이동하면 잔상이 남기 때문에 기존의 자리에 출력된 부분은 지워주고 이동한 자리에 새로 출력해야 하기 때문에 다음과 같이 함수를 만들었다. 블록을 출력하는 함수에서 ▣ 부분만 공백 두개로 바꾸어주었다.

void rvprintsq(int arr[4][4])	//잔상 삭제
{
	int i, j;
	
	for(i=0; i<4; i++)
	{
		gotoxy(x, y+i);
		
		for(j=0; j<4; j++)
		{
			if(arr[i][j]==1)
				printf("  ");	//스페이스 두 칸
	
			else if(arr[i][j]==0)
				gotoxy(x+2*(j+1), y+i);
		}
	}
}

그 다음엔 회전과 이동이다. 회전할 때에는 블록의 중앙에 가상의 점을 기준으로 다음과 같이 단순연산 방식으로 좌표를 회전시켜주었다.

void turnL(void)	//좌로 회전 
{
	int n=0;
	rvprintsq(acblock);	//잔상 삭제
    
	n=acblock[0][0];
	acblock[0][0]=acblock[3][0];
	acblock[3][0]=acblock[3][3];
	acblock[3][3]=acblock[0][3];
	acblock[0][3]=n;
	
	n=acblock[1][0];
	acblock[1][0]=acblock[3][1];
	acblock[3][1]=acblock[2][3];
	acblock[2][3]=acblock[0][2];
	acblock[0][2]=n;
	
	n=acblock[2][0];
	acblock[2][0]=acblock[3][2];
	acblock[3][2]=acblock[1][3];
	acblock[1][3]=acblock[0][1];
	acblock[0][1]=n;
	
	n=acblock[1][1];
	acblock[1][1]=acblock[2][1];
	acblock[2][1]=acblock[2][2];
	acblock[2][2]=acblock[1][2];
	acblock[1][2]=n;
	
    	printsq(acblock);	//회전된 모습 출력
}

블록의 이동은 아까 선언했던 전역변수 x, y를 활용해 단순히 방향키 입력을 받을 시 좌우이동은 x+=2와 x-=2, 하로 이동은 y-=1 해주면 된다.

void moveL(void)	//좌로 이동 
{
	rvprintsq(acblock);	//잔상 삭제
	
	if(x>10)	//블록이 게임 화면을 벗어나지 않도록
		x-=2;
		
	printsq(acblock);	//이동한 자리에 출력
}

우측, 하단으로 위와 비슷하게 하면 된다. 하지만 이때, 블록이 게임 화면을 벗어나지 않도록 선언한 if(x>10) 이 구문은 좌표로 블록의 위치를 제한하는데, 이 방식은 왼쪽 벽으로 붙을 때 블록이 L 모양이어서 좌측 칸이 전부 비어있음에도 밑 사진처럼 더 가지 못하게 된다.

그래서 후에 저번에 설계한 게임 테두리를 배열에 저장해주어 좌표를 계산하여 충돌여부를 연산하는 함수를 만들 것이다. 후에 이미 쌓인 블록과도 겹치지 않게 해야하기 때문에 이는 임시방편으로 생각하고 사용하자.

자 이제, 키보드를 입력 받을 때 위에서 만든 함수들을 실행하는 함수를 만들 것이다.

일반 키가 아닌 방향키와 일부 특수키를 입력받을 때 주의해야 할 것이 있다. 이들은 아스키 코드를 두 번 반환한다는 것이다. 일단 방향키는 상하좌우 모두 224를 반환 한 다음 왼쪽 키는 75, 오른쪽 키는 77, 위쪽키는 72, 아랫쪽 키는 80을 반환한다.

if(kbiht())
{
    int a = 0;
    
    a =_getch();
    {
    	if(a==224)
        {
        	if(a==75)
         	    printf("왼쪽 방향키");
        }
    }  
}
    

그래서 다음과 같은 방식으로 값을 두 번 받아야 한다. _getch() 함수는 <conio.h> 헤더 안에 있으니 이를 코드 맨 위에 선언해주도록 하자.

int kb(void)
{
	int kb=0;

	kb=_getch();
	
	if(kb==224)
	{
		kb=_getch();
			
		if(kb==75)
			moveL();	
		
		else if(kb==77)
			moveR();
			
		else if(kb==80)
		{
			moveD();
			return 1;
		}	
	}
		
	else if(kb==100)
		turnL();
		
	else if(kb==115)
		turnR();
	
	return 0;	
}

여기서 키보드 입력을 받을 때 한 가지 맹점이 있다. c언어는 절차지향 프로그래밍 언어여서 앞에 실행한 함수가 끝나기 전까진 다음 함수가 실행되지 못 한다. 하지만 게임을 만들때는 게임이 진행되는 것과 키보드 입력을 받는 것을 동시에 해야 한다. 내가 만들고 있는 테트리스는 1초에 한 칸씩 밑으로 내려가며, 키보드 입력을 받으면 회전, 이동도 동시에 가능하게 하려고 한다.

Sleep(1000);	//1초 기다리기(1000밀리세컨드)
kbhit();	//입력받기

이때 이렇게 그냥 Sleep 함수를 사용하여 1초를 기다리게 하면 그 기다리는 1초동안 키보드 입력도 받지 못 하고 게임의 다른 요소들도 진행이 되지 않는다.

for(i=0; i<100; i++) //100 * 10 = 1초(1000밀리세컨드)
{
	Sleep(10);	//10밀리세컨드 기다리기
    	kbhit();	//키보드 입력
}

하지만 이를 이렇게 구현하면 1초동안 0.01초 마다 키보드 입력을 받게 된다. 1초 지연과 키보드 입력 받는 것을 동시에 할 수 있게 되는 것이다. 이를 응용하여 함수를 작성하자.

void moveac(void)
{
	int i;

	while(1) // 무한반복
	{	
		printsq(acblock); //블록 출력
		for(i=0; i<100; i++)	//100*10 밀리세컨트 = 1초
		{
			if(kbhit()) //키보드 입력이 있다면
			{
				if(kb()==1); 
					continue;	//밑으로 이동할 때 지연시간 초기화(0.5초 지났을 때 밑으로 하강시 다시 1초 기다려야 1칸 하강)
			}
				
			Sleep(10);
		}
		rvprintsq(acblock); //잔상 지우기
		y+=1; //1초마다 한 칸씩 밑으로 내려감
	}
	
}

자. 이렇게 하면 블록이 회전과 이동을 할 수 있게 됐다. 하지만 지금까지 만든 코드들로만 구현을 하면 블록은 멈추지 않고 계속 밑으로 하강만 하게 된다. 다음에 특정 부분에 도달하는 것을 감지하고 조건 충족시 정지하는 부분까지 구현해야겠다.

나름 머리 쓴답시고 열심히 키보드 잡고 했는데 막상 정리하고 보니 분량이 얼마 되지 않는다. 공부란 원래 이런 것이고 인생이란게 원래 이런건가 싶다. 앞으로도 열심히 해야지 뭐

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

0개의 댓글