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

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

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 | https://itadventure.tistory.com/148

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


테트리스 게임의 최종버전 전단계입니다 :)

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

 

캔바스. 테트리스

 

dreamplan7.cafe24.com

크레이는 레벨 8에 897점 정도이지만
순발력 좋은 여러분들은 더욱 높은 성적도 거두실 수도 있지 않을까요? ㅎㅎ

오늘 여기까지 진도가 나갈건 아니구요 :)
앞으로 2강 정도 더 나가면 진도가 모두 진행될 것 같습니다.

오늘은 현재 블럭조각이 5가지였던 부분을 2가지를 더 늘려서 7가지로,
그리고 레벨이 오를때 난이도가 점점 높아지는 부분과,

그리고 게임이 끝나면 '게임오버 - GAME OVER' 처리가 되도록 하는 부분까지 진행됩니다
소스가 꽤 많이 바뀌었기 때문에 각 부분에 대해 흝어내려가듯 설명드리는 방식으로 진행하겠습니다.

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


먼저 게임중과 게임오버라는 상태를 알아야 하기 때문에
이 변수를 담을 전역변수를 하나 선언해야 합니다.

var Mode=1; // 1=게임중, 2=게임오버

아울러 2개의 상수를 선언할텐데요.

상수는 const 선언문으로 선언해줍니다.

상수란 변하지 않는 수이기 때문에 다른 값을 대입해줄 수 없고,

다만 Mode 라는 전역 변수에 값을 대입하거나 비교할 목적으로만 사용됩니다.

그냥 직접 Mode 변수에 1, 2 를 넣는 방법도 있지만, 이렇게 함으로써 나중에 봐도 잘 기억날테고

다른 사람이 보더라도 직관적으로 무엇을 의미하는지 알 수 있게 되지요.

const MODE_GAME=1;
const MODE_GAMEOVER=2;

그리고 Run 함수에 이 부분이 추가됩니다.

블록을 한칸씩 내리기에 앞서 CheckConflict() 함수로 충돌여부를 검사하는데요.

이 때 이미 충돌해 있으면 더 내려갈 곳이 없는 상황입니다.

그렇다면 사실상 게임이 끝난 것이지요.

그럴 경우 Mode 값을 게임오버 상태로 바꿉니다.

function Run()
{
	// 블럭을 떨어뜨리지도 않았는데 충돌해 있으면
	// 게임오버
	if(CheckConflict()) 
		Mode=MODE_GAMEOVER;
          :

이어서 진행되는 블럭을 한칸 내리고, 10줄이 꽉차면 지우고 이런 부분은 게임중일 때만 처리해야지 게임오버 상태에서는 더 이상 처리할 수 없지요.

이 후 처리 부분을 모두 게임중일 경우에만 처리하도록 수정합니다.

        :
	if(Mode==MODE_GAME)
	{	
           :
    }

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

게임중일 때와 게임오버일 때는 여러가지 이벤트상황에서의 처리가 달라집니다.

우선 키보드 상하좌우 키로 입력하는 부분은 게임중일 때만 작동되야 할텐데요.

아래와 같이 if문으로 Mode 값이 게임중일 때만 작동되도록 처리를 제한해 줍니다.

// 키입력
function onKeyDown(event)
{
	// 게임중일 때만 이동 회전키가 작동
	if(Mode==MODE_GAME)
	{
		if(event.which==37)	// 왼쪽키
		{
			tetrix_block_x--;
			if(CheckConflict())tetrix_block_x++;
			else onDraw();
		}
           :
	}
}

그리고 Draw 이벤트에서도 게임이 끝난 경우 Game Over 표시를 해주는 부분이 추가되었습니다.

// draw 이벤트
function onDraw()
{
           :
    if(Mode==MODE_GAMEOVER)
	{
		Context.fillStyle="red";
		Context.fillText("GAME OVER", tetrix_blockbox_left + 40, tetrix_blockbox_top + 250);
	}
}

게임이 끝난 다음 다시 시작하려면 어떻게 해야 할 까요?

Enter 키를 누르면 다시 시작하도록 합시다.

그렇다면 onKeyDown(event) 함수에 이 부분을 추가해주면 되는데요.

게임오버일 때만 작동되도록 해주면 됩니다.

// 키입력
function onKeyDown(event)
{
             :
	// 게임오버일 때 재시작 단축키는 Enter 키
	else if(Mode==MODE_GAMEOVER)
	{
		if(event.which==13)	// Enter 키를 누르면
		{
			// 게임 재시작
			         :
		}
	}
}

"게임 재시작"이라고 써진 부분에 재시작할 수 있는 코드를 넣어야 할 텐데요.

어떤 부분들을 초기화해주면 다시 게임이 처음부터 다시 시작될까요?

이를 테면 블럭조각같은 경우는 다시 초기화해줄 필요는 없습니다.

초기화해줄 부분을 한번 점검해봅시다.

첫째. 블럭상자를 초기화해줍니다.

둘째, 그리고 새로운 블럭조각을 위쪽에 하나 만들어 놓습니다

셋째, 점수를 0점으로 바꾸어 주고요.

네번째는, Mode 변수를 다시 MODE_GAME 으로 바꾸어 게임중으로 변경해 줍니다.

뭐 이 정도면

블럭상자를 초기화해주고 블럭을 새로 생성 및 점수를 0점으로 바꾸고

Mode 변수를 MODE_GAME 으로 바꾸어 줍니다.

그러면 그 이후 알아서 다시 게임이 처음부터 시작되지요 :)

			// 게임 재시작
			tetrix_blockbox_init();	// 블럭상자 초기화
			tetrix_block_number=Math.floor(Math.random()*6.9);
			tetrix_block_this = tetrix_block[tetrix_block_number].slice();
			score=0;
			Mode=MODE_GAME;

다음으로 블록 조각이 2가지 추가되었는데요.

블록조각 추가는 아주 간단합니다.

tetrix_block_init() 함수에 추가할 블럭의 모양을 추가해주면 됩니다.

추가되는 코드는 아래와 같은데, 별 희안한 모양을 넣길 원하시면 언제든지 넣어주시면 됩니다 :)

// 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(1,0);
	tetrix_block.push(tmp);

	// 일곱째 블럭
	// □□□
	//   □
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(0,2); tmp.push(1,2);
	tetrix_block.push(tmp);
}

이번의 키포인트 중 하나는 또한 레벨링입니다.

블럭을 쌓을수록 exp(경험치)가 증가하고

경험치가 증가하면 레벨이 오릅니다.

레벨이 오르면 블럭이 떨어지는 속도 또한 빨라지지요.

경험치, 레벨, 떨어지는 속도를 위해 전역변수를 선언합니다.

var RunEvent;
var RunEventTime = 500;
var level=1;
var exp=0;

전역변수가 3개일줄 알았는데 4개이지요? RunEvent 는 타이머 이벤트 핸들러라고 해서,

시간이 지날때마다 호출하는 Run 함수를 제어하는 자동차의 핸들 같은 겁니다 :)

setInterval 함수를 실행해주는 부분을 약간 바꿔봅시다

$(document).ready(function(){
	Init();
	RunEvent = setInterval(Run, RunEventTime);
});

RunEvent 에 setInterval 의 결과를 대입해주는 건데,

이렇게 RunEvent 에 담아두면 필요할 때 자동 호출되는 부분을

정지할 수도 해제할 수도 있습니다.

여기서는 떨어지는 속도를 더 빠르게 할 목적으로 임시로 담아두는 것이지요 :)

이제 경험치와 레벨이 올라가는 부분을 살펴볼까요?

경험치가 올라가는 조건은 아주 단순화하였습니다.

그냥 한줄을 쌓을때 점수가 10점이 올라가면서 경험치도 1 올라갑니다.

         :
				// 합계가 10이면 꽉찬 것
				if(sum==10)
				{
					// 위의 내용을 아래로 복사해준다.
					for(k=i;k>0;--k)
						for(j=0;j<10;++j)
							tetrix_blockbox[k][j]=tetrix_blockbox[k-1][j];
					score+=10;

					// 난이도 레벨을 증가시키기 위해 경험치 증가
					exp++;

그러다가 경험치가 10이 되면 레벨이 1단계 올라가는 것이지요.

					if(exp>=10){
						level++; exp=0;

레벨이 올라갈 때는 그냥 숫자만 올라가는 것이 아닙니다.

바로 하강 속도가 빨라지는데요.

처음에 설정값이 500이었던 RunEventTime 값이 레벨이 올라갈 때마다 50씩 줄어듭니다.

						RunEventTime-=50;

그리고 RunEvent 의 작동을 중지합니다.

간단히 Run 함수 호출을 계속하던 것을 중지하는 것이지요.

						clearInterval(RunEvent);

왜 중지할까요? 그건 바로 속도를 재지정해서 다시 작동해주기 위함입니다 :)

이렇게 되면 0.5초마다 한번씩 호출하던 Run 함수가 레벨이 한단계 오르면,

0.45초마다 한번씩 호출, 0.4초마다 한번씩 호출, .., 나중에는 엄청나게 짧은 주기로 호출하게 됩니다. 실제 레벨 8 정도 가면 속도를 감당할 수가 없지요 :)

						RunEvent = setInterval(Run, RunEventTime);

전체적으로 설명드리고 싶지만, 너무 길어질 것 같아서,

약간은 날림으로 진행되었군요 ㅎㅎ

전체 소스 보시면서 이해가 필요하신 분은 질문해주시면 최대한 답변드리겠습니다 :)

<!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 Mode=1; // 1=게임중, 2=게임오버
const MODE_GAME=1;
const MODE_GAMEOVER=2;

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

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

var score;

var RunEvent;
var RunEventTime = 500;
var level=1;
var exp=0;

// 테트리스 블럭박스 초기화
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);

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

	// 일곱째 블럭
	// □□□
	//   □
	tmp=new Array();
	tmp.push(0,0); tmp.push(0,1); tmp.push(0,2); tmp.push(1,2);
	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");		
		tetrix_block_init();	// 5가지 블럭 모양 초기화
		tetrix_blockbox_init();	// 블럭상자 초기화
		tetrix_block_number=Math.floor(Math.random()*6.9);
		tetrix_block_this = tetrix_block[tetrix_block_number].slice();
		score=0;
		init=true;
	}
}

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_y < 0 )continue;	// y좌표가 0보다 적은 사각형은 중돌 검사를 안함
		// 겹치는 경우
		if(check_x < 0 || check_x >=10 || check_y >= 20 || tetrix_blockbox[check_y][check_x]!=0)return true;
	}
	return false;
}

function Run()
{
	// 블럭을 떨어뜨리지도 않았는데 충돌해 있으면
	// 게임오버
	if(CheckConflict()) 
		Mode=MODE_GAMEOVER;

	if(Mode==MODE_GAME)
	{	
		// 블럭을 한칸 떨어뜨리고
		tetrix_block_y++;

		// 겹침검사	
		if(CheckConflict())
		{
			// 다시 위로 이동시킨 다음
			tetrix_block_y--;
			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];
				tetrix_blockbox[check_y][check_x]=1;
			}
			// 꽉참 줄 검사
			for(i=0;i<20;++i)
			{
				// 한줄 단위로 0이 아닌 블럭을 세서 
				sum=0;
				for(j=0;j<10;++j)
					if(tetrix_blockbox[i][j]!=0)
						sum++;

				// 합계가 10이면 꽉찬 것
				if(sum==10)
				{
					// 위의 내용을 아래로 복사해준다.
					for(k=i;k>0;--k)
						for(j=0;j<10;++j)
							tetrix_blockbox[k][j]=tetrix_blockbox[k-1][j];
					score+=10;

					// 난이도 레벨을 증가시키기 위해 경험치 증가
					exp++;
					if(exp>=10){
						level++; exp=0;
						RunEventTime-=50;
						clearInterval(RunEvent);
						RunEvent = setInterval(Run, RunEventTime);
					}

				}
			}

			// 블럭을 다시 제일 위로 생성시키고
			tetrix_block_y=0;
			tetrix_block_x=3;
			// 블럭번호도 바꿔 주자
			tetrix_block_number=Math.floor(Math.random()*6.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: case 5: case 6:
			// 첫번째 블럭
			// □□□□
			// 두번째 블럭
			// □□□
			//  □
			// 세번째 블럭
			// □□
			//  □□
			// 네번째 블럭
			//  □□
			// □□
			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(Mode==MODE_GAME)
	{

		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();
		}
	}
	// 게임오버일 때 재시작 단축키는 Enter 키
	else if(Mode==MODE_GAMEOVER)
	{
		if(event.which==13)	// Enter 키를 누르면
		{
			// 재시작
			tetrix_blockbox_init();	// 블럭상자 초기화
			tetrix_block_number=Math.floor(Math.random()*6.9);
			tetrix_block_this = tetrix_block[tetrix_block_number].slice();
			score=0;
			Mode=MODE_GAME;
		}
	}
}

// draw 이벤트
function onDraw()
{
	if(init==false)return;
	// 전체 테두리
	Context.strokeStyle="#000";
	Context.lineWidth=1;
	Context.strokeRect(0, 0, myCanvas.width-1, myCanvas.height-1);
	Context.fillStyle="#fcfcfc";
	Context.fillRect(1, 1, myCanvas.width-2, myCanvas.height-2);
	// 블럭 표시
	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);
		}
	// 점수표시
	Context.font = "30px 나눔고딕";
	Context.fillStyle="#eee";
	Context.fillRect(10, 60, 250, 40);
	Context.fillRect(10, 120, 250, 40);
	Context.fillStyle="blue";
	Context.fillText("Score " + score, 50, 90);
	Context.fillText("Level " + level, 50, 150);

	if(Mode==MODE_GAMEOVER)
	{
		Context.fillStyle="red";
		Context.fillText("GAME OVER", tetrix_blockbox_left + 40, tetrix_blockbox_top + 250);
	}
}

$(document).ready(function(){
	Init();
	RunEvent = setInterval(Run, RunEventTime);
});

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

</script>

<canvas id="MyCanvas" width=800 height=600>
Canvas is not supported.
</canvas>
<br/>
블럭조각 2가지 추가<br/>
난이도가 점점 높아짐<br/>
게임오버 처리<br/>

<span id=debug></span>
</body>
</html>

금번 강좌의 실제 실행 주소는 아래와 같습니다 :)

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

 

캔바스. 테트리스

 

dreamplan7.cafe24.com

오늘 소스는 꽤 많은 부분이 진행되었습니다.
소스 내용을 이해하고자 하시는 분께서는 부분마다
반복해서 눈에 익혀두시길 권해드립니다.

도전하시는 모든 분들께서 성공하시길 바랍니다.

여기까지 읽어주셔서 감사합니다 :)

 

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

 

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

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

itadventure.tistory.com