JavaScript 스네이크 튜토리얼 설명

본문에서는 HTML, CSS 및 JavaScript를 활용하여 뱀 게임을 만드는 과정을 상세히 안내합니다.

외부 라이브러리는 일절 사용하지 않으며, 게임은 웹 브라우저 환경에서 구동됩니다. 이 프로젝트는 문제 해결 능력을 향상시키는 데 매우 유익한 훈련 기회가 될 것입니다.

프로젝트 개요

뱀 게임은 간단하면서도 중독성 있는 게임으로, 뱀이 장애물을 피해 먹이를 먹으며 성장하는 방식입니다. 먹이를 획득할 때마다 뱀의 길이가 늘어나 난이도가 점차 상승합니다.

뱀은 벽이나 자신의 몸통에 부딪히면 안 됩니다. 따라서 게임이 진행될수록 뱀의 길이가 길어지고, 게임 플레이가 더욱 어려워집니다.

본 튜토리얼의 목표는 바로 이러한 기본적인 뱀 게임을 직접 구현하는 것입니다.

전체 게임 코드는 GitHub 저장소에서 확인하실 수 있습니다. 또한, GitHub 페이지에서 라이브 버전으로 게임을 직접 체험해 볼 수 있습니다.

필수 조건

이 프로젝트는 HTML, CSS, JavaScript를 기반으로 합니다. HTML과 CSS는 기본적인 수준만 사용하며, JavaScript에 초점을 맞출 것입니다. 따라서 본 튜토리얼을 따라 하려면 JavaScript에 대한 기본적인 이해가 필수적입니다. 만약 JavaScript 학습이 필요하다면 관련 자료를 참고하시는 것을 추천합니다.

코드를 작성하기 위한 코드 편집기와, 웹 브라우저가 필요합니다. 웹 브라우저는 아마 이 글을 읽고 계시다면 이미 가지고 계실 겁니다.

프로젝트 설정

먼저 프로젝트 폴더를 생성하고, 그 안에 `index.html` 파일을 생성하여 다음 코드를 입력합니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://wilku.top/javascript-snake-tutorial-explained/./styles.css" />
    <title>Snake</title>
  </head>
  <body>
    <div id="game-over-screen">
      <h1>Game Over</h1>
    </div>
    <canvas id="canvas" width="420" height="420"> </canvas>
    <script src="./snake.js"></script>
  </body>
</html>

위 코드는 기본적인 ‘게임 오버’ 화면을 구성합니다. JavaScript를 사용하여 이 화면의 표시 여부를 전환할 것입니다. 또한, 게임 미로, 뱀, 먹이를 그릴 캔버스 요소를 정의합니다. 스타일시트 및 JavaScript 파일도 연결되어 있습니다.

다음으로 스타일을 적용하기 위한 `styles.css` 파일을 생성하고, 다음 CSS 코드를 추가합니다.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Courier New', Courier, monospace;
}

body {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #00FFFF;
}

#game-over-screen {
    background-color: #FF00FF;
    width: 500px;
    height: 200px;
    border: 5px solid black;
    position: absolute;
    align-items: center;
    justify-content: center;
    display: none;
}

위의 코드는 모든 요소의 기본 간격을 제거하고, 글꼴을 설정하며, 요소의 크기 계산 방식을 정의합니다. body 태그에는 높이를 뷰포트 높이로 설정하고, 모든 항목을 중앙에 배치하며, 배경색을 설정합니다.

마지막으로 ‘게임 오버’ 화면에 대한 스타일을 지정합니다. 화면 크기, 배경색, 테두리 스타일을 설정하고, 위치를 절대값으로 설정하여 중앙에 배치했습니다. 기본적으로 숨겨져 있도록 `display: none` 속성을 지정했습니다.

다음 단계는 JavaScript 코드를 작성할 `snake.js` 파일을 생성하는 것입니다.

전역 변수 생성

이제 JavaScript 파일 `snake.js`를 열고 사용할 전역 변수를 선언합니다. 파일 최상단에 다음 코드를 추가합니다.

// HTML 요소에 대한 참조 생성
let gameOverScreen = document.getElementById("game-over-screen");
let canvas = document.getElementById("canvas");

// 캔버스에 그림을 그리는 데 사용할 컨텍스트 생성
let ctx = canvas.getContext("2d");

이 변수들은 ‘게임 오버’ 화면과 캔버스 요소에 대한 참조를 저장합니다. 또한 캔버스에 그림을 그리는 데 필요한 컨텍스트를 생성합니다.

다음으로 아래 코드 블록을 추가합니다.

// 미로 정의
let gridSize = 400;
let unitLength = 10;

`gridSize`는 게임 그리드의 크기를 픽셀 단위로 정의하며, `unitLength`는 게임 내에서 사용될 기본 단위 길이를 정의합니다. 이 단위 길이는 미로 벽의 두께, 뱀의 크기, 먹이의 크기 등 여러 요소에 공통적으로 사용됩니다.

다음으로 게임 상태를 추적하는 데 필요한 게임 플레이 변수를 추가합니다.

// 게임 플레이 변수
let snake = [];
let foodPosition = { x: 0, y: 0 };
let direction = "right";
let collided = false;

`snake` 변수는 뱀이 차지하는 위치를 배열 형태로 저장합니다. 배열의 각 요소는 뱀의 한 부분을 나타내며, x 및 y 좌표를 가집니다. 배열의 첫 번째 요소는 뱀의 꼬리를, 마지막 요소는 뱀의 머리를 나타냅니다. 뱀이 이동할 때마다 머리 위치가 배열의 끝에 추가되고, 꼬리 위치가 배열에서 삭제되어 뱀의 길이를 유지합니다.

`foodPosition` 변수는 먹이의 현재 위치를 x, y 좌표 형태로 저장하며, `direction` 변수는 뱀이 현재 이동하고 있는 방향을 저장합니다. `collided` 변수는 충돌이 발생했을 때 true로 변경되는 플래그 역할을 합니다.

함수 선언

게임 개발의 효율성과 관리를 위해, 게임 로직을 여러 개의 함수로 나눕니다. 이 섹션에서는 각 함수의 이름과 역할을 간략하게 설명합니다.

function setUp() {}
function doesSnakeOccupyPosition(x, y) {}
function checkForCollision() {}
function generateFood() {}
function move() {}
function turn(newDirection) {}
function onKeyDown(e) {}
function gameLoop() {}

`setUp` 함수는 게임 초기 설정을 담당하며, `checkForCollision` 함수는 뱀이 벽이나 자신의 몸통과 충돌했는지 확인합니다. `doesSnakeOccupyPosition` 함수는 특정 x, y 좌표가 뱀의 몸통에 의해 점유되어 있는지 확인하여 먹이를 생성할 수 있는 위치를 찾는 데 사용됩니다.

`move` 함수는 뱀을 현재 이동 방향으로 이동시키고, `turn` 함수는 뱀의 이동 방향을 변경합니다. `onKeyDown` 함수는 사용자의 키보드 입력을 감지하여 뱀의 방향을 제어합니다. 마지막으로 `gameLoop` 함수는 게임의 핵심 루프를 담당하여 뱀을 움직이고 충돌을 확인합니다.

함수 정의

이제 위에서 선언한 함수들을 정의하고 각 함수가 어떻게 작동하는지 자세히 살펴보겠습니다. 각 함수에 대한 설명과 코드 주석을 통해 이해를 돕도록 하겠습니다.

설정 함수

`setUp` 함수는 다음 세 가지 작업을 수행합니다.

  • 캔버스에 미로 테두리를 그립니다.
  • 뱀을 초기화하고 캔버스에 그립니다.
  • 초기 먹이 위치를 생성합니다.

해당 코드는 다음과 같습니다.

  // 캔버스에 테두리 그리기
  // 캔버스 크기는 그리드 크기에 양쪽 테두리 두께를 더한 값
  canvasSideLength = gridSize + unitLength * 2;

  // 전체 캔버스를 덮는 검정 사각형 그리기
  ctx.fillRect(0, 0, canvasSideLength, canvasSideLength);

  // 검정 사각형의 중앙 부분을 지워 게임 공간 생성
  // 테두리 역할을 하는 검정 윤곽선 남김
  ctx.clearRect(unitLength, unitLength, gridSize, gridSize);

  // 뱀의 머리와 꼬리의 초기 위치 저장
  // 뱀의 초기 길이는 60px 또는 6 단위
  const headPosition = Math.floor(gridSize / 2) + 30;
  const tailPosition = Math.floor(gridSize / 2) - 30;

  // 꼬리부터 머리까지 unitLength 간격으로 반복
  for (let i = tailPosition; i <= headPosition; i += unitLength) {
    // 뱀의 몸체 위치 저장 및 캔버스에 그리기
    snake.push({ x: i, y: Math.floor(gridSize / 2) });
    ctx.fillRect(i, Math.floor(gridSize / 2), unitLength, unitLength);
  }

  // 먹이 생성
  generateFood();

doesSnakeOccupyPosition 함수

이 함수는 x 및 y 좌표를 입력받아 뱀의 몸체 중 해당 위치를 점유하는 부분이 있는지 확인합니다. JavaScript 배열의 `find` 메서드를 사용하여 좌표가 일치하는 위치를 찾습니다.

function doesSnakeOccupyPosition(x, y) {
    return !!snake.find((position) => {
      return position.x == x && position.y == y;
    });
  }

충돌 확인 함수

이 함수는 뱀이 벽이나 자신의 몸통과 충돌했는지 확인하고, 충돌이 감지되면 `collided` 변수를 `true`로 설정합니다. 먼저 왼쪽, 오른쪽, 위쪽, 아래쪽 벽과 뱀 자신의 몸통과의 충돌을 차례로 확인합니다.

벽과의 충돌은 뱀 머리의 x 좌표가 `gridSize`보다 크거나 0보다 작거나, y 좌표가 `gridSize`보다 크거나 0보다 작은지 확인하여 판별합니다. 자기 몸통과의 충돌은 뱀 머리 위치가 뱀 몸통의 다른 부분과 일치하는지 확인하여 판별합니다. 이 모든 확인을 결합하면 `checkForCollision` 함수의 코드는 다음과 같습니다.

function checkForCollision() {
    const headPosition = snake.slice(-1)[0];
    // 왼쪽 및 오른쪽 벽과 충돌 확인
    if (headPosition.x < 0 || headPosition.x >= gridSize) {
      collided = true;
    }
    // 위쪽 및 아래쪽 벽과 충돌 확인
    if (headPosition.y < 0 || headPosition.y >= gridSize) {
      collided = true;
    }
    // 뱀 자신의 몸통과 충돌 확인
    const body = snake.slice(0, -1);
    if (body.find((position) => position.x == headPosition.x && position.y == headPosition.y)) {
        collided = true;
    }
  }

먹이 생성 함수

`generateFood` 함수는 `do-while` 루프를 사용하여 뱀이 점유하지 않는 위치에 먹이를 생성합니다. 먹이 위치를 찾으면 `foodPosition` 변수에 저장하고 캔버스에 먹이를 그립니다. 함수의 코드는 다음과 같습니다.

function generateFood() {
    let x = 0, y = 0;
    do {
      x = Math.floor((Math.random() * gridSize) / 10) * 10;
      y = Math.floor((Math.random() * gridSize) / 10) * 10;
    } while (doesSnakeOccupyPosition(x, y));

    foodPosition = { x, y };
    ctx.fillRect(x, y, unitLength, unitLength);
  }

이동 함수

`move` 함수는 뱀 머리 위치의 복사본을 생성한 후, 현재 이동 방향에 따라 뱀 머리의 x 또는 y 좌표를 변경합니다. 예를 들어 x 좌표를 증가시키는 것은 뱀을 오른쪽으로 이동시키는 것과 같습니다.

새로운 머리 위치를 계산한 후에는 `snake` 배열에 추가하고, 캔버스에 새로운 뱀 머리를 그립니다. 다음으로 뱀이 먹이를 먹었는지 확인합니다. 먹이를 먹었다면 `generateFood` 함수를 호출하여 새로운 먹이를 생성합니다.

만약 뱀이 먹이를 먹지 않았다면 뱀 배열의 첫 번째 요소를 제거하여 꼬리를 제거합니다. 이렇게 하면 뱀의 길이는 유지되면서 이동하는 효과를 낼 수 있습니다.

function move() {
        // 뱀 머리 위치 복사
        const headPosition = Object.assign({}, snake.slice(-1)[0]);
    
        switch (direction) {
          case "left":
            headPosition.x -= unitLength;
            break;
          case "right":
            headPosition.x += unitLength;
            break;
          case "up":
            headPosition.y -= unitLength;
            break;
          case "down":
            headPosition.y += unitLength;
        }
    
        // 새로운 머리 위치를 배열에 추가
        snake.push(headPosition);
        ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength);

    
        // 먹이를 먹었는지 확인
        const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y;
    
        if (isEating) {
          // 새로운 먹이 생성
          generateFood();
        } else {
          // 꼬리 제거
          const tailPosition = snake.shift();
          ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength);
        }
    }

회전 함수

`turn` 함수는 새로운 이동 방향을 입력받아 뱀의 이동 방향을 변경합니다. 뱀은 현재 이동 방향과 수직인 방향으로만 회전할 수 있습니다. 예를 들어, 위 또는 아래로 이동 중인 뱀은 왼쪽 또는 오른쪽으로만 회전할 수 있습니다. 이 규칙을 반영하여 `turn` 함수를 구현하면 다음과 같습니다.

function turn(newDirection) {
    switch (newDirection) {
      case "left":
      case "right":
        // 위 또는 아래로 이동 중일 때만 좌우 회전 허용
        if (direction == "up" || direction == "down") {
          direction = newDirection;
        }
        break;
      case "up":
      case "down":
        // 좌우로 이동 중일 때만 상하 회전 허용
        if (direction == "left" || direction == "right") {
          direction = newDirection;
        }
        break;
    }
  }

onKeyDown 함수

`onKeyDown` 함수는 키보드 화살표 키 입력에 따라 `turn` 함수를 호출하는 이벤트 핸들러입니다. 입력된 키에 따라 뱀의 이동 방향을 변경합니다. 함수의 코드는 다음과 같습니다.

function onKeyDown(e) {
        switch (e.key) {
          case "ArrowDown":
            turn("down");
            break;
          case "ArrowUp":
            turn("up");
            break;
          case "ArrowLeft":
            turn("left");
            break;
          case "ArrowRight":
            turn("right");
            break;
        }
    }
    

게임 루프 함수

`gameLoop` 함수는 게임을 계속 실행하는 데 필요한 함수로, 정기적으로 호출됩니다. 이 함수는 `move` 함수와 `checkForCollision` 함수를 호출합니다. 또한 충돌 여부를 확인하고, 충돌이 발생하면 게임 루프를 중단하고 ‘게임 오버’ 화면을 표시합니다. 함수 코드는 다음과 같습니다.

function gameLoop() {
    move();
    checkForCollision();
    if (collided) {
      clearInterval(timer);
      gameOverScreen.style.display = "flex";
    }
  }

게임 시작하기

게임을 시작하려면 다음 코드 줄을 추가합니다.

setUp();
document.addEventListener("keydown", onKeyDown);
let timer = setInterval(gameLoop, 200);
    

먼저 `setUp` 함수를 호출하여 게임을 초기화합니다. 다음으로 `keydown` 이벤트 리스너를 추가하여 키보드 입력을 처리합니다. 마지막으로 `setInterval` 함수를 사용하여 `gameLoop` 함수를 정기적으로 호출하는 타이머를 시작합니다.

결론

여기까지 진행했다면 작성한 JavaScript 파일은 GitHub 저장소의 코드와 동일해야 합니다. 만약 문제가 발생하면 저장소를 다시 확인해 보세요. 다음 단계로 JavaScript를 사용하여 이미지 슬라이더를 만드는 방법을 학습해 보시는 것도 좋습니다.