Eclipse에서 Node.js (Nodeclipse)를 사용하는 방법을

다음과 같이 4가지로 정리하였다.

1. Node.js 프로젝트 생성 및 개발 준비

2. MariaDB 기반 CRUD 게시판 만들기

3. 게시판 예제 보강

4. GitHub에서 Node.js 소스 가져오기


앞서 작성한 Node.js + MariaDB 게시판 예제 코드에서

다음과 같이 사용된 컨넥션 풀링 코드는

모든 컨트롤 파일(js)에 있어야 하는 코드이다.

var mysql = require('mysql');
var pool = mysql.createPool({
    connectionLimit: 5,
    host     : 'localhost',
    user     : 'root',
    password : '비밀번호',
    database : 'board'   
});

이렇게 사용하면

접속 정보가 바뀔 때 마다 모든 파일을 찾아서 수정해야 하고,

컨넥션 풀링을 제대로 사용할 수 없다.


여기에서는 컨넥션 풀링을 공통 파일에 넣고

호출해서 사용하는방법을 간단하게 정리한다.


그리고, 기존 예제(board1)를 수정하지 않고

간단한 파일 복제로 board2를 만들어서 구현한다.


먼저, routes 폴더에 있는 board1.js를

복사(Ctrl + C)해서 붙여넣기 (Ctrl + P) 한다.

새로운 이름(board2.js)을 입력하고 저장한다.

생성한 board2.js 파일을 열면

board1 경로가 사용된 것을 볼 수 있다.

board1을 모두 board2로 바꾸어 준다.


views 폴더에 있는 board1 폴더를 선택해서 복사(Ctrl + C)하고

views 폴더를 선택한 뒤에 붙여넣기 (Ctrl + P) 한다.

폴더가 맞지 않으면 복사가 되지 않으니 폴더를 잘 선택해야 한다.

새 폴더명으로 board2를 입력한다.

새로 생성한 board2 폴더 하위에 있는 3개의 ejs 파일을 열어서

board1을 board2로 모두 바꾸어 준다.

app.js 파일을 열어서

복사해서 생성한 board2.js 파일을 등록한다.


Node.js를 다시 시작한 뒤에

웹 브라우저에서 http://localhost:3000/board2/list 로 접속한다.


정리하면

컨트롤 파일(js)과 HTML(ejs) 파일을 복사해서 생성하고

컨트롤 파일(js)을 app.js에 등록하면

간단하게 새로운 게시판(board2)을 생성할 수 있다.


이제 새로 생성한 board2 의 컨넥션 풀링 코드를 공통 코드로 구현한다.


먼저, routes 폴더 하위에 mysqlConn.js 파일을 생성하고

커넥션 풀링과 관련된 코드를 넣어준다.

var mysql = require('mysql');
var pool = mysql.createPool({
    connectionLimit: 5,
    host     : 'localhost',
    user     : 'root',
    password : 'gujc1004',
    database : 'board'   
});

module.exports = pool;

module.exports 는 생성한 커넥션 풀링을

파일 외부에서 사용하도록 지정하는 코드이다.


board2.js 파일에서

커넥션 풀링 관련 코드를 지우고,

다음과 같이 require 함수를 이용해서

공통 파일(mysqlConn.js)을 가지고 오도록 지정한다.

var express = require('express');
var router = express.Router();

//   MySQL 로드
var pool = require('./mysqlConn');

router.get('/', function(req, res, next) {
    res.redirect('/board2/list');
});

router.get('/list', function(req,res,next){
    pool.getConnection(function (err, connection) {
        var sql = "SELECT BRDNO, BRDTITLE, BRDWRITER, DATE_FORMAT(BRDDATE,'%Y-%m-%d') BRDDATE" +
                   " FROM TBL_BOARD";
        connection.query(sql, function (err, rows) {
            if (err) console.error("err : " + err);

            res.render('board2/list', {rows: rows});
            connection.release();
        });
    });
});

~~ 생략 ~~

공통 부분을 별도 파일로 빼고, 그 파일을 호출하도록 하면 된다.

다만, 모든 컨트롤 파일을 위와 같이 작성하면

커넥션 풀링이 제대로 작동하지 않을 수 있기 때문에,

실제로 사용 할 때는 app.js 등에서 글로별 변수로 선언해서 사용하는 것이 좋다.















Eclipse에서 Node.js (Nodeclipse)를 사용하는 방법을

다음과 같이 4가지로 정리하였다.

1. Node.js 프로젝트 생성 및 개발 준비

2. MariaDB 기반 CRUD 게시판 만들기

3. 게시판 예제 보강

4. GitHub에서 Node.js 소스 가져오기


앞서 작성한 게시판 예제를 GitHub에서 가져오는(Import) 방법을 정리한다.


먼저, File > Import 메뉴를 실행하다.


Git > Projects from Git을 선택한다.

Clone URI 를 선택한다.


게시판 소스가 제공되는 주소를 입력한다.

URI에 https://github.com/gujc71/boardJS.git 입력한다.

URI 만 입력하면 나머지는 자동으로 채워진다.

master가 선택된 상태에서 다음 버튼을 클릭한다.

다운로드 받은 소스를 저장한 디텍토리 지정한다.

[Browse] 버튼으로 폴더를 지정하면 된다.

[Import as general project]를 선택하고 다음 버튼을 클릭한다.

프로젝트 이름을 지정하거나 확인하고 종료(Finish) 버튼을 클릭한다.


다음 그림과 같이 게시판 소스가 생성된 것을 볼 수 있다.


프로젝트를 선택하고 컨택스트 메뉴를 실행한 후에

Run As > npm install을 실행해서

필요한 라이브러리들을 설치한다.


[node_modules]가 추가된 것을 볼 수 있다.

[node_modules] 폴더 앞에 ?가 있는 것은

github에 등록되지 않은 파일이라는 의미이다.

라이브러리는 용량이 많기 때문에 가급적 등록하지 않는다.

따라서 git 명령어로 ignore에 등록하거나 그대로 두면 된다.


routs 폴더의 board1.js과

views 폴더의 ejs 파일 3개가 잘 설치되었는지 확인하고

그림과 같이 적당한 DB 접속 정보를 입력하고 실행한다.


www 파일을 실행해서

게시판이 잘 실행되는지 확인한다.








'Node.js > 기타' 카테고리의 다른 글

우리동네, 아이랑 놀곳 Ver.서울  (0) 2019.05.29
nineBatis (9batis)  (2) 2017.08.16
1. Node.js 프로젝트 생성 및 개발 준비  (0) 2017.08.05
2. MariaDB 기반 CRUD 게시판 만들기  (23) 2017.08.05
3. 게시판 예제 보강  (4) 2017.08.05

많은 사람들이 좋아하는 테트리스 게임을

두 가지 방법으로 개발하면서

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

덤으로 간단한 게임 개발법과

자바스크립트 Canvas 사용법도 익힐 수 있다.


자바 스크립트를 이용하여 구현하고 정리하였지만

여기서 정리하는 주요 개념에 대한 내용은

개발 언어에 관계없이 동일하게 사용할 수 있다.

시각적인 표현에 조금 차이가 있을 수 있지만

대부분의 개발언어에서 동일하게 구현할 수 있다.


인터넷을 검색해 보면

여러가지 방법으로 개발된 소스를 찾을 수 있다.

하나의 많이 참고되는 방식이 있고,

다소 비효율적이긴 하지만

재미난 방식으로 개발한 소스들도 있다.

비효율적으로 작성된 코드가 쉽게 작성되어 개념을 익히기 좋은 장점이 있고,

모든 코드가 비효율적이 것은 아니기 때문에,

남의 소스를 많이 보고 다양하게 개발해 보는 것이

중요하다고 생각하여 두가지 방식으로 정리하였다.


테트리스를 제작하는 방법에 대한 설명과 예제는

다음 사이트에 잘 정리되어 있다 (영어).



이 내용에서 첫 사이트의 예제와

나머지 사이트의 예제로 나눌수 있고,

이 게임들은 두가지 방식으로 개발되었다.

근본적인 차이는 테트리스 도형의 회전 방식의 차이이다.

테트리스 도형에 대한 용어가 사람에 따라 조금씩 차이가 있는데

Shape, Piece, Block, Tetromino등으로 사용한다.

여기서는 도형, Shape, Block을 같은 의미로 사용한다.


테트리스는 O, I, T, S, X, J, L의 7개 도형을

특정 버튼이나 키보드를 이용하여 오른쪽으로 90도씩 회전시키고

한 라인을 모두 채우면 사라지는 게임이다.

출처: Code in Complete


이 도형을 회전 시키는

첫번째 방법은 각 7 개의 도형에 대한 정보를 배열에 넣고

회전 명령에 따라

그때 그때 90 도씩 계산하여 회전시키는 것이다.


또다른 방법은

7 개 도형의 회전 후 모습을

모두 배열에 담아서 저장하고

상황에 맞추어 보여주는 방식이다.

다음 그림과 같이 90 도로 회전하는 4 개의 도형 정보를 저장한다.

이 방식은 제법 많은 데이터를 저장 하기 때문에

데이터를 16 진수로 변환해서 사용한다.

출처: Code in Complete

편의상,

앞서 7개의 도형을 배열에 저장하고

동적으로 회전을 계산하여 구현하는 것을 배열 개발법 (또는 배열법),

두번째의 모든 도형 모습을 저장하여 사용하는

개발 방법을 Bitmask개발법(또는 bitmask법)으로 호칭한다.

Bitmask로 개발하는 방법은 Code in Complete에 잘 설명되어있다.


이해를 쉽게 하기 위해

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

1. 배열 방식의 도형 회전

2. Bitmask 방식의 도형 회전

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

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

5. requestAnimationFrame




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

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

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

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)의 개념을 가지고 있기 때문이다.

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



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

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

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

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

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

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

6. requestAnimationFrame


이번에는 Bitmask를 이용한 테트리스 도형 회전에 대하여 정리한다.

Bitmask를 이용한 테트리스 도형 회전은

7개의 테트리스 도형에 대한 모든 회전(4가지) 정보를 가지고 있는 것으로

데이터 양을 줄이고 관리를 편리하게 하기 위해

도형 정보를 bit로 변환해서 저장한다.


그림과 같이 하나의 행이 하나의 워드(word - 4bit)가 되고,

색이 있는 부분을 1 로 표기하고

색이 없는 부분은 0 으로 지정하면,

2 진수로 처리되어 오른쪽 표와 같이 된다.

즉, L 자 도형은

2 진수로 0100 0100 0110 0000 이 되고

16 진수로 4460 이 된다 (진수 변환은 본문 마지막의 해설 참조).


이렇게 모든 도형과 회전후 도형 모습을

다음과 같이 2 진수로 처리하여 16진수 값으로 저장한다.

출처: Code in Complete

데이터를 저장하는 방법을 이해하면

다음 코드와 같이 간단하게 구현할 수 있다.

위 그림의 계산 값과 다음 코드의 계산값(shapes)에 차이가 있다.

둘다 방식은 같은데 도형의 배열내 위치가 조금씩 다르기 때문이다.

이미지와 기타 코드는 Code in Complete에서 가져왔고

다음 샘플은 stackoverflow에서 가져와서 수정했기 때문에 차이가 있다.


이상의 코드에서 7 개의 도형(행)과 4 개의 각도(열)를

2 차원 배열로 구성해서 shapes 변수에 저장한다.

배열법보다 상당히 줄어든 코드를 볼 수 있다 [라인 13].

배열법에서는 현재 도형의 모양을 별도의 변수에서 저장했지만

Bitmask에서는 도형의 모양 (curShapeType – 행 위치)과

도형의 현재 각도(curRotation)에 대한 정보 [라인 23]만 있으면 된다.

curRotation 변수에 현재 각도에 대한

배열의 위치 (열 위치)를 다음과 같이 저장한다.

0 이면 기본 각도 (0 or 360)

1 이면 90 도

2 이면 180 도

3 이면 270 도를 의미하는 숫자가 된다.

즉 왼쪽으로 회전하는 것은

앞으로 가야 하기 때문에 curRotation 값에 -1을 계산하고 [라인 46]

오른쪽으로 회전하는 것은

뒤로 가야 하기 때문에 curRotation 값에 +1 계산한다 [라인 52].

다만, 왼쪽으로 회전해서 0 보다 작으면

다시 3 부터 시작하게 되고 [라인 47],

오른쪽으로 회전해서 3 보다 크면 0 부터 시작하도록 처리한다 [라인 52].


마지막으로 가장 중요한 도형을 그리는 방법을 정리한다 [라인 33].

이 부분이 가장 어렵고 중요한데,

데이터 저장을 2 진수로 저장하는 일종의 암호화를 하고,

이것을 다시 쉽게 풀어 쓰는 복호화를 하기 때문이다.

비트 연산을 잘하는 사람이 아닌 이상

33 라인을 이해하기 어렵다.

어렵다기 보다는 개발 중에 비트 연산을 보는 경우가 드물어

익숙하지 않기 때문에 어렵게 느껴질 수 있다.

기본 개념은 앞서 2 진수로 계산한 값을

기본 값과 이진 연산을 통해서 값이 있으면 (1 이면)

테트리스 도형을 그리기 위한 사각형을 그린다 [라인 34].

예로, L 자 도형은 16 진수 0x4460 으로 저장되어 있다.

이 값을 다시 2 진수 0100 0100 0110 0000 로 변환해서

하나의 문자(bit)가 1 이면 사각형을 출력하도록 하면 된다.

다만, 16 진수 값을 2 진수의 문자열로 바꾸는 것이 까다롭고 복잡해서

비트 연산(bitmask)을 통해서 간단하게 구현한다.


먼저 두 개의 for문을 사용하면

0 부터 15 (4 * 4 = 16)까지의 값을 구할 수 있다 [라인 31, 32].

즉, 16 번의 비트 이동(>>)을 하게 된다.


16 진수 0x8000은

2 진수로 1000 0000 0000 0000 이다.

이것을 16 번 비트 이동(>>)하면

0x8000 >> 0  : 1000 0000 0000 0000
0x8000 >> 1  : 0100 0000 0000 0000
0x8000 >> 2  : 0010 0000 0000 0000
0x8000 >> 3  : 0001 0000 0000 0000
….
0x8000 >> 15 : 0000 0000 0000 0001 이 된다.

이 각각의 값을

L 자 도형의 2진수인

0100 0100 0110 0000 과 & (and) 비트 연산을 하면

두 개의 값이 1일때만 1 이 반환되기 때문에

이때만 사각형을 그리도록 한다 [라인 33].


예로 처음에는 다음과 같이 0 이 반환된다.

0x8000 >> 0  : 1000 0000 0000 0000
0x4460       : 0100 0100 0110 0000
-------------------------------
& 결과         0000 0000 0000 0000  => 0x0000

두 번째에는 1 이 반환된다.

0x8000 >> 1  : 0100 0000 0000 0000
0x4460       : 0100 0100 0110 0000
-------------------------------
& 결과         0100 0000 0000 0000  => 0x4000

이렇게 16번을 비교하면

테트리스 도형을 그리기 위한 4개의 사각형을 얻을 수 있다.


16 진수로 비트 연산(>>, &)을 하면

자바 스크립트 내부에서

위와 같은 이진 연산을 알아서 처리해준다.


지금까지 테트리스의 도형을

회전시키는 2가지 방법에 대해서 정리하였다.

배열법은 코드 양이 많은 것 같지만 단순한 원리가 적용되었고,

Bitmask 방법은 코드 양이 적지만

익숙하지 않은 bit에 대한 개념과 사용법을 알아야 한다.

둘 다 장단점이 있고,

인터넷으로 검색해 보면 Bitmask로 개발한 예제가 더 많고

더 나은 방법이라는 글을 많이 볼 수 있다.


개인적으로도 Bitmask가 더 좋은 방식이라고 생각하지만

개발을 시작하는 사람들에게는

배열법도 게임에 대한 이해를 돕기에 좋은 방식이라고 생각한다.

더우기 배열법 예제에서 나타난 문제를 해결하면서

실력향상을 도모할 수 있다.

그리고, 실제 게임을 제작하면

Bitmask는 복잡한 방식으로 구현되기 때문에

초보자가 이해하기 어렵다 (Code in Complete참조).

배열법이 코드양이 많아 보여서 복잡하게 느껴질 수 있지만

단순한 구조라 실제 계발에서도 직관적으로 구조를 파악할 수 있다.

(여기서 참고한 예제는 DIV를 사용해서 조금 더 어려울 수 있다.)


이렇게 테트리스의 도형을 회전시키는 방법을 통해

다양한 방식으로 개발할 수 있다는 것을 맛보고,

이것을 좀더 심화해서 간단하지만 즐길 수 있는

실제 게임을 제작하면서 어떤 차이가 있는지 정리한다.


2진수를 16진수로 변환하는 방법

출처: Code in Complete

이 그림에서 보듯이 가장 오른쪽 부터

2 ^ 0 으로 1

2 ^ 1 으로 2

2 ^ 2 으로 4

2 ^ 3 으로 8 의 값이 된다.

해당 자리에 값이 지정되면 (2진수 이므로 1 이면)

1, 2, 4, 8의 숫자가 해당 위치의 값이 되어 사용된다.


예로,

2진수 1000 은 오른쪽에서 4번째에 값이 있기 때문에 2 ^ 3 으로 16 진수 8 이 된다.

2진수 1100 은 2 ^ 3 과 2 ^ 2 에 값이 있으니 8 + 4 로 16진수 C (십진수 12) 가 된다.



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

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

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

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 차원 배열 사용법이 핵심이다.


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

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

잘 익혀 두길 바란다.







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

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

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

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

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

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

6. requestAnimationFrame


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

Bitmask를 이용한 회전 방법을 기초로

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


개발 단계는 앞서 정리한 배열법을 이용한 테트리스와 동일하게

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


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

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

3. 이동 중에 도형 회전

4. 채워진 행 제거

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



구현에 대한 개념은 앞서 앞서 정리한 배열법과 동일하다.

구현된 코드도 거의 유사하여,

차이나는 부분에 대해서만 간단하게 정리한다.


개발의 첫 단계로

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

구현 방법은

시간의 흐름(setInterval)에 따라 도형의 좌표(x, y)를 바꾸어 주고,

해당 좌표를 기준으로 도형을 구성하는 4 개의 사각형을 그려준다.


이상의 코드를 배열법으로 구현한 코드와 비교해 보면

회전에서 정리한 것과 같이

7 개의 도형을 저장하는 방식만 차이가 있고

거의 동일한 것을 알 수 있다.

즉, 배열법은 0 과 1 의 값을 가지는 3 차원 배열로 저장하고(shapes)

Bitmask는 16진수로 된 2 차원 배열로 저장하는 차이가 있다 [라인 8].


테트리스 도형 정보를 저장하는 방식외에도

복원한 정보에 맞추어 (0x8000 >> (y * 4 + x))

도형을 구성하는 사각형을 그리는 방식도 차이가 있다 [라인 40].


앞서 작성한 Bitmask 회전 예제 코드에,

시간의 흐름 (setInterval)에 따른 [라인 21]

위치 변경(sPos.y++)을 추가하여 작성하였다 [라인 45].

도형을 그리는 위치도 현재 위치(sPos.y)에

도형을 구성하는 사각형들의 위치를 계산하여 그려준다 [라인 41].


두번째로,

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

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

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

다음 행에 다른 도형이 있는지 확인하는 기능을 구현하는 것이 핵심으로

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

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

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


다음 행(정확하게는 칸)에 도형이 있는지 확인하기 위해,

게임판 (10 * 20) 전체에 대한 정보를 gamePanel에 저장해서 사용한다.


이러한 처리는 배열법이든 Bitmask든 동일하지만

도형 정보를 처리하는 데서 발생하는 차이를 구현해야 한다.

var gamePanel = [];
~~ 생략 ~~
function playingTetris() {
    if ( intersects(sPos.y + 1, sPos.x)) {
        for (var i = 0; i < 4; i++)
            for (var j = 0; j < 4; j++)
                if ((curShape & (0x8000 >> (i * 4 + j))) && gamePanel[sPos.y+i]) {
                    gamePanel[sPos.y+i][sPos.x+j] = 1;
                }
       
        curShape = getNextShape();       
        sPos = {x:0, y:0};
        if ( intersects(sPos.y, sPos.x)) {
            clearInterval(intervalHandler);
            alert("Game Over");
        }
    } else {
        sPos.y++;
    }
    draw();
}   

function intersects(y, x) {
    for (var i = 0; i < 4; i++)
        for (var j = 0; j < 4; j++)
            if (curShape & (0x8000 >> (i * 4 + j)))
                if (y+i >= 20 || x+j < 0 || x+j >= 10 || gamePanel[y+i][x+j])
                    return true;
    return false;
}

function draw() {
    ~~ 생략 ~~
    for (var y = 0; y < gamePanel.length; y++) {
        for (var x = 0; x < gamePanel[y].length; x++) {
            if (gamePanel[y][x]) {
                ctx.fillRect(x * 20, y * 20, 19, 19);
            }
        }
    }
   
    for (var y = 0; y < 4; y++) {
        for (var x = 0; x < 4; x++) {
            if (curShape & (0x8000 >> (y * 4 + x))) {
                ctx.fillRect((sPos.x+x) * 20, (sPos.y+y) * 20, 19, 19);
            }
        }
    }
}

전체 코드

이상의 코드는 배열법에서 작성한 것과 거의 동일하고

빨간색으로 표시한 코드만 차이가 있다 (0x8000 >> (i * 4 + j).

Bitmask법은 데이터를 2 진수(bit)로 계산해서

16 진수로 줄여서 저장하고,

비트 연산(>>, &)을 통해서 복원해서 사용한다.

복원하는 방법은 Bitmask회전에 정리하였다.


playingTetris() 함수에서

도형을 gamePanel에 도형 정보를 저장하는 부분은 참고 예제와 차이가 있다.

여기에서는 gamePanel에 1을 저장했지만

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

참고 예제에서는 도형 정보를 하나의 클래스(Json)로 보고,

다음과 같이 해당 클래스를 넣어준다.

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

하나의 도형은 4 개의 사각형을 가지니

gamePanel에 4 개의 동일한 도형 정보를 중복하여 저장한다.


배열법과 Bitmask법으로 구현할 때의 차이를 강조하고

소스를 단순화하여 쉽게 설명하기 위해

이상과 같이 가급적 같은 코드로 작성했지만

참고 예제처럼 작성하는 것도 좋은 방법이니 소스를 확인해 보길 바란다.

참고 예제에서는 도형 정보들을

다음과 같이 도형의 모습에 대한 정보(blocks)와 색상(color)을

하나의 클래스(Json)으로 저장해서 사용하였다.

var i = { blocks: [0x0F00, 0x2222, 0x00F0, 0x4444], color: 'cyan'   };
var j = { blocks: [0x44C0, 0x8E00, 0x6440, 0x0E20], color: 'blue'   };
var l = { blocks: [0x4460, 0x0E80, 0xC440, 0x2E00], color: 'orange' };
var o = { blocks: [0xCC00, 0xCC00, 0xCC00, 0xCC00], color: 'yellow' };
var s = { blocks: [0x06C0, 0x8C40, 0x6C00, 0x4620], color: 'green'  };
var t = { blocks: [0x0E40, 0x4C40, 0x4E00, 0x4640], color: 'purple' };
var z = { blocks: [0x0C60, 0x4C80, 0xC600, 0x2640], color: 'red'    };

또 다른 참고 예제와의 차이는

도형 정보를 저장하는 shapes(참고 예제는 blocks)에 있다.

본 예제는 행을 Y축, 열을 X축으로 사용하여 shapes[y][x]로 사용하나

참고 예제는 행을 X축, 열을 Y축으로 사용하여 shapes[x][y]로 사용한다.

즉, 참고 예제는 다음 그림의 오른쪽과 같은 방식으로 사용되었다.

위의 그림에서

첫 그림이 테트리스 도형이 떨어지는 화면이다.

이 화면을 본 예제에서는 왼쪽 그림처럼 게임판(gamePanel)을 저장하고

참고한 예제에서는 오른쪽처럼 게임판(blicks)을 저장한다.


일반적으로 X축과 Y축을 표현할 때, X축이 먼저 나오도록 작성한다.

즉, X * Y로 표현하는데, 이 표현법을 그대로 배열로 작성한 것 같다.

본 예제에서는 Y * X로 사용하였다.


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

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

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

도형을 왼쪽 / 오른쪽 (X)으로 이동시키는 것은

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

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

게임판을 새로 그려서 구현한다.


그냥 떨어지는 기능은

타이머를 사용하지 않고,

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

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


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

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

function moveShape(value) {
    if ( !intersects(sPos.y, sPos.x+value)) {
        sPos.x += value;
        draw();
    }
}
function dropShape() {
    for (var y=sPos.y; y<ROW_CNT; y++) {
        if ( !intersects(sPos.y+1, sPos.x)) {
            sPos.y++;
            draw();
        } else {
            break;
        }
    }
}
function rotateShape() {
    curRotation = (curRotation + 1) % 4;
    curShape = shapes[curShapeType][curRotation];
    draw();
}

전체코드

이동 함수는 직접 도형 데이터를 조절하는 것이 아니라서

배열법과 Bitmask 법에서 동일하게 구현된다.

rotateShape()함수에서 구현된 도형의 회전만 차이가 있다.


Bitmask에서는 회전된 도형에 대한 정보를 가지고 있기 때문에

회전 각도를 4 개의 숫자를 가지는

변수(curRotation)의 값을 1 씩(90 도씩) 증가 시켜서

도형의 정보를 가르키도록 구현한다.


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

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

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


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

gamePanel의 모든 행을 검사해서

각 행의 모든 열의 값이 1 이면 해당 행을 제거한다.

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

전체코드

행 제거도 도형 정보를 건드리지 않기 때문에

배열법과 차이가 없다.


다만, 배열의 한 행을 제거하는 방법이

참고 예제는 다음과 같이 단순한 방법으로 구현되었다.

    function removeLines() {
        var x, y, complete, n = 0;
        for(y = ny ; y > 0 ; --y) {
            complete = true;
            for(x = 0 ; x < nx ; ++x) {
                if (!getBlock(x, y))
                    complete = false;
            }
            if (complete) {
                removeLine(y);
                y = y + 1; // recheck same line
                n++;
            }
        }
        if (n > 0) {
        addRows(n);
        addScore(100*Math.pow(2,n-1)); // 1: 100, 2: 200, 3: 400, 4: 800
      }
    }
    function removeLine(n) {
        var x, y;
        for(y = n ; y >= 0 ; --y) {
            for(x = 0 ; x < nx ; ++x)
                setBlock(x, y, (y == 0) ? null : getBlock(x, y-1));
        }
    }

removeLines() 함수에서 한 행이 모두 채워졌으면 (complete==true),

removeLine() 함수를 호출해서 해당 행을 삭제한다.

행을 삭제하는 방법은

해당 행(n) 부터 0 번째 행까지 반복하면서

현재 행(y)에 이전 행(y-1)의 값(getBlock)을 채워 넣는 것(setBlock)이다.

즉, 한 행씩 뒤로 당겨서 삭제하는 방식을 사용하였다.

(본 예제와 행열 구조가 반대라서, 참고 예제에서는 한 열을 뒤로 당기도록 구현)


그리고, 행을 삭제한 개수(n)만큼

게임판 정보 배열(blocks)의 앞에 행을 추가한다 (addRows).


이러한 방식은 앞서 작성한 방식보다 비효율적이다.

즉, 배열을 이용하여 구현한 예제에서 발췌한 방식이 조금 더 낫다.

배열법 방식은 Y축에 대해서 for문을 한번만 사용하지만

Bitmask 참고 예제에서 사용한 방식은 Y축에 대해서 for 문을 여러 번 사용한다.

알고리즘의 속도 표기법으로 설명 가능하니 생각해 보길 바란다.

배열법 방식은 새로운 배열을 추가로 사용하기 때문에 메모리를 더 사용한다.


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

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

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


키보드 제어는 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 < 4; i++)
            for (var j = 0; j < 4; j++)
                if ((curShape & (0x8000 >> (i * 4 + j))) && gamePanel[sPos.y+i]) {
                    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() {
    ~~ 생략 ~~
 
    ctx.fillStyle = 'black';
    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 < 4; y++) {
        for (var x = 0; x < 4; x++) {
            if (curShape & (0x8000 >> (y * 4 + 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>

전체코드

키보드 제어와 도형별 색상도

배열법과 Bitmask를 이용한 코드를 모두 동일하게 구현하였다.

앞 서와 마찬가지로 빨간색으로 표시한 코드만 차이가 있다 (0x8000 >> (i * 4 + j).


이상의 코드를 정리하면

배열법과 Bitmask법 둘다 개발하는 코드는 동일한 것을 알 수 있다.

차이는 데이터를 저장한 방법(shapes)과

데이터를 복원하기 위한 코드(0x8000 >> (i * 4 + j)만 차이가 있다.


테트리스 개발 과정을 정리한 이유는

다양하게 개발하는 방법을 이상과 같이 정리하기 위한 것도 있지만

어느 한 소스만 좋을 것이라는 편견을 갖지 않기 위한 의미도 있다.

개념이나 방식들은 Bitmask법의 예제가 월등해 보이나 지나치게 어렵게 작성하였고,

배열법은 다소 이상한 방식(div 사용)으로 구현하였지만

쉽고 편리하게 사용할 수 있도록 작성되었다.

이 둘의 모든 코드를 정리하지 못하였으니,

잘 살펴보면 실력향상에 많은 도움이 될 것이다.




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

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

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

1. 개요

2. 배열 방식의 도형 회전

3. Bitmask 방식의 도형 회전

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

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

6. requestAnimationFrame


Bitmask를 사용한 예제에서는

시간의 흐름을 setTimeout() 함수나 setInterval() 함수가 아닌

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


setTimeout() 함수나 setInterval() 함수는 다음과 같은 문제가 있다.


setInterval() 함수는 언제나 콜백함수를 호출하기 때문에 브라우저의 다른 탭이 선택된 경우와 같이 실제 화면을 다시 그릴 필요가 없는 경우에도 화면을 다시 그린다. 그래서 시스템의 리소스 낭비를 초래하여, 불필요한 전력을 소모하게 만든다. 또한, 디스플레이 갱신 전에 캔버스를 여러 번 고치더라도 디스플레이 갱신 바로 직전의 캔버스 상태만 적용이 되기 때문에, 프레임 손실이 발생할 수도 있다.

출처: Beautiful Code


게임과 같은 처리에는 requestAnimationFrame() 가 더 적합하다는 의미로

IE 10 이후나 Firefox, Chrome등에서 사용할 수 있다.


다음 예제는 MSDN에서 제공하는 예제

개인적으로 가장 쉬운 예제인 것 같아서 이 코드를 기본으로 정리하였다.

예제를 실행하고, "Start" 버튼을 누르면

빨간색의 도형이 왼쪽에서 오른쪽으로 천천이 이동한다.


먼저, 39 라인에서 requestAnimationFrame()을 새로 정의하였다.

재정의한 것은 IE, FireFox, Chrome등의 웹브라우저에 맞추어

적절한 requestAnimationFrame()을 사용하기 위한 것으로 [라인 40~43]

requestAnimationFrame()을 지원하지 않는 브라우저일 경우에는

기존과 같이 setTimeout()을 사용하도록 하였다 [라인 44~46].

setTimeout()의 실행 간격은 1000 / 60 (=16.667)로

이 값은 밀리 세컨드이니 0.0167초에 한번씩 실행된다.

이렇게 지정한 이유는 requestAnimationFrame() 함수가

0.0167초에 한번씩 실행되도록 실행 시간이 고정되어 있다.


실행을 중지하는 cancelAnimationFrame()도

requestAnimationFrame()와 동일한 방식으로 작성되었다 [라인 49~57].


실행 화면에서 “Start” 버튼을 누르면

Start()함수가 호출되고 [라인 30]

다시 requestAnimationFrame(requestAFrame)가 호출되면서

render()가 콜백 함수로 실행된다 [라인 31].


render() 함수에서는

화면에 있는 빨간 DIV의 왼쪽(left) 값을 3px씩 증가시켜서

왼쪽에서 오른쪽으로 움직이게 한다 [라인 26].


setTimeout()과 requestAnimationFrame()의 차이를 보기 위해서는

 “Start” 버튼을 눌러서 실행을 시킨 뒤에

웹 브라우저를 작게(Minimize) 한 뒤에 복원을 시켜보면 된다.

도형의 이동이 웹 브라우저를 작게 했을 때의 위치에서 멈추었다가

복원할 때 다시 해당 위치에서 시작되는 것을 볼 수 있다.


이 코드를 테트리스에 적용하는 것은 문제가 있다.

기존에 작성한 코드는 0.4 (400 / 1000) 초에 한번씩 실행되는데

requestAnimationFrame()는 무조건 0.0167 초마다 실행된다.

지나치게 빨리 실행되어,

도형들이 빨리 떨어지게 된다.


requestAnimationFrame()는 실행 시간을 지정할 수 없기 때문에

약간의 트릭이 필요하고,

이 방법이 Bitmask 참고 예제에 잘 구현되어 있다.

다음 코드는 이 예제에서 추출한 코드로,

requestAnimationFrame()을 지정된 시간 간격으로 실행되게 구현한다.

function render() {
    elm.style.left = ((lpos += 3) % 600) + "px";
                                             
}
var dt = 0, step = 0.4;
var last = now = timestamp();
function timestamp() { return new Date().getTime(); }
   
function frame() {
    now = timestamp();
    update(Math.min(1, (now - last) / 1000.0));
    last = now;
    requestId = window.requestAFrame(frame);
}
function update(idt) {
    dt = dt + idt;
    if (dt > step) {
        dt = dt - step;
        render();
    }
}
   
function speedUp() {
    step -= 0.05
    if (step < 0.1) step = 0.1;
}
   
function start() {
    frame()
}

전체코드

수정/추가된 코드가 많아 보이지만 원리는 간단하다.

현재 시간과 과거 시간의 차이를 계산해서

차이가 지정된 시간 (0.4초)보다 크면

도형의 위치를 이동시켜 준다.


코드와 연결해서 정리하면

현재 시간(now)과 과거 시간(last)의 차이(now - last)를 계산해서(dt)

차이가 지정된 시간 (0.4초)보다 크면 (dt > step)

도형의 위치를 이동시켜 준다 (render()).


조금 더 자세하게 정리하면,

현재 시간은 현재 호출된 시간(now)을 의미하며

과거 시간은 마지막으로 호출된 시간(이전 now)을 의미한다.

현재 시간과 과거 시간의 차이(idt)는 거의 항상 0.0167 초 이다.

(밀리 세컨드이기 때문에 1000 으로 나누어 준다. (now - last) / 1000)

requestAnimationFrame() 함수가 실행되는 시간이기 때문이다.


이 값을 계속 누적해서 (dt = dt + idt) 경과 시간을 계산하게 되고

경과 시간(dt)이 지정된 시간 (0.4 : step)보다 크면 (dt > step)

도형의 위치를 이동시켜 준다 (render()).

즉, 경과시간(dt)은 지정된 시간(0.4 초)이 될때까지 0.0167 초씩 증가한다.

지정된 시간이 되면 도형을 이동 시키고, 경과 시간을 초기화 한다 (dt - step).

초기화 방법이 0 이 아닌 dt – step 인 것은

미세한 시간 차이라도 처리해 주기 위한 것이다.


기존 코드에 추가한 이번 예제는 속도 조절 기능도 구현했다.

HTML 버튼을 추가하고,

이 버튼을 클릭하면 speedUp() 함수를 호출하도록 작성했다.

speedUp() 함수에서는 지정된 시간(step)을 0.05초씩 작게(-) 해서

0.4초 마다 호출되던 것을 0.1 초까지 빨라지도록 했다.

이것은 테트리스의 난이도 조절에 사용할 수 있다.


이상의 코드를 앞서 정리한

Btmask 기반 회전 방식을 토대로 제작한 마지막 예제에 적용한다.


requestAnimationFrame() 예제에 추가한(두번째 예제) 코드를

Btmask 마지막 예제에 그대로 추가해 주면

requestAnimationFrame()를 사용하여

게임 난이도를 조절하는 예제를 구현할 수 있다.

function update(idt) {
    dt = dt + idt;
    if (dt > step) {
        dt = dt - step;
        playingTetris();
    }
}
window.onload = function(){
    requestId = window.requestAFrame(frame);
}

전체코드

추가 후 바꾸어 줄 코드는 이상의 코드(빨간색)과 같이

setInterval() 대신 requestAnimationFrame(),

도형을 이동시키는 rander() 대신

테트리스를 실행하는 playingTetris()을 호출하면 된다.


게임을 실행한 후,

한 행을 채우면 갑자기 도형들이 빨라지는 것을 볼 수 있다.



지금까지 테트리스를 제작하는 두 가지 방법을 정리하였다.

두가지 제작 방법으로 작성된 참고 예제에서

쉽고 좋다고 여겨지는 코드를 추출하여 정리하였다.

두 예제 모두 비효율적인(?) 코드와 재미난 코드로 구성되어 있어

배울점이 많으니 더 살펴 보길 바란다.

작성한 예제 코드에 적절한 난이도(시간) 조절과

다음 도형을 볼 수 있는 미리보기,

게임 진행에 따른 점수 기능을 구현하면

제법 그럴 듯한 테트리스가 될 것이다.

(참고 예제에 구현되어 있다)

어렵지 않으니 각자 구현해 보길 바라고 댓글로 공유하면 아무 멋진 일이 될 것 같다.




테이블 정렬(table sorting)

자바 스크립트 기본 문법을 이해하기 위한 예제였고

테이블 컬럼 이동 (Drag and Drop Table Columns) 예제를 구현하면서

자바 스크립트의 이벤트(마우스) 처리와 CSS 이용에 대한 방법을 정리한다.

테이블 컬럼 이동은

특정 컬럼(A)을 마우스 버튼을 누른 체 움직여서 (Drag)

다른 컬럼(B)에서 멈출 경우 (Drop)

특정 컬럼(A)을 해당 컬럼(B)의 앞으로 이동시키는 것을 의미한다.

개발 진행 과정은 이해를 쉽게 하기 위해

다음과 같이 단계별로 진행한다.

  1. 마우스 이벤트 기본 구현
  2. CSS 클래스 문제 해결
  3. 컬럼 이동(Drag and Drop) 처리
  4. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 헤드 보기
  5. 컬럼 이동(Drag and Drop) 중에 이동 컬럼 전체 보기
  6. JQuery


예제 코드는 다음 사이트의 코드를

단계별로 풀어서 쉬운 부문만 정리하였으니

실제로 사용할 경우에는

다음 사이트의 코드를 활용하거나

dragtable같은 라이브러리를 사용하는 것이 좋다.

JQuery의 Sortable 예제도(웹페이지 중간의 Sort tables)

좋은 참고가 될 수 있다.


테이블 컬럼 이동 (Drag and Drop)은

4가지 마우스 액션에 의해 이루어 진다.

먼저, 이동하려는 컬럼의 헤드를 마우스로 누르고(Mouse Down)

이동할 위치의 컬럼으로 마우스를 움직여서(Mouse Move)

클릭한 마우스 버튼을 놓으면 (Mouse Up)

이동하려는 컬럼을 이동할 컬럼의 앞으로 옮겨준다.

이때, 이동하려는 컬럼과 이동할 위치의 컬럼을

CSS를 이용하여 구별해주는 것이 좋다.

특히, 이동할 위치의 컬럼으로

마우스가 올라오면(Mouse Over) 배경색을 지정하고,

다른 컬럼으로 이동하면

즉, 마우스가 현재 컬럼에서 떠나면(Mouse Out) 원상태로 되돌린다.

따라서 Mouse Move 이벤트는

Mouse Over와 Out으로 구분하여 처리한다.


이상의 개념으로 직접 구현해 본 후,

다음 코드를 보는 것이

실력 향상에 도움이 되니 시도해 보길 바란다.


위 코드에서 CSS 부분은 [라인 5 ~25]

Table 태그에 대한 스타일과

이동하려는 헤드(TH)의 CSS 클래스(dragging - 라인 16)와

이동한 새로운 위치의 CSS 클래스(hovering - 라인 21)이다.


다음으로 자바 스크립트에서는

문서가 로드 되고(onload) 나면 [라인 28]

모든 헤드(TH)를 찾아서 (getElementsByTagName – 라인 29)

사용할 마우스 이벤트에 이벤트 핸들러들을 지정한다.

여기서 컬럼의 헤드로 TH 태그를 사용했지만

TH 태그를 사용하지 않고 TD 태그를 사용할 경우에는

첫 번째 행(TR)의 TD가 헤드가 된다.


웹페이지에서 마우스를 누르고 움직이면(드래그하면)

기본적으로 해당 범위 안에 있는 내용들이 선택(select)된다.

여기서는 드래그하면 컬럼을 이동시킬 것이기 때문에

내용이 선택되지 못하게(return false)

onselectstart 이벤트에서 처리해 준다 [라인 31].


마지막 부분은 HTML 부분으로

테이블(Table) 태그를 생성하고

3개의 행(TR)과 10개의 컬럼(TH, TD)을 생성하였다 [라인 64~].


마우스 이벤트에 대해서 좀더 상세하게 정리하면

특정 컬럼을 마우스로 누르면(MouseDown – 라인 40),

현재 눌려진 컬럼(this)이 이동할 컬럼이라고 여겨

dragTD 변수에 넣어서 보관한다 [라인 41].

그리고 시각적인 표시를 위해

이 컬럼에 CSS dragging 클래스를 지정한다. [라인 42]

이 컬럼은 다른 컬럼보다 조금 어둡게(background:#eee) 처리한다.


이동할 대상 컬럼의 헤드에 마우스가 올라오면(Over – 라인 45)

Hovering 클래스를 지정해서

다른 색상(background:#ccc)으로 표시한다 [라인 47].


이 컬럼에서 마우스가 떠나면(Out – 라인 - 50)

Hovering 클래스를 지정해서 배경색을 바꾸었던 것을

Hovering 클래스를 지워서 원상태로 돌린다 [라인 52].


옮기려는 컬럼에서

눌렀던 마우스 버튼을 놓으면 (Mouse Up – 라인 55)

현재 컬럼에 지정된 Hovering 클래스를 지우고 [라인 56]

드레그 하던 컬럼(dragTD)에 지정된

dragging 클래스를 지워주고 [라인 57]

드레그 하는 필드를 초기화 한다 [라인 58].

컬럼에 마우스가 오거나(over), 떠나면(out)

CSS 클래스를 이용하여

배경색을 지정하거나 제거하게 되는데,

마우스가 눌러진 상태에서만 작동하고

그냥 마우스가 올라오거나 떠났을 때는 아무 것도 하지 않아야 한다.

즉, 마우스가 눌러진 상태를 확인하기 위해

dragTD 변수의 값이 있는지 확인한다 [라인 46, 51].

dragTD 변수의 값은 마우스 버튼이 눌러졌을 때는

해당 컬럼의 헤드를 가지고 있고 [라인41],

놓았을 때는(Up) null을 가지고 있다 [라인58].


마우스 이벤트와 관련된 상세한 정보는 자료를 찾아보기 바라고

여기에서는 사용하지 않았지만

각 이벤트 핸들러에서 파라미터로 사용된 ev에 대해서 정리한다.

ev 변수는 Event 객체를 나타내는 것으로

현재 발생한 이벤트의 각종 정보를 가지고 있다.

클릭 이벤트의 경우 클릭된 개체를

event.target (ev.target)으로 알 수 있다.

여기서는 this를 사용하였다.

이외에 클릭된 위치 좌표나,

클릭된 마우스 버튼(왼쪽, 오른쪽)등의 정보를 알 수 있다.


마우스 이벤트 외에도 많은 이벤트 들이 있으니

잘 익혀두어야 하고,

이상의 마우스 동작은 마우스를 이용하여 움직이는

대부분의 프로그램에서 사용되는 방식이니

잘 이해해 두는 것이 좋다.

(페이지 마지막에 있는 예제 참조)


정리하면,

마우스 버튼을 누르면(down)

해당 개체 정보를 보관하고 표시를 한다.

마우스 버튼을 놓으면(up)

보관된 정보를 초기화하고

작업 대상으로 표시한 것을 원래대로 되돌린다.

작업 대상이 있을 경우에만,

마우스의 움직임(Move-over, out)에 따라

필요한 행동을 하도록 작성한다.



두 번째 구현으로

테이블 헤드(TH)의 배경색(background:#aaa)을 지정해 본다.

기존 코드의 HTML에서 Style로 지정해도 되지만

다음과 같이 tableHead라는 클래스를 추가해서 테스트 한다.

.tableHead{
    background:#aaa;
    cursor:pointer
}
</style>   
<script>
window.onload = function() {
    var head = document.getElementsByTagName("th");
    for (i=0; i<head.length; i++) {
        head[i].onselectstart = function() { return false }
        head[i].onmousedown = mousedown;
        head[i].onmouseover = mouseover;
        head[i].onmouseout = mouseout;
        head[i].onmouseup   = mouseup;
        head[i].className = "tableHead";
    }

}


이렇게 추가하고

드레그를 해보면 버그가 발생하는 것을 알 수 있다.

tableHead에 의해 배경색이 어두운 색으로 지정되었는데,

드레그를 하고 난 뒤에

배경색들이 모두 사라지는 것을 볼 수 있다.

이 버그를 해결한다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
}

function mouseover(ev){
    if (dragTD === null) { return;}
    addClass(this, "hovering");
}

function mouseout(ev){
    if (dragTD === null) { return;}
    removeClass(this, "hovering");
}

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
    dragTD = null;
}

function addClass(src, classname) {
    if (src.className.indexOf(classname) === -1 ) {
        src.className += " " + classname;
    }
}

function removeClass(src, classname) {
    src.className = src.className.replace(" " + classname, "");
}

전체 코드

버그가 발생한 것은

CSS 클래스를 사용할 때

className에 값을 바로 지정했기 때문에 생긴 것이다.

테이블 헤더에 CSS 클래스가 없을 때는

다른 CSS 클래스를 넣고 빼도 (className = "") 문제가 없었다.

하지만 head[i].className = "tableHead"로

이미 CSS 클래스가 지정되어 있는데

this.className = "dragging"나 "hovering"으로 지정하면

기존 것이 지워지고 새로운 CSS 클래스가 설정된다.

초기화 하기 위해 className = "" 로 지정하면

기존 CSS 클래스가 지워지게 된다.

기존에 있던 tableHead를

추가해 주는 것이 방법이 될 수 있겠지만,

여러 개가 사용되면 다시 문제가 생기기 때문에 좋지 않다.

웹 개발시 많은 CSS 클래스가 사용되는데

매번 추가해주는 작업을 할 수 없다.


일반적으로 많이 사용하는 방식은 다음과 같다.

         className += " " + classname

대입(=)하는 것이 아니고,

문자열을 추가(+)하는 방식으로 사용한다.

다만, 기존에 있는 클래스명과 연결되는 것을 막기 위해

공백 하나(" ")를 추가해 준다.

즉, className속성에 "A B C D ..." 으로 이어지게 구성한다.

반대로 CSS 클래스를 제거할 때는

className.replace(" " + classname, "")와 같이

해당 클래스 이름을 찾아서 공백으로 바꾸어(replace) 준다.

이것을 좀더 쉽게 사용하기 위해

addClass(), removeClass()로 함수화 해서 사용했다.


세번째로 실제 컬럼 이동(Drag and Drop) 을 구현해 본다.

마우스 버튼을 누른 상태에서 이동한 후

마우스 버튼을 놓으면 (up)

눌렀을 때의 컬럼(dragTD)을

놓았을 때의 컬럼 앞으로 옮겨주면 된다.

컬럼(열)을 이동하는 방법은

앞서 행을 정렬하면서 사용한 것처럼

insertBefore 함수를 사용하지만

구현이 조금 더 어렵다.


테이블 태그는

하나의 행에 모든 컬럼의 정보가 있기 때문에

테이블 정렬에서는 하나의 행(row)을 이동시켰다.

하지만 컬럼을 이동하는 것은

하나의 컬럼에 모든 행의 정보가 있는 것이 아니기 때문에

처리가 복잡해 진다.

컬럼의 정보는 행에만 있기 때문이다.

즉, 컬럼을 이동시키려면

모든 행의 해당 컬럼을 찾아서 이동시켜야 한다.

따라서 이동시킬 컬럼을 찾기 위해

행 내의 컬럼의 위치 정보를 이용한다.

function mouseup(ev){
    removeClass(this, "hovering");
    removeClass(dragTD, "dragging");
   
    var srcInx = dragTD.cellIndex;
    var tarInx = this.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        tds = rows[x].cells;
        rows[x].insertBefore(tds[srcInx], tds[tarInx])
    }
   
    dragTD = null;
}

전체 코드

컬럼의 위치를 나타내는 속성은 cellIndex이다.

즉, 이동시키고자 하는 컬럼을 cellIndex로 찾고 (srcInx)

이동할 위치의 컬럼을 cellIndex로 찾아서 (tarInx)

insertBefore로 옮겨 주면 된다.

         rows[x].insertBefore(tds[srcInx], tds[tarInx])

이것을 모든 행에 대해서 진행해야 하기 때문에

행의 개수(table.rows.length) 만큼 반복한다.


지금까지는 컬럼을 드레그 할 때

선택한 컬럼의 배경색을 바꾸도록 표시하였다.

네번째로 좀더 쉽고 편히 볼 수 있도록

이동하는 마우스 옆에

현재 선택된 컬럼의 내용이 나타나도록 구현한다.

구현 방법은

마우스가 움직일 때 마다(MouseMove)

커서의 좌표를 구해서

컬럼의 내용을 보여준 개체(div)에 지정한다.

<style>
.draggedDiv {
    width:auto;
    height:auto;
    padding:2px 8px;
    border:1px solid #000;
    position:absolute;
    background:#eee;
}
</style>   
<script>
window.onload = function() {
    ~~ 생략 ~~
    document.documentElement.onmouseup = documentMouseup;
    document.documentElement.onmousemove = documentMouseMove;
}
function documentMouseup(ev){
    if (dragTD) {
        removeClass(dragTD, "dragging");
        dragTD = null;
        draggedDiv.parentNode.removeChild(draggedDiv);
        draggedDiv = null;   
    }
}
function documentMouseMove(ev){
    if (!draggedDiv) { return;}
   
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
}
var dragTD = null, draggedDiv=null;
function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.innerHTML = this.innerHTML;
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
}
function mouseup(ev){
    draggedDiv.parentNode.removeChild(draggedDiv);
    draggedDiv = null;
    ~~ 생략 ~~
}

전체코드

이동하려는 컬럼을

별도의 창에서 보여주기 위해서

별도의 창을 동적으로 생성하고 삭제하도록 구현했다.

별도 창(DIV)을 생성하는 것은 (createElement("div"))

이동할 컬럼을 선택하는 시점

즉, 마우스 버튼이 눌려질 때이다 (Mouse Down).

삭제(removeChild)되는 시점은

이동하려는 위치(컬럼)을 선택한 시점

즉, 눌려진 마우스 버튼을 놓았을 때이다 (Mouse Up).

이상의 코드를 확인해 보면

전역으로 선언된 draggedDiv 변수에

mousedown, mouseup 함수에서

div를 생성하고 삭제하는 것을 확인 할 수 있다.


onload 이벤트에서 별도 창을 생성하고

mousedown, mouseup 함수에서

보이거나 숨기는 방식으로 구현 할 수도 있으니 직접 구현해 보길 바란다.


별도 창을 생성한 뒤,

이동할 컬럼의 내용(this.innerHTML)을

별도 창에 넣어주고(draggedDiv.innerHTML),

마우스의 움직임(Mouse Move)에 따라서

별도 창의 좌표(left, top)을 바꾸어 준다.


컬럼의 이동은 테이블의 헤드 컬럼으로 제한을 하기 때문에

모든 이벤트를 테이블 헤드에 대하여 적용하였다.

하지만, 별도 창은 마우스가

테이블 태그에 있거나 웹 페이지에 있으나

마우스를 누르고 움직일 때는 항상 보여야 하기 때문에

문서(document)의 Mouse Move 이벤트에 적용한다 (documentMouseMove).

Event (ev) 클래스에서 제공하는

pageX, pageY 속성을 통해서

현재 마우스 커서의 위치 값을 찾을 수 있다.


문서(document)의 Mouse Up 이벤트를 사용한 것은 (documentMouseup)

컬럼을 이동하기 위해서 마우스 버튼을 누른 상태에서

테이블 태그의 헤더가 아닌

다른 태그나 웹 페이지에서

마우스 버튼을 놓았을 경우를 처리하기 위한 것이다.

이 경우 기존의 mouseup()에서 작성한

드래그 종료(취소) 코드와 동일하게 작성한다.

이동하겠다고 한 컬럼에 적용된

CSS클래스(dragging)를 제거하고

이동 컬럼(dragTD) 변수에 Null을 지정하여

다른 이벤트(over, out)이 작동하지 않게 한다.


마지막 구현으로 별도 창에 컬럼의 헤드뿐 아니라

컬럼 전체의 내용이 나오게 구현해 본다.

하나의 컬럼에 10개의 행이 있다면 모두 나타나도록 구현한다.


구현 방법은 네 번째로 구현한 것과 동일하다.

다만, 컬럼의 헤드 내용을 별도 창에 넣어주었던 것을

테이블 태그를 생성하여

모든 행의 해당 컬럼을 복사해서 넣어준다.

function mousedown(ev){
    dragTD = this;
    addClass(this, "dragging");
   
    draggedDiv = document.createElement("div")
    draggedDiv.className = "draggedDiv";
    draggedDiv.style.top = ev.pageY + 5 + "px";
    draggedDiv.style.left = ev.pageX + 10 + "px";
    document.body.appendChild(draggedDiv);
   
    var dragTable = document.createElement("table")
    draggedDiv.appendChild(dragTable);
    var srcInx = dragTD.cellIndex;
    var table = document.getElementById("tableOne");
    var rows = table.rows;
   
    for (var x=0; x<rows.length; x++) {
        var tr = rows[x].cloneNode(false);
        dragTable.appendChild(tr);
        var tds = rows[x].cells[srcInx].cloneNode(true);
        tr.appendChild(tds);
    }   
}

전체코드

먼저, 별도 창에 새로운 테이블 태그를

생성하여 추가한다.

그리고, 이동하기 위해 선택한 컬럼의 위치(cellIndex)를 파악하고(srcInx),

기존 테이블의 행 정보를 가지고 와서

해당 컬럼(srcInx)의 위치(cellIndex)에 있는

컬럼만 복제한다(cloneNode).


모든 행을 복제해야 하기 때문에

행의 개수(rows.length)만큼 반복하고,

각 행(TR - rows[x])을 먼저 복제한다.

새로운 테이블에 행이 없기 때문에

행을 먼저 만들어야 열을 추가할 수 있다.

행을 복제하고

행의 이동할 컬럼만(rows[x].cells[srcInx])만 복제해서 추가한다.


새로 생성하지 않고 복제를 하는 이유는

각 행과 열(컬럼)이 가지는 정보(내용, CSS등)들을

그대로 복제해야 실제 테이블에서 보이는 것과

별도 창에서 보이는 것이 같아 지기 때문이다.


복제에 사용한 cloneNode 메소드의 파라이터로

행을 복제할 때는 false를

열을 복제할 때는 true를 지정했다.

false는 자식 정보를 복제하지 않겠다는 것을 의미하는 것으로

행을 복제할 때 자식 정보를 복제하면

모든 열(컬럼)이 복제되기 때문이다.

행의 스타일만 복제하고

열은 지정된 열(srcInx)에 대한 모든 것을 복제한다.


JQuery 변환 방법에 대해서는

테이블 정렬(table Sorting)등에서 정리했기 때문에

별도로 정리하지 않는다.

다만, 앞서 두 번째 구현에서

정리한 addClass(), removeClass() 함수는 사용하지 않는다.

동일한 이름의 동일한 기능이 JQuery에서 제공되고 있다.

함수가 아니라 메소드로 사용하는 차이가 있다.

    AAA.addClass("dragging")
    AAA.removeClass("dragging")

전체코드



응용

이상의 MouseDown, MouseMove, MouseUp 등의

마우스 이벤트는 다양하게 활용될 수 있다.


조금 작은 예제로 팝업 윈도우를 구현 할 수 있다.

최근의 웹 개발에서는 팝업 윈도우를 DIV로 구현한다.

그림과 같이 팝업 윈도우의 헤드를

마우스를 누르고, 움직이고, 놓으면

해당 팝업 윈도우가 따라 움직이도록 구현한다.


전체 코드

좀 큰 예제는 파워포인트를 구현하는 것이다.

파워포인트의 슬라이드 위에서 

마우스를 누르고, 움직이고, 놓으면

움직인 만큼을 크기로 하는 도형이 생성된다.

다시 해당 도형을

마우스를 누르고, 움직이고, 놓으면

놓은 위치로 도형을 이동시켜 준다.

이러한 기능을 잘 구현한 예제가 다음 주소에 있다.

(SVG를 사용한 것으로 구현 방법에 차이가 있다.)

https://jgraph.github.io/mxgraph/javascript/


단계 1: 마우스 드레그 1 -  좌에서 우로

단계 2: 마우스 드레그 2 - 우에서 좌로 추가

단계 3: 도형 생성

단계 4: 도형 이동



한 개발자가 한탄하는 말투로 이야기 했다.

며칠 전 고객이 웹 페이지에 출력되는 오류 메시지 중 톰캣 버전이 출력되는 것을 없애달라고 했단다.

인터넷 검색으로 어렵게 찾아서 수정해 줬더니

수정한 걸 본 고객이

그게 아니라면서 오류가 나면 오류 안내 페이지(404 not found)가 나오게 해달라고 했다는 것이다.

똑바로 이야기 했으면 한 번에 할 일을 괜히 시간 낭비했다고 말했다.


출처: https://commons.wikimedia.org

이야기가 끝나자 마자 그 개발자가 잘 못한 것이라고 말했다.

고객은 SW 개발을 잘 모르고 말을 하기 때문에,

고객의 의도를 파악해서 일하는 게 개발자의 능력이라고 말했다.

더욱이 웹 보안 상 오류 메시지를 웹 페이지에 그냥 출력 하면 안 된다.


이런 일은 흔하게 발생한다.

개발자는 대부분 항상 을이기 때문에,

고객이든 직장 상사든, 동료와 협력하든 누군가의 지시나 요청에 의해 일을 하게 되고

상대의 의도를 제대로 파악하지 못하면 본인이 손해를 보게 된다.

또, 이렇게 일이 엇나가면 서로에 대해서 불평하고 비난하다가 불신만 쌓여서

SW 개발이 어려워지는 것이 아닌가 한다.


다른 예로, 나에게 머리카락을 자르는 일은 참 어려운 일이다.

외모에 별로 관심이 없어서 아는 지식이 없다.

어릴 때는 그냥 짧게 깎았고,

나이가 들어서는 인상이 부드러워 보이고 탈모도 감춰야 해서

머리카락을 길게 하려는데

어떻게 깎아달라고 말해야 하는지 아직도 모른다.

머리카락을 기르기 시작하고는 한 달에 한번씩 고통(?)이었다.


출처: http://riflessiacconciature.com/servizi-capelli/

B 클럽 미용사는 조금만 말을 잘 못해도 무조건 짧게 깎았다.

심지어 제대로 깎은 후에 어떻게 말하면 된다는 말을 듣고 기억해서 다음 번에 이야기 했지만 소용없었다.

그러다 아내와 그 친구들이 좋다고 강조한 동네 미용실을 갔는데

최근 몇 년간은 편하게 미용실을 다니고 있다.

처음 몇 달은 이런 저런 요구 사항을 떠듬거리면서 말했고

미용사는 깎은 후, 맘에 드는지를 확인했다.

그 후로는 알아서 깎아주기 때문에 퇴근길에 잠깐 들리면 된다.


좋은 미용사(동네 미용사)와 안 좋은 미용사(B 클럽 미용사)의 차이는

나의 의도를 이해해서 머리카락을 깎아주고

나를 기억해 주는 것이라고 생각한다.


SW 개발도 이와 같다고 생각한다.

진정한 개발 능력은 개발 년차나 알고리즘 작성 능력이 아니라

상대가 원하는 것을 제대로 구현해 주는 것이지 않나 싶다.


'개발 이야기' 카테고리의 다른 글

두 마리 소  (0) 2017.10.13
꿩 잡는 게 매다  (1) 2017.08.05
누울 자리 봐 가며 발 뻗어라.  (0) 2017.05.04
꼴뚜기가 뛰니 망둥이도 뛴다  (0) 2017.05.04
하룻강아지 범 무서운 줄 모른다  (0) 2017.04.29

+ Recent posts