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

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

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

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


이제 테트리스 막판강좌도 금번을 제외하면 1번 남았군요 :)
바로 들어가도록 하겠습니다.

이번 시간에는 그래픽을 좀 더 뽀대나게 하는 부분을 다뤄볼텐데요.
단순히 단색으로 블럭을 표시하던 지난번에 비해 최종버전처럼 이미지를 넣어 좀 더 화려한
( 요새 게임에 비하면 비할 바는 못되겠지만요 ... )
그래픽 효과를 내는 것이 목적입니다.

그렇다면 최종 버전에는 무슨 내용이 들어가냐구요?
배경 음악과 효과음이 들어갑니다 :)

이번 강좌를 위해서는 800x600 해상도의 배경이미지 1개와
23x23 해상도의 테트리스 블럭 7가지가 준비물이 필요한데요.

크레이가 준비했으니 걱정하지 않으셔도 되고,
뭐 직접 준비해주셔도 무방합니다 :)

한번 준비물을 먼저 살펴볼까요?
먼저 7가지의 테트리스 블럭 조각 이미지가 필요합니다.
크레이는 건전한 기독교인인지라 십자가 문양으로 준비했습니다.
직접 만든 거니 누구나 사용하셔도 무방합니다 ㅎㅎ

그리고 1개의 배경이미지가 필요한데요.
무료이미지를 제공하는 픽사베이에서 다운받은 이미지입니다.

이 이미지는 모두 크레이가 준비한 아래 URL에 있으니,
그대로 사용하시면 됩니다.

테트리스 블럭조각의 사각형 이미지 7개

https://i.imgur.com/zdluTLl.png

https://i.imgur.com/BCpg8vG.png

https://i.imgur.com/X4MIbXi.png

https://i.imgur.com/80Xv589.png

https://i.imgur.com/ZaQNHG6.png

https://i.imgur.com/9RXm6Tp.png

https://i.imgur.com/TnoZ1LF.png

배경이미지

https://i.imgur.com/PTrYSqH.png

이 이미지는 먼저 이미지 객체라는 걸 만들어서 이미지 URL을 정해주어야 캔버스에서 사용할 수가 있는데요.
이미지 객체를 만드는 방법은 아래와 같습니다.
실제로 전역변수로 배경이미지를 담을 변수를 이렇게 선언해 줍니다.

var bgImage = new Image();

그리고 Init() 함수 안에서 배경이미지로 들어갈 이미지 URL을 대입해주면 됩니다.

bgImage.src="https://i.imgur.com/PTrYSqH.png";

그렇다면 7가지 색상의 테트리스 블록조각은 어떻게 하면 될까요?
우선 전역 변수는 배열로 선언해놓고서,

var blockImage = Array();

이미지 객체를 만들어 배열에 push 해주는 과정을 7번 해주면 7개의 배열이 생성되는데요.
아래와 같이 간결히 초기화해줄 수 있습니다. init() 함수 안에서 실행해주면 됩니다.
이런 방식을 이미지 객체 배열이라고도 부릅니다.

for(i=0;i<7;++i)blockImage.push(new Image());

하지만 아직 7개의 이미지 객체 배열의 이미지 주소 URL 은 설정하지 않은 상태인데요
이 코드에 이어 배열 첨자를 주어가며 ( 첨자 : [0], [1], ... ) 이미지 url을 정해주면 됩니다.
그러면 각각 이미지 객체의 이미지 주소가 정해지지요.

blockImage[0].src="https://i.imgur.com/zdluTLl.png";
blockImage[1].src="https://i.imgur.com/BCpg8vG.png";
blockImage[2].src="https://i.imgur.com/X4MIbXi.png";
blockImage[3].src="https://i.imgur.com/80Xv589.png";
blockImage[4].src="https://i.imgur.com/ZaQNHG6.png";
blockImage[5].src="https://i.imgur.com/9RXm6Tp.png";
blockImage[6].src="https://i.imgur.com/TnoZ1LF.png";

이제 바뀔 부분들을 생각해봅시다.
제일먼저 배경이미지가 바뀌는데요.
onDraw() 함수에서 배경이미지를 그려주는 부분이 추가됩니다.

// draw 이벤트
function onDraw()
{
               :
	Context.drawImage(bgImage, 0, 0);

아울러 떨어지는 테트리스 블록번호가 종전에는 블럭박스에 쌓일 때
쌓인 위치에 단순히 1이란 값으로 세팅했었는데요.
이번에는 떨어진 블록번호(0~6)에 1을 더한 값으로 세팅됩니다.
1을 더해주는 이유는 0은 '블록이 없다'라는 의미로 사용되기 때문입니다.

Run() 함수에 블럭 쌓이는 부분의 코드가 아래와 같이 변경되었습니다.

      :
	// 블럭을 블럭판에 박는다
	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]=tetrix_block_number + 1;
	}
        :

그리고 마지막으로 떨어지고 있는 블럭과 쌓여 있는 블럭을 화면에 그려주는 부분이 변경되었습니다.
onDraw() 함수의 20행 10열을 반복하는 반복문 안쪽에서,

// 블럭 표시
for(i=0;i<20;++i)
	for(j=0;j<10;++j)
	{

블럭박스에 값에 따라 이번에 표시할 블럭의 종류를 결정하는 isblock 변수를 설정하는 부분이 추가됩니다.
블럭이 없으면 -1값을, 1~7값의 블럭이 존재하면 1을 빼준 0~6값을 산출합니다.

if(tetrix_blockbox[i][j]==0)
{
	Context.fillStyle="#999";
	isblock=-1;
}
else {
	isblock=tetrix_blockbox[i][j]-1;
}

블럭박스가 0일지라도 떨어지는 블록조각이 있는 경우 블록조각의 이미지를 뿌려주어야 할텐데요.
역시 떨어지는 블록조각 중 1개의 좌표가 현재 좌표와 동일할 경우
이번에 표시할 블록의 이미지 번호를 해당하는 순번으로 변경해줍니다.

// 떨어지는 블럭표시
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)
	{
		isblock=tetrix_block_number;
		break;
	}
}

그리고 블럭이미지가 -1인 경우 회색 배경이미지를 그려주고,
0~6번인 경우 블럭이미지를 그려주면 됩니다

if(isblock==-1)
	Context.fillRect(x, y, tetrix_blockbox_boxsize-1, tetrix_blockbox_boxsize-1);
else Context.drawImage(blockImage[isblock], x, y);

그러면 블럭박스에 쌓인 블럭과 떨어지는 블럭이미지가 아래와 같이 알록달록하게 보이겠지요 :)

전체 소스 공개합니다.

<!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;

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;


var bgImage = new Image();
var blockImage = Array();

// 초기화
function Init()
{
	if(init==false)
	{
		myCanvas=document.getElementById("MyCanvas");
		Context=myCanvas.getContext("2d");
		for(i=0;i<7;++i)blockImage.push(new Image());
		bgImage.src="https://i.imgur.com/PTrYSqH.png";
		blockImage[0].src="https://i.imgur.com/zdluTLl.png";
		blockImage[1].src="https://i.imgur.com/BCpg8vG.png";
		blockImage[2].src="https://i.imgur.com/X4MIbXi.png";
		blockImage[3].src="https://i.imgur.com/80Xv589.png";
		blockImage[4].src="https://i.imgur.com/ZaQNHG6.png";
		blockImage[5].src="https://i.imgur.com/9RXm6Tp.png";
		blockImage[6].src="https://i.imgur.com/TnoZ1LF.png";
		tetrix_block_init();	// 7가지 블럭 모양 초기화
		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]=tetrix_block_number + 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;
			level=1; exp=0;
			RunEventTime=500;
			clearInterval(RunEvent);
			RunEvent = setInterval(Run, RunEventTime);
		}
	}
}

// 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);
	Context.drawImage(bgImage, 0, 0);
	// 블럭 표시
	for(i=0;i<20;++i)
		for(j=0;j<10;++j)
		{
			if(tetrix_blockbox[i][j]==0)
			{
				Context.fillStyle="#999";
				isblock=-1;
			}
			else {
				isblock=tetrix_blockbox[i][j]-1;
			}
			
			// 떨어지는 블럭표시
			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)
				{
					isblock=tetrix_block_number;
					break;
				}
			}

			x=tetrix_blockbox_left + j*tetrix_blockbox_boxsize;
			y=tetrix_blockbox_top + i*tetrix_blockbox_boxsize;

			if(isblock==-1)
				Context.fillRect(x, y, tetrix_blockbox_boxsize-1, tetrix_blockbox_boxsize-1);
			else Context.drawImage(blockImage[isblock], x, y);
		}
	// 점수표시
	Context.font = "bold 30px 나눔고딕";
	Context.fillStyle="#eee";
	Context.strokeStyle="#fff";
	Context.fillStyle="blue";

	Context.fillText("Score " + score, 50, 90);
	Context.strokeText("Score " + score, 50, 90);

	Context.fillText("Level " + level, 50, 130);
	Context.strokeText("Level " + level, 50, 130);

	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/>
블럭 7가지 색상 이미지화<br/>
배경이미지 추가 / 색상 구성 일부 변경<br/>
점수, 레벨 글자 테두리 글자 형식으로 변경

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

오늘 강좌 내용까지 반영된 예제 페이지 결과는아래에서 직접 확인해보실 수 있습니다.
최종버전에서 배경음악만 빠진 것이지요 :)

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

 

캔바스. 테트리스

 

dreamplan7.cafe24.com

오늘도 여기까지 읽어주셔서 감사합니다.
이번 시간이 테트리스 게임의 제일 어려운 관문입니다.
오늘까지 내용을 반복해서 소화시키면 테트리스 게임에 대해서는 정복을 했다고 볼 수 있고
다음번에 다룰 배경음악, 효과음은 비교적 수월한 편입니다 :)

수고하셨습니다~

 

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

 

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

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

itadventure.tistory.com