본문 바로가기
JavaScript

[JavaScript] 벽돌깨기 게임 만들기 (1) <canvas> 에 도형 그리기

by teamnova 2024. 8. 12.
728x90

안녕하세요.

오늘은 자바스크립트와 HTML5 '<canvas>'를 이용해서 아래 동영상처럼 간단한 벽돌깨기 게임을 만들어보겠습니다.

 

 

이 시리즈는 총 3편으로 구성되고, 첫번째 편에서는 캔버스에 공, 벽돌, 패들을 그리는 방법을 다룹니다.

그럼 시작해보겠습니다.

 

1. Index.html 파일 생성

 

먼저 HTML 파일을 설정하여 게임의 기본 구조를 만듭니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>벽돌 깨기 게임</title>
    <style>
        canvas {
            background: #eee;
            display: block;
            margin: 0 auto;
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="480" height="320"></canvas>
    <script src="game.js"></script>
</body>
</html>

 

2.  공 그리기

이제 'game.js' 파일을 만들고, 캔버스에 공을 그리려보겠습니다.

// 캔버스와 2D 컨텍스트 가져오기
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

// 공의 위치와 반지름 설정
let x = canvas.width / 2; // 공의 최초 위치 x 좌표
let y = canvas.height - 30; // 공의 최초 위치 y 좌표
let dx = 2; // 공의 x축 이동 속도
let dy = -2; // 공의 y축 이동 속도
let ballRadius = 10; // 공의 반지름 크기

// 공 그리기 함수
function drawBall() {
    ctx.beginPath(); // 새로운 경로 시작
    ctx.arc(x, y, ballRadius, 0, Math.PI * 2); // 원 그리기
    ctx.fillStyle = "#0095DD"; // 공의 색상
    ctx.fill(); // 원 채우기
    ctx.closePath(); // 경로 닫기
}

// 애니메이션 프레임마다 실행되는 함수
function draw() {
	// 이전 프레임을 지우고, 새로운 프레임을 그리기 위해 준비
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBall(); // 공 그리기

    // 공의 위치 업데이트
    x += dx;
    y += dy;

    requestAnimationFrame(draw); // 다음 프레임 요청
}

draw(); // 첫 번째 프레임 실행

 

 

위 코드를 실행하면 다음 영상처럼 화면에 공이 매 프레임마다 이동하는 것을 확인할 수 있습니다.

 

 

3. 패들 추가하기

다음으로 공을 튕겨내기 위한 패들을 캔버스 하단에 추가해보겠습니다.

이 패들은 마우스의 움직임을 따라 좌우로 이동해야 합니다. game.js에 아래 내용을 추가합니다.

 

// 패들 설정
let paddleHeight = 10; // 패들 세로 길이
let paddleWidth = 75; // 패들 가로 길이
let paddleX = (canvas.width - paddleWidth) / 2; // 패들의 초기 위치

// 패들 그리기 함수
function drawPaddle() {
    ctx.beginPath(); // 경로 그리기 시작
    ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); // 사각형 그리기
    ctx.fillStyle = "#0095DD"; // 패들 색상
    ctx.fill(); // 내부 채우기
    ctx.closePath(); // 경로 닫기
}

// 마우스 이동에 대한 이벤트 리스너 설정
document.addEventListener("mousemove", mouseMoveHandler, false);

// 마우스 이동시 패들이 마우스 위치를 따라가도록 구현
function mouseMoveHandler(e) {
    let relativeX = e.clientX - canvas.offsetLeft;
    if(relativeX > 0 && relativeX < canvas.width) {
        paddleX = relativeX - paddleWidth / 2;
    }
}

 

 

그리고 draw() 함수에 아래 처럼 패들 그리기 함수를 추가합니다.

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBall();
    drawPaddle(); // 패들 그리기 추가

    x += dx;
    y += dy;

    requestAnimationFrame(draw);
}

 

여기까지 진행하면 다음과 같이 패들이 추가된 것을 확인할 수 있습니다.

 

4. 벽돌 추가하기

이제  다음 코드를 game.js에 추가해서 캔버스 상단에 벽돌을 그려보겠습니다.

// 벽돌 설정
let brickRowCount = 3; // 벽돌 열 수
let brickColumnCount = 5; // 벽돌 행 수
let brickWidth = 75; // 벽돌 가로 길이
let brickHeight = 20;  // 벽돌 세로 길이
let brickPadding = 10;  // 벽돌 간 간격
let brickOffsetTop = 30; // 벽돌과 캔버스 위쪽 가장자리 사이의 간격
let brickOffsetLeft = 30; // 벽돌과 캔버스 왼쪽 가장자리 사이의 간격

/** 현재 화면에 존재하는 벽돌의 상태를 추적하고 관리하기 위해 2차원 배열 사용
 * bricks[c][r] 형태
 * x, y : 벽돌의 위치를 지정하기 위한 속성, 초기에 0으로 설정하고, 이후 실제 위치를 계산할 때 업데이트.
 * status : 벽돌이 화면에 표시되어 있는지(1), 깨져서 사라졌는지(0)을 나타냄
 */
let bricks = []; 
for(let c = 0; c < brickColumnCount; c++) {
    bricks[c] = []; // 열을 위한 빈 배열 생성
    for(let r = 0; r < brickRowCount; r++) {
        bricks[c][r] = { x: 0, y: 0, status: 1 }; // 벽돌 상태 초기화
    }
}

// 벽돌 그리기 함수 - 2차원 배열을 순회하면서 status가 1인 벽돌만 화면에 그린다.
function drawBricks() {
    for(let c = 0; c < brickColumnCount; c++) {
        for(let r = 0; r < brickRowCount; r++) {
            if(bricks[c][r].status == 1) {
                let brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
                let brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
                bricks[c][r].x = brickX;
                bricks[c][r].y = brickY;
                ctx.beginPath();
                ctx.rect(brickX, brickY, brickWidth, brickHeight);
                ctx.fillStyle = "#0095DD";
                ctx.fill();
                ctx.closePath();
            }
        }
    }
}

 

그리고 draw() 함수에 아래 처럼 벽돌 그리기 함수를 추가합니다.

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks(); // 벽돌 그리기 추가
    drawBall();
    drawPaddle();

    x += dx;
    y += dy;

    requestAnimationFrame(draw);
}

 

여기까지 잘 진행되었다면, 다음처럼 화면에 공, 패들, 벽돌이 그려지게 됩니다.

 

 

5. 현재까지 전체 코드

index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>벽돌 깨기 게임</title>
    <style>
        canvas {
            background: #eee;
            display: block;
            margin: 0 auto;
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="480" height="320"></canvas>
    <script src="game.js"></script>
</body>
</html>
game.js
// 캔버스와 2D 컨텍스트 가져오기
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

// 공의 위치와 반지름 설정
let x = canvas.width / 2; // 공의 최초 위치 x 좌표
let y = canvas.height - 30; // 공의 최초 위치 y 좌표
let dx = 2; // 공의 x축 이동 속도
let dy = -2; // 공의 y축 이동 속도
let ballRadius = 10; // 공의 반지름 크기

// 패들 설정
let paddleHeight = 10; // 패들 세로 길이
let paddleWidth = 75; // 패들 가로 길이
let paddleX = (canvas.width - paddleWidth) / 2; // 패들의 초기 위치

// 벽돌 설정
let brickRowCount = 3; // 벽돌 열 수
let brickColumnCount = 5; // 벽돌 행 수
let brickWidth = 75; // 벽돌 가로 길이
let brickHeight = 20;  // 벽돌 세로 길이
let brickPadding = 10;  // 벽돌 간 간격
let brickOffsetTop = 30; // 벽돌과 캔버스 위쪽 가장자리 사이의 간격
let brickOffsetLeft = 30; // 벽돌과 캔버스 왼쪽 가장자리 사이의 간격

/** 현재 화면에 존재하는 벽돌의 상태를 추적하고 관리하기 위해 2차원 배열 사용
 * bricks[c][r] 형태
 * x, y : 벽돌의 위치를 지정하기 위한 속성, 초기에 0으로 설정하고, 이후 실제 위치를 계산할 때 업데이트.
 * status : 벽돌이 화면에 표시되어 있는지(1), 깨져서 사라졌는지(0)을 나타냄
 */
let bricks = []; 
for(let c = 0; c < brickColumnCount; c++) {
    bricks[c] = []; // 열을 위한 빈 배열 생성
    for(let r = 0; r < brickRowCount; r++) {
        bricks[c][r] = { x: 0, y: 0, status: 1 }; // 벽돌 상태 초기화
    }
}

// 공 그리기 함수
function drawBall() {
    ctx.beginPath(); // 새로운 경로 시작
    ctx.arc(x, y, ballRadius, 0, Math.PI * 2); // 원 그리기
    ctx.fillStyle = "#0095DD"; // 공의 색상
    ctx.fill(); // 원 채우기
    ctx.closePath(); // 경로 닫기
}

// 패들 그리기 함수
function drawPaddle() {
    ctx.beginPath(); // 경로 그리기 시작
    ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight); // 사각형 그리기
    ctx.fillStyle = "#0095DD"; // 패들 색상
    ctx.fill(); // 내부 채우기
    ctx.closePath(); // 경로 닫기
}

// 벽돌 그리기 함수 - 2차원 배열을 순회하면서 status가 1인 벽돌만 화면에 그린다.
function drawBricks() {
    for(let c = 0; c < brickColumnCount; c++) {
        for(let r = 0; r < brickRowCount; r++) {
            if(bricks[c][r].status == 1) {
                let brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
                let brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
                bricks[c][r].x = brickX;
                bricks[c][r].y = brickY;
                ctx.beginPath();
                ctx.rect(brickX, brickY, brickWidth, brickHeight);
                ctx.fillStyle = "#0095DD";
                ctx.fill();
                ctx.closePath();
            }
        }
    }
}

// 마우스 이동에 대한 이벤트 리스너 설정
document.addEventListener("mousemove", mouseMoveHandler, false);

// 마우스 이동시 패들이 마우스 위치를 따라가도록 구현
function mouseMoveHandler(e) {
    let relativeX = e.clientX - canvas.offsetLeft;
    if(relativeX > 0 && relativeX < canvas.width) {
        paddleX = relativeX - paddleWidth / 2;
    }
}

// 애니메이션 프레임마다 실행되는 함수
function draw() {
	// 이전 프레임을 지우고, 새로운 프레임을 그리기 위해 준비
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks(); // 벽돌 그리기
    drawBall(); // 공 그리기
    drawPaddle(); // 패들 그리기

    // 공의 위치 업데이트
    x += dx;
    y += dy;

    requestAnimationFrame(draw); // 다음 프레임 요청
}

draw(); // 첫 번째 프레임 실행

 


 

오늘은 캔버스에 필요한 도형을 그리는 방법을 알아보았습니다.

다음 시간에는 패들로 공을 튕겨서 벽돌을 깨트리는 것을 구현해보겠습니다.