javascript - 벽돌깨기

김동하·2020년 9월 30일
0

javascript

목록 보기
40/58

공 움직이기

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

// 원을 만드는 코드

ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI*2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();

// 테두리만 있는 직사각형 만드는 코드

ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgba(0, 0, 255, 0.5)";
ctx.stroke();
ctx.closePath();

공이 화면 안에서 움직이는 것은 매 프레임마다 공이 새롭게 만들어지는 동시에 화면이 refresh되는 것이다. setInterval로 draw함수가 계속 실행되게 해주면 된다. 원이 프레임마다 움직이게 하려면 변수 좌표를 사용해야 한다.ctx.arc()에 지정 좌표 대신 x,y를 넣는다. 그리고 x와 y 변수를 canvas의 width와 height를 이용하여 정의한다. 이제 매 프레임마다 x,y의 값을 변화시켜야 한다. 그렇기 위해서는 일정 값을 빼주거나 더해줘야 한다. dx, dy에 값을 할당하고 x, y에 더해준다.

const dx = 2;
const dy = -2;

function draw() {
    ctx.beginPath();
    ctx.arc(x, y, 10, 0, Math.PI * 2);
    ctx.fillStyle = "red";
    ctx.fill();
    ctx.closePath();
    x += dx;
    y += dy;
}
setInterval(draw, 10)

매 프레임마다 x는 증가하고 y는 감소한다. 이제 원이 움직인 궤적을 없애기 위해서 매프레임마다 캔버스를 초기화하면 된다.

ctx.clearRect(0, 0, canvas.width, canvas.height)

 하지만 공은 캔버스 너머 저 세상 끝으로 사라진다. 벽에 맞으면 튕기도록 즉, 벽에 닿으면 좌표를 바꾸도록 설정해야 한다. 일단 필요한 것은 공의 반지름이다. 공의 반지름을 공 만드는 메소드에 대입한다.

상단을 설정하기 위해서 일단 좌상단 기준으로 프레임은 (0,0)으로 시작하는 것을 생각해야 한다. 즉, y와 dy를 더한 좌표(매 프레임 이동하는 값)이 0보다 작으면 프레임을 넘어간 것이다. 반대로 하단은 y + dy 위치 값이 canvas.height보다 크면 넘어가는 것이다. 똑같이 x는 canvas.width를 기준으로 프레임 밖인지 확인하면 된다. ballRadius를 가지고 수정하면 완벽하게 튕겨냄을 재현 가능하다.

패들 만들기

공을 쳐내는 패들을 만들어야 한다. 그리고 패들은 키보드로 조정할 수 있어야 한다. 일단 왼쪽인지 오른쪽인지 확인하는 두개의 변수와 keydown / keyup 이벤트 리스너, 각 이벤트 리스너가 실행됐을 때 함수, paddle의 좌표를 변화시키기가 필요하다.

먼저 버튼을 누르는 것을 boolen 변수로 초기화. 디폴트는 false. keydown이 되면 keydown 함수가 실행된다. 인자로 이벤트를 받고 왼쪽/오른쪽 중 어떤 키를 눌렀냐에 따라 boolen 변수를 true로 바꿔준다.

오른쪽 e.keyCode === 39, 왼쪽 e.keyCode === 37. keyup 함수도 똑같이 해준다.

이제 패들을 이동시켜야 한다. keydown일 때 이동하고 keyup일 때 멈춘다. draw()함수에 rightPressed가 true일 때 x좌표 값을 더해준다. leftPressed는 반대. 이제 패들이 화면 밖으로 안 나가게 좌표값에 제한을 둔다.

   if (rightPressed && paddleX < canvas.width - paddleWidth) {
        paddleX += 5;
    }
    if (leftPressed && paddleX > 0) {
        paddleX -= 5;
    }

게임 오버

공이 프레임 하단으로 떨어지면 게임 오버가 되는 것이다. 그리고 페이지를 리로딩 하면 된다.

   if (y + dy < ballRadius) {
        dy = -dy
    } else if (y + dy > canvas.height - ballRadius) {
        document.location.reload();
    }

공이 아래로 떨어졌을 때 경우만 수정한다. 이제 패들이 공을 튕겨내게 하면 된다. 공의 중심이 패들의 내부에 있는지 확인하면 된다. 즉, 공이 캔버스 밑에 닿는 순간 패들 안에 있는지 확인해야 한다.

paddleX는 paddle의 위치에 따라 변화하는 값이다.

  if (y + dy < ballRadius) {
        dy = -dy
    } else if (y + dy > canvas.height - ballRadius) {
        if (x > paddleX && x < paddleX + paddleWidth) {
            console.log(x, paddleX)
            dy = -dy;
        } else {
            document.location.reload();
        }
    }
if (x > paddleX && x < paddleX + paddleWidth) {
    dy = -dy;
}

이 부분에서 x > paddleX가 이해가 안 됐는데 이 코드가 없다면 공이 그냥 하단만 맞아도 튕겨진다. x, 즉 원의 중심이 paddleX보다 클 때는 하단부로 떨어질 때다. 그러니까 저 코드를 이용해서 하단부에 떨어질 때만 조건을 걸어두는 것이다?!

벽돌 만들기

벽돌은 2차원 배열을 통해 만든다. 가로, 세로, 행, 열, offset이 필요하다. 사실 여기서부터 굉장히 헷갈린다... 일단 2차원 배열을 만들고 배열의 개체엔 화면에 벽돌을 그릴 위치를 나타낼 x,y 위치를 저장한다.

let bricks = []
for (let col = 0; col < brickColumnCount; col++) {
    bricks[c] = []
    for (let row = 0; row < brickRowCount; row++) {
        bricks[col][row] = { x: 0, y: 0 }
    }
}

(벽돌 위치를 초기화를 먼저 하는 건가? drawBricks함수 전에 위 코드를 한 번 더 쓰는 이유를 모르겠음)

그리고 drawBricks함수를 만들어서 벽돌을 그린다.

function drawBricks() {
    for (let col = 0; col < brickColumnCount; col++) {
        for (let row = 0; row < brickRowCount; row++) {
            bricks[col][row].x = 0;
            bricks[col][row].y = 0;
            ctx.beginPath();
            ctx.rect(0, 0, brickWidth, brickHeight);
            ctx.fillStyle = "#0095DD";
            ctx.fill();
            ctx.closePath();
        }
    }
}

이제 offset을 이용해 각 x,y 값에 변수를 준다. (생각하기 좀 어려움...)

 let brickX = (col * (brickWidth + brickPadding)) + brickOffsetLeft;
 let brickY = (row * (brickHeight + brickPadding)) + brickOffsetTop;

변수들을 drawBricks()에 x,y대신 넣어준다. ctx.rect의 좌상단 값도 바꿔야 떨어져 위치한다.

벽돌 충돌 감지

충돌 감지 함수를 만들어야 한다. 모든 벽돌의 위치를 순회하고 각 벽돌의 좌표를 공의 위치와 비교! 패들에 공을 튕겼던 것과 비슷하다. 충돌이 된 벽돌 객체를 저장할 b 변수를 정의한다.

공의 위치가 어떤 벽돌의 범위에 있을 때 조건을 만족해야 방향을 바꾼다. 패들에 공이 부딪혔을 때와 비슷하다.

function collisionDetection() {
    for (let col = 0; col < brickColumnCount; col++) {
        for (let row = 0; row < brickRowCount; row++) {
            let b = bricks[col][row]
            if (x > b.x && x < b.x + brickWidth && y > b.y && y < b.y + brickHeight) {
                dy = -dy;
            }
        }
    }
}

벽돌 부수기

벽돌에 닿았을 때 없어질 벽돌이란 것을 확인해야한다. 각 벽돌 객체에 status라는 프로퍼티를 추가해서 공이 닿으면 값을 0으로 바뀐 값들을 처리하면 된다. 새롭게 벽돌을 그리기 전에 status를 확인해서 값이 0인 벽돌은 그리지 않는다. 즉, statue가 1인 벽돌만 새로 그린다. 그리고 collisonDectection 함수에도 status 값을 포함한다. status가 1이라면 공의 위치를 바꾸고 벽돌의 status를 0으로 만든다.

점수를 계산하자

 ctx.font = "16px Arial";
 ctx.fillStyle = "#0095DD";
 ctx.fillText("Score: "+score, 8, 20);

ctx의 메소드를 이용해서 벽돌이 깨질 때마다 score가 올라갈 수 있게 만든다. score가 총 벽돌 수와 같다면 게임 끝!

마우스로 이동하기

movermove 이벤트 리스너를 만든다. 마우스 포인터 좌표에 따라 패들 좌표를 업데이트 해야한다. (어려움)

먼저 상대적인 relativeX의 값을 구한다. 이 값은 그리고 e.clientX에서 캔버스 왼쪽 가장자리(canvas.offsetLeft)사이의 마우스 위치를 뺀 값이다.

function mouseMoveHandler(e) {
    let relativeX = e.clientX - canvas.offsetLeft;
    if (relativeX > 0 && relativeX < canvas.width) {
        paddleX = relativeX - paddleWidth / 2;
    }
}

어렵다..

상대적인 X 포인터 위치가 0보다 크고 캔버스 폭보다 적으면 포인터가 경계 내에 있게 되고, paddleX의 위치(패들 왼쪽 가장자리에 배치되어 있다)는 패들 width의 반을 뺀 relativeX의 값으로 설정되므로 이동은 실제로 패들 가운데 상대적으로 이동이 됩니다.

출처 : MDN

생명 만들기

스코어랑 똑같았다. lives 변수 만들고 죽으면 깎자.

로직을 추가해야하는데 떨어지면 lives를 깎고 if(!lives)라면 게임을 종료하는 것이다. 만약 lives가 있다면 x,y,dx,dy,paddleX 를 다시 준다. (이 부분 헷갈림)

 lives--;
            if (!lives) {
                document.location.reload();
            } else {
                x = canvas.width / 2;
                y = canvas.height - 30;
                dx = 3;
                dy = -3;
                paddleX = (canvas.width - paddleWidth) / 2;
            }
        }

참고 : https://developer.mozilla.org/ko/docs/Games/Tutorials/%EC%88%9C%EC%88%98%ED%95%9C_%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC_%EC%9D%B4%EC%9A%A9%ED%95%9C_2D_%EB%B2%BD%EB%8F%8C%EA%B9%A8%EA%B8%B0_%EA%B2%8C%EC%9E%84/Finishing_up

profile
프론트엔드 개발

0개의 댓글