목차

Canvas로 벽돌깨기 만들기

들어가기 전

이 포스트는 Billmill.org의 Canvas Tutorial중 벽돌깨기 튜토리얼을 기반으로 작성되었습니다. 원작자에게 수정 등을 허락받은 포스트입니다.

Thanks Bill :)

목차 및 내용 중 다른 부분이 있을 수 있으며 원 포스트를 보고싶으신 분은 아래 주소를 이용해 주세요.

Billmill.org - Billmill GitHub

자 이제 Canvas와 Javascript, jQuery를 사용하여 벽돌깨기를 만들어 보겠습니다.

Canvas를 처음 접하신다면...

Canvas를 처음 접하셨거나 해본적이 없으실 경우! 아래 링크를 통해서 제가 이전에 올린 포스트를 한번 보시는 것도 좋을 듯합니다!

About Javascript - 15. 캔버스와 그래픽 바로 가기

1. 색이 있는 원을 그리기

소스보기
데모보기

1번 색이 있는 원 그리기 소스와 데모는 위 링크로 확인 가능합니다.


처음은 색이 있는 원을 만들어 봅시다.

시작은 아무 것도 없겠죠? 우선 도화지를 만들어 준 다음 벽돌을 깰 공을 그려줘야 합니다.

여기서 말하는 도화지란 Canvas를 의미합니다.

우선 HTML에 Canvas를 그려 줍시다.

<canvas id="canvas" width="300" height="300"></canvas>

이로써 기본 도화지는 준비 됐네요.


하지만 화면에는 아무것도 보이지 않습니다.


생성한 Canvas에 Javascript를 이용하여 그림-원-을 그려봅시다.

//canvas 가져오기 
var ctx = $('#canvas')[0].getContext("2d");
 
//캔버스 객체에 원 그리기 
ctx.beginPath();
ctx.arc(75, 75, 10, 0, Math.PI*2, true);
ctx.closePath();

자 이제 html파일을 열어보면 원이 그려졌습니다!

원


위 그림과 같은 간단한 원이 보입니다.

2. 움직이는 공 만들기

소스보기
데모보기


첫 튜토리얼로 멈춰있는 공을 만들어봤습니다.

하지만 우리가 원하는 공은 벡터를 가진 공이죠..!

기존 소스에 아래와 같은 변수를 추가해 준 후 초기화 해줍니다.

//init 
var WIDTH, HEIGHT;  // canvas의 크기를 담을 변수 
var x = 150,        // 공의 x, y 좌표 값 
    y = 150,
    radius = 10;    // 공의 반지름 
var dx = 2,         // 매 프레임 이동하는 공의 방향 
    dy = 4;         // x로 2의속도 y로 4만큼 움직인다. 
 
var anim;           //애니메이션을 담을 변수 
 
function init() {
    //canvas 가져오기 
    ctx = $('#canvas')[0].getContext('2d');
    WIDTH = $('#canvas').width();
    HEIGHT = $('#canvas').height();
 
    //animation 
    anim = window.requestAnimationFrame(draw);
}
 
init();

특이한 점은 setInterval()로 애니메이션을 사용하지 않고 requestAnimationFrame()을 사용하고 있습니다.


setInterval은 다른탭이 선택되어 있어도 언제나 콜백 함수를 호출하기 때문에 시스템 리소스낭비가 생기며, 디스플레이 갱신 전에 캔버스를 여러번 고치더라도 실제로 적용되는건 디스플레이 갱신바로 전 것만 적용 되기 때문에 프레임 손실이 발생 할 수 있습니다!


그래서 Bill의 원래 예제와는 다르게 requestAnimationFrame을 사용해 주었습니다.


하지만 위 소스는 오류가 발생할 것 입니다.

requestAnimationFrame에서 호출하는 draw 메서드를 그려주지 않았기 때문이죠.

draw 메서드는 매 프레임마다 어떻게 그려줄지 정의 되어있습니다.

function draw() {
    clear();
    ball(x, y, radius);
 
    x += dx;
    y += dy;
 
    if(>= WIDTH - radius || x <= 0 + radius){
      dx = -dx;
    }
    if( y >= HEIGHT - radius || y <= 0 + radius){
      dy = -dy;
    }
 
    anim = window.requestAnimationFrame(draw);
}
 
function clear() {
    ctx.clearRect(0, 0, WIDTH, HEIGHT);
}
 
function ball(x, y, r) {
    ctx.fillStyle = '#03158a';
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}

clear함수를 통해서 매 호출 시 캔버스를 Clear해줍니다.

그 후 ball 메서드를 이용해서 공을 그려 줍니다.

draw 내부에서는 공이 벽에 닿을 때마다 dx, dy값을 -로 곱해줘서 반대 방향으로 가게끔 처리해줍니다.

이렇게 해주면 아래 그림과 같이 움직입니다.

2번


벽에 부딪히면 반대 방향으로 바뀝니다. 마치 상자에 갇혀있는 것 같군요

3. 폐달 만들어 주기

소스보기
데모보기


벽돌깨기는 공이 하단으로 떨어지면 게임오버가 됩니다.

하지만 무조건 떨어지면 게임오버가 아니고 사용자가 폐달로 제대로 막지 못할 경우 게임오버가 되는건 다들 알고 계시죠?

폐달을 만들어 주고 게임오버 기능을 넣어 줍시다.

var paddlex, paddleh, paddlew; //폐달 변수를 추가해 줍니다. 
 
function init_faddle() {
    paddlex = WIDTH / 2;
    paddleh = 10;
    paddlew = 75;
}
// 폐달에 위치와 크기를 초기화 해 줍니다. 
init_faddle();
 
// 사각형을 그리기 위한 함수 
// 폐달 뿐 아니라 나중에 벽돌을 그려줄때도 활용 합니다. 
function rect(x, y, w, h){
  ctx.beginPath();
  ctx.rect(x, y, w, h);
  ctx.closePath();
  ctx.fill();
}
 
function draw() {
    clear();
    ball(x, y, radius);
    //폐달을 그린다. 
    rect(paddlex, HEIGHT - paddleh, paddlew, paddleh);
 
    x += dx;
    y += dy;
 
    if (>= WIDTH - radius || x <= 0 + radius) {
        dx = -dx;
    }
    if (<= 0 + radius) {
        dy = -dy;
    }else if(>= HEIGHT - radius){
        //하단일 경우 폐달의 위치를 고려해서 방향을 반대로 변경한다. 
        if(> paddlex && x < paddlex + paddlew){
          dy = -dy;
        }else{
          //Game Over 
          window.cancelAnimaionFrame(anim);
        }
    }
 
    anim = window.requestAnimationFrame(draw);
}

추가된 부분은 주석이 붙어 있으니 보실때 참고 하시면 됩니다.

간단하게 첨언하자면 폐달 관련된 정보를 담는 변수를 추가 합니다.

그 변수를 이용하여 폐달을 그려주고 사각형을 그리는 rect() 메서드를 추가해 줍니다.

이 때 중요한건 공이 하단으로 왔을 때 폐달의 위치랑 비교해서 폐달을 벗어난 위치라면 게임오버 처리 합니다.

게임오버 될 경우 현재 애니메이션을 담은 anie변수를 cancelAnimaionFrame에 넣어주시면 되며 애니메이션이 멈추게 됩니다.


3번
위와 같이 처음에 폐달에 부딪혀 위로 튕기나 다시 하단으로 돌아올 때는 폐달의 위치를 벗어났기 때문에 게임오버(공이 멈추게) 됩니다.

4. 폐달을 움직이기 (키보드 & 마우스)

소스보기
데모보기


폐달이 현재 움직이지 못하고 있습니다.

당연히 키보드나 마우스 이벤트를 줘야 움직이겠지요?

이벤트 사용을 위한 아래 변수를 선언해 줍니다.

//true로 된 방향으로 이동하게 됩니다.(키보드 필요) 
var is_leftPannel = false,
    is_rightPannel = false;
//브라우저 좌표상 캔버스의 최소 x, 최대 x 좌표입니다. (마우스 필요) 
var canvasMinX, canvasMaxX;
 
//init 메서드에 위 변수에 대한 초기화를 추가해 줍시다. 
init(){
    ...
    canvasMinX = $('#canvas').offset().left;
    canvasMaxX = canvasMinX + WIDTH;
    ...
}

위에 변수는 키보드 입력 시 필요한 것이고, 아래 변수는 마우스 위치때문에 필요한 변수입니다.

필요한 변수를 선언했으니 이벤트를 선언해 줍시다.

//Event 
$(document).on('keydown', function(e) {
    if (e.which == 37) {
        is_leftPannel = true;
    } else if (e.which == 39) {
        is_rightPannel = true;
    }
});
 
$(document).on('keyup', function(e) {
    if (e.which == 37) {
        is_leftPannel = false;
    } else if (e.which == 39) {
        is_rightPannel = false;
    }
});
 
function onMouseMove(e) {
    if (e.pageX >= canvasMinX && e.pageX <= canvasMaxX) {
        paddlex = e.pageX - canvasMinX - (paddlew/2);
    }
}
 
$(document).mousemove(onMouseMove);

키보드와 마우스 이벤트를 추가해 줬습니다.
keydown(키를 누를 때) is_...Pannel 변수가 각각 true로 변경되고
keyUp(키를 땔 때) is_...Pannel 변수가 각각 false로 변경됩니다.

즉 누를 때 만 사태가 변경되는 것이죠.

마우스의 경우 브라우저 상 캔버스의 위치를 마우스 위치와 비교하여, 마우스의 위치가 폐달 가운데가 되게 처리해 줍니다.

마우스의 경우 현재 동작 할 테지만 키보드는 눌러도 폐달이 움직이지 않습니다.

왜냐면 그릴 때마다 저 값을 어떻게 사용할지 안정해 줬기 때문이죠

자 그럼 정해 줘야겠죠? draw() 메서드에 아래 코드를 추가해 줍니다.

draw(){
    ...
    if (is_leftPannel && paddlex > 0) {
        paddlex -= 5;
    }
    if (is_rightPannel && paddlex + paddlew < WIDTH) {
        paddlex += 5;
    }
    ...
}

위와 같이 해주게 되면 매 프레임마다 draw()를 그려 줄 때 is_...Pannel 변수를 확인 하여 폐달의 위치를 다르게 그려줍니다.
매번 그려 줄때마다 paddlex값이 변경되기 때문이죠.

4번


사용자 컨트롤을 받아서 폐달이 움직이게 됩니다.

이제야 좀 모양이 나오는 것 같습니다.

5. 벽돌 추가 하기

소스보기
데모보기


공은 왔다갔다 하는데.. 심심합니다.

벽돌을 만들어 줄 때가 온 것 같습니다.

이전에 만든 rect메서드를 재활용하여 벽돌을 만들어 줍시다.

만들어 주기 이전에 이번에도 벽돌정보를 담는 변수를 먼저 추가해 줍니다.

//bricks 
 
//bricks[x][y]의 다중배열, 벽돌의 상태를 저장합니다. 
//1이면 유효한 벽돌 2면 파괴된 벽돌입니다. 
var bricks;
 
var NROWS; // 가로 벽돌 개수 
var NCOLS; // 세로 벽돌 개수 
var BRICKWIDTH; // 벽돌의 가로 길이 
var BRICKHEIGHT; // 벽돌의 세로 길이 
var PADDING; // 벽돌간의 간격 

우선 초기화를 해줍시다

function init_bricks() {
    NROWS = 5;
    NCOLS = 5;
    PADDING = 1;
    BRICKWIDTH = (WIDTH / NCOLS);
    BRICKHEIGHT = 15;
 
    bricks = new Array(NROWS);
    for (= 0; i < NROWS; i++) {
        bricks[i] = new Array(NCOLS);
        for (= 0; j < NCOLS; j++) {
            bricks[i][j] = 1;
        }
    }
}
init_bricks();

위에서 선언한 변수의 기본 값을 넣어 줍니다.

그 후 bricks에 new Array로 다중 배열을 선언합니다.
NROWS, NCOLS 개수만큼의 배열을 생성하여 벽돌이 유효하다는 값인 1을 넣어줍니다.

초기화가 되었으니 이 변수들을 활용하여 매번 draw()를 호출 할때 벽돌을 그려 주도록 합시다.

function draw() {
    ...
 
    //draw bricks 
    for (= 0; i < NROWS; i++) {
        for (= 0; j < NCOLS; j++) {
            if (bricks[i][j] == 1) {
                rect(* BRICKWIDTH, i * BRICKHEIGHT
                    , BRICKWIDTH - PADDING, BRICKHEIGHT - PADDING);
            }
        }
    }
 
    //Have We Hit a Bricks? 
    var row = Math.floor(y/(BRICKHEIGHT+PADDING));
    var col = Math.floor(x/(BRICKWIDTH+PADDING));
    if(row < NROWS){
        if(bricks[row][col] == 1){
            dy = -dy;
            bricks[row][col] = 0;
        }
    }
    ...
}

주석 draw bricks 부분에서는 bricks다중배열을 확인하여 값이 1인 위치에 벽돌을 그려 주고 있습니다.

아래 Have We Hit a Bricks? 주석 부분에서 벽돌과 공이 부딪히는 처리를 해줍니다.
현재 공의 위치 / (벽돌 세로 혹은 가로 길이 + 간격)으로 row와 col을 확인 하여 해당 값으로 bricks의 배열의 값이 1일 경우 0으로 변경해줍니다.
이때 공의 y방향은 반대로 바꿔주면서 벽돌이 깨진 효과를 만들어 줍니다.

5번


벽돌깨기의 모습이 이제 갖춰진 것 같습니다.

6. 마무리

소스보기
데모보기


사실 5번에서 모든게 끝났다고 해도 과언이 아니긴합니다.

하지만 공의 각도가 패널에 의해서 미묘하게 변한다면 더 재밌는 블록깨기가 될 것 같습니다.

if (>= WIDTH - radius || x <= 0 + radius) {
    dx = -dx;
}
if (<= 0 + radius) {
    dy = -dy;
} else if (>= HEIGHT - radius) {
    if (> paddlex && x < paddlex + paddlew) {
 
        //공이 닫은 패널 위치에 따라서 dx의 값을 변경해줍니다 
        dx = -((paddlex + (paddlew/2) - x)/(paddlew)) * 10;
 
        dy = -dy;
    } else {
        is_gameover = true;
    }
}

위 같은 방식으로 패널위치에 따른 공의 각도변화가 가능해집니다.



6번
지금까지 캔버스를 사용하여 기본적인 벽돌깨기를 만들어 보았습니다.

하지만 여기서 끝이 아닙니다.!

이외에도 공의 색을 바꾸거나 블록의 색을 바꿔주거나 점수를 추가 한다던지 여러가지 방식으로 응용 할 수 있겠죠?

마치며

6편으로 나누는거보다 한번에 보는게 나을 것 같아서 글을 나누지 않았습니다.
흔쾌히 포스트 허락해 주신 Bill에게도 너무 감사드립니다.
이만 글을 마치겠습니다.

참고

Billmill.org - Canvas Tutorial
MSDN - requestAnimationFrame