React 두더지게임 만들기

zin·2023년 9월 24일
1

React로 두더지 게임 만들기

게임 소개

추억의 두더지게임으로 화면에 두더지가 랜덤으로 나타나면, 두더지를 클릭하여 점수를 획득하는 게임이다.

  1. 5x5 격자로 배치된 총 25개 두더지가 Start버튼 클릭시, 랜덤으로 하나씩 나타나고, 숨는다.
  2. 두더지를 빠르게 클릭해서 점수 1+증가, Stop버튼 클릭시 일시정지 가능하다.
  3. 시간제한 30초, End버튼 클릭시 게임 종료되고, 점수결과가 팝업된다.

컴포넌트 구조

React 프로젝트 기본 생성

npx create-react-app molegame --template basic-react

Mole.jsx

  • 두더지 이미지를 렌더링하는 컴포넌트
import React from 'react'
import moleImg from '../images/mole.png'
import '../index.css'

export default function Mole({show}) {
    return (
        <img src={moleImg} alt="mole" className={`mole ${show ? 'show' : 'hidden'}`}/>
    )
}

MoleGame.js

  • 게임 로직 및 상태를 관리하는 컨테이너 컴포넌트
import React, { useState, useEffect } from 'react';
import Mole from './Mole';
import moleImg from '../images/mole.png'
import '../index.css';

export default function MoleGame() {
  const [moles, setMoles] = useState(Array.from({ length: 5 * 5 }, () => false));
    const [isGameRunning, setIsGameRunning] = useState(false);
    //두더지 클릭시 점수
    const [score, setScore] = useState(0)
    const moleClickScore = () => {
        setScore(score+1)
    }
    //타이머 30초
    const [time, setTime] = useState(30)

    useEffect(() => {
        let moleInterval;

        if (isGameRunning) {
        // 게임시작, 랜덤 두더지 나오기
        moleInterval = setInterval(() => {
            const randomIndex = Math.floor(Math.random() * moles.length);
            const newMoles = [...moles];
            newMoles[randomIndex] = true;
            setMoles(newMoles);

            // 0.5초 후에 두더지 다시 숨김
            setTimeout(() => {
            newMoles[randomIndex] = false;
            setMoles(newMoles);
            }, 500);
            setTime((prevTime)=>{
                if(prevTime === 1){
                    alert(`짝짝짝! 점수는 ${score} 입니다.`)
                    setIsGameRunning(false)
                    setScore(0)
                    return 30
                }
                return prevTime - 1
            })
        }, 500);
        }
        return () => {
        clearInterval(moleInterval);
        };
    }, [isGameRunning, moles, score]);

    const startGame = () => {
        setIsGameRunning(true);
    };
    const stopGame = () => {
        setIsGameRunning(false);
    };
    const endGame = () => {
        setIsGameRunning(false);
        setMoles(Array.from({ length: 5 * 5 }, () => false))
        setScore(0)
        setTime(30)
    }
    return (
        <div className='wrap'>
            <div className='moleTit'>
                <h1>
                    추억의<img src={moleImg} alt='logo'/><br/>
                    두더지 게임
                </h1>
                <div className='startEnd'>
                    <button onClick={startGame} type='button' disabled={isGameRunning}>
                    Start
                    </button>
                    <button onClick={stopGame} type='button' disabled={!isGameRunning}>
                    Stop
                    </button>
                    <button onClick={endGame} type='button'>
                    End
                    </button>
                </div>
                <div className='moleScoreTime'>
                    <button type='button'>Score : {score}</button>
                    <button type='button'>Timer : {time}</button>
                </div>
            </div>
            <div className='moleList'>
                <ol>
                    {moles.map((show, index) => (
                    <li key={index} onClick={()=> show && moleClickScore()}>
                        <Mole show={show} />
                    </li>
                    ))}
                </ol>
            </div>
        </div>
    );
}

index.css

  • 간단한 스타일, 두더지 애니메이션 적용
* {margin: 0; padding: 0;}
button {
    cursor: pointer;
    background: none;
    border: 0;
}
.wrap {
    padding: 3% 1%;
    display: flex;
    flex-direction: column;
    width: 100%;
    gap: 20px;
    max-width: 500px;
    margin: auto;
}
.moleList {
    margin: auto;
    border-radius: 10px;
    background: #9ec15e;
}
.moleList ol {
    display: grid; 
    grid-template-columns: repeat(5, 0fr);
    list-style: none;
}
.moleList li {
    width: 100px;
    height: 100px;
}
h1 {
    text-align: center;
    color: rgb(116, 78, 54);
}
h1 img {width: 50px;}
img {
    width: 100%;
    vertical-align: top;
}
.startEnd {
    padding-top: 3%;
    display: flex;
    justify-content: center;
    gap: 10px;
}
.startEnd button {
    border: 0;
    color: #fff;
    width: 70px;
    font-size: 1rem;
    border-radius: 10px;
    height: 40px;
    background: brown;
}
.moleScoreTime {
    padding-top: 4%;
    display: flex;
    justify-content: space-around;
}
.moleScoreTime button {
    color: #562706;
    font-size: 1.2rem;
}
.mole {
    height: 0;
    cursor: pointer;
    transition: transform 0.2s ease-in;
    transform: translateY(50%);
}
.mole.show {
    transform: translateY(0);
    height: 100px;
}
@media (max-width: 500px) {
    .wrap {padding: 10% 1%;}
    .moleList li {
        width: 60px; 
        height: 60px;
    }
    .mole {
        transform: translateY(20%);
    }
    .mole.show {
        transform: translateY(0);
        height: 60px;
    }
}

게임 구현 내용

  • useState 훅으로 게임시작,중단 점수,타이머 상태 관리
  • useEffect 훅으로 게임루프를 설정하고 관리
  • 게임 시작, 일시정지, 종료 함수로 게임 상태 구현
  • css로 두더지 나타남과 숨김 애니메이션

어려웠던 점/ 문제 해결

  1. 게임종료가 타이머가 0이될때와 End버튼을 누를때 점수결과 alert이중 실행됨
  2. 타이머 디폴트값 30초인데, 타이머가 0이될때는 29초로 설정됨
    ->
setTimeout(() => {
  newMoles[randomIndex] = false;
  setMoles(newMoles);
}, 500);
setTime((prevTime)=>{
  if(prevTime === 1){
    alert(`짝짝짝! 점수는 ${score} 입니다.`)
    setIsGameRunning(false)
    setScore(0)
    return 30
  }
  return prevTime - 1
})

setTimeout 함수는 비동기적으로 동작하기 때문에 prevTime이 업데이트 되기전에 값으로 사용되서 29로 나온거였다.
setTimeout바깥으로 setTime을 분리해주었고, 타이머가 1초 남았을 때, 게임 종료 알림(alert)을 띄우고, 게임 상태를 초기화하도록 수정했다.

리팩토링 계획

추가/수정할 기능

  • 타이머 줄어들수록 랜덤한 두더지 나오는 속도 증가
  • 두더지 한개씩말고, 랜덤으로 여러개 나오기
  • 점수 최고점 설정하기
  • Stop클릭시 두더지클릭 및 점수 반영 안되게 하기
  • 같은 두더지는 한번이상 클릭 안되기
  • 컴포넌트 더 분리하기 타이머, 게임컨트롤..

마무리 느낀점

리액트로 간단한걸 만들고싶어 어떤걸 만들지 고민하다 게임을 생각했고, 추억의 두더지게임으로 정했다.

//두더지 생성
//두더지 클릭이벤트
//게임 점수 상태관리

이런식으로 주석으로 구현할 기능 부터 정리하고, 시작했다.
코드는 짜고보니 간단한데, 수정의 수정을 거듭하여..대략 2시간 좀 넘게 걸렸다. 그래도 오랜만에 리액트로 혼자 이것저것 만들어보니 좀 익숙해진것도 같다...🤓;
이제 코드 중복되는것 정리하고, 리팩토링 내용도 블로그에 업뎃해야겠다.

profile
프론트엔드 가보자고-!

0개의 댓글