728x90
반응형

 미니 게임 만들기

 

목차

1. 게임에 대한 간단한 설명

2. 게임 실행 영상

3. 주요 코드

4. 어려웠던 점

 

1. 게임에 대한 간단한 설명

프로젝트의 일부분으로 미디어 카테고리에 전기차를 주제로 게임을 만들어보았다.

 해당 페이지에 들어오면 본 게임 시작 전에 미니 게임이 진행된다. 키보드를 이용해 지도 위에 있는 전기차를 이동시켜 주유소에 도달하면 본 게임으로 들어가게 된다. 

 본 게임은 스페이스바를 눌러 다가오는 장애물을 점프로 피해 목적지에 도달하는 게임이다. 목적지인 주유소는 Score가 150점에 도달하면 화면상에 나타나게 해 놓았고 주유소에 도달하면 게임 성공에 관련 된 모달이 뜬다. 성공 모달에서 찌리릿 코인을 받을 수 있으며 이 코인을 10개 모으면 쿠폰으로 교환할 수 있도록 다른 카테고리에 구현해 놓았다. 

 게임은 여러번 할 수 있지만 코인은 하루에 1개만 수집할 수 있도록 설정해 두었으며 게임을 진행하다 목적지에 도달하기 전에 장애물에 부딪히면 게임 실패에 관련 된 모달이 뜨고 실패 모달에서 다시하기 버튼을 눌러 게임을 다시 실행할 수 있다. 

 

2. 게임 실행 영상

3. 주요 코드

3 - 1. 게임 화면을 구성한 Html

    <!-- 게임 화면 -->
    <div class="game-wrap">
        <div class="bakcground">
            <div class="ground"></div>
            <a th:href="@{/home}">
                <div class="sun" th:style="'background-image:url(' + @{/eco-house.png} + ')'"></div>
            </a>
        </div>
        <div class="score">
            <p>SCORE</p>
            <span></span>
        </div>
        <canvas id="canvas" height="200"></canvas>
    </div>
    <!-- 안내 -->
    <h3 class="info">SPACE BAR로 JUMP 하세요! <br> 주유소에 도달하면 코인이 지급됩니다.</h3>
    <!-- 성공/실패 모달 -->
    <div class="game-over">
        <div class="pop-up">
            <img class="game-over-img" th:src="@{/link.png}" alt="게임오버" />
            <h2>충전소를 찾지 못했다!</h2>
            <p><span class="total-score"></span> 점</p>
            <button class="replay">다시하기</button>
            <button class="home">홈으로</button>
            <button class="get-coins" style="display: none;">찌리릿 코인 받기</button>
        </div>
    </div>

3 - 2. 관련 CSS 

본 게임 배경에 나오는 폭죽은 아래 링크에서 사용한 방법을 프로젝트 html에 적용 하였다.

https://mzero.tistory.com/13

 

Tistory에 마우스 애니메이션 효과 적용하는 방법

현재 나의 Tistory에 들어오면 마우스 움직임에 따라 애니메이션 효과가 따라 오는 걸 볼 수 있다. Tistory 외에도 자신이 만든 웹 사이트에서 마우스 커서에 효과를 넣을 수 있다. 1. 관련 사이트 먼

mzero.tistory.com

<style>
	.game-wrap {
        background-color: #000;
        position: relative;
        width: 100%;
        height: 100%;
    }

    .sun {
        position: absolute;
        top: 10px;
        right: 20px;
        width: 5rem;
        height: 5rem;
        background-image: url("/src/main/resources/static/der.png");
        background-size: cover;
        animation: rotate 5s linear infinite;
    }

    @keyframes rotate {
        to {
            rotate: 1turn;
        }
    }

    .ground {
        position: absolute;
        bottom: 0.8rem;
        left: 0;
        width: 100%;
        height: 0.2rem;
        background: #fff;
        z-index: 0;
    }

    .score {
        color: #fff;
        display: flex;
        gap: 6px;
        position: absolute;
        top: 8px;
        left: 0;
        font-size: 1.5rem;
    }

    .info {
        color: #fff;
        width: fit-content;
        margin: 0 auto;
        padding: 10px;
        border: 4px solid;
        border-radius: 8px;
        text-align: center;
        cursor: default;
    }

    #canvas {
        margin-top: 12rem;
        position: relative;
        z-index: 1;
    }

    .game-over {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.4);
        z-index: 10;
    }

    .game-over .pop-up {
        display: flex;
        flex-direction: column;
        justify-content: center;
        gap: 8px;
        align-items: center;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        max-width: 400px;
        width: 100%;
        height: fit-content;
        padding: 40px 0 32px;
        background-color: #fff;
    }

    .game-over .pop-up .replay {
        padding: 4px 8px;
        margin-top: 8px;
        border: 3px solid #191919;
        border-radius: 4px;
        background-color: #fff;
        font-size: 1.5rem;
        font-weight: bold;
        cursor: pointer;
    }

    .game-over .pop-up .home {
        padding: 4px 8px;
        margin-top: 8px;
        border: 3px solid #191919;
        border-radius: 4px;
        background-color: #fff;
        font-size: 1.5rem;
        font-weight: bold;
        cursor: pointer;
    }

    .game-over .pop-up .get-coins {
        padding: 4px 8px;
        margin-top: 8px;
        border: 3px solid #191919;
        border-radius: 4px;
        background-color: #fff;
        font-size: 1.5rem;
        font-weight: bold;
        cursor: pointer;
    }

    @media (max-width: 420px) {
        .game-over .pop-up {
            max-width: 300px;
        }
    }

    .game-over-img {
        width: 20rem;
        height: 20rem;
    }
</style>

3 - 3. 화면 구성과 관련된 Java Script

  • 본 게임 화면 우측 상단에 회전하는 집을 클릭하면 "/home" 경로로 이동하도록 이벤트 리스너를 설정하였다 .
  • let canvaslet ctx 부분은 게임 화면을 렌더링하는데 사용할 Canvas 요소와 2D Context를 가져온다.
  • electricCar 객체는 전기 자동차의 속성과 동작을 정의하고 있다. x, y는 자동차의 위치, widthheight는 자동차 이미지의 크기를 나타낸다. draw, jump, fall 메서드를 통해 자동차를 그리고, 점프와 낙하 동작을 처리하였다.
  • Box 클래스는 장애물을 나타낸다. draw 메서드를 통해 장애물을 생성하는데, 장애물의 위치는 화면 오른쪽 끝에서 시작하며 이미지는 장애물 이미지는 2개를 넣어 랜덤으로 번갈아 나오게 설정하였다.
	<script>
		const homeButton = document.querySelector(".home");
        homeButton.addEventListener("click", function () {
            window.location.href = "/home";
        });

        let canvas = document.getElementById("canvas");
        let ctx = canvas.getContext("2d");
        let currentCar = 0;
        canvas.width = window.innerWidth;

        var carImages = new Image();
        carImages.src = "/cargame.png";

		//전기차
        let electricCar = {
            x: 20,
            y: 50,
            width: 80,
            height: 80,
            draw() {
                ctx.drawImage(
                    carImages,
                    this.x,
                    this.y,
                    this.width,
                    this.height
                );
            },
            jump() {
                if (this.y > 40) {
                    this.y -= 60;
                }
            },
            fall() {
                if (this.y < 120) {
                    this.y += 10;
                }
            },
        };

        // 장애물
        var boxImages = [new Image(), new Image()];
        boxImages[0].src = "/pong.png";
        boxImages[1].src = "/pong2.png";

        class Box {
            constructor() {
                this.width = 40;
                this.height = 40;
                this.x = canvas.width - this.width;
                this.y = 155;

                // 장애물 이미지 선택
                const randomImageIndex = Math.floor(Math.random() * boxImages.length);
                this.image = boxImages[randomImageIndex];
            }
            draw() {
                ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
            }
        }
        
         // spaceBar
        var jumpSwitch = false;
        let lastSpacePressTime = 0;

        document.addEventListener("keydown", function (e) {
            if (e.code === "Space") {
                const currentTime = Date.now();
                const timeSinceLastPress = currentTime - lastSpacePressTime;

                if (timeSinceLastPress > 500) {
                    jumpSwitch = true;
                    lastSpacePressTime = currentTime;
                }
            }
        });
	</script>

3 - 4. 장애물, 도착지 생성 및 Score 관련 Java Script

  • Destination 클래스에서는 도착지의 속성과 초기 위치를 설정하고, 도착지가 다른 장애물들과 충돌하지 않도록 확인하도록 하였다. 만약 충돌이 발생하면 새로운 도착지를 생성한다.
  • createBox 함수는 장애물을 생성하고, 생성된 장애물이 다른 장애물이나 도착지와 충돌하지 않게 하였다. 만약 충돌이 있으면 다시 호출되어 장애물을 다시 생성하게 된다
  • updateScore 함수는 Score를 1씩 증가시키고, 게임화면 좌측 상단에 있는 Score를 업데이트한다.
	<script>
        // 도착지 이미지 로드
        let destinationImage = new Image();
        destinationImage.src = "/link.png";

        class Destination {
            constructor() {
                this.width = 100;
                this.height = 100;
                this.x = canvas.width - this.width; // 장애물과 동일한 위치에서 시작
                this.y = 95;

                // 새로 생성한 도착지와 다른 장애물들과의 충돌 확인
                let isCollision = manyBoxes.some(existingBox => {
                    return (
                        this.x < existingBox.x + existingBox.width &&
                        this.x + this.width > existingBox.x
                    );
                });

                // 충돌이 있으면 다시 생성 시도
                if (isCollision) {
                    return new Destination();
                }
            }

            draw() {
                ctx.drawImage(destinationImage, this.x, this.y, this.width, this.height);
            }
        }

        let manyBoxes = [];
        let destination;

        // 장애물 생성 함수
        function createBox() {
            let box = new Box();

            // 새로 생성한 장애물과 다른 장애물들 및 도착지와의 충돌 확인
            let isCollisionWithOtherBoxes = manyBoxes.some(existingBox => {
                return (
                    box.x < existingBox.x + existingBox.width &&
                    box.x + box.width > existingBox.x
                );
            });

            let isCollisionWithDestination =
                destination &&
                box.x < destination.x + destination.width &&
                box.x + box.width > destination.x;

            // 충돌이 없으면 배열에 추가, 있으면 다시 생성 시도.
            if (!isCollisionWithOtherBoxes && !isCollisionWithDestination) {
                manyBoxes.push(box);
            } else {
                createBox();  // 다시 생성 시도.
            }
        }
        
        // score
        let score = 0;
        let scoreInterval;

        function updateScore() {
            score += 1;
            document.querySelector(".score span").textContent = score;
        }
	</script>

3 - 5. 게임 진행시 반복적으로 실행되는 게임 Frame 관련 Java Script

    • requestAnimationFrame(frameRun)은 브라우저에게 다음 프레임에서 frameRun 함수를 호출하도록 요청하는 함수로 이를 통해 게임 루프가 지속적으로 실행한다.
    • if (timer % 10 === 0) 은 매 10프레임마다 스코어를 업데이트한다.
    • if (Math.random() < 0.005) 부분은 게임에서 무작위로 장애물을 생성하기 위한 확률적인 조건을 나타낸다. Math.random() 함수는 0 이상 1 미만의 값을 반환한다. 즉, 매 프레임에서 Math.random() 함수를 호출하여 0 이상 1 미만의 난수를 생성하고, 이 값이 0.005 미만일 때에만 실행된다. 이렇게 함으로써 매 프레임마다 장애물을 생성할 확률을 매우 낮게 만들고, 게임이 무작위로 장애물을 생성할 수 있다.
    • if (score == 150 && !isDestinationVisible) 은 도착지 생성 조건을 나타내고 있다. score가 150점 일 때 도착지를 생성하여 화면상에 나타나도록 설정하였다.
    • if (isDestinationVisible) 은 도착지가 화면에 표시되는 경우 실행되어 도착지의 위치를 업데이트하고, 플레이어가 도착지에 도달하면 게임이 성공으로 처리됩니다.
	<script>
    	let isDestinationVisible = false;
    	let timer = 0;
        let jumpTimer = 0;
        let animation;

        // 프레임마다 실행하기
        function frameRun() {
            animation = requestAnimationFrame(frameRun);
            timer++;
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // score 처리
            if (timer % 10 === 0) {
                updateScore();
            }
            
            // 무작위로 장애물 소환
            if (Math.random() < 0.005) {
                createBox();
            }

            // 도착지 생성 조건
            if (score == 150 && !isDestinationVisible) {
                isDestinationVisible = true;
                destination = new Destination();
            }

            if (isDestinationVisible) {
                destination.x -= 2;
                destination.draw();

                // 도착지에 도달하면 성공 처리
                if (electricCar.x + electricCar.width >= destination.x && electricCar.y + electricCar.height >= destination.y &&
                    electricCar.x <= destination.x + destination.width && electricCar.y <= destination.y + destination.height) {
                    showGameSuccess();
                    cancelAnimationFrame(animation);
                    clearInterval(scoreInterval);
                    return;
                }
            }

            // x좌표가 0미만이면 제거
            manyBoxes.forEach((a, i, o) => {
                if (a.x < 5) {
                    o.splice(i, 1);
                }
                a.x -= 2;
                // 충돌 체크
                crash(electricCar, a);
                a.draw();
            });

            // 점프!
            if (jumpSwitch == true) {
                electricCar.jump();
                jumpTimer++;
            }
            if (jumpSwitch == false) {
                if (electricCar.y < 120) {
                    electricCar.y++;
                }
            }
            if (jumpTimer > 40) {
                jumpSwitch = false;
                jumpTimer = 0;
            }
            electricCar.draw();
        }
	</script>

3 - 6. 충돌확인과 게임리셋 Java Script

	<script>
	// 충돌확인
        function crash(electricCar, box) {
            let xCalculate = box.x - (electricCar.x + electricCar.width);
            let yCalculate = box.y - (electricCar.y + electricCar.height);
            if (xCalculate < 0 && yCalculate < 0) {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                cancelAnimationFrame(animation);
                clearInterval(scoreInterval);
                const totalScore = document.querySelector(".total-score");
                totalScore.textContent = `${score}`;
                const sun = document.querySelector(".sun");
                const gameOver = document.querySelector(".game-over");
                sun.style.animationPlayState = "paused";
                gameOver.style.display = "block";
            }
        }
        
        // 리셋 버튼
        const replayBtn = document.querySelector(".replay");
        replayBtn.addEventListener("click", () => {
            resetGame();
        });

        // 게임리셋
        function resetGame() {
            cancelAnimationFrame(animation);
            clearInterval(scoreInterval);
            score = 0;
            document.querySelector(".score span").textContent = score;
            manyBoxes = [];
            currentCar = 0;
            jumpSwitch = false;
            lastSpacePressTime = 0;

            isDestinationVisible = false;
            destination = null;

            frameRun();

            const sun = document.querySelector(".sun");
            const gameOver = document.querySelector(".game-over");
            sun.style.animationPlayState = "running";
            gameOver.style.display = "none";

            // 스코어 인터벌 제거
            if (scoreInterval) {
                clearInterval(scoreInterval);
            }
            
            // 스코어 인터벌 다시시작
            scoreInterval = setInterval(updateScore, 2000);
        }
</script>

3 - 7. 게임 성공 시 뜨는 모달 Java Script

	<script>
    	// 도착지에 도달하면 게임 성공 팝업 표시
        function showGameSuccess() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            cancelAnimationFrame(animation);
            clearInterval(scoreInterval);

            const totalScore = document.querySelector(".total-score");
            totalScore.textContent = `${score}`;

            const sun = document.querySelector(".sun");
            const gameSuccess = document.querySelector(".game-over");

            sun.style.animationPlayState = "paused";
            gameSuccess.style.display = "block";

            let gameOverMsgElement = document.querySelector('.game-over .pop-up h2');
            gameOverMsgElement.textContent = '충전소에 도착했습니다!';

            const getCoinsButton = document.querySelector(".get-coins");
            getCoinsButton.style.display = "block"; // 버튼 표시

            getCoinsButton.addEventListener("click", async function () {
                const response = await fetch("/getCoins", {
                    method: "GET",
                });

                if (response.ok) {
                    const message = await response.text();
                    alert(message);
                    // 필요하다면 이후에 페이지를 새로고침하거나 다른 동작을 수행하세요.
                } else {
                    console.error("찌리릿 코인을 받는 중 오류가 발생했습니다: " + response.status);
                }
            });

            // "다시하기" 버튼을 숨깁니다.
            const replayButton = document.querySelector(".replay");
            replayButton.style.display = "none";
        }

	</script>

 

4. 어려웠던 점

 구현하는데 가장 시간이 오래 걸렸던 기능은 장애물이 생성되는 시간과 간격을 조절 하는 것이었다. 0.001초 단위로 장애물 생성의 변화가 크게 달라지다보니 적당한 시간을 찾기가 어려웠다.

 또한 게임을 하다보면 목적지와 장애물이 겹쳐나오거나 목적지를 지나치는 변수가 발생했다. 그래서 처음에 도착지를 지나치면 그 다음 설정한 점수에 도달했을 때도 목적지가 다시 생성되게 구현하고 싶었는데 아무리 코드를 작성해도 목적지가 한번 나오면 그 다음에 나오지 않았다. 결국 다른 방법으로 문제를 해결했는데 목적지인 주유소의 크기를 크게 만들어 목적지 근처에만 도달해도 성공할 수 있도록 수정하였다.

 

 

728x90
반응형

+ Recent posts