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

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

 

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

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


천상의 소리를 들어보셨나요?
크레이 개인적으로 소향이란 가수는 "
대한민국의 자랑"이라고 생각합니다 :)

다분히 개인적인 의견일 수도 있지만,
그런 개인이 생각보다 의외로 많다는 사실이라는 점은 부인할 수 없는 사실입니다.

https://youtu.be/UKdiAMesWEU

음악은 참 신기합니다. 사람을 즐겁게 하기도 하고 때로는 울리기도 하지요.
사람의 마음을 뒤흔들어 놓는가 하면, 영혼을 자극하여 감동의 눈물을 흘리게도 합니다.

드디어 테트리스 강좌의 끝판 왕(?)인데요 :)

이번 시간에는 배경음악과 효과음에 대해서 다루어 보도록 하겠습니다.
단순한 게임이라도 아무 소리 없이 플레이하는 것과 무언가 음악이 들려오는 것과
어떤 액션에 대해 효과음이 나지 않는 것과 나는 것과는 차이가 있습니다.
그것도 약간 차이가 있는게 아니라 엄청난 차이가 있습니다.

이런 연구 보고가 있습니다.
사람의 눈은 처음 보는 것에는 흥미를 가지지만, 똑같은 것을 계속 보는 것에는 싫증을 내게 마련이라고 합니다
하지만 사람의 귀는 처음 보는 것에 낳설어 하지만, 똑같은 음악을 계속 듣노라면 어느새 그 소리를 듣고 싶어 합니다.

그래서 항상 새 음악이 나오면 대부분 처음에는 별로다~ 라고 생각하면서도
몇번인가 듣게 되면 듣는 귀가 익숙해져서 더 듣고 싶어하는 까닭이기도 합니다.

배경음악과 효과음은 작동적인 부분에는 아무 영향이 없으면서도,
사람의 귀에는 상당히 중요한 역활을 하는 부분이기도 하지요.


배경음악이나 효과음도 이미지처럼 하나의 객체입니다.
바로 오디오 객체라고 하는데요.
오디오 객체이미지 객체처럼 자바스크립트로 오브젝트 선언을 하고,
오디오 소스 파일의 URL 경로를 정해주는 것으로 사용할 수 있습니다.
URL 경로에 해당하는 음악의 종류는 wav 파일이나 mp3 파일을 사용할 수가 있지요.

먼저 전역변수로 오디오 객체가 될 변수를 선언해주어야 합니다.
아래 소스가 오디오 객체를 선언하는 부분인데 앞에서부터 배경음악, 블록 쌓는 소리,
블록 10개 꽉찼을때 블럭이 사라지는 소리입니다.

var bgSound = new Audio();
var blockSound = new Audio();
var block10Sound = new Audio();

그리고 초기화하는 Init() 함수에 오디오 소스 파일의 경로를 정해주면 됩니다.

bgSound.src="http://dreamplan7.cafe24.com/canvas/sound/bensound-summer.mp3";
bgSound.volume=0.2;
bgSound.playbackRate=1.0;
blockSound.src="http://dreamplan7.cafe24.com/canvas/sound/sound55.mp3";
block10Sound.src="http://dreamplan7.cafe24.com/canvas/sound/sound39.mp3";

보충 설명드리자면 오디오 객체는 재생할 소리의 크기나 재생 속도를 마음대로 정해줄 수가 있는데요.
만일 배경음악의 소리크기를 30%로 작게 재생하려면 아래와 같이 오디오 객체명에
'.volume'이라는 명칭을 적고 0.3값을 대입해주면 됩니다.

bgSound.volume=0.3;

이러한 과정을 속성값을 변경한다고 하는데요.
이 경우를 'bgSound 객체의 volume 속성값을 0.2로 대입'이라고 표현하기도 합니다.

그렇다면 재생 속도를 2배속으로 변경하려면요?
아래와 같이 'bgSound 객체의 playbackRate 속성값을 2.0으로 대입' 해주시면 됩니다.

bgSound.playbackRate=2.0;

하지만 볼륨값이나 재생속도를 바꾼다고 해서 바로 음악이나 소리가 재생되는 것은 아닙니다.

그러면 언제 음악이나 소리가 재생이 되는 걸까요?
바로 플레이버튼을 눌러주는 방법이 있는데,
오디오 객체의 플레이 버튼을 누르는 방법으로 오디오 객체의 play() 메소드를 실행해주는 방법이 있습니다.

메소드란 무엇일까요? 자바 스크립트에서 함수와 비슷한 것인데요.
바로 객체에 딸린 함수라고 표현하면 아주 적절합니다.

대개 객체명.함수(); 와 같은 방식으로 많이 사용되며,
오디오 객체에 딸린 함수로는 대표적으로 .play() 재생, .pause() 일시중지가 많이 쓰이는데
아래 URL 에 아주 자세한 안내가 있습니다.

https://www.w3schools.com/jsref/dom_obj_audio.asp

이를테면 아래와 같은 명령을 실행하면 배경음악이 1회 재생됩니다.

bgSound.play();

배경음악은 끊임없이 계속 재생이 되어야 할텐데요.
끝났는지 어떻게 판단해야 할까요?
고민할 필요가 없습니다 :)

배경음악은 계속해서 연속으로 play() 메소드를 호출하면, 처음부터 재생하는게 아니라 재생을 계속하고,
재생이 끝나고 나면 다시 처음부터 자동으로 재생이 시작됩니다.

게임오버를 제외한 게임중에만 재생하기 위해 지속적으로 실행되는 Run() 함수의 게임중 부분에 넣어주면 됩니다.
그러면 알아서 배경음악이 무한으로 반복 재생됩니다.

function Run()
{
          :
	if(Mode==MODE_GAME)
	{	
		bgSound.play();

게임오버일 때 배경음악을 중지하려면 어떻게 해야 할까요?
역시 Run() 함수 내에서, 아래와 같이 게임오버시 처리하는 부분을 추가해주는 것이지요

	else if(Mode==MODE_GAMEOVER)
	{
		bgSound.pause();
	}

블럭이 쌓일 때 블럭이 쌓이는 소리를 '턱-'하고 1회 재생하려면, 아래와 같이 실행해주면 됩니다.
블럭 쌓이는 소리는 그리 길지 않기 때문에 소리가 겹칠 일은 거의 없습니다.
겸사로 블럭이 쌓일 때 득점이 1점 더 올라가도록 수정해주었습니다 :)

// 블럭을 블럭판에 박는다
for(k=0;k<size;k+=2)
{
        :
}
blockSound.play();
score++;

블럭이 10줄 꽉 찼을 경우에도 효과음이 재생됩니다.
역시 다음 코드를 중간에 넣어주는 것으로 효과음이 '쓰슥~'하고 1회 재생이 되지요.

// 난이도 레벨을 증가시키기 위해 경험치 증가
exp++;
if(exp>=10){
      :
}
block10Sound.play();

여기까지 하면 끝난듯 합니다만,
여기서 한 단계 더 나아가 다른 효과를 넣어보았습니다.

그것은 바로 레벨이 올라가면서 난이도가 올라갈 수록 점점 긴장감을 높이는 효과를 주기 위해서,
배경음악 속도가 빨라지는 기법을 사용하였는데요.

관련 부분의 소스를 한번 보실까요?

// 난이도 레벨을 증가시키기 위해 경험치 증가
exp++;
if(exp>=10){ 
          :
	bgSound.playbackRate=1.0 + (level-1) * 0.05;
}

레벨별로 0.05단계씩 배경음악 재생속도가 빨라집니다.

1레벨에서는 1.0 배속
2레벨은 1.05 배속
3레벨은 1.1배속
4레벨은 1.15배속

위와같이 점점 빨라지는데요.
7레벨 정도되면 1.3배속 정도가 되는데 배경음악이 엄청 빠르게 느껴집니다.

이렇게 레벨별로 빨라지는 효과를 주었다면 한가지 더 수정할 부분이 있습니다.
바로 게임을 재시작할 때 재생속도를 1배속으로 원복시켜 주어야 하는 부분이지요.

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

따라하시는 분이 계시다면 여기까지 이해하시는데 어려움은 없으셨나 모르겠습니다.
크레이가 좀 대충 적은 부분들이 있어서 이해가 어려운 부분은 부끄러워(?) 마시고 질문 주시면
답변 드리도록 하겠습니다 :)

이 것으로 테트리스 만들기 소스 해설의 막을 내립니다.
전체 소스 아래에 공개하오니 참고해 주시기 바라며
아래 웹페이지 URL 에 접속하시면 최종 버전을 플레이해보실수 있습니다.

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

 

캔바스. 테트리스

 

dreamplan7.cafe24.com

여기까지 읽어주신 모든 분들께 감사의 말씀 드립니다.

테트리스 전체 소스

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

var bgSound = new Audio();
var blockSound = new Audio();
var block10Sound = new Audio();

// 초기화
function Init()
{
	if(init==false)
	{
		myCanvas=document.getElementById("MyCanvas");
		Context=myCanvas.getContext("2d");

		// 이미지 로딩
		bgImage.src="https://i.imgur.com/PTrYSqH.png";
		for(i=0;i<7;++i)blockImage.push(new Image());
		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";

		// 사운드 로딩
		bgSound.src="http://dreamplan7.cafe24.com/canvas/sound/bensound-summer.mp3";
		bgSound.volume=0.2;
		bgSound.playbackRate=1.0;
		blockSound.src="http://dreamplan7.cafe24.com/canvas/sound/sound55.mp3";
		block10Sound.src="http://dreamplan7.cafe24.com/canvas/sound/sound39.mp3";

		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)
	{	
		bgSound.play();

		// 블럭을 한칸 떨어뜨리고
		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;
			}
			blockSound.play();
			score++;

			// 꽉참 줄 검사
			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);
						bgSound.playbackRate=1.0 + (level-1) * 0.05;
					}
					block10Sound.play();
				}
			}

			// 블럭을 다시 제일 위로 생성시키고
			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();

		}
	}
	else if(Mode==MODE_GAMEOVER)
	{
		bgSound.pause();
	}

	// 그리기 이벤트
	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;
			bgSound.playbackRate=1.0;
			level=1; exp=0;
			RunEventTime=500;
			clearInterval(RunEvent);
			RunEvent = setInterval(Run, RunEventTime);
			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);
	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/>
배경음악 추가<br/>
블럭 쌓을때, 10줄 찼을 때 효과음 추가<br/>

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

 

테트리스 다음 크레이의 모험은 자바스크립트와 캔바스로 구현하는 3차원 웹에 도전합니다.

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

 

캔버스와 함께하는 자바스크립트, 13번째 시간 - 3D세계의 창조

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

itadventure.tistory.com