[JavaScript] 미니게임 프로젝트 | 기억력 테스트🧠

승연·2022년 7월 17일
1

Game World

목록 보기
5/6
post-thumbnail

Game World


미니 프로젝트 네 번째 게임으로 기억력 테스트 게임을 만들었습니다.

🙋‍♀️ 게임 설명 좀 해주세요

PC의 블록 선택 순서를 기억한 후, 순서를 똑같이 맞추는 게임입니다.

🤔 어떤 기능이 있나요?

  • 블록 맞추기
    • 사용자는 PC의 블록 선택 순서를 기억한 다음 똑같이 선택해야 한다.
    • PC의 블록 선택 개수는 1부터 시작한다.
    • 스테이지가 진행될수록 PC의 블록 선택 개수는 1씩 늘어난다.

  • 게임 종료 후 결과 출력
    • 사용자가 순서를 틀리면 게임은 종료된다.
    • 게임 종료 시 사용자가 획득한 점수를 출력한다.

🔎 사용 기술은 무엇인가요?

HTML, CSS, JavaScript를 이용하여 만들었습니다.

🙊 어디서 해볼 수 있나요?

https://sypear.github.io/game-world/game-mt.html



Game World - 기억력 테스트🧠


1. 어떻게 만들어야 할까?

✏️ 게임 규칙 정하기


❓ 어떻게 만들지?


정리 내용 요약

  • 게임 규칙
    • 사용자는 PC의 블록 선택 순서를 기억한 다음 똑같이 선택해야 함
    • 순서를 잘못 맞추면 게임 종료
  • 게임판 사이즈
    • 3x3으로 고정
  • PC의 블록 선택 개수
    • 1부터 시작
    • 스테이지 진행에 따라 블록 선택 개수 1씩 증가
  • PC의 선택 값
    • 랜덤 함수를 이용해서 생성
    • 반복문을 이용하여 블록 선택 개수만큼 생성한 후 배열에 저장
    • 중복 허용
  • 사용자가 클릭한 값과 PC 선택 값 비교
    • HTML을 이용해 블록마다 id를 부여
    • 비교 시에는 해당 id 값을 통해 비교



2. PC의 선택 값 만들기

2-1. 랜덤으로 선택 값 만들기

let answerCount = 1; // 정답 개수
let answerArr = []; // 정답을 저장하는 배열

function createAnswer() {
    for (let i = 0; i < answerCount; i++) {
        answerArr.push(getRandom(9, 0));
    }
}

function getRandom(max, min) {
    return parseInt(Math.random() * (max - min)) + min;
}

Math.random() 메소드를 이용해 0부터 9 미만의 값을 정답 개수 만큼 무작위로 뽑았습니다. 뽑은 값은 정답을 저장하는 배열에 넣었습니다.

2-2. PC의 선택 값 보여주기

배경색이 바뀌며 PC의 선택 값을 보여주는 기능을 만들었습니다.


HTML

<div id="container" class="no-drag">
	<div class="board">
      
      ...
      
      <main class="game">
          <div class="item-wrapper">
              <div class="item" data-id="0"></div>
              <div class="item" data-id="1"></div>
              <div class="item" data-id="2"></div>
              <div class="item" data-id="3"></div>
              <div class="item" data-id="4"></div>
              <div class="item" data-id="5"></div>
              <div class="item" data-id="6"></div>
              <div class="item" data-id="7"></div>
              <div class="item" data-id="8"></div>
          </div>
      </main>
      
      ...
      
  </div>
</div>

.item은 각각의 블록을 의미합니다.


CSS

.bgChange {
    animation: bgChangeEffect 400ms 2 alternate;
}

@keyframes bgChangeEffect {
    from {
        background-color: #FFF;
    }

    to {
        background-color: #DEA5A4;
    }
}

CSS를 통해 배경색이 흰색에서 빨간색으로 변하는 애니메이션을 만들었습니다.
그리고 그 애니메이션을 실행하는 .bgChange 클래스를 만들었습니다.

JavaScript

const items = document.getElementsByClassName("item");

function selectAnswerOnPC() {
    let cnt = 0;

    let selectAnswerPromise = new Promise((resolve, reject) => {
        let selectAnswerTimer = setInterval(() => {
            // 배경색이 변하는 애니메이션 재시작을 위해 이미 select 클래스가 부착되어 있다면 제거
            if (items[answerArr[cnt]].classList.contains("bgChange")) {
                items[answerArr[cnt]].classList.remove("bgChange");
                void items[answerArr[cnt]].offsetWidth;
            }

            items[answerArr[cnt++]].classList.add("bgChange");

            if (cnt === answerCount) {
                clearInterval(selectAnswerTimer);
    
                resolve();
            }
        }, 800);
    });

    selectAnswerPromise.then(() => {
        // selectAnswerPromise 성공인 경우 실행할 코드
        setTimeout(waitGameStart, 800);
    });
}

자바스크립트로 setInterval()을 이용해 0.8초마다 PC의 선택 값을 하나씩 보여주도록 했습니다.

이때 classList.add()를 통해 .bgChange 클래스를 추가하여 배경색이 바뀌는 애니메이션이 동작하도록 했습니다.

선택 값에 .bgChange 클래스가 이미 부착되어 있다면 애니메이션이 동작하지 않으므로 classList.remove()를 통해 .bgChange 클래스를 제거한 후 다시 .bgChange 클래스를 추가하였습니다.

🙋‍♀️ 코드 중간에 void HTMLElement.offsetWidth은 왜 넣은 건가요?

items[answerArr[cnt]].classList.remove("bgChange");
items[answerArr[cnt]].classList.add("bgChange");

이런 식으로 클래스 remove 후 add를 하면 애니메이션이 재시작될 줄 알았는데, 실제로는 애니메이션이 재시작되지 않았습니다.

items[answerArr[cnt]].classList.remove("bgChange");
void items[answerArr[cnt]].offsetWidth;
items[answerArr[cnt]].classList.add("bgChange");

검색 결과 이렇게 remove, add 중간에 void HTMLElement.offsetWidth 코드를 추가하면 애니메이션 재시작이 가능하다는 걸 알게 되었고, 코드 추가 후 실제로도 애니메이션 재시작이 잘 됐습니다.

🤷‍♀️ void HTMLElement.offsetWidth가 대체 뭐길래?

대체 왜 되는 걸까..🤔? 이유를 알기 위해 검색을 좀 더 해봤습니다.

결국 void HTMLElement.offsetWidth는 HTML 엘리먼트의 너비를 구한 후 최종적으로는 undefined를 반환하는 코드라는 것을 알 수 있었습니다.


콘솔에 찍어보니 실제로 void items[answerArr[cnt]].offsetWidthundefined 값을 반환하고 있었습니다.

즉, 해당 코드는 브라우저에 의미 없는 계산을 하게 시키는 코드였습니다.

😯 의미 없는 계산은 왜 시키는 거죠?

클래스를 붙이고 지울 때마다 변경 사항을 렌더링 하는 것은 리소스가 많이 드는 일입니다.

그래서 일반적인 브라우저들은 이렇게 리소스가 많이 드는 변경 사항들은 바로 적용 하지 않고 기록만 해놓은 후, 작업이 끝난 뒤에 일괄 처리를 진행한다고 합니다.


제가 처음에 짰던 코드를 실행하면

items[answerArr[cnt]].classList.remove("bgChange");
items[answerArr[cnt]].classList.add("bgChange");

브라우저는 아래와 같이 동작하게 됩니다.

  1. bgChange 클래스를 제거한다.
  2. 브라우저는 변경 사항을 기록하지만, 화면을 렌더링하지 않는다.
  3. bgChange 클래스를 추가한다.
  4. 함수를 다 읽은 브라우저는 이제 기록해놓은 일들을 처리하려고 하는데.. 잉? 처음이랑 달라진 게 없네?🤔
  5. 화면은 그대로 유지된다.

이런 이유로 애니메이션이 재시작되지 않은 것이었습니다.


중간에 offsetWidth를 구하는 코드를 추가한다면 브라우저는 offsetWidth를 알아내기 위해 변경 사항을 일괄 처리하는 계획을 포기하고 바로 당장 페이지 렌더링을 수행하게 됩니다.

따라서 아래와 같이 클래스 제거 후 offsetWidth를 요청하는 코드를 넣으면

items[answerArr[cnt]].classList.remove("bgChange");
void items[answerArr[cnt]].offsetWidth;
items[answerArr[cnt]].classList.add("bgChange");

브라우저는 아래와 같이 동작하게 됩니다.

  1. bgChange 클래스를 제거한다.
  2. 브라우저는 변경 사항을 기록하지만, 화면을 렌더링하지 않는다.
  3. 브라우저는 offsetWidth를 구한 후 화면을 렌더링한다.
  4. bgChange 클래스를 추가한다.
  5. bgChange 클래스 제거가 화면에 반영된 상태이므로(3번 과정) 애니메이션이 동작한다.

그러나 이렇게 의미 없는 계산을 시키는 일은 성능이 저하될 수 있으므로 최대한 피해야 한다고 합니다.

그래서 저는 classList.contains() 메소드를 이용해 bgChange 클래스가 이미 붙어있는 경우에만 해당 연산을 수행하도록 하는 조건문을 추가하여 구현했습니다.




3. 정답/오답 판별하기

3-1. 사용자 클릭 값 알아내기

const itemWrapper = document.getElementsByClassName("item-wrapper")[0];

itemWrapper.addEventListener("click", function(e) {
  
    ...
    
    let targetId = parseInt(e.target.dataset.id);

    checkCorrectAnswer(targetId);
});

클릭 이벤트가 발생할 때마다 사용자가 클릭한 블록의 id 값을 얻어왔습니다.
이렇게 얻어온 id 값을 PC의 선택 값과 비교하는 함수로 전달했습니다.


3-2. PC의 선택 값과 비교하기

function checkCorrectAnswer(targetId) {
    // 사용자가 선택한 블록의 id와 정답이 일치하면 맞은 것으로 판단
    if (targetId === answerArr[playerSelectionCount++]) {
        // 사용자의 선택 횟수가 정답 개수(PC의 선택값 개수)와 같아지면 전부 맞은 것으로 판단
        if (playerSelectionCount === answerCount) {
            clearStage();
        }
    } else {
        stopGame();
    }
}

⭕ 사용자의 선택이 맞은 경우

사용자가 클릭한 블록의 id와 정답이 일치하면 맞은 것으로 판단했습니다.

그리고 사용자의 선택 횟수가 정답 개수와 같아지면 전부 맞은 것으로 판단하여 다음 스테이지로 넘어가도록 했습니다.

❌ 사용자의 선택이 틀린 경우

게임이 종료되도록 했습니다.


profile
✈️ https://sypear.tistory.com/

0개의 댓글