테트리스 게임을 두 가지 방법으로 개발하면서

하나의 제품이나 기능을 다양하게 개발하는 방법을

다음과 같이 단계별로 정리하였다.

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

4. 배열기반 회전 방식의 테트리스

5. Bitmask기반 회전 방식의 테트리스

6. requestAnimationFrame


앞서 정리했던 두 가지 회전 방법 중

배열을 이용한 회전 방법을 기초로

테트리스를 구현하는 방법(4 번)에 대하여 정리한다.

테트리스 구현을 위해 가장 중요한 개념은 회전으로,

이 개념에 몇 가지 게임 관련 개념을 더 하여

실제 사용 가능한 테트리스를 구현한다.


이해를 쉽게 하기 위해

테트리스를 개발하는 과정을 다음과 같이 세분화하여 구현하면서 정리한다.


1. 하나의 도형 움직이기 (down)

2. 도형들 움직이기와 게임 종료

3. 이동 중에 도형 회전

4. 채워진 행 제거

5. 키보드 조작과 도형별 색깔 지정



개발의 첫 단계로

테트리스 도형이 위에서 아래로 내려오는 기능을 구현한다.

단순한 도형 이동이지만

구현을 위해 알아야 할 게임의 기본 개념이 있다.


게임도 동영상처럼

시간의 흐름에 따라 그림을 그려서 움직이게 보인다.

동영상에서 초당 24 프레임으로 제작되었다는 등의 말을 하듯이

게임에서도 초당 몇 번 화면을 그릴 것인지를(프레임) 결정해야 한다.

그린다(draw)는 말은

앞서 회전에서 도형을 그리는 것처럼

Canvas에 도형(rectangle)을 그리는 것을 의미한다.


테트리스 도형이 위에서 아래로 내려오는 것은

시간의 흐름(초당 몇 번)에 따라

도형을 이동 시키고 새로 그려준다는 말이 된다.

도형을 이동 시킨다는 말은 현재 그림을 그린 좌표(x,y) 값을

시간의 흐름에 따라 증가시킨다는 의미이다.

시간의 흐름을 따른다는 것은

쓰레드(Thread)나 타이머(Timer)로

지정된 시간마다 특정 함수나 작업을 호출한다는 의미이다.


정리하면,

지정된 시간마다 도형의 좌표를 증가시켜서

지정된 위치에 도형을 그리면 이동하는 것처럼 보이게 된다.


단순한 개념이지만

처음 개발하는 사람에게는 쉽지 않은 개념일 수 있다.

그래도, 배열법에서 정리한 마지막 코드

이 개념에 대한 구현을 시도해 보길 바란다.


이 코드가 제법 복잡해 보이지만

배열법에서 정리한 마지막 코드

다음의 코드를 추가하거나 수정한 것이다.

<canvas id="tetrisCanvas" width="200" height="400"></canvas>

<script>
~~ 생략 ~~
var sPos = {x:0, y:0};

var intervalHandler = setInterval(
    function () {
          draw();
    }, 400
);   

function draw() {
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, 200, 400);
    ctx.rect(0, 0, 200, 400);
    ctx.strokeStyle="blue";
    ctx.stroke();
   
    ctx.fillStyle = 'black';

    for (var y = 0; y < 4; y++) {
        for (var x = 0; x < 4; x++) {
            if (curShape[y][x]) {
                ctx.fillRect((sPos.x+x) * 20, (sPos.y+y) * 20, 19, 19);
            }
        }
    }
    if (sPos.y++ === 18){
        clearInterval(intervalHandler);
    }
}
</script>

테트리스 도형을 그릴 Canvas의 크기를

100 * 100 에서 200 * 400 으로 수정하였다.

위에서 아래로 내려올(도형이 움직일) 공간을 주기 위해

높이(height)를 400 으로 지정하였다 [라인 5].


이 크기 변경에 따라 Canvas를 새로 그리기 위해 지우는

도형의 크기도 200 * 400 으로 수정하였다 [라인 50].

추가적으로, 도형이 움직이는 공간을 조금 명확하게 하기 위해

(게임판을 구별하기 위해)

Canvas 외곽선을 파란(blue)색으로 표시하였다 [라인 51~53].


자바 스크립트에는 시간이 되면 지정된 함수를 호출하는 타이머로

setInterval() 함수가 제공된다.

setInterval() 함수는 42 라인에서 작성한 것처럼

400 ms (0.4초) 마다 콜백(callback) 함수를 호출하고

콜백 함수에서는 테트리스 도형을 그리는 draw()함수를 호출한다.

즉, 1 초에 2.5번 도형을 그린다.


도형의 위치(x, y)를 지정하기 위해

X와 Y 좌표를 Json으로 가지는 sPos 변수를 사용했다 [라인 40].

도형이 위에서 아래로 이동할 것이기 때문에 Y 좌표만 있어도 되지만

뒤이어 보강할 것이라 미리 선언해 두었다.


이 sPos.y 변수는 두 군데에서 사용된다.

시간이 흐를 때 마다 (draw()함수가 호출될 때 마다)

sPos.y 변수의 값을 1 씩 증가시킨다 [라인 64].

1 씩 증가하다 18 이 되면 타이머를 중지 시킨다 (clearInterval).

18 이 되면 중지하는 것은 도형이 무한이 떨어지는 것을 막기 위한 것으로

다음 예제에서 삭제할 것이니 그냥 넘어간다.


Canvas의 크기는 200 * 400 이다.

테트리스 도형을 구성하는 사각형의 크기는 20 * 20 의 크기를 가진다.

즉, sPos.y 변수의 값 1 은

Canvas에서 사각형 하나의 크기인 20 을 가지게 되고,

Canvas의 높이가 400 이니

sPos.y 변수의 값은 최대 20 (=400 / 20)을 가지게 된다.

즉, 테트리스 게임판은 높이가 20 칸(너비는 10칸)으로

구성된 바둑판 (10 * 20) 같은 형태가 된다.

그리고, 테트리스 도형은 이 바둑판 위를 한칸씩 아래로 이동(sPos.y++)하게 된다.


다음의 코드를 draw() 함수에 추가하여 실행한다.

    for (var x = 0; x <= 200; x+=20) {
        ctx.beginPath();
        ctx.moveTo(x,0);
        ctx.lineTo(x,400);
        ctx.stroke();
    }
    for (var y = 0; y <= 400; y+=20) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(200, y);
        ctx.stroke();
    }

Canvas에 20 * 20 크기의 격자를 그리는 코드로

10 * 20 개의 격자가 생긴 것을 알 수 있다.


이상의 코드는 타이머 사용방법과 [라인 42, 65]

위치 변수(sPos)의 값 1 이

Canvas에서 20의 크기를 가진다는 것이 핵심 개념으로 [라인 60],

앞서의 개념에 더하여 정리하면,

지정된 시간마다(setInterval = 400) 도형의 좌표(sPos.y)를 증가시켜서(+1)

지정된 위치(sPos.y)에 도형을 구성하는 사각형을 그리면(fillRect)

이동하는 것처럼 보이게 된다.


두번째로,

하나의 도형이 떨어져 바닥에 쌓이고

또 다른 도형이 떨어져 그 위에 쌓이는 기능을 구현한다.


이 기능을 구현하기 위해서 필요한 개념은

하나의 도형이 이동할 때 마다,

다음 행에 먼저 떨어진 다른 도형이 있는지 확인하는 기능이다.

한 칸씩 이동할 때 마다 다른 도형이 있는지 확인해서

다른 도형이 없으면 이동하고 (y++),

다른 도형이 있으면 멈추게 된다.

다음 행(정확하게는 칸)에 도형이 있는지 확인하려면

앞서 내려간 도형들에 대한 정보를 알아야 한다.

즉, 게임판 (10 * 20) 전체에 대한 정보를 가지고 있어야 한다.

게임판의 크기는 10 * 20 의 2 차원 배열로 Canvas에 그려진 격자의 갯수이다.


그림과 같이 첫 번째 도형이 내려와 바닥에 닿으면,

이 위치 정보가 어딘가(변수)에 저장되어 있어야 한다.

그리고, 하나의 도형이 이동할 때 마다 게임판을 새로 그리기 때문에

저장한 정보를 이용하여 쌓여진 도형들도 같이 그려줘야 한다.


도형의 정보를 저장하는 shapes 변수의 내용은 생략하였고 [라인 7],

앞서의 코드보다 양이 제법 많아 보이지만

핵심 코드는

게임판의 정보를 저장하는 gamePanel 배열 변수이다 [라인 12].

대부분의 코드가 이 변수를 처리하기 위해 추가 / 변경되었다.

gamePanel 배열 변수는 다음 그림과 같이

왼쪽의 게임판을

우측과 같은 2차원 배열 변수에

도형(실제로는 도형을 구성하는 사각형)이 있으면 1을 [라인 31]

없으면 0 으로 채워서 구성한다.


12 라인부터 18 라인은

gamePanel를 2차원 배열로 선언하고 초기화하는 코드이다.

앞서 2차원 배열을 slice() 함수를 이용하여 초기화 했는데

이와 같이 다소 불편하게 생성 및 초기화하는 것이

자바스크립트의 기본 특징이다.


다음으로 신경 써야 하는 것은 intersects(y, x) 함수로

다음 행(칸)에 값이 있는지 없는지를 확인한다.

파라미터로 다음 행(sPos.y+1)에 대한 정보를 받아서 확인한다.

아직 좌우 이동은 하지 않기 때문에

x좌표는 고정할 수도 있지만 일단 변수로 지정한다.

(개념상 같이 처리되어 자연스럽게 구현된다.)


intersects() 함수에서 for문이 두 번 사용되어

하나의 도형을 구성하는 세부 도형(rectangle)이 있는지 확인하고 [라인 49],

있으면 게임판 변수(gamePanel)에서

먼저 떨어진 다른 도형의 세부 도형의 정보가 있는지 확인한다 [라인 50].

다른 정보가 있으면 이동하지 말라는 의미로 true를 반환한다.

intersects() 함수는 앞서의 예제에서 사용했던,

이동 위치가 18 이면 중지하도록 작성한 코드를 제대로 구현한 코드이다 (sPos.y++ === 18).


이 문장에 3개의 조건이 더 있는 것은 [라인 50]

이동하려는 위치가 바닥을 벗어나거나 (y+i >= 20)

좌우 이동시 왼쪽으로 벗어나거나(x+j < 0)

오른쪽으로 벗어나도(x+j >= 10)

이동하지 말라는 의미로 true를 반환한다.


이전과 다르게 타이머에서 draw() 함수가 아닌

playingTetris() 함수를 호출한다 [라인 51].

playingTetris() 함수는 게임을 컨트롤 하는 함수로

적절한 작업을 처리하고,

Canvas에 도형들을 그리는 draw() 함수를 호출한다 [라인 55].


playingTetris() 함수에서는

다음 위치(sPos.y + 1)에 다른 도형이 있는지

intersects() 함수를 호출하여 확인하여 없으면 [라인 27]

현재 위치를 다음 위치로 바꾼 뒤(sPos.y++) 게임판을 다시 그린다 [라인 41, 43].


반대로 다음 위치에 도형이 있으면 [라인 28],

게임판(gamePanel) 변수에

현재 도형을 채운다는 의미로 현재 도형의 정보(1)를 채운다 [라인 31].

현재 도형(curShape)의 정보를 찾기 위해

2 개의 for문을 이용하여

4 * 4 의 2 차원 배열 정보를 모두 찾아 본 후,

배열(curShape) 값이 1 이면 [라인 30]

도형의 위치 정보(sPos)와 for 문의 인자로

gamePanel 변수에서 채워야 할 위치를 ( [sPos.y+i][sPos.x+j] ) 계산하여

도형이 있다는 의미로 1 을 지정한다 [라인 31].

하나의 도형은 4개의 1 (=사각형)로 구성되기 때문이다.

(이해가 되지 않을 경우 회전 참조)


getNextShape() 함수로 다음 도형을 구하고 [라인 34],

위치 정보(sPos)를 초기화(0, 0) 한다 [라인 35].

그리고 다시 새로운 도형과 좌표에 대하여

intersects() 함수를 호출하여 게임 종료를 확인한다 [라인 36].

다음 위치(sPos)가 0, 0 인데 true가 반환되면

게임판이 모두 찼다는 의미로 게임을 종료하게 된다 [라인 37].


draw() 함수에서는 게임판(gamePanel)의 정보가 채워져 있으면 [라인 55]

해당 위치(x * 20, y * 20)에

사각형을 그려서 도형이 있다는 것을 나타낸다[라인 66].

이것은 현재 도형을 그리는 것과 변수만 다를 뿐 같은 방식이다 [라인 71~78].


회전에서 사용했던

getNextShape() 함수를 이용하여 다음 도형을 지정한다 [라인 80].


마지막으로 위치 정보를 저장하는 변수의 초기값을 지정할 때,

x 값을 0 으로 지정하였다 [라인 11, 35].

이 값 때문에 그림과 같이 도형들이 왼쪽에 붙어서 떨어진다.

x 값을 3 (= (10 - 4) / 2) 으로 수정하여

다음 그림과 같이 현재 도형이 게임판 중앙에 오도록 수정한다.


정리하면, 게임판(canvas)에 대한 정보를 

10 * 20의 2 차원 변수 gamePanel에 저장하고,

현재 도형(curShape)을 이동할 때 마다 (sPos.y++)

gamePanel에서 해당 위치에 먼저 도착한 도형이 있는지 확인한다.

도형이 있으면, 현재 도형의 정보를 gamePanel에 넣어 주고

새로운 도형을 무작위로 지정한 뒤 위치 정보를 초기화 한다.

첫 위치(0, 0)에서 이동할 수 없으면

해당 열에 도형이 꽉 찬 것으로 게임을 종료한다.


세번째로, 내려오는 도형을 버튼을 이용하여 조작하는 기능을 구현한다.

도형을 오른쪽으로 회전시키거나

왼쪽이나 오른쪽으로 이동시키거나, 그냥 떨어지도록 구현한다.

먼저, 도형을 왼쪽 / 오른쪽 (X)으로 이동시키는 것은 간단하다.

좌우 이동은 도형을 아래로(Y)로 이동시키는 코드에서

이미 같이 구현하였기 때문에,

왼쪽은 현재 X축 변수(sPos.x) 값을 -1 하고,

오른쪽은 현재 X축 변수(sPos.x) 값을 +1 한 후,

게임판을 새로 그려주면 된다.


그냥 떨어지는 기능은

타이머를 사용하지 않고,

현재 위치 (sPos.y)에서 바닥(20)에 닿을 때 까지

1 씩 증가시키면서 게임판을 계속 새로 그려준다.


회전은 앞서서 정리한 내용중

오른쪽으로 회전하는 개념을 적용하면 된다.

<canvas id="tetrisCanvas" width="200" height="400"></canvas>
<button onclick="moveShape(-1)">Left</button>
<button onclick="rotateShape()">Rotate</button>
<button onclick="moveShape(1)">Right</button>
<button onclick="dropShape()">Drop</button>

<script>
~~ 생략 ~~
function getNextShape() {
    curShapeType = Math.floor((Math.random() * 7));
    return shapes[curShapeType]
}
~~ 생략 ~~
function moveShape(value) {
    if ( !intersects(sPos.y, sPos.x+value)) {
        sPos.x += value;
        draw();
    }
}

function dropShape() {
    for (var y=sPos.y; y<20; y++) {
        if ( !intersects(sPos.y+1, sPos.x)) {
            sPos.y++;
            draw();
        }
    }
}

function rotateShape() {
    var ret = [];
    for (var y = 0; y < 4; y++) {
        ret[y] = curShape[y].slice();
    }
   
    var size = shapeSize[curShapeType];
    for (var y = 0; y < size; y++) {
        for (var x = 0; x < size; x++) {
            ret[y][x] = curShape[(size-1) - x][y];
        }
    }
    curShape = ret;
    draw();
}

</script>

전체코드

먼저 그림과 같이 도형을 조작하는데 사용할

HTML 버튼(button) 태그를 기능에 맞추어 4 개 생성한다.


왼쪽(Left)과 오른쪽은(Right) 버튼은

-1 또는 +1 을 파라미터로 moveShape() 함수를 호출한다.

moveShape() 함수에서는 파라미터(value)로 받은 값을

도형의 위치에 더해준 뒤 (sPos.x += value),

게임판을 새로 그린다 (draw).


떨어지는(Drop) 버튼은 dropShape() 함수를 호출한다.

dropShape() 함수에서는

현재 위치에서 바닥(20)에 닿을 때까지(y=sPos.y; y<20; y++)

1씩 증가시키면서 게임판을 계속 새로 그린다 (draw).


moveShape()와 dropShape() 함수에서 공통으로

이동 전에 이동이 가능한지 확인하는

intersects() 함수를 사용하였다.

도형을 이동시킬때 마다 이동이 가능한지 확인해야 한다.

예로, 도형이 이미 왼쪽 끝(0)에 있는데,

계속 왼쪽 이동 버튼을 누르면 아무 것도 하지 않도록 한다.


회전(rotate) 버튼은 rotateShape() 함수를 호출하고,

rotateShape() 함수에서는 앞서 작성한 회전(오른쪽) 코드를 그대로 사용한다.

다만, 회전한 결과(ret)를 다시 현재 도형 변수 curShape에 넣어주고

게임판을 새로 그린다 (draw).


구현 기능이 많고 어려운 것 같지만

앞서 정리한 개념과 코드가 거의 비슷하게 작성되어

쉽게 구현할 수 있다.


이상의 코드에 버그는 아니지만 조금 부족한 코드가 있다.

해답을 설명하지 않으니 찾아서 수정해 보길 바라고,

다음 예제에 수정되어 있다. (힌트 dropShape)


네 번째는 다음 표의 19 행처럼

게임판의 한 행이 도형의 사각형으로 다 찼을 경우,

해당 행을 제거하는 기능을 구현한다.


구현하는 방법은 게임판의 정보를 가지고 있는 2차원 배열,

즉, gamePanel의 모든 행을 검사해서

하나의 행에 대한 모든 열의 값이 1 이면 해당 행을 제거한다.

해당 행을 제거하는 방법,

즉 2차원 배열에서 1차원을 제거하는 방법이 핵심이고

구현 방법도 다양하다.

다음 코드는 배열을 이용하여 구현한 예제에서 발췌한 코드이다.

var ROW_CNT = 20;
var COL_CNT = 10;

function removeRow() {
    var newRows = [];
    var k = ROW_CNT;
    for (var y = ROW_CNT-1; y>=0; y--) {
        for (var x = 0; x < COL_CNT; x++) {
            if (!gamePanel[y][x]) {
                newRows[--k] = gamePanel[y].slice();
                break;
            }
        }
    }
    for (var y = 0; y < k; y++) {
        newRows[y] = [];
        for (var x = 0; x < COL_CNT; x++)
            newRows[y][x] = 0;
    }
    return newRows;
}

전체코드

먼저, 행의 개수와 열의 개수를

숫자에서 상수로 바꾸어 작성하였다.

가급적 고정된 값은 상수로 처리하는 것이 일반적이고,

여기에서는 20 이란 숫자가 행의 개수와

도형을 구성하는 사각형의 크기를 의미하여,

두 가지로 사용되기 때문에 상수로 작성해야 한다.

지금까지는 개념을 먼저 알아야 해서 무시하고 작성했다.


현재 도형을 이동시킬 때,

다음 위치에 다른 도형이 있을 경우(intersects===true)

게임판에 현재 도형 정보를 넘긴 뒤에

게임판의 모든 행(Y축)을 검사한다 (removeRow).

즉, 여기서는 생략했지만 playingTetris() 함수에서

removeRow() 함수를 호출해서 사용하니 전체 코드에서 확인하길 바란다.


게임판(gamePanel)의 모든 행을 검사하기 위해,

행의 개수만큼 반복하면서 (for)

각 행의 열의 개수만큼 반복해서 값이 있는지 확인한다 (if).

여기에 조금의 스킬과 개념이 필요하다.


배열을 이용하여 구현한 예제에서 사용한 코드를 참고한 것으로

새로운 게임판 배열 변수(newRows)를 생성하고,

기존의 게임판 배열 변수의 각 행을 하나씩 복제(slice)해서 넣는다.

즉, 각 행의 열을 검사하다

열에서 값이 없는 열이 없으면 (if !) 해당 열을 복제한다.

값이 있을 때는 복제하지 않기 때문에

해당 행이 빠져서 삭제하는 것처럼 구현된다.


이때 새로운 게임판 배열 변수(newRows)의 행 변화를 나타내는 변수 k와

기존의 게임판 배열 변수(gamePanel)의 행 변화를 나타내는 변수 y의

변화를 잘 알아야 한다.

기존의 게임판 배열 변수(gamePanel)의 모든 행을 검사하기 때문에

y는 0 부터 개수(20)만큼 반복한다.

새로운 배열 변수 newRows는 각 행의 열 값 중

빈 값이 있을 때만 변화(감소)시키기 때문에

gamePanel의 크기(y)보다 작다.

즉 두 값의 (y-k) 차이가 삭제된 행의 개수가 된다.


다만, 검사 순서를 앞 (0)이 아닌 뒤 (19)에서 시작한다.

행이 삭제될 때,

뒤의 행이 앞으로 당겨지는 것이 아니고

앞의 행이 뒤로 당겨지는 것이기 때문이다.

다음 그림과 같이 19 행이 다 찬 상태에서

18 행이 19 행을 차지하게 된다.

이런 상태에서는 뒤에서부터 처리하는 것이 편리하지만

개인의 공부를 위해 어려운 길(?)로 가보는 것도 좋다.

앞에서부터 이동하는 것을 구현해 보길 바란다.


따라서 이렇게 복제를 한 뒤에

삭제된 행의 개수만큼 newRows의 앞부분에 추가하여

전체 개수를 맞추어 새로운 게임판 변수를 만든다.


여기서 마지막으로 알아야 할 개념은 if문의 사용이다.

하나의 행에 대하여 모든 열을 검사하는 것이 아니고,

중간에 하나라도 빈 값이 있으면 더 이상 검사하지 않고 중지한다 (break).

10 개의 모든 열을 검사하는 것보다

앞의 몇 개로 검사를 끝나는 경우가 많기 때문에

사소하지만 최적화를 위한 첫 걸음과 같은 코드 작성법이다.


마지막으로 떨어지는 현재 도형을

버튼이 아닌 키보드로 제어하는 것과

각 도형별로 다른 색상을 지정하는 기능을 구현한다.


키보드 제어는 HTML 버튼의 클릭이벤트 대신에

웹 문서의 키보드 이벤트를 이용하여 각 함수를 호출하면 된다.


도형별 색상은 구현 방법이 다양한데

여기서는 간단하게 구현한다.

게임판의 정보를 저장할 때, 도형의 종류를 저장하고

각 도형의 종류에 따라서 다른 색으로 도형을 그리면 된다.

다만, 도형에 대한 정보를 저장한다고 말하지만

도형의 정보가 아닌 도형을 구성하는 사각형의 정보가 저장된다.

    ~~ 생략 ~~

var KEY = { ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40 };
var shapeColor = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];

    ~~ 생략 ~~
   
function playingTetris() {
    if ( intersects(sPos.y + 1, sPos.x)) {
        for (var i = 0; i < curShapeSize; i++)
            for (var j = 0; j < curShapeSize; j++)
                if (curShape[i][j]) {
                    gamePanel[sPos.y+i][sPos.x+j] = curShapeType+1;
                }
       
        curShape = getNextShape();       
        sPos = {x: (COL_CNT-4) / 2, y:0};
        if ( intersects(sPos.y, sPos.x)) {
            clearInterval(intervalHandler);
            alert("Game Over");
        }
        gamePanel = removeRow();       
    } else {
        sPos.y++;
    }
    draw();
}

function draw() {
    ~~ 생략 ~~
                           
    for (var y = 0; y < gamePanel.length; y++) {
        for (var x = 0; x < gamePanel[y].length; x++) {
            if (gamePanel[y][x]) {
                ctx.fillStyle = shapeColor[gamePanel[y][x]-1];
                ctx.fillRect(x * 20, y * 20, 19, 19);
            }
        }
    }
    ctx.fillStyle = shapeColor[curShapeType];
    for (var y = 0; y < curShapeSize; y++) {
        for (var x = 0; x < curShapeSize; x++) {
            if (curShape[y][x]) {
                ctx.fillRect((sPos.x+x) * 20, (sPos.y+y) * 20, 19, 19);
            }
        }
    }
}

function keydown(ev) {
    var handled = false;
    switch(ev.keyCode) {
        case KEY.LEFT:   moveShape(-1); handled = true; break;
        case KEY.RIGHT:  moveShape( 1); handled = true; break;
        case KEY.UP:     rotateShape(); handled = true; break;
        case KEY.DOWN:   dropShape();      handled = true; break;
        case KEY.ESC:    clearInterval(intervalHandler); handled = true; break;
    }

    if (handled) {
        ev.preventDefault();
    }
}
   
document.addEventListener('keydown', keydown, false);

</script>

전체코드

기능을 구현하기 위해 기존의 코드에 추가된 코드의 양이

위 코드에 표시한 것과 같이 얼마 되지 않는다.


먼저, 키보드 제어부터 정리하면

addEventListener를 이용하여

웹 페이지 문서(document)에서 발생하는 모든 키보드 입력을 가져오도록 한다.

Keydown 이벤트에 keydown 함수(이벤트 핸들러)를 지정하였다.


이때, 눌러진 키보드 키 값(keyCode)으로

어떤 키가 눌러졌는지 확인해서

버튼에서 호출한 것과 같이 적절한 함수를 호출한다.

지정된 키가 눌러진 경우(handled = true),

preventDefault()를 호출해서 이벤트 진행을 중지 시킨다.

이미 게임에서 처리했기 때문에

해당 키를 눌렀을 때 발생할 일을 하지 않도록 하는 것이다.

예로, 웹 페이지가 디자인 된 상태에서 테트리스가 운영될 경우

상하 스크롤이 생길 수 있다.

이 경우 down 버튼을 누르면 도형이 그냥 떨어져야 하는데

페이지가 아래로 스크롤 될 수 있다.

이것을 방지하기 위해 preventDefault()를 호출한다.


제어용으로 사용할 키들은 값으로 사용하지 않고,

Json 변수 KEY를 상수처럼 사용하였다.

상수를 여러 개 선언하여 사용하는 것보다 JSon으로 사용하는 것이

쉽게 사용할 수 있고, 객체지향적인 개발 느낌도 나서 좋다.


다음으로 위 코드에서 빨간색으로 표시한 코드가

도형별로 다른 색상을 출력한다.


도형의 종류는 모든 도형 정보를 가지는 shapes 에서

보관하는 도형의 순서(O, I, T, S, X, J, L)를 의미하는 것으로,

현재 도형의 종류를 curShapeType에 0 부터 6까지의 숫자로 보관한다.

다음 그림과 같이 도형 L 은 6, S는 3 , T는 2 의 값을 가진다.

이것을 이용해서 7 가지의 HTML 색상(red, orange 등) 정보를 저장하는

배열 변수(shapeColor)에 curShapeType을 지정해서

해당 도형의 색상을 찾을 수 있다.

즉, 색상("red", "orange", "yellow", "green", "blue", "indigo", "violet")의 순서와

도형의 순서(O, I, T, S, X, J, L)를 연결하여 색상을 지정한다.

그리고, 도형을 그리기 전에 이 색을 지정하고 (fillStyle)

도형을 그리면 된다 (fillRect).


다만, 떨어지는 도형은 이렇게 그릴 수 있지만

이미 떨어진 도형, 즉 게임판에 저장된 도형은 다른 처리를 해야 한다.

기존에는 게임판에 도형에 대한 정보를 저장할 때

값이 있다는 의미로 1 을 지정했다.

이번에는 현재 도형의 종류(curShapeType)를 지정한다.

다만, curShapeType은 배열이라 0 부터 시작하는데

0 은 게임판에서 도형이 없다는 의미라

1 부터 시작하도록 curShapeType에 +1 을 해주고

그릴 때는 -1 을 해서 처리하였다.

즉, 도형 L 은 7 (6), S는 4 (3) , T는 3 (2) 의 값을 가진다.

그림으로 정리하면,

L 자 도형의 경우 떨어질 때는 6 으로,

게임판에 저장할 때는 7 로 저장하여 구현한다.

테트리스 도형을 이동할 경우와

gamePanel에 저장할 때로 나누어서 정리하였는데

유사한 기능을 2 가지로 구현해서 혼동할 수 있어 다시 정리한다.


도형이 이동할 경우에는 도형의 종류(curShapeType)가 0 부터 시작되고,

ctx.fillStyle = shapeColor[curShapeType];


도형의 정보를 저장할 때는 +1, 그릴 때는 -1로 계산해서 구현한다.

gamePanel[sPos.y+i][sPos.x+j] = curShapeType+1;

ctx.fillStyle = shapeColor[gamePanel[y][x]-1];



지금까지 배열을 이용한 회전에 몇 가지 기능을 구현하면서

간단한 테트리스를 제작하는 방법을 정리하였다.


이 과정에서 중요한 개념은 배열의 사용이다.

화면에 보여주기 위한 게임판(canvas)에 대한 정보를

어떻게 저장하고 관리하는 지가 핵심 기술이랄 수 있다.

즉, 2 차원 배열 사용법이 핵심이다.


구현한지가 오래되어서 정확하지 않지만

제법 많은 게임들(특히 보드게임)을 배열을 이용하여 개발하였던 것으로 기억하니

잘 익혀 두길 바란다.







+ Recent posts