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

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

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

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

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

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

6. requestAnimationFrame


다소 개념이 쉬운 배열을 이용한 방식을 먼저 정리한다.

배열 방식은 이 사이트에서 제공하는 방식으로

해당 소스가 제법 복잡해서

테트리스 도형의 회전과 관련된 부분만

추출해서 다음과 같이 정리하였다.

배열을 사용한 것 외에

테트리스 도형들을 HTML DIV로 보여준다는 특징이있다.

보통 게임(개발 언어에 관계없이)은 Canvas에 그려서 제작하는데

DIV을 동적으로 생성하고 삭제하는 식으로 구현하였다.

추천하지 않는 방식이지만

시각적인 부분을 제외하면 쉽고 일반적인 개발 방법을 사용하였다.

이 부분은 이 개발자만의 특정이 아니라

많은 개발자가 사용하는 일반적인 방법이다.

(짧은 시간 찾다 보니 다른 소스를 못 찾았음).


먼저 테트리스 도형을 출력할 canvas [라인 2]와

회전 방향키[라인 3, 4],

도형의 종류를 순서 데로 보여줄 HTML 버튼을 생성한다 [라인 5].

자바 스크립트에서

shapes라는 3차원 배열 변수로 도형을 저장한다 [라인 13].

3차원은 도형 리스트,

2차원은 도형 하나에 대한 정보

1차원은 도형을 구성하는 셀 정보이다.

1차원에 있는 각 셀의 정보는

해당 위치에 도형을 구성할 정보가 있으면 1,

그냥 빈 공간으로 정보가 없으면 0을 가진다.

각 도형은 이어진 4 개의 셀로 모양이 구성되고

이 배열에서 1의 값 위치를 이어서 보면

주석으로 지정된 문자(O, I, T, S, X, J, L)와 일치하는 것을 알 수 있다 (표참조).

0 은 빈(Null)값을 의미한다.


추가적으로

3차원 배열은 7개의 2차원 배열을

2차원 배열은 4개의 1차원 배열을

1차원 배열은 4개의 원소를 가진다.

3차원 배열은 도형의 개수만큼 가지는 것이고,

1, 2차원은 도형의 모양을 x, y축으로 가지는 것이다.

각각 4개씩 4 * 4로 가지고 있다.

그리고, 각 도형은 4개의 셀(도형-사각형)로 구성되기 때문에

각 도형은 4개의 1을 가진다.


도형들(shapes) 중에서 화면에 출력할 도형을 curShape에 보관하고,

현재 도형의 종류를 curShapeType 변수에 저장한다 [라인 44].

도형의 종류는 배열 변수 shapes의 차원 위치이다.

shapes는 3차원 배열로 7개의 2차원을 가지고 있다.

이 7개의 순서(배열이라 0~6)이 도형의 종류(curShapeType )가 된다.


가장 기본적인 함수는 그리기(draw)함수 이다.

저장된 데이터를 상황에 맞추어 그리는 것이 주요 내용이기 때문이다.

draw()함수에서

canvas의 색을 흰색(white)으로 지정하고 [라인 49],

canvas의 크기(100*100)만큼 사각형(fillrect)을 그린다 [라인 50].

canvas 크기의 사각형을 그린다는 것은

canvas의 모든 내용을 지운다는 의미이다.

회전 버튼을 누를 때 마다,

매번 전체를 지우고 회전된 도형의 모양을 새로 그린다.

다음으로 색상을 검은색(black)으로 지정하고,

테트리스 도형(shape)을 그린다.

배열의 값에서 1 이 있으면 (if (curShape[y][x]) – 라인 54)

작은 도형(20*20)을 그린다 [라인 55].

자바 스크립트의 특성으로

변수의 값이 0 이거나 null, undefined 이면

IF문에서 false가 반환되어

if (curShape[y][x]===0) 과 같이 작성하지 않아도 된다.

따라서, curShape[y][x]의 값이 1 일때만

테트리스 도형을 그리기 위한 하나의 사각형을 그린다.


현재 출력하려는 도형의 정보를 가진 변수 curShape은

앞서 curShape=shapes[0]으로 [라인 44]

3차원 shapes에서 2차원을 배정 받았다.

즉, 하나의 도형 4 * 4 크기의

2 차원 배열에 대한 값을 가지고 있기 때문에

두 개의 for문을 이용하여 값을 확인한다 [라인 52, 53].


실행 화면에서 Next Shape 버튼을 클릭하면,

새로운 도형 종류를 curShapeType 변수에 저장하고

도형들(shapes) 중에서 curShapeType에 맞는 도형을 curShape에 저장한다 [라인 61].

도형 종류는 도형 정보를 가지고 있는 shapes의

색인 값(순서)을 의미하기 때문에

새로운 도형 종류는 curShapeType변수의 값에 1 더한 값으로 지정한다.

다만, 도형이 7 (배열이라 0~6)개이기 때문에

curShapeType의 값이 7 이되면 다시 0 이 되게 한다.

if문을 사용할 수 있겠지만 ( if (curShapeType===7) curShapeType=0)

나머지(%)를 구하는 방식으로 구현하였다 [라인 62].


좌우 버튼을 클릭하면

현재 도형을 회전시켜서 출력한다.

rotateLeft(), rotateRight() 함수에서 회전한 값을 반환 받아

curShape 변수에 저장하고 [라인 68, 73]

그리고, 이것을 화면에 출력한다 (draw).

코드로는 회전 방법에 대해서 이해하기 어려울 수 있다 [라인 77, 86].

먼저, 왼쪽으로 회전하는 코드를

다음 표와 같이 정리 할 수 있다.

[참고] 다음 표의 좌표는 (Y, X)로 지정되었다.

원본
왼쪽으로 회전

0123

0123
00, 00, 10, 20, 3
00, 31, 32, 33, 3
11, 01, 11, 21, 3
10, 21, 22, 23, 2
22, 02, 12, 22, 3
20, 11, 12, 13, 1
33, 03, 13, 23, 3
30, 01, 02, 03, 0

표에서 왼쪽에 있는 것이

각 도형에 대한 4 *4의 크기를 가진 2차원 배열 정보이다.

이 배열을 왼쪽으로 이동하는 것은

각 행을 열로(90도 회전) 변환하는 것을 의미한다 [오른쪽 표].

표에서 노란색으로 표시한 셀처럼,

왼쪽의 표에서 하나의 행이 그대로 90도 회전해서

오른쪽 표가 만들어 진 것을 볼 수 있다.

즉, 우측에 있는 표처럼

0, 0 의 위치에 기존의 0, 3 에 있는 데이터를

0, 1 의 위치에 기존의 0, 2 에 있는 데이터를

.....

배치하는 식으로 코드를 구성하여

새로운 배열을 생성한다 [라인 79, 88].

이상의 코드에서는

이 표의 내용을 그대로 코드로 작성하였다 [라인 79, 88].

오른쪽으로 회전하는 것은 다음 표와 같이 반대로 처리한다.

원본
오른쪽으로 회전

0123

0123
00, 00, 10, 20, 3
03, 02, 01, 00, 0
11, 01, 11, 21, 3
13, 12, 11, 10, 1
22, 02, 12, 22, 3
23, 22, 21, 20, 2
33, 03, 13, 23, 3
33, 32, 31, 30, 3

이상으로 테트리스 도형을 관리하고 회전하는 방법을 정리하였다.

테트리스에서 사용하는 7가지 도형을

    1. 3차원 배열에 저장하고

    2. 배열 값의 위치를 조작하여 회전시킨다.

마지막으로 이 배열의 값을(값이 1 이면) 이용하여

3. Canvas에 사각형을 그려서 테트리스 도형을 그려준다.

코드 양이 많아 보이지만

이 세가지의 간단한 구조로 되어 있으니 잘 기억하고

충분히 이해한 뒤에 다음 내용들을 확인하는 것이 좋다.


Bitmask 방식을 정리하기 전에

배열 방식의 회전에 대한 몇 가지 보강을 정리한다.

먼저, 이상의 코드에서 사용한 것과 같이 [라인 78, 87]

배열의 위치를 직접 지정해서 구현하는 것은 가변성이 떨어진다.

즉, 4 * 4로 고정된 경우에는 문제가 없지만

5 * 5나 3 * 3으로 바꾸어 처리할 경우 쉽게 수정할 수 없다.

이러한 직접적인 코드보다는 for문을 이용하는 것이 더 좋을 수 있다.


앞서서 정리했던 회전 관련 표와 동일한 다음 표에서

빨간색으로 표시된 값을 자세히 보면 공통점을 찾을 수 있다.

원본
왼쪽으로 회전

0123

0123
00, 00, 10, 20, 3
00, 31, 32, 33, 3
11, 01, 11, 21, 3
10, 21, 22, 23, 2
22, 02, 12, 22, 3
20, 11, 12, 13, 1
33, 03, 13, 23, 3
30, 01, 02, 03, 0

위 표에서 

빨간색과 파란색으로 표시된 값의 변화를 보면

회전 후의 Y값이 0 으로 고정일 때 원본의 Y값은 0~3 까지의 값

회전 후의 X값이 0~3 까지의 값일 때 원본의 X값은 3 으로 고정되어 있다.

이것을 X축과 Y축 각각에 대한 for 문을 이용하여

다음과 같이 작성할 수 있다.

(코드를 보기전 위 개념을 직접 구현해 보는 것이 좋다)

function rotateLeft(piece) {
    var ret = [];
    for (var y = 0; y < 4; y++) {
        ret[y] = piece[y].slice();
    }
   
    for (var y = 0; y < 4; y++) {
        for (var x = 0; x < 4; x++) {
            ret[y][x] = piece[x][3 - y];
        }
    }
    return ret;
}

예제 1-2

앞서의 문장을 다음과 같이 공식화 할 수 있다.

회전 후의 Y값이 0으로 고정일 때 원본의 Y값은 0~3까지의 값

회전 [0] = 원본 [0~3]

회전 후의 X값이 0~3까지의 값일 때 원본의 X값은 3으로 고정되어

회전 [0][0~3] = 원본 [0~3][3]

으로 정리할 수 있다.

이 코드는 가독성이 떨어지는데, 0~3을 x로 치환하면

회전 [0][x] = 원본 [x][3]

이 된다.

이 식을 Y값이 0 이상일 경우의 값을

표에서 확인하면 다음과 같다.

회전 [0][x] = 원본 [x][3]
회전 [1][x] = 원본 [x][2]
회전 [2][x] = 원본 [x][1]
회전 [3][x] = 원본 [x][0]

숫자 0부터 3까지의 값을 y라고 하면

회전 [y][x] = 원본 [x][3-y]

가 되고, 이것을 2개의 For문을 이용하여

이상의 코드와 같이 작성하였다.

실행시켜 보면,

기존의 코드와 동일하게 작동하는 것을 알 수 있다.


이상의 코드에서 회색으로 처리한 For문은

2차원 배열을 초기화 하는 자바스크립트 방법이다.

회전할 데이터를 저장할 ret 변수를 1차원으로 선언하고

For문을 이용하여 4개의 요인을 추가한다.

추가할 때 4개의 요인을 가진 새로운 1차원 배열을 지정하면

2차원 배열이 되는데,

여기에서는 Slice 함수를 이용하여

기존 배열에서 복사하는 방법을 사용하였다.

앞서 참고한 사이트에서 사용한 방식으로

2차원 배열을 초기화하여 사용할 때 많이 사용한다.

다음 코드가 좀더 일반적인 방식이다.

    for (var y = 0; y < 4; y++) {
        ret[y] = [];
        for (var x = 0; x < 4; x++) {
            ret[y][x] = 0;
        }
    }


오른쪽으로 회전하는 것도 왼쪽과 동일한 방법으로 구현한다.

다음 표를 자세히 보면,

회전 후의 Y값이 0 으로 고정일 때 원본의 Y값은 3~0 까지의 값

회전 후의 X값이 0~3 까지의 값일 때 원본의 X값은 0 으로 고정되어 있다.

왼쪽과 동일한 방법으로 구현해 보길 바란다.

원본
오른쪽으로 회전

0123

0123
00, 00, 10, 20, 3
03, 02, 01, 00, 0
11, 01, 11, 21, 3
13, 12, 11, 10, 1
22, 02, 12, 22, 3
23, 22, 21, 20, 2
33, 03, 13, 23, 3
33, 32, 31, 30, 3


function rotateRight(piece) {
    var ret = [];
    for (var y = 0; y < 4; y++) {
        ret[y] = piece[y].slice();
    }
   
    for (var y = 0; y < 4; y++) {
        for (var x = 0; x < 4; x++) {
            ret[y][x] = piece[3 - x][y];
        }
    }
    return ret;
}

예제 1-2


기존 소스에 다음과 같이 코드를 추가한 뒤 실행해 본다.

function draw() {
    ctx.fillStyle = 'white';
    ctx.fillRect(0, 0, 100, 100);
    ctx.fillStyle = 'black';
    for (var x = 0; x <= 100; x+=20) {
        ctx.beginPath();
        ctx.moveTo(x,0);
        ctx.lineTo(x,100);
        ctx.stroke();
    }
    for (var y = 0; y <= 100; y+=20) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(100, y);
        ctx.stroke();
    }

    ~~ 생략 ~~

이 코드는 테트리스 도형을 그리는

Canvas에 배경선을 그리는 코드로

다음과 같은 화면을 볼 수 있다.

처음에 있는 I 자는 문제가 없어 보이지만

두 번째에 있는 T 자의 경우

세 번째와 네 번째의 도형이

한쪽으로 치우친 것을 볼 수 있다.

첫 번째와 두 번째의 T 자는 3 *3의 범위에 있지만

세 번째와 네 번째는 4 * 4의 범위에 있다.

I 자는 전체 도형이 네 칸을 사용하기 때문에 문제가 없지만

T 자는 실제 크기는 세 칸인데,

정보를 저장한 배열의 크기는 4칸(4*4)이라

회전할 때 위치가 틀어지는 문제가 발생한다.

문제 해결과 관련된 토론은

인터넷에서 제법 많은 토론이 이루어 지니 찾아보길 바란다.

여기에서는 2가지 방법으로 해결해 본다.


첫 번째 방법은 bitmask에서 언급했던

모든 회전 모양을 저장하는 방법이다.

두 번째 방법은 앞서의 방식에서

각 도형의 크기만큼만 회전에 참여시키는 것이다.

새로운 방법인 bitmask는 뒤에서 따로 정리하고

먼저, 각 도형의 크기만큼만 회전시키는 방법을 정리한다.


O 자는 2 * 2, I 자는 4 * 4의 크기를 가지고

나머지 도형은 모두 3 * 3의 크기를 가진다.

따라서, 이 정보를 저장하도록 하고

회전할 때 이 크기만큼만 회전하게 수정하면 된다.

var shapes = [
   [[1, 1, 0, 0],        // 'O'
    [1, 1, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]],
   [[0, 0, 1, 0],        // 'I'
    [0, 0, 1, 0],
    [0, 0, 1, 0],
    [0, 0, 1, 0]],
   [[1, 1, 1, 0],        // 'T'
    [0, 1, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]],
   [[0, 1, 1, 0],        // 'S'
    [1, 1, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]],
   [[1, 1, 0, 0],        // 'Z'
    [0, 1, 1, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0]],
   [[0, 1, 0, 0],        // 'J'
    [0, 1, 0, 0],
    [1, 1, 0, 0],
    [0, 0, 0, 0]],
   [[1, 0, 0, 0],        // 'L'
    [1, 0, 0, 0],
    [1, 1, 0, 0],
    [0, 0, 0, 0]]
];

var shapeSize = [2,4,3,3,3,3,3];

function rotateLeft(piece) {
    ~~ 생략 ~~
   
    var size = shapeSize[curShapeType];
    for (var y = 0; y < size; y++) {
        for (var x = 0; x < size; x++) {
            ret[y][x] = piece[x][(size-1) - y];
        }
    }
    return ret;
}

function rotateRight(piece) {
    ~~ 생략 ~~
   
    var size = shapeSize[curShapeType];
    for (var y = 0; y < size; y++) {
        for (var x = 0; x < size; x++) {
            ret[y][x] = piece[(size-1) - x][y];
        }
    }
    return ret;
}

예제 1-3

먼저, 도형 I를 제외하고

shapes에 저장된 도형의 위치를 모두 수정한다.

도형 정보가 배열 중앙에 있는데,

다음 표 처럼 0, 0 에서 시작하도록 이동 시켜준다.


도형의 크기를 shapeSize에 배열로 저장하고,

현재 도형의 종류(curShapeType)에 따라

적절한 크기를 사용한다.

도형의 크기 만큼만 회전에 참여 시킨다의 의미는

For문의 최종값을 도형의 크기로 지정한다는 것이다.


실행후, 다음 그림과 같이 출력되는 것을 확인할 수 있다.

위의 T 자가 수정전, 아래 T자가 수정 후 그림으로

제대로 회전되는 것을 볼 수 있다.

마지막으로 다음 그림을 보면

3 * 3 의 도형들은 빨간색으로 표시한

중심이 존재하게 된다.

S 자처럼 위를 중심으로 잡아도 되고

Z 자처럼 아래를 중심으로 잡아도 된다.

가급적 둘다 같은 방식으로 중심으로 잡아주는 것이 좋다.

다만, L 자는

왼쪽으로 치우쳐있다.

공백 부분을 중심으로 회전하는 데,

오른쪽으로 한칸 이동시켜 J자와 일치 시켜주는 것이 좋을 수 있다.


코드 양이 많아서 복잡해 보이지만

실제로는 간단하게 작성된

테트리스 도형 회전에 대한 배열법을 정리하였다.

다음으로 조금 어려운 개념을 사용한

Bitmask 방식에 대하여 정리하였다.


문제

정리하지 않았지만,

도형의 회전시 새로운 배열(ret)을 생성하여 사용하였다.

자바 스크립트 배열은

기본적으로 참조(Refence)의 개념을 가지고 있기 때문이다.

대부분의 게임에서 배열이 매우 중요한 개념이니 찾아 보길 바란다.



+ Recent posts