본문 바로가기
JavaScript

[JavaScript] 벽돌깨기 게임 만들기 (3) 게임 시작, 게임 오버, 게임 클리어, 다시 시작 구현하기

by teamnova 2024. 8. 22.
728x90

ㅇ안녕하세요.

오늘은 벽돌깨기 게임 만들기 마지막 시간입니다.

 

이전 시간에 canvas에 원하는 도형을 그리고, 각 도형이 서로 닿았을 때 원하는 대로 동작하도록 처리하는 방법에 대해서 배웠습니다.

이번에는 실제 게임처럼 동작하도록 몇가지 기능을 추가해서 게임을 마무리 해보겠습니다.

 

이전 코드를 보고 싶으시면 아래 링크를 참고해주세요.

 

2024.08.12 - [JavaScript] - [JavaScript] 벽돌깨기 게임 만들기 (1) 에 도형 그리기

2024.08.12 - [JavaScript] - [JavaScript] 벽돌깨기 게임 만들기 (2) 공 튕기기

 

 

1. index.html 수정

먼저 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;
        }
        #startButton, #restartButton {
            display: block;
            margin: 10px auto;
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
        #restartButton {
            display: none; /* 처음에는 숨김 처리 */
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="480" height="320"></canvas>
    <button id="startButton">게임 시작</button>
    <button id="restartButton">다시 시작</button>
    <script src="game.js"></script>
</body>
</html>

 

 

2. 게임 시작 기능 구현

다음으로 game.js 에서 게임시작 기능을 구현합니다.

게임시작 버튼을 클릭하면 캔버스 화면 중앙에 3, 2, 1 숫자 카운트가 뜬 후 게임이 시작되도록 해보겠습니다.

이를 위해서 기존에 변수를 초기화했던 부분을 정리해서 init()로 메서드화 해주었습니다.

 

// 사용하는 변수 정리
let x, y, dx, dy, ballRadius, paddleHeight, paddleWidth, paddleX;
let brickRowCount, brickColumnCount, brickWidth, brickHeight, brickPadding, brickOffsetTop, brickOffsetLeft;
let bricks, gameInterval, gameStarted = false;

// 게임 시작 버튼과 다시 시작 버튼 요소 가져오기
const startButton = document.getElementById("startButton");
const restartButton = document.getElementById("restartButton");

// 게임 초기화 함수
function init() {
    // 공, 패들, 벽돌 등 게임 상태 초기화
    x = canvas.width / 2;
    y = canvas.height - 30;
    dx = 2;
    dy = -2;
    ballRadius = 10;
    paddleHeight = 10;
    paddleWidth = 75;
    paddleX = (canvas.width - paddleWidth) / 2;

    brickRowCount = 3;
    brickColumnCount = 5;
    brickWidth = 75;
    brickHeight = 20;
    brickPadding = 10;
    brickOffsetTop = 30;
    brickOffsetLeft = 30;

    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 };
        }
    }

    gameStarted = false; // 게임 시작 상태 초기화
}

// 게임 시작 함수
function startGame() {
    startButton.style.display = "none"; // 게임 시작 버튼 숨기기
    restartButton.style.display = "none"; // 다시 시작 버튼 숨기기
    let countdown = 3; // 3초 카운트다운
    gameInterval = setInterval(() => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.font = "24px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.textAlign = "center";
        ctx.fillText(countdown, canvas.width / 2, canvas.height / 2);
        countdown--;

        if(countdown < 0) {
            clearInterval(gameInterval);
            gameInterval = setInterval(draw, 10); // 게임 시작
        }
    }, 1000);
}

// 게임 시작 버튼 클릭 이벤트 리스너
startButton.addEventListener("click", () => {
    init();
    startGame();
});

 

 

3. 게임 오버 기능 구현

 

다음으로 공이 바닥에 닿았을 때 게임이 종료되면서, 게임 오버 메시지가 출력되도록 하겠습니다.

이를 위해 gameOver() 함수를 추가하고, 공이 바닥에 닿을 때 이 함수를 호출합니다.

 

// 게임 오버 함수
function gameOver() {
    clearInterval(gameInterval); // 게임 루프 종료
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "24px Arial";
    ctx.fillStyle = "#FF0000"; // 빨간색으로 "Game Over" 표시
    ctx.textAlign = "center";
    ctx.fillText("Game Over", canvas.width / 2, canvas.height / 2 - 20);
    restartButton.style.display = "block"; // "다시 시작" 버튼 표시
}

// draw 함수에 게임 오버 처리 추가
function draw() {
 	//...(생략)...
 
    if(y + dy > canvas.height - ballRadius) {
        gameOver(); // 공이 바닥에 닿으면 게임 오버
        return;
    }

    //...(생략)...
}

 

4. 게임 클리어 처리

 

모든 벽돌을 깨뜨리면 게임이 클리어되도록 하고, "Game Clear" 메시지를 표시하겠습니다. 이를 위해 벽돌이 모두 사라졌는지를 확인하는 checkBricks() 함수를 사용합니다.

// 벽돌이 모두 사라졌는지 확인하는 함수
function checkBricks() {
    for(let c = 0; c < brickColumnCount; c++) {
        for(let r = 0; r < brickRowCount; r++) {
            if(bricks[c][r].status == 1) {
                return false; // 벽돌이 남아 있으면 false 반환
            }
        }
    }
    return true; // 모든 벽돌이 사라졌으면 true 반환
}

// 게임 클리어 함수
function gameClear() {
    clearInterval(gameInterval); // 게임 루프 종료
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "24px Arial";
    ctx.fillStyle = "#0095DD"; // 파란색으로 "Game Clear" 표시
    ctx.textAlign = "center";
    ctx.fillText("Game Clear!", canvas.width / 2, canvas.height / 2);
    restartButton.style.display = "block"; // "다시 시작" 버튼 표시
}

// draw 함수에 게임 클리어 처리 추가
function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks();
    drawBall();
    drawPaddle();
    collisionDetection();

    if(checkBricks()) {
        gameClear(); // 모든 벽돌이 깨지면 게임 클리어
        return;
    }

    if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
        dx = -dx;
    }

    if(y + dy > canvas.height - ballRadius) {
        gameOver(); // 공이 바닥에 닿으면 게임 오버
        return;
    }

    if(y + dy < ballRadius || (y + dy > canvas.height - paddleHeight - ballRadius && x > paddleX && x < paddleX + paddleWidth)) {
        dy = -dy;
    }

    x += dx;
    y += dy;

    requestAnimationFrame(draw);
}

 

5. 다시 시작 버튼 구현

마지막으로 게임 오버 또는 게임 클리어 후 "다시 시작" 버튼을 눌러서 게임을 초기화하고 다시 시작할 수 있도록 구현합니다.

 

// 다시 시작 버튼 클릭 이벤트 리스너
restartButton.addEventListener("click", () => {
    init(); // 게임 상태 초기화
    startGame(); // 게임 시작
});

 

 

여기까지 진행하면 벽돌깨기 게임이 완성됩니다.

 

 

 

 

6. 전체 코드

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;
        }
        #startButton, #restartButton {
            display: block;
            margin: 10px auto;
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
        #restartButton {
            display: none; /* 처음에는 숨김 처리 */
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="480" height="320"></canvas>
    <button id="startButton">게임 시작</button>
    <button id="restartButton">다시 시작</button>
    <script src="game.js"></script>
</body>
</html>

 

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

let x, y, dx, dy, ballRadius, paddleHeight, paddleWidth, paddleX;
let brickRowCount, brickColumnCount, brickWidth, brickHeight, brickPadding, brickOffsetTop, brickOffsetLeft;
let bricks, gameInterval, gameStarted = false;

const startButton = document.getElementById("startButton");
const restartButton = document.getElementById("restartButton");

function init() {
    // 공의 위치와 이동 속도 설정
    x = canvas.width / 2;
    y = canvas.height - 30;
    dx = 2; // 공의 x축 이동 속도
    dy = -2; // 공의 y축 이동 속도
    ballRadius = 10;

    // 패들 설정
    paddleHeight = 10;
    paddleWidth = 75;
    paddleX = (canvas.width - paddleWidth) / 2;

    // 벽돌 설정
    brickRowCount = 3;
    brickColumnCount = 5;
    brickWidth = 75;
    brickHeight = 20;
    brickPadding = 10;
    brickOffsetTop = 30;
    brickOffsetLeft = 30;

    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 };
        }
    }

    gameStarted = false; // 게임이 아직 시작되지 않음
}

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();
}

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();
            }
        }
    }
}

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

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

document.addEventListener("mousemove", mouseMoveHandler, false);

function checkBricks() {
    for(let c = 0; c < brickColumnCount; c++) {
        for(let r = 0; r < brickRowCount; r++) {
            if(bricks[c][r].status == 1) {
                return false;
            }
        }
    }
    return true;
}

function gameClear() {
    clearInterval(gameInterval);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "24px Arial";
    ctx.fillStyle = "#0095DD";
    ctx.textAlign = "center";
    ctx.fillText("Game Clear!", canvas.width / 2, canvas.height / 2);
    restartButton.style.display = "block";
}

function gameOver() {
    clearInterval(gameInterval);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "24px Arial";
    ctx.fillStyle = "#FF0000";
    ctx.textAlign = "center";
    ctx.fillText("Game Over", canvas.width / 2, canvas.height / 2 - 20);
    restartButton.style.display = "block";
}

function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawBricks();
    drawBall();
    drawPaddle();
    collisionDetection();

    if(checkBricks()) {
        gameClear();
        return;
    }

    if(x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
        dx = -dx;
    }

    if(y + dy > canvas.height - ballRadius) {
        gameOver();
        return;
    }

    if(y + dy < ballRadius || (y + dy > canvas.height - paddleHeight - ballRadius && x > paddleX && x < paddleX + paddleWidth)) {
        dy = -dy;
    }

    x += dx;
    y += dy;
}

function startGame() {
    startButton.style.display = "none";
    restartButton.style.display = "none";
    let countdown = 3;
    gameInterval = setInterval(() => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.font = "24px Arial";
        ctx.fillStyle = "#0095DD";
        ctx.textAlign = "center";
        ctx.fillText(countdown, canvas.width / 2, canvas.height / 2);
        countdown--;

        if(countdown < 0) {
            clearInterval(gameInterval);
            gameInterval = setInterval(draw, 10); // 게임 시작
        }
    }, 1000);
}

startButton.addEventListener("click", () => {
    init();
    startGame();
});

restartButton.addEventListener("click", () => {
    init();
    startGame();
});