출처 : Breakout Game | HTML5 Canvas API udemy - Brad traversy
canvas를 dom에 그리는 것은 간단히 canvas tag를 활용하여 만들 수 있다.
<canvas id="canvas" width="800" height="600"></canvas>
Canvas API 중에는 CanvasRenderingContext2D interface 가 있는데, canvas element의 표면을 그리는데 2d rendering context를 제공한다. 원이나 선을 그리거나 문자를 적거나 다양한 이미지 등을 그리는 데에 사용이 된다.
Canvas의 2D rendering context를 얻기 위해서는 canvas element에서 '2d'를 인자로 받는 getContext() 를 부르면 된다.
JS)
const ctx = canvas.getContext("2d");
이제 이 canvas API를 활용하여 간단한 벽돌 깨기 게임을 만들어보자.
아래 받침대를 키보드 화살표왼쪽과 오른쪽을 이용하여 움직이며 움직이는 공을 받아쳐 벽돌을 깨는 게임. 벽돌을 깰 때마다 점수가 1점씩 올라가며, 중간에 공을 놓치면 게임이 다시 처음부터 시작된다.
코드 짜기 전에 간단히 해결해야할 코드 로직 짜기.
1. canvas context 만들기 ✔️
2. 공 만들고 그리기
3. 받침대 만들고 그리기
4. 벽돌 만들고 그리기
5. 점수판 만들고 그리기
6. requestAnimationFrame(cb)을 활용하여 update 함수 만들기 (움직이는 물체들을 계속해서 화면에 그리기 위해)
7. 받침대 움직이게 만들기
8. 키보드 이벤트를 활용하여 화살표 오른쪽, 왼쪽 누를 때마다 받침대가 움직이도록 만들기
9. 공 움직이게 만들기
10. 캔버스 내에서만 공이 움직이게끔 설정하기
11. 벽돌을 깰때마다 점수 증가시키기, 벽돌 없애기
12. 공을 놓칠 경우 - 벽돌 전부 다시 그리기, 점수 리셋하기
캔버스 내에 공과, 받침대를 그려보자.
먼저 각각의 공과 받침대의 x와 y 위치, 너비와 높이 등을 객체를 통해 설정해준 후 이를 활용하여 화면에 그리는 함수를 만든다.
// Create ball props
const ball = {
x: canvas.width / 2,
y: canvas.height / 2,
size: 10,
speed: 4,
dx: 4,
dy: -4,
};
// Create paddle props
const paddle = {
x: canvas.width / 2 - 40,
y: canvas.height - 20,
w: 80,
h: 10,
speed: 8,
dx: 0,
};
dx와 dy는 물체가 움직이게 될 때 각각 x축과 y축에서 속도이다.
ball은 위로 움직이도록 만들기 위해 dy값에 -를 붙혔다. 받침대는 화살표 키로만 움직이게끔 만들고자 dx 값을 0으로 설정하였다. 나중에 keydown 이벤트를 통하여 dx 값을 speed로 바꾸어줄 예정!
이제 공과 받침대를 화면에 그리는 함수를 만들어주기.
도형을 그리기 위해 다음의 methods이 사용된다.
beginPath는 캔버스 내에서 새로운 path를 만들때, closePath는 path를 마칠 때 쓰이는 method이다.
beginPath를 먼저 적어준 후, 만들고자 하는 도형이나 선 등을 원하는 색깔등으로 그려줄 수 있다.
fill()은 도형의 색을 칠해주는 method이다.
fillStyle()은 칠하고자 하는 색을 지정한다.
fillRect()는 직사각형을 그리는 method이다. 총 4개의 인자를 필요로 하고 x축과 y축 위치, 너비와 높이를 적는다.
arc()는 원을 그리는 method이다. 총 5개의 인자를 필요로 하고 x축과 y축 위치, 반지름 길이 를 적어준다. 0은 원의 시작 각도, 2 * Math.PI는 원이 끝나는 각도(360도)이다.
// Draw ball
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.size, 0, 2 * Math.PI);
ctx.fillStyle = "#0095dd";
ctx.fill();
ctx.closePath();
}
// Draw paddle
function drawPaddle() {
ctx.beginPath();
ctx.fillRect(paddle.x, paddle.y, paddle.w, paddle.h);
ctx.fillStyle = "#0095dd";
ctx.fill();
ctx.closePath();
}
다음으로 벽돌을 그려보자.
가로 행에 9개 세로 열로 5개의 벽돌을 만들기 위해 for loop를 이용할 수 있다.
공과 받침대의 필요한 정보를 담은 객체를 만들었던 방식으로 벽돌도 객체를 만들어준다. 벽돌이 공에 부딪히면 보이지 못하게끔 만들어야 하기 때문에 visible property도 만들어준다.
const brickRowCount = 9; //한 행에 들어있는 item 갯수
const brickColumnCount = 5; // 한 열에 들어 있는 item 갯수
// Create brick props
const brickInfo = {
w: 70,
h: 20,
padding: 10,
offsetX: 45,
offsetY: 60,
visible: true,
};
// Create bricks
const bricks = [];
for (let i = 0; i < brickRowCount; i++) {
bricks[i] = [];
for (let j = 0; j < brickColumnCount; j++) {
const x = i * (brickInfo.w + brickInfo.padding) + brickInfo.offsetX;
const y = j * (brickInfo.h + brickInfo.padding) + brickInfo.offsetY;
bricks[i][j] = { x, y, ...brickInfo };
}
}
열로 묶어진 배열의 형태를 가지고 있는 벽돌들을 forEach를 이용하여 화면에 그려준다.
// Draw bricks on canvas
function drawBricks() {
bricks.forEach((column) => {
column.forEach((brick) => {
ctx.beginPath();
ctx.rect(brick.x, brick.y, brick.w, brick.h);
ctx.fillStyle = brick.visible ? "#0095dd" : "transparent";
ctx.fill();
ctx.closePath();
});
});
}
캔버스의 상단 오른쪽에 점수판을 그려준다.
let score = 0;
// Draw score on canvas
function drawScore() {
ctx.font = "20px Arial";
ctx.fillText(`Score: ${score}`, canvas.width - 100, 30);
}
원하는 text를 기입하는 method 이다. 원하는 text, x축과 y축의 위치 순으로 인자를 받는다. maxWidth는 선택사항이다.
원하는 font 이름과 크기를 설정하는 method이다.
requestAnimationFrame(cb)은 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출하게 한다. 이 메소드는 리페인트 이전에 실행할 콜백을 인자로 받는다.
다른 위치에 있는 다른 물체들을 계속해서 캔버스 위에 그려낸다.
즉 계속해서 움직이는 공과 받침대를 화면에 그리기 위해 이 method를 활용할 수 있다.
update 함수 내에 requestAnimationFrame(update)를 적으줌으로써 계속해서 update함수를 호출한다. 이를 통해 계속해서 바뀌는 도형들을 화면상에 표현한다.
// Draw everything
function draw() {
// clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
drawPaddle();
drawScore();
drawBricks();
}
// Update canvas drawing and animation
function update() {
movePaddle();
moveBall();
// Draw everything
draw();
requestAnimationFrame(update);
}
update();
update 함수를 만들었으니 이제 도형들을 동적으로 만드는 함수 movePaddle()을 만들어보자.
받침대가 canvas밖까지 움직이지 못하게 만들기 위해 경계선을 설정해준다.
// Move paddle on canvas
function movePaddle() {
paddle.x += paddle.dx;
// Wall detection
if (paddle.x + paddle.w > canvas.width) {
paddle.x = canvas.width - paddle.w;
}
if (paddle.x < 0) {
paddle.x = 0;
}
}
받침대의 dx 값이 0으로 지정되어 있기 때문에 아직은 받침대가 움직이지 않는다. 키보드 이벤트를 활용하여, 화살표 왼쪽과 오른쪽을 누를 때에만 받침대가 움직이게끔 만들 수 있다.
keydown event와 keyup event를 활용할 수 있다.
// Keydown event
function keyDown(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
paddle.dx = paddle.speed;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
paddle.dx = -paddle.speed;
}
}
function keyUp(e) {
if (
e.key === "Right" ||
e.key === "ArrowRight" ||
e.key === "Left" ||
e.key === "ArrowLeft"
) {
paddle.dx = 0;
}
}
// Keyboard event handlers
document.addEventListener("keydown", keyDown);
document.addEventListener("keyup", keyUp);
// Move ball on canvas
function moveBall() {
ball.x += ball.dx;
ball.y += ball.dy;
// Wall collision (right/ left)
if (ball.x + ball.size > canvas.width || ball.x - ball.size < 0) {
ball.dx *= -1; // ball.dx = ball.dx * -1
}
// Wall collision (top/bottom)
if (ball.y + ball.size > canvas.height || ball.y - ball.size < 0) {
ball.dy *= -1;
}
// paddle collision
if (
ball.x - ball.size > paddle.x && //check left side
ball.x + ball.size < paddle.x + paddle.w && // check right side
ball.y + ball.size > paddle.y // running into the paddle
) {
ball.dy = -ball.speed;
}
// Brick collision
bricks.forEach((column) => {
column.forEach((brick) => {
if (brick.visible) {
if (
ball.x - ball.size > brick.x && // left brick side check
ball.x + ball.size < brick.x + brick.w && // right brick side check
ball.y + ball.size > brick.y && // top brick side check
ball.y - ball.size < brick.y + brick.h // bottom brick side check)
) {
ball.dy *= -1;
brick.visible = false;
increaseScore();
}
}
});
});
// Hit bottom wall - Lose
if (ball.y + ball.size > canvas.height) {
showAllBricks();
score = 0;
}
}
// Increase score
function increaseScore() {
score++;
if (score % (brickRowCount * brickColumnCount) === 0) {
showAllBricks();
}
}
// Make all bricks appear
function showAllBricks() {
bricks.forEach((column) => {
column.forEach((brick) => (brick.visible = true));
});
}
공이 받침대의 왼쪽 면에 부딪혔을 때, 오른쪽 면에 부딪혔을 때, 그 사이에 부딪혔을 때 등 모든 조건들을 염두해주어야한다!
벽돌에 공이 부딪혔을 때는 bricks가 배열이라는 점을 이용하여 forEach를 활용해 각각의 brick의 visible 속성을 false로 바꾸어줄 수 있다.
Canvas API 활용한 벽돌깨기 게임 끝!
실제 애니메이션 효과를 주는 requestAnimationFrame을 활용하여 다양한 게임이나 애니메이션들을 만들어 보면서 더 연습해보면 좋을 것 같다.