JavaScript

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

teamnova 2024. 8. 22. 12:00
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();
});