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

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

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

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()을 호출하면 된다.


게임을 실행한 후,

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



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

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

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

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

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

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

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

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

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

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

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




+ Recent posts