2048 게임 만들기 - 타일 이동 로직

지은·2023년 6월 30일
0

🚂 토이 프로젝트

목록 보기
6/10

2048 게임은 ↑ ↓ → ← 방향키를 이용해 타일들을 이동시켜 같은 숫자끼리 합하고 최종적으로 2048을 만드는 게임이다.

키보드 이벤트 등록

먼저 사용자의 키보드 입력을 받기 위해 window 객체에 keydown 이벤트 리스너를 등록해야한다.
useEffect 훅 안에서 addEventListener()를 사용하여 컴포넌트가 마운트되었을 때 이벤트 리스너를 등록하고, 컴포넌트가 언마운트될 때 실행되는 리턴 문에서 removeEventLister()를 사용해 컴포넌트가 언마운트될 때 이벤트 리스너를 제거한다.
이렇게 하면 컴포넌트의 라이프사이클과 이벤트 리스너를 동기화시킬 수 있다.

Grid.js

export default function Grid() {
  const [tileList, setTileList] = useState(getInitialGrid);

  useEffect(() => {
    window.addEventListener("keydown", handleKeyboard); // 마운트시 실행

    return () => { // 언마운트시 실행
      window.removeEventListener("keydown", handleKeyboard);
    };
  });

  function handleKeyboard(e) {
    console.log(e.key); // 사용자가 입력한 키가 출력된다.
  }

  // ...생략
}

useEffect 훅의 리턴문에서 이벤트 리스너를 제거해줘야 하는 이유

window.addEventListenerwindow.removeEventListener이벤트 리스너를 전역적으로 추가하고 제거하는 메소드이다.
따라서 컴포넌트가 언마운트된 후 이벤트 리스너를 제거해주지 않으면 컴포넌트 언마운트된 이후에도 계속해서 이벤트가 발생할 수 있으며, 아래와 같은 문제를 발생할 수 있다.

  1. 메모리 누수 : 이벤트 리스너가 계속 활성화된 상태로 유지되면, 컴포넌트 인스턴스가 메모리에 유지되는 동안 계속해서 메모리를 소비하게 된다. 이는 장기적인 컴포넌트 마운트/언마운트 시나리오에서 메모리 누수로 이어질 수 있다.
  2. 예기치 않은 동작 : 컴포넌트가 언마운트된 후에도 이벤트가 발생하면, 해당 컴포넌트의 상태나 프로퍼티에 예상치 못한 동작이 발생할 수 있다.

따라서 이벤트 리스너를 추가한 경우에는 컴포넌트 라이프사이클에서 이벤트 리스너를 제거해주는 것이 중요하다.

🔌 useEffect 훅의 반환 함수를 사용해 정리(clean-up)하는 작업을 해주자.


입력된 방향키에 따라 분기하기

먼저 handleKeyboard() 함수에서 이벤트 객체의 key 속성을 이용해 위, 아래, 좌, 우 방향키 각각의 경우에 함수를 실행하도록 해주었다.

function handleKeyboard(e) {
  switch (e.key) {
    case 'ArrowUp':
      moveUp();
      break;
    case 'ArrowDown':
      moveDown();
      break;
    case 'ArrowLeft':
      moveLeft();
      break;
    case 'ArrowRight':
      moveRight();
      break;
    default:
      break;
  }
}

function moveUp() {
  slideTile(0, -1);
}
function moveDown() {
  slideTile(0, 1);
}
function moveLeft() {
  slideTile(-1, 0);
}
function moveRight() {
  slideTile(1, 0);
}

move_() 함수는 slideTile() 함수에 사용자가 어느 방향으로 타일을 이동시키고 싶어하는지를 xy로 방향값을 전달한다.


타일 이동 로직

1. 상하좌우 구분

자 이제 시작이다..🙂
먼저 인자로 받은 xy를 이용해서 아래 변수들을 선언해준다.

  • isVerticalMove : 수직으로 이동했는지(위, 아래) / 수평으로 이동했는지(좌,우) 나타내는 boolean 값
  • isMinus : -1 쪽으로 이동했는지(위, 왼쪽) / 1 쪽으로 이동했는지(아래, 오른쪽)를 나타내는 boolean 값
    나중에 이 값을 이용해서 타일들을 그리드의 0번째 줄으로 몰 것인지, 마지막 줄로 몰 것인지가 결정하는데 사용할 것이다.
const isVerticalMove = y !== 0;
const isMinus = x < 0 || y < 0;

이제 isVerticalMoveisMinus 값을 통해 방향키가 위, 아래, 좌, 우 중 어떤 것인지 구별할 수 있게 되었다.

isVerticalMoveisMinus
truetrue
아래truefalse
falsetrue
falsefalse

2. 정렬하기

이제 sort() 메소드를 이용해 tileList를 재정렬해보자.
tileList는 아래와 같은 데이터다.

tileList = [
  {x: 1, y: 2, value:2, id: 1},
  {x: 3, y: 1, value: 2, id: 2}
]

먼저 1차 정렬 기준은 이동 방향이 수직이냐 수평이냐이다.
이동 방향이 수직이라면 x를 기준으로 정렬하고, 수평이라면 y를 기준으로 정렬한다.

const sortedTileList = [...tileList].sort((tile1, tile2) => {
 const result = isVerticalMove ? tile1.x - tile2.x : tile1.y - tile2.y;
 return result;
});

하지만, 만약 두 타일이 같은 열이나 행에 있을 경우에는 위의 정렬 공식에서 0이 반환된다.
이 경우에는 2차 정렬을 해줘야 한다.

수직 이동의 경우, 1차 정렬 때 x 값을 사용했기 때문에 2차 정렬에서는 y 값을 사용해주면 되고,
수평 이동의 경우, 1차 정렬 때 y 값을 사용했기 때문에 2차 정렬에서는 x 값을 사용해 정렬해주면 된다.

음의 방향(위/왼쪽)일 경우 오름차순으로 정렬해주고, 양의 방향(아래/오른쪽)일 경우 내림차순으로 정렬해준다.

const sortedTileList = [...tileList].sort((tile1, tile2) => {
      const result = isVerticalMove ? tile1.x - tile2.x : tile1.y - tile2.y;

      if (result !== 0) { // 1차 정렬때 정렬이 됐으면 그냥 반환
        return result;
      } else { // 정렬이 안됐으면 2차 정렬한다.
        if (isVerticalMove) {
          return isMinus ? tile1.y - tile2.y : tile2.y - tile1.y; // 위로 이동이라면 오름차순, 아래로 이동이라면 내림차순
        } else {
          return isMinus ? tile1.x - tile2.x : tile2.x - tile2.x; // 왼쪽 이동이라면 오름차순, 오른쪽 이동이라면 내림차순
        }
      }
    });

3. 정렬된 배열을 이용해 x, y 값 재할당해주기

이제 이렇게 정렬된 배열 sortedTileList을 순회하며, i 값을 이용해 타일의 xy를 변경해줄 것이다.

먼저 초기 인덱스를 나타내는 initialPostion 이라는 변수에 isMinus 값에 따라 0 또는 3을 할당해준다.
그리고 position 변수를 초기화해준다.
이후 position의 값을 1씩 늘려주면서 position값을 타일의 xy 좌표에 할당해줄 것이다.

let initialPosition = isMinus ? 0 : 3;
// isMinus가 true라면 ? 맨 위 혹은 맨 왼쪽으로 밀어야 하므로 0 : 맨 아래 혹은 맨 오른쪽으로 밀어야 하므로 3
let position = initialPosition;

정렬된 배열 sortedTileList에 반복문을 순회하면서
만약 수직 이동이었다면 배열 안의 모든 타일들의 y값을 조정해줄 것이고, 수평 이동이었다면 x 값을 조정해줄 것이다.

그리고 위/왼쪽 이동이었다면(isMinustrue) position 값이 0에서 시작하여 1씩 증가시켜주고,
아래/오른쪽 이동이었다면(isMinusfalse) position 값이 3에서 시작하여 1씩 감소시켜줄 것이다.

for (let i = 0; i < sortedTileList.length; i++) {
  if (isVerticalMove) { // 수직 이동
    sortedTileList[i].y = position; // y값 조정
    position = isMinus ? position + 1 : position - 1; // isMinus값에 따라  position 증감

  } else { // 수평 이동
    sortedTileList[i].x = position; // x값 조정
    position = isMinus ? position + 1 : position - 1;
  }
}

setTileList(sortedTileList);

여기에 조건을 하나 더 추가해야 한다.
타일리스트는 단순히 1차원 배열로 이루어져있기 때문에, 어디에서 행이나 열이 바뀌는지 알 수 없다.
따라서 현재 타일의 x값과 다음 타일의 x을 비교해서 값이 다르다면 행이 바뀐 것이므로 position 값을 다시 초기값으로 초기화시켜줘야 한다.

for (let i = 0; i < sortedTileList.length; i++) {
  if (isVerticalMove) {
    sortedTileList[i].y = position;
    position = isMinus ? position + 1 : position - 1;
    
    // 만약 현재 타일의 x좌표가 다음 타일의 x좌표와 다르다면 (열이 변경되었다면)
    if (sortedTileList[i].x !== sortedTileList[i + 1]?.x) {
        position = initialPosition; // position 값을 초기화한다.
    }
  } else {
    sortedTileList[i].x = position;
    position = isMinus ? position + 1 : position - 1;
    
    // 만약 현재 타일의 y좌표가 다음 타일의 y좌표와 다르다면 (행이 변경되었다면)
    if (sortedTileList[i].y !== sortedTileList[i + 1]?.y) {
        position = initialPosition; // position 값을 초기화한다.
    }
  }
}

이때 sortedTileList[i + 1]가 존재하지 않는 경우, 참조 에러가 발생해서 옵셔널 체이닝 연산자(?.)를 추가해줬다.

이제 이렇게 값을 변경해준 sortedListsetTileList() 함수에 전달하면 된다.

profile
블로그 이전 -> https://janechun.tistory.com

4개의 댓글

comment-user-thumbnail
2023년 7월 1일

프로젝트 진행이 잘되가는거같아요! 화이팅입니다!

답글 달기
comment-user-thumbnail
2023년 7월 1일

나라면 어떻게 했을까 상상하면서 읽으니까 아주 술술 읽혔어요!! 고생하셨습니당 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 7월 2일

아이디어부터 너무 좋은 것 같아요 ...! 응원합니당

답글 달기
comment-user-thumbnail
2023년 7월 2일

이번꺼 넘 창의적이에오 고생하셨슴돠 ㅎㅎㅎ !!!

답글 달기