Vanilla JS로 간단한 게임 만들기(1)

JU CHEOLJIN·2021년 7월 22일
4

Project

목록 보기
4/9

당근을 찾아라!

프로젝트 소개

제작 기간

2021.07.19 ~ 2021.07.18

사용 기술

  • HTML / CSS
  • JavaScript(ES6+)

구현 사항

1. 기본적인 레이아웃 및 애니메이션 효과.
2. 게임 시작 기능

  • 게임 시작 클릭 시에 당근 및 벌레 랜덤 배치
  • 게임 시작 시에 타이머 및 스코어 표시

3. 게임 플레이 기능

  • 당근 클릭 시 없애고 스코어 창에 반영(남아 있는 당근 수)
  • 벌레 클릭 시에 효과음과 함께 게임 종료
  • 타이머 종료 시에 효과음과 함께 게임 종료

4. 게임 정지 기능

  • 게임 정지 버튼 눌렀을 경우 타이머 멈춤.
  • 재시작 버튼을 통해 다시 시작.

5. 효과음

제작하게 된 이유

사실, 드림코딩 엘리님의 브라우저101 강의를 수강하게 됐던 이유 중의 하나가 바로 이 게임을 만드는 과정이었다. 게임 개발을 꿈꾸고 있지는 않지만 게임의 경우 여러가지 기능을 구현해야하고 사용자와의 상호작용이 기본이기 때문에 작게나마 경험해보고 싶었기 때문이었다. 특히 가능하다면 강의에서 제공하는 틀을 벗어나서 나만의 요소들을 넣어 변화시켜보고 싶은 생각도 들었다.

물론, 초롱초롱했던 눈과 마음은 게임을 직접 만들어보면서 기가 팍 죽기는 했지만 어려웠던만큼 정말 즐거웠던 과정이었다. 계속 나오는 에러창을 보며 "왜 그래?", "미안해" 라는 말을 많이도 했지만 하나씩 해결이 되가는 과정은 정말 기분이 좋고 재미있었다.

프로젝트 리뷰

기본적인 구조 및 디자인

<!-- HTML -->
<body>
    <section class="game">
      <header class="game__header">
        <button type="button" class="game__button">
          <i class="fas fa-play"></i>
        </button>
        <div class="game__timer">00:00</div>
        <div class="game__score">0</div>
      </header>
      <section class="game__field"></section>
    </section>
    <section class="modal modal--hide">
      <button class="modal__refresh">
        <i class="fas fa-redo"></i>
      </button>
      <span class="modal__message">You Lost 🥲</span>
    </section>
  </body>

기본 HTML 구조는 위처럼 구성을 했다. 게임의 최초 시연 모습을 보면 시작 버튼, 타이머, 스코어가 묶여있는 헤더 부분이 있고 당근과 벌레가 생성되는 필드 영역, 그리고 별개의 모달창(팝업)이 있었다.

여기서 모달창이나 타이머, 스코어의 경우에는 JavaScript 를 이용해서 동적으로 보여줄 예정이지만 스타일링을 하는데 편의를 위해서 임의의 값을 넣고 시작했다.

게임에 활력 불어넣기!

정적으로 만들어 놓은 페이지에 JavaScript 를 이용해서 활력을 불어넣을 때는 정말로 기분이 좋아진다. 초보 코린이의 입장에서 간단한 것을 할 때도 고민을 하고 문제를 겪게 되는 일이 많지만 그럼에도 즐겁다.

1. 당근과 벌레 생성하기!

우리가 만들게 될 게임은 당근을 골라내야하는 게임이다. 그렇기에 플레이어가 클릭해서 없앨 당근, 피해야 할 벌레를 없애는 일이 먼저였다.

// 게임 아이템들을 생성하는 함수
function addItem(className, count, imgPath) {
  // 아이템 생성
  let x1 = 0;
  let y1 = 0;
  let x2 = fieldRect.width - imgSize;
  let y2 = fieldRect.height - imgSize;
  for (let i = 1; i <= count; i++) {
    let item = document.createElement("img");
    item.setAttribute("class", className);
    item.setAttribute("src", imgPath);
    item.style.position = "absolute";
    const x = randomNumber(x2, x1);
    const y = randomNumber(y2, y1);
    item.style.top = `${y}px`;
    item.style.left = `${x}px`;
    field.appendChild(item);
  }
}

처음 기능 구현을 고민할 때는 쉬울 것이라는 생각이 들었다. 랜덤으로 포지션을 정하는 것은 Math.random() 을 사용하면 어렵지 않은 문제였고 이전의 과제들을 통해서 DOM 요소를 원하는 위치에 배치하는 방법도 알고 있었다. 하지만 생각했던 방법으로 구현을 해보니 원하는 결과가 나오지 않았다.

랜덤 숫자를 발생시켜서 위치를 정해주다보니 원하는 범위에 들어오지 않고 게임 화면을 나가버리는 일들이 생겼다.

// 아이템들의 위치를 랜덤하게 할 수 있도록 랜덤 숫자 생성하는 함수
function randomNumber(max, min) {
  // 난수 만들기(위치)
  const number = Math.random() * (max - min) + min;
  return number;
}

위와 같은 코드를 통해서 범위를 지정할 수 있었지만 게임 필드의 크기를 지정하는 것도 문제였다. 처음에는 구체적인 값을 통해서 제어를 시도했다. 대략 원하는 범위에 들어오도록 조정하는 것이 어려운 일은 아니었지만 올바른 방법은 아니라는 생각이 들었다.

그래서 getBoundingClientRect() 을 통해서 게임 필드의 width와 height를 구하고 이를 통해 생성될 범위를 구할 수 있도록 했다. 그럼에도 불구하고 필드를 벗어나는 요소들이 생겼는데 이는 포지션을 사용하는 경우에 요소의 왼쪽 상단을 기준으로 위치가 정해지기 때문이다. 이를 해결하기 위해서 생성될 이미지의 width, height만큼을 빼서 해결했다.

이 코드를 구현하면서 처음에 나는 구체적인 값을 넣어 처리를 했다. 다른 코드를 구현하면서 계속 찝찝한 기분이 드는 것 같아서 검색과 엘리님의 도움을 통해서 코드를 수정할 수 있었다. 쉽게 넘어가기 보다는 옳은 방법인지 고민하도록 해야겠다는 생각이 드는 순간이었다. 🥲 항상 올바른 코드인지 고민하자.

2. 타이머 시작 및 정지!

게임 시작 버튼을 눌렀을 때 타이머가 등장하며 자동으로 시작되고 정지 버튼을 누른 경우나 게임이 종료 된 경우에 타이머가 멈출 수 있도록 했다.

function startTimer() {
  let remainingTime = GAME_DEFAULT_TIME;
  printTime(remainingTime);
  timer = setInterval(() => {
    if (remainingTime <= 0) {
      clearInterval(timer);
      return;
    }
    printTime(--remainingTime);
    timeOver(remainingTime);
  }, 1000);
}

function stopTimer() {
  clearInterval(timer);
}

처음에는 setTimeout() 을 이용해서 구현을 하려고 했다. 그래서 IIFE 와 반복문을 사용해서 구현을 했는데 시작하고 멈추는 것에 있어서 불편함이 많았다. 또, 타이머가 0이 된 경우에 게임을 종료하고 다시 시작하도록 하는데 있어서 활용도가 많이 떨어졌다.

그래서 다른 코드를 작성하다가 막막함을 느끼고 다른 방법을 찾기 시작했다. 그래서 알게 된 것이 setInterval() 이었다. clearInterval() 를 이용해서 멈추는 것도 쉽기 때문에 만족스러웠다.

타이머가 만족스럽게 작동하도록 하기 위해서 printTime() 함수를 통해서 게임 화면에 계속해서 업데이트 될 수 있도록 했고 조건문을 통해서 남은 시간이 0보다 크다면 1초 간격으로 남은 시간 업데이트를 계속 할 수 있도록 했다.

function timeOver(time) {
  if (time === 0) {
    gameOver();
  }
}

또한, timeOver() 함수를 통해서 남은 시간이 0인 경우에 게임이 종료되도록 했다. 나는 타임오버, 벌레 클릭 시에 게임오버, 게임 클리어를 다 따로 선언하여 만들었는데 비슷한 역할을 하고 있기 때문에 중복된 코드를 줄이는 측면에서 하나의 함수로 만들고 parameter 를 통해서 조정이 되도록 하는 것이 더 좋은 방법으로 보인다.

3. 남은 당근 개수 알리기!

사용자가 땅에 남은 당근을 보고 직접 숫자를 알 수도 있지만 명시적으로 보여주기 위해서 필요하다. 당근과 벌레의 수를 늘릴 수록 숫자로 표현해 주는 것이 사용자에게 더 좋은 경험을 줄 수 있기 때문이다.

function updateCarrot(carrot) {
  gameScore.innerHTML = `${carrot}`;
}

코드는 간단하게 구성했다. gameScore 를 통해서 DOM 관련 된 요소를 가져왔고 여기에 innerHTML 을 통해서 입력 받는 당근의 수를 업데이트 할 수 있도록 했다. 여기서 중요한 점은 사용자가 당근을 클릭하는 경우에 사라지면서 점수에 실시간으로 반영이 되야 한다는 점이다.

function onItemClick(event, currentCarrot) {
  if (event.target.className === "carrot") {
    event.target.remove();
    score++;
    updateCarrot(CARROT_COUNT - score);
    if (score === CARROT_COUNT) {
      gameClear();
    }
  }
  if (event.target.className === "bug") {
    gameOver();
  }
}

onItemClick 라는 함수를 만들어서 당근과 벌레를 클릭하는 경우 모두 사용할 수 있도록 했다. 조건문을 통해서 클릭 된 요소의 클래스가 carrot 인 경우에는 지울 수 있도록 했다. 또한, score 변수의 값을 0으로 초기화 해두고 각 당근이 클릭 되는 경우에 1씩 증가할 수 있도록 했다. 이를 이용해서 CARROT_COUNT - score 를 입력 받은 함수가 남은 당근의 수를 알려주는 것이다.

추가로, score 값에 따라서 gameClear() 를 사용할 수 있도록 해서 게임의 승리조건을 명시했다. 반대로 벌레를 클릭하는 경우에는 바로 게임이 종료 될 수 있도록 했다.

엘리님의 강의에서는 className 대신에 matches() 를 사용했는데 어떤 차이가 있는지는 추가로 공부해 볼 예정이다.

기억에 남았던 코드✨

// 1번 -> 요소들을 생성해주는 함수
function onCreate() {
  addItem("carrot", CARROT_COUNT, "img/carrot.png");
  addItem("bug", BUG_COUNT, "img/bug.png");
}

// 2번 -> 플레이 버튼 관련 이벤트 리스너
gameBtn.addEventListener("click", () => {
  if (isStarted) {
    stopGame();
  } else {
    startGame();
  }
});

이번 게임 만들기를 진행하면서 인상 깊었던 코드는 위의 2가지를 뽑고 싶다. 두 코드는 모두 처음에 내가 작성했던 코드에서 많은 변화가 있었던 코드들이다.

정말로 기능 구현만 고민하면서 코드를 작성했던 나는 재사용성에 대해서는 고민을 제대로 하지 못했다. 처음 플레이 버튼 관련 이벤트 리스너를 작성했을 때도 조건문을 어떻게 사용하면 좋을지만 고민하고 안에 온갖 내용들을 작성했다.

아이템들을 생성하는 함수의 경우도 마찬가지로, 당근이랑 벌레를 생성하면 되지라는 생각으로 작성했다. 대략 아래처럼.

function 아이템생성() {
  let item = document.createElement("img");
    item.setAttribute("class", "당근");
    item.setAttribute("src", "당근 이미지 주소");
    item.setAttribute("class", "벌레");
    item.setAttribute("src", "벌레 이미지 주소");

여기에 당근과 벌레가 아닌 다른 요소도 나올 수 있도록 추가하고 싶다면 어떻게 해야할까? 아마 저 함수를 찾아다가 밑에다가 비슷한 내용을 적어줘야 했을 것이다. 만약 이런 작업을 수없이 반복해야한다면? 생각만 해도 끔찍하다 🤣

이벤트 리스너도 마찬가지다... 조건문 안에 "시작할 때는 이거하고 저거하고 다 해줘~!", 게임이 이미 시작되어 있으면 "이것도 하고 또 저것도 해야지" 라고 작성되어 있었다면 읽는 사람들이 괴로웠을 것이다...🥲

하고 싶은 말이 정말 많은 게임 만들기였지만 하나만 더 이야기 하자면. isStarted 라는 boolean 값을 가지고 있는 변수를 이용해서 게임의 시작 상태와 정지 상태를 구별할 수 있도록 한 점이 좋았다. 처음 구현을 했을 때 나는 플레이 & 정지 아이콘의 클래스 명을 통해서 이를 해결하도록 했었다. 동작은 잘 됐지만 재시작 버튼 등에서도 비슷한 동작들을 수행하도록 하다보니 어려움이 생겨났다. 게임이 시작되어 있는지 멈춘 상태인지 판단할 수 있는 근거가 없다보니 생각지 못한 버그들도 생겨났고. 빨간 에러창 옆에 숫자가 계속 올라가는 것을 보면서 "왜 그래!! 왜 그러냐구!!" 외치던 시간들이 떠오른다.

이번 게임 만들기를 직접 해본 후에 엘리님의 강의를 들으면서 느낀 점은 내가 생각했던 것 이상으로 함수들을 작게 나눠서 사용하신다는 점이었다. 그런 작업 방식은 비슷한 코드를 작성해야 하는 경우를 매우 줄일 수 있었고 확장성도 좋았다.

"하나의 함수에는 되도록 하나의 기능만 넣자" 라는 말을 떠올리며 작성했지만 기능 구현에만 몰두하면서 놓쳤다는 생각이 든다. 이제 이렇게 작성한 코드를 리팩토링 하면서 모듈화도 해보게 될텐데 기대가 된다.

아래는 완성된 게임의 모습이다!

profile
사회에 도움이 되는 것은 꿈, 바로 옆의 도움이 되는 것은 평생 목표인 개발자.

0개의 댓글