2048 게임 만들기(하)

자바스크립트 2048 게임 스크린샷

<이 화면은 스크린샷 그림입니다>

2048 게임 만들기(상)에서는 게임에 사용될 상수, 변수들의 선언과 화면을 그리는 데 필요한 함수들을 만들어 보았습니다. 이번에는 Text Game Maker JS를 사용하여 키입력을 받는 방법과 데이터를 조작하는 함수들을 만들어 게임을 완성해 봅시다.

소스 코드

2048 게임 만들기(하)의 소스 코드를 여기를 클릭하여 확인해 주세요. 전체 코드를 복사한 후 TM 스타터 프로그램의 main.js 파일에 붙여넣기합니다. 그 다음 index.html을 더블 클릭하면 아래와 같이 웹 브라우저가 나타나게 됩니다.

자바스크립트 2048 게임 브라우저 스크린샷

main.js의 코드를 살펴봅시다. reset 함수까지(199번째 줄)는 변경사항이 없고 move 함수부터 추가된 코드들을 살펴봅시다.

function move(moveDir){
  var isMoved = false;
  var rowDir = 1, rowNext = 0, rowFirst = 0;
  var colDir = 1, colNext = 0, colFirst = 0;

  switch (moveDir) {
    case 'UP':
      rowDir =  1;
      rowNext = -1;
      rowFirst = 1;
      break;
    case 'DOWN':
      rowDir = -1;
      rowNext =  1;
      rowFirst = NUM_OF_ROW-2;
      break;
    case 'LEFT':
      colDir =  1;
      colNext = -1;
      colFirst = 1;
      break;
    case 'RIGHT':
      colDir = -1;
      colNext =  1;
      colFirst = NUM_OF_COLUMN-2;
      break;
  }

  for(var i=rowFirst; i>=0&&i<NUM_OF_ROW; i+=rowDir){
    for(var j=colFirst; j>=0&&j<NUM_OF_COLUMN; j+=colDir){
      var currentBlock = boardData[i][j];
      if(currentBlock.value==0) continue;
      var nextBlock = boardData[i+rowNext][j+colNext];
      var willMove = nextBlock.value==0;
      var willMerge = nextBlock.value==currentBlock.value && !currentBlock.isMerged && !nextBlock.isMerged;
      if(willMove || willMerge){
        if(willMerge){
          currentBlock.value *= 2;
          currentBlock.isMerged = true;
          score += currentBlock.value;
          biggestNumber = Math.max(currentBlock.value, biggestNumber);
        }
        boardData[i+rowNext][j+colNext] = currentBlock;
        boardData[i][j] = resetBlockData();
        isMoved = true;
      }
    }
  }
 return isMoved;
}

move 함수는 방향("LEFT", "RIGHT", "UP", "DOWN")을 문자열로 받아 모든 블록들을 해당 방향으로 한칸씩만 이동시키는 함수입니다. 예를 들어 "UP"을 받은 경우, 3번째줄의 블럭들은 2번째 줄에 머무르게 됩니다.(3번재 블럭들이 1번째 줄까지 올라가지 않습니다) 또한 이동할 수 있는 조건에 충족되는 블록들만 이동하고, 이동하지 않거나 다른 블록과 합쳐지기도 합니다. 2048 게임의 가장 핵심 함수라고 할 수 있습니다.

이 함수는 블록들을 한칸씩만 옮기기 때문에 블록이 계속하여 이동해야 하는 경우 이 함수가 계속하여 호출되어야 합니다. 이것을 컨트롤하기 위해 전역(global) 변수인 isAutoMoving와 지역(local) 변수인 isMoved 변수를 사용합니다.

isMoved는 함수 호출 후에 이동한 블럭이 있는지를 저장합니다. 이동한 블럭이 있다면 isMovedtrue로 return되어 전역변수인 isAutoMoving를 true로 설정합니다.

isAutoMovingtrue인 경우 이 함수는 계속하여 호출되어 더이상 이동할 블럭이 없을때까지 호출됩니다. 이부분은 뒤에 다시 자세히 살펴보겠습니다.

rowDir, rowNext, rowFirst, colDir, colNext, colFirst는 2차원 배열을 반복하는 방향, 2차원 배열 시작위치, 진행방향의 다음 블럭 위치를 계산하기 위한 변수들입니다.

예를 들어 "UP"을 받은 경우 4x4사이즈에서 블럭들의 이동 순서는

  1. 2번째줄 블럭들을 1번째줄로 이동
  2. 3번째줄 블럭들을 2번째줄로 이동
  3. 4번째줄 블럭들을 3번째줄로 이동

입니다.

만약 반대로 4번째줄->2번째줄 순서로 이동하게 되면 함수 한번 호출에 모든 블럭이 다 이동하게 되므로 2번째줄->4번째줄 순서로 이동합니다.

즉 반복문에서 사용되는 값들은

  • 2번째줄부터 시작: rowFirst = 1(0이 첫번째 줄이죠)
  • 반복문은 위에서 아래로(2번째줄->4번째줄) 진행: rowDir = 1
  • 다음번 블럭은 현재블럭에서 row가 하나 적은 위치: rowNext = -1

이 됩니다. 밑에서 위로 올리는 경우 반복문을 왼쪽에서 시작할지, 오른쪽에서 시작할지는 중요하지 않으므로 기본값 그대로 colDir = 1(왼쪽에서 오른쪽), colNext = 0(다음블럭의 좌우위치는 현재블럭위치와 같음), colFirst = 0(왼쪽 첫번째 위치부터 전체 위치)를 사용합니다.

나머지 방향도 동일한 원리로 변수값들을 지정해 주었습니다. 다음으로 이 함수의 반복문 코드를 살펴봅시다.

for(var i=rowFirst; i>=0&&i<NUM_OF_ROW; i+=rowDir){
    for(var j=colFirst; j>=0&&j<NUM_OF_COLUMN; j+=colDir){
      var currentBlock = boardData[i][j]; //현재위치의 블럭을 저장
      if(currentBlock.value==0) continue; //현재위치의 블럭의 값이 0이면 반복을 건너뜀
      var nextBlock = boardData[i+rowNext][j+colNext]; //진행방향의 다음 블럭을 저장
      var willMove = nextBlock.value==0; //다음 블럭이 0이면 이동이 가능
      var willMerge = nextBlock.value==currentBlock.value && !currentBlock.isMerged && !nextBlock.isMerged; //다음 블럭과 현재 블럭의 값이 같고, 둘다 값이 합쳐진적이 없는 경우(isMerged가 false인 경우) merge가 가능
      if(willMove || willMerge){ //이동, 혹은 merge가 가능한 경우
        if(willMerge){ //merge가 가능한 경우
          currentBlock.value *= 2; //현재블럭의 값을 2배로 증가시킴
          currentBlock.isMerged = true; //merge됐음을 저장
          score += currentBlock.value; //게임 점수를 증가시킴
          biggestNumber = Math.max(currentBlock.value, biggestNumber); //게임 중 가장 큰 숫자를 갱신
        }
        boardData[i+rowNext][j+colNext] = currentBlock; //진행방향의 다음 블럭 위치에 현재블럭을 대입
        boardData[i][j] = resetBlockData(); //현재 블럭 위치에 블럭을 초기화
        isMoved = true; //이동이 있었음을 저장
      }
    }
  }

계속해서 다음 함수를 살펴봅시다.

function resetBoardMergeData(boardData){
  for(var i=0; i<boardData.length; i++){
    for(var j=0; j<boardData[i].length; j++){
      boardData[i][j].isMerged = false;
    }
  }
}

바로 위 함수에서 merge가 있는 경우 그 값들을 블럭의 isMerged에 저장했었습니다. 이 함수는 그 값들을 false로 초기화 하는 역할을 합니다. autoMoving(isAutoMovingtrue)이 끝나는 경우 호출됩니다.

function checkGameOver(){
  var isGameOver = true;
  for(var i=0; i<boardData.length; i++){
    for(var j=0; j<boardData[i].length; j++){
      if(boardData[i][j].value == 0) isGameOver = false;
      else if(i<boardData.length-1 && boardData[i][j].value == boardData[i+1][j].value) isGameOver = false;
      else if(j<boardData[i].length-1 && boardData[i][j].value == boardData[i][j+1].value) isGameOver = false;
      if(!isGameOver) break;
    }
  }
  return isGameOver;
}

게임의 종료 조건입니다. isGameOvertrue로 하고 2차원 배열상의 블럭들을 하나씩 돌아가면서 게임종료가 아닌 조건을 찾습니다. 현재 위치의 블럭이 아래의 조건에 만족하면 게임 종료가 아닙니다.

  • 블록의 값이 0임->블록이 이동할 수 있음
  • 가로, 세로 상 옆 블록과 값이 같음-> merge후 이동할 수 있음

마지막 함수를 살펴봅시다.

var mainInterval = window.setInterval(function(){
  if(TMI.keyboard.checkKeyPressed(KEYSET.ESC)){
    reset();
  }

  if(!isGameOver && !isAutoMoving){
    if(TMI.keyboard.checkKeyPressed(KEYSET.UP))    moveDir = 'UP';
    if(TMI.keyboard.checkKeyPressed(KEYSET.DOWN))  moveDir = 'DOWN';
    if(TMI.keyboard.checkKeyPressed(KEYSET.LEFT))  moveDir = 'LEFT';
    if(TMI.keyboard.checkKeyPressed(KEYSET.RIGHT)) moveDir = 'RIGHT';
  }

  if(moveDir){
    var isBoardUpdated = false;
    if(move(moveDir)){
      isBoardUpdated = true;
      isAutoMoving = true;
    }
    else {
      moveDir = null;
      if(isAutoMoving){
        isBoardUpdated = true;
        isAutoMoving = false;
        resetBoardMergeData(boardData);
        putNewBlock(boardData);
        isGameOver = checkGameOver();
        TMI.keyboard.clearKeyPressed();
      }
    }

    if(isBoardUpdated){
      drawBoard();
      drawScore();
    }
    if(isGameOver) drawGameOver();
    if(biggestNumber >= 2048) drawWin();
  }

  TMD.print('debug-data', {
    moveDir: moveDir,
    isAutoMoving: isAutoMoving,
    isGameOver: isGameOver,
    biggestNumber: biggestNumber,
  });
}, SPEED);

키 입력을 받고 키입력에 따라 게임을 진행하는 부분입니다. 부분적으로 상세하게 살펴봅시다.

var mainInterval = window.setInterval(function(){
 //...
}, SPEED);

window.setInterval 함수(자바스크립트의 기본함수입니다)로 SPEED에 저장된 값인 50 millisecond 마다 //...의 코드를 반복하게 됩니다.

다음은 반복되는 코드들를 살펴봅시다.

  if(TMI.keyboard.checkKeyPressed(KEYSET.ESC)){
    reset();
  }

TMI.keyboard.checkKeyPressed 함수는 이전 확인 이후로 해당 키가 눌렸는지를 확인하는 함수입니다. 게임 진행중에 언제라도 ESC 키를 누르면 게임이 초기화 됩니다.

계속해서 if(!isGameOver && !isAutoMoving) 안의 코드를 살펴봅시다.

  if(!isGameOver && !isAutoMoving){
    if(TMI.keyboard.checkKeyPressed(KEYSET.UP))    moveDir = 'UP';
    if(TMI.keyboard.checkKeyPressed(KEYSET.DOWN))  moveDir = 'DOWN';
    if(TMI.keyboard.checkKeyPressed(KEYSET.LEFT))  moveDir = 'LEFT';
    if(TMI.keyboard.checkKeyPressed(KEYSET.RIGHT)) moveDir = 'RIGHT';
  }

게임종료가 아니고, 자동이동중이 아닌 경우(isAutoMoving == true가 아닌 경우)에 키입력을 받는 부분입니다.

  if(moveDir){
    var isBoardUpdated = false;
    if(move(moveDir)){
      isBoardUpdated = true;
      isAutoMoving = true;
    }
    else {
      moveDir = null;
      if(isAutoMoving){
        isBoardUpdated = true;
        isAutoMoving = false;
        resetBoardMergeData(boardData);
        putNewBlock(boardData);
        isGameOver = checkGameOver();
        TMI.keyboard.clearKeyPressed();
      }
    }

    if(isBoardUpdated){
      drawBoard();
      drawScore();
    }
    if(isGameOver) drawGameOver();
    if(biggestNumber >= 2048) drawWin();
  }

키입력을 받아서 moveDir에 값이 있는 경우에만 실행되는 코드입니다. isBoardUpdated는 화면을 업데이트할 필요가 있는지를 저장하는 변수입니다.

moveDir에 값이 있으면 우선 move함수를 호출하고 이동이 있으면 화면을 다시 출력(isBoardUpdated = true)하고 자동이동이 시작되거나 계속 유지(isAutoMoving = true)됩니다.

        isBoardUpdated = true;
        isAutoMoving = false;
        resetBoardMergeData(boardData);
        putNewBlock(boardData);
        isGameOver = checkGameOver();
        TMI.keyboard.clearKeyPressed();

이렇게 이동을 다시 초기화하게 됩니다.

  TMD.print('debug-data', {
    moveDir: moveDir,
    isAutoMoving: isAutoMoving,
    isGameOver: isGameOver,
    biggestNumber: biggestNumber,
  });

마지막으로 원래 마지막에 있던 debug-data를 interval 속으로 넣었습니다. 이제 실시간 변수 정보를 바로 확인할 수 있습니다.

예제 코드의 실행

이 화면은 실제 실행중인 프로그램으로 입니다. 키 입력을 하기 위해 우선 게임 화면을 마우스로 한번 클릭한 후 화살표키와 엔터키, ESC키를 눌러서 게임이 제대로 작동하는지를 살펴봅시다.


이상으로 2048 게임 만들기 강의가 끝났습니다. 수고하셨습니다!

댓글

댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP