본문 바로가기
자바스크립트와 캔버스

자바스크립트와 캔버스 9, 테트리스를 만들어봐-5

1. HTML 표준 CANVAS 기술 소개 / https://itadventure.tistory.com/130

2. 자바스크립트와 CANVAS 두번째시간. 캔바스에 눈을 내리자 / https://itadventure.tistory.com/131

3. 자바스크립트와 캔버스 3번째 시간, 공튀기기 놀이 / https://itadventure.tistory.com/132

4. 자바스크립트와 캔버스 4번째 시간, 마우스의 파동을 느껴봐! | https://itadventure.tistory.com/133

5. 자바스크립트와 캔버스, 테트리스를 만들어봐-1 | https://itadventure.tistory.com/136

6. 자바스크립트와 캔버스, 테트리스를 만들어봐-2 | https://itadventure.tistory.com/139

7. 자바스크립트와 캔버스, 테트리스를 만들어봐-3 | https://itadventure.tistory.com/142

8. 자바스크립트와 캔버스, 테트리스를 만들어봐-4 | https://itadventure.tistory.com/146

◐ 9. 자바스크립트와 캔버스, 테트리스를 만들어봐-5 


지구는 둥급니다.

둥그런 지구는 혼자서 스스로 자전하며 회전하기도 하고 태양 주위를 크게 공전하기도 합니다. 지구 주위를 돌며 빙글빙글 도는 달 또한 끊임없이 회전을 합니다.

우주의 신비한 그 원리는 어찌 보면 우리가 매일 매일 겪는 기적이 아닐까요?

아마도 우리가 하늘을 날아 다니고, 물속에서도 숨을 쉴수 있고 어느 곳이든 한순간에 이동할 수 있는 순간이동 능력이 있어서 매일 매일 그 것을 경험하며 산다면 우리는 그것을 기적이라고 부르지 않을 겁니다. 그냥 일상이라고 부르겠지요 :) 아주 당연한 것처럼요.

반대로 생각해볼까요? 지금도, 하늘에서 비가 내리는 것, 무지개가 피고 수증기가 올라가 구름이 되는 것, 빛의 기술을 이용해서 TV를 볼 수 있는 일들은 어찌 보면 매일 매일 일어나는 기적이 아닐까요? :)

우주공간이 아주 희박한 확율적으로 우연히 생겨났다고 과학에서는 말하지만,

우주공간이 그렇게 우연히 생겨났다고 해도 태양계가 그렇게 질서정연하게 회전을 거듭하며 유지보수를 할 수는 없는 노릇입니다.

적어도 우주공간의 매일 매일 일어나는 그 기적은 창조주 신이 있어서 매일 매일 그렇게 규정짓지 않으면 절대로 일어날 수 없는 일이지요.

우연한 일은 한번이면 족하지, 매일 매일 계속 일어날 수는 없지 않습니까? :)

성서 / 로마서 1장 20절 말씀
"창세로부터 그의 보이지 아니하는 것들 곧 그의 영원하신 능력과 신성이
그가 만드신 만물에 분명히 보여 알려졌나니 그러므로 그들이 핑계하지 못할지니라"

지구도 그렇게 그렇게 회전하지만,

여지껏 바닥으로 하강만 하던 테트리스 블록 조각도 사실 회전할 수 있습니다. 어떻게요?

우리는 캔바스라는 우주 공간에 테트리스 게임을 창조하는 창조자이지 않습니까? :D :D :D


이번시간에는 테트리스 블록조각을 회전하는 부분을 다루도록 하겠습니다.

그 목적을 위해 소스 구조가 약간 바뀔 겁니다.

그럼 하나씩 살펴볼까요? :)

테트리스 조각 블럭의 모양은 tetrix_block 배열 변수에 담겨져 있습니다.

그런데 만일 블럭조각을 회전시키면 어떻게 될까요?

두번째 블럭을 예로 들면, 아래와 같은 모양이 될 겁니다.

이렇게 회전시킨 채로 블럭을 쌓았다고 치자면,

그 다음번에 언젠가 이 모양의 블럭이 다시 위에서 생겨날 기회가 있을 때,

이 모양에서 떨어지기 시작할 겁니다.

그러나 일반적으로 테트리스 게임은 그렇지 않습니다.

"나는 특이하게 만들거야" 생각하시는 분들이야 논외로 치더라도요 :)

그래서 처음 블럭조각 모양을 변형시키지 않기 위해서 별도의

현재 떨어지고 있는 사본의 변수가 하나 필요합니다.

5개의 블럭조각을 원본이라 생각하고

떨어지기 시작할 때, 하나의 원본을 복사한 사본이 생겨나는 방법이지요.

그러면 사본을 회전을 몇번 하든 구워 삶든 처음 모양의 블록들은 전혀 영향이 없습니다.

이를 위해 블럭모양을 보관할 사본 변수를 전역 변수로 하나 선언합니다.

// 현재 사용중인 블록
var tetrix_block_this;

그리고 Init() 함수에서 블럭번호를 지정하게 되어 있는 부분이 있었는데,

아예 바꿔서 블럭번호에 해당하는 블럭을 사본에 복사해 버리는 겁니다.

참고로 slice() 함수는 배열을 복사할 때 원본이 변경되지 않도록 배열을 참조가 아닌 복제본으로 복사하는 기능입니다. 자세한 원리는 좀 복잡하니 우선 이런게 있다고만 알아두셔도 됩니다.

// 초기화
function Init()
{
	if(init==false)
	{
                :
		tetrix_block_number=Math.floor(Math.random()*4.9);
		tetrix_block_this = tetrix_block[tetrix_block_number].slice();
	}
}

여기서 하나 기능이 바뀐 부분이 있습니다.

지난번에는 처음 시작할 때 무조건 블록번호가 1번이었으나

이번에는 랜덤(아무거나 무작위로)하게 번호가 배정되도록 변경되었습니다.

tetrix_block_number=Math.floor(Math.random()*4.9);

Math.random() 함수는 0~0.999999.. 의 범위중 아무 숫자가 생겨나는 함수인데요.

거기에 4.9를 곱하면 0~4.89999 의 랜덤 숫자가 됩니다.

그리고 Math.floor 함수로 소숫점을 잘라버리면 0~4의 숫자 중 하나가 됩니다.

5.0으로 하지 않고 4.9로 하는 이유는 아주 가끔 5가 되는 경우를 경험해서 입니다.

값이 5가 되면 6번째 블록이 되는데 6번째 블록이 없으면 자바스크립트 오류가 발생합니다. 그래서 미연에 방지하는 것이지요 :)

이제 블록조각 복사 때문에 소스가 변경되는 부분을 잠시 살펴보실까요?

우선 블록 충돌 검사 부분이 블록조각이 이 변수로 변경됩니다.

function CheckConflict()
{
	var size=tetrix_block_this.length;
	for(k=0;k<size;k+=2)
	{
		check_y = tetrix_block_y + tetrix_block_this[k];
		check_x = tetrix_block_x + tetrix_block_this[k+1];
		// 겹치는 경우
		if(check_x < 0 || check_x >=10 || check_y >= 20 || tetrix_blockbox[check_y][check_x]!=0)return true;
	}
	return false;
}

블럭조각을 쌓는 부분도 이 변수로 바뀌고,

		// 블럭을 블럭판에 박는다
		for(k=0;k<size;k+=2)
		{
			check_y = tetrix_block_y + tetrix_block_this[k];
			check_x = tetrix_block_x + tetrix_block_this[k+1];
			tetrix_blockbox[check_y][check_x]=1;
		}

새로운 블럭조각을 생성하는 부분도 일부 바뀌었지요.

		// 블럭을 다시 제일 위로 생성시키고
		tetrix_block_y=0;
		tetrix_block_x=3;
		// 블럭번호도 바꿔 주자
		tetrix_block_number=Math.floor(Math.random()*4.9);
		tetrix_block_this=tetrix_block[tetrix_block_number].slice();

그리고 떨어지는 블록을 표시하는 부분 또한 바뀌었습니다.

			// 떨어지는 블럭표시
			var size=tetrix_block_this.length;
			for(k=0;k<size;k+=2)
			{
				if(tetrix_block_y+tetrix_block_this[k]==i
				   && tetrix_block_x+tetrix_block_this[k+1]==j)
					Context.fillStyle="blue"; 
			}

이 모든 것의 목적은 바로 블럭조각을 회전시키기 위한 것이지요 :)

이제 블럭조각을 회전시키는 부분을 살펴볼까요?

블럭조각은 윗 방향 화살표키를 누르면 작동하도록 만들건데요.

좀 내용이 길기 때문에 별도로 RotateBlock()이라는 함수를 만들어서

지난 번에 만든 윗방향 화살표를 누르면 해당 함수를 호출하도록 구성합니다.

	if(event.which==38)	// 위쪽키(회전)
	{
		RotateBlock();
		onDraw();
	}

RotateBlock() 이라는 함수 안에서 블럭조각을 회전하는 기능을 만들면 됩니다.

블럭조각을 회전시키는 데에는 감안해야 할 부분이 좀 있습니다.

첫번째는 회전하는 중심점의 위치입니다.

두번째는 정사각형 모양은 회전할 필요가 없다는 것이고,

세번째는 블럭조각을 회전하는 공식입니다.

아래 그림에서 흰색을 회전 중심점이라 칩시다.

각 블럭은 (0, 1) y, x 좌표 기준으로 블럭 자신을 회전시킵니다.

작대기 블럭의 경우 상대좌표값이 ( 0,0 ), ( 0,1), (0,2), (0,3) 인데

(0,1) 중심점을 기준으로 회전하려면, 해당 좌표를 (0,0)으로 생각하고
상대좌표를 먼저 바꾸어 주어야 합니다.

(-1,1), (0,1), (1,1), (2,1)

복잡한 행렬공식이 필요할 것 같지만, 사실 공식은 간단합니다.

중심점을 기준으로 하여 각 4개의 사각형의 새로운 y좌표는 이전 x좌표가 되면 되고,

새로운 x좌표는 이전 y좌표의 마이너스 값이 되면 됩니다.

y ← x

x ← -y

더욱 간단히는 X, Y위치를 바꾸고
변경된 X만 마이너스로 바꿔준다 생각하시면 되는데요.

이를테면 (0, 0) 좌표를 하나 예로 들자면, 중심점이 (0, 1)이기 때문에 이 중심점 (0, 1)을 기준으로 생각한다면
그 상대좌표가 (0, -1)인 셈인데요.
이 상대좌표로 공식을 적용해 봅시다.

y ← x 이니, x ← -y 이니 (-1, 0)으로 바뀝니다.

그리고 나서 블록을 중심점 기준이 아닌 원래 위치로 다시 이동하면 (-1, 1) 좌표가 되는 것이지요.

그림으로 예를 들어보자면, 원본 블럭의 좌표가 아래와 같다고 볼때.

아래와 같이 좌표를 바꾸어서 생각합니다.

맨 처음에는 가로로 긴 모양이었다가 각 좌표가 아래와 같이 바뀝니다.
이 떄는 변경된 X좌표는 모두 0이므로 마이너스가 전혀 적용되지 않습니다.

그 다음 다시 90도 회전하면, 아래와 같이 이동합니다. 이 때는  변경된 X 좌표에 마이너스를 적용하게 됩니다.

다시 90도 회전하면, 아래와 같이 되지요. 중심축을 기준으로 Y좌표 높이가 -2까지 올라가벼렸습니다.

마지막으로 90도를 더 회전하면 아래와 같이 됩니다. 다시 원래 위치로 돌아오는 것이지요.

테트리스의 모든 블록을 이 규칙으로 회전시킬 수 있습니다.
그냥 바꿔주는 식으로 설명드리긴 했지만, 사실은 이 것이 90도를 회전하는 행렬공식이기도 합니다.

이런식으로 모든 사각형의 좌표를 이동하면 됩니다
처음엔 은근히 헷갈리지만 좀 더 깊이 살펴보면 모두 일치하게 되는 것을 알 수 있습니다.

위 공식을 반영한 RotateBlock() 함수는 이렇게 생겼습니다.

function RotateBlock()
{
	switch(tetrix_block_number)
	{
		case 0: case 1: case 2: case 3:
			// 첫번째 블럭
			// □□□□
			// 두번째 블럭
			// □□□
			//  □
			// 세번째 블럭
			// □□
			//  □□
			// 네번째 블럭
			//  □□
			// □□
			centerY=0; centerX=1;	// ( 0, 1 ) 지점을 중심
			break;
		case 4:
			// 다섯번째 블럭
			// □□
			// □□
			return;
	}

	// 회전
	// x ← -y
	// y ← x
	// 이전 형태를 미리 기억
	tetrix_block_save = tetrix_block_this.slice();
	for(i=0;i<tetrix_block_this.length;i+=2)
	{
		y=tetrix_block_this[i+1] - centerX;
		x=-(tetrix_block_this[i] - centerY);
		tetrix_block_this[i]=y + centerY;
		tetrix_block_this[i+1]=x + centerX;
	}

	// 충돌인 경우 원상복귀
	if(CheckConflict())
		tetrix_block_this=tetrix_block_save.slice();
}

이 함수의 경우에도 역시 회전이 불가능하면, 다시 원상태로 돌리는 부분이 적용되어 있습니다.

설명은 이쯤해서 마치고 본 강좌의 전체 소스 공개합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>캔바스. 테트리스</title>
</head>
<body>
https://blog.naver.com/ephraimdrlee 
/ 크레이의 세컨드라이프 탐구생활 네이버 블로그<br/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
// 기본 초기화
var init=false;
var myCanvas;
var Context;

var tetrix_blockbox_boxsize=25;
var tetrix_blockbox_top=50;
var tetrix_blockbox_left=280;

// tetrix_blockbox[row][col]; 20행 10열
var tetrix_blockbox;

// 테트리스 블럭박스 초기화
function tetrix_blockbox_init()
{
	// 20행 10열의 박스 생성
	tetrix_blockbox=new Array();
	for(i=0;i<20;++i)
	{
		tetrix_blockbox.push(new Array(10));
		// 모두 0으로 채운다
		for(j=0;j<10;++j)tetrix_blockbox[i][j]=0;
	}
}

// 5가지 타입 블록
var tetrix_block;

// 현재 사용중인 블록
var tetrix_block_this;

// 5가지 블럭 초기화
function tetrix_block_init()
{
	tetrix_block=new Array();

	// 첫번째 블럭
	// □□□□
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(0,2); tmp.push(0,3);
	tetrix_block.push(tmp);

	// 두번째 블럭
	// □□□
	//  □
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(0,2); tmp.push(1,1);
	tetrix_block.push(tmp);

	// 세번째 블럭
	// □□
	//  □□
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(1,1); tmp.push(1,2);
	tetrix_block.push(tmp);

	// 네번째 블럭
	//  □□
	// □□
	tmp=new Array();
	tmp.push(0,1); tmp.push(0,2); tmp.push(1,0); tmp.push(1,1);
	tetrix_block.push(tmp);

	// 다섯번째 블럭
	// □□
	// □□
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(1,0); tmp.push(1,1);
	tetrix_block.push(tmp);
}

// 현재 떨어지는 블록번호와 좌표
var tetrix_block_number=1;
var tetrix_block_x=3;
var tetrix_block_y=0;

// 초기화
function Init()
{
	if(init==false)
	{
		myCanvas=document.getElementById("MyCanvas");
		Context=myCanvas.getContext("2d");		
		init=true;
		tetrix_block_init();	// 5가지 블럭 모양 초기화
		tetrix_blockbox_init();	// 블럭상자 초기화
		tetrix_block_number=Math.floor(Math.random()*4.9);
		tetrix_block_this = tetrix_block[tetrix_block_number].slice();
	}
}

function CheckConflict()
{
	var size=tetrix_block_this.length;
	for(k=0;k<size;k+=2)
	{
		check_y = tetrix_block_y + tetrix_block_this[k];
		check_x = tetrix_block_x + tetrix_block_this[k+1];
		// 겹치는 경우
		if(check_x < 0 || check_x >=10 || check_y >= 20 || tetrix_blockbox[check_y][check_x]!=0)return true;
	}
	return false;
}

function Run()
{
	var size=tetrix_block_this.length;

	// 블럭을 한칸 떨어뜨리고
	tetrix_block_y++;

	// 겹침검사	
	if(CheckConflict())
	{
		// 다시 위로 이동시킨 다음
		tetrix_block_y--;
		// 블럭을 블럭판에 박는다
		for(k=0;k<size;k+=2)
		{
			check_y = tetrix_block_y + tetrix_block_this[k];
			check_x = tetrix_block_x + tetrix_block_this[k+1];
			tetrix_blockbox[check_y][check_x]=1;
		}

		// 블럭을 다시 제일 위로 생성시키고
		tetrix_block_y=0;
		tetrix_block_x=3;
		// 블럭번호도 바꿔 주자
		tetrix_block_number=Math.floor(Math.random()*4.9);
		tetrix_block_this=tetrix_block[tetrix_block_number].slice();
	}

	// 그리기 이벤트
	onDraw();
}

function RotateBlock()
{
	switch(tetrix_block_number)
	{
		case 0: case 1: case 2: case 3:
			// 첫번째 블럭
			// □□□□
			// 두번째 블럭
			// □□□
			//  □
			// 세번째 블럭
			// □□
			//  □□
			// 네번째 블럭
			//  □□
			// □□
			centerY=0; centerX=1;	// ( 0, 1 ) 지점을 중심
			break;
		case 4:
			// 다섯번째 블럭
			// □□
			// □□
			return;
	}

	// 회전
	// x ← -y
	// y ← x
	// 이전 형태를 미리 기억
	tetrix_block_save = tetrix_block_this.slice();
	for(i=0;i<tetrix_block_this.length;i+=2)
	{
		y=tetrix_block_this[i+1] - centerX;
		x=-(tetrix_block_this[i] - centerY);
		tetrix_block_this[i]=y + centerY;
		tetrix_block_this[i+1]=x + centerX;
	}

	// 충돌인 경우 원상복귀
	if(CheckConflict())
		tetrix_block_this=tetrix_block_save.slice();
}

// 키입력
function onKeyDown(event)
{
	if(event.which==37)	// 왼쪽키
	{
		tetrix_block_x--;
		if(CheckConflict())tetrix_block_x++;
		else onDraw();
	}
	if(event.which==39)	// 오른쪽키
	{
		tetrix_block_x++;
		if(CheckConflict())tetrix_block_x--;
		else onDraw();
	}
	if(event.which==40 || event.which==32)	// 아래쪽키, 스페이스키 
	{
		tetrix_block_y++;
		if(CheckConflict())tetrix_block_y--;
		else onDraw();
	}
	if(event.which==38)	// 위쪽키(회전)
	{
		RotateBlock();
		onDraw();
	}
}

// draw 이벤트
function onDraw()
{
	if(init==false)return;
	// 전체 테두리
	Context.strokeStyle="#000";
	Context.lineWidth=1;
	Context.strokeRect(0, 0, myCanvas.width-1, myCanvas.height-1);
	// 블럭 표시
	for(i=0;i<20;++i)
		for(j=0;j<10;++j)
		{
			if(tetrix_blockbox[i][j]==0)
				Context.fillStyle="#ccc";
			else
				Context.fillStyle="green";
			
			// 떨어지는 블럭표시
			var size=tetrix_block_this.length;
			for(k=0;k<size;k+=2)
			{
				if(tetrix_block_y+tetrix_block_this[k]==i
				   && tetrix_block_x+tetrix_block_this[k+1]==j)
					Context.fillStyle="blue"; 
			}

			x=tetrix_blockbox_left + j*tetrix_blockbox_boxsize;
			y=tetrix_blockbox_top + i*tetrix_blockbox_boxsize;
			Context.fillRect(x, y, tetrix_blockbox_boxsize-2, tetrix_blockbox_boxsize-2);
		}
}

$(document).ready(function(){
	Init();
	setInterval(Run, 500);
});

$(document).keydown(function( event ){
	onKeyDown(event);
});

</script>
<canvas id="MyCanvas" width=800 height=600>
Canvas is not supported.
</canvas>
회전 기능을 위한 초기블럭 복사 형태 추가<br/>
회전 중심점을 기준으로 회전 구현<br/>
블럭 랜덤으로 생성
<span id=debug></span>
</body>
</html>

이번 강좌의 완벽 체험 페이지는 아래에 있습니다 :)

하지만, 블럭을 쌓아 10칸을 채워도 줄이 없어지지는 않는데요.

다음 시간에 10칸을 모두 쌓으면 줄이 사라지는 부분을 다뤄보도록 하겠습니다.

http://dreamplan7.cafe24.com/canvas/cray09.php

 

캔바스. 테트리스

 

dreamplan7.cafe24.com

여기까지 읽어주시느라 수고 많으셨습니다 :)
부디 도전하시는 분은 성공하시길 바랍니다

감사합니다.

 

다음강좌 보러 가기 / https://itadventure.tistory.com/152

 

자바스크립트와 캔버스 10, 테트리스를 만들어봐-6

1. HTML 표준 CANVAS 기술 소개 / https://itadventure.tistory.com/130 2. 자바스크립트와 CANVAS 두번째시간. 캔바스에 눈을 내리자 / https://itadventure.tistory.com/131 3. 자바스크립트와 캔버스 3번째 시..

itadventure.tistory.com