이번 글에서는 HTML, CSS, JavaScript를 이용해 스네이크 게임을 만드는 방법을 설명하겠습니다.
추가 라이브러리를 사용하지 않습니다. 게임은 브라우저에서 실행됩니다. 이 게임을 만드는 것은 문제 해결 근육을 스트레칭하고 단련하는 데 도움이 되는 재미있는 운동입니다.
목차
프로젝트 개요
스네이크(Snake)는 장애물을 피하면서 음식을 향해 뱀의 움직임을 안내하는 간단한 게임입니다. 뱀은 먹이에 도달하면 그것을 먹고 더 오래 자랍니다. 게임이 진행됨에 따라 뱀의 길이가 점점 길어집니다.
뱀은 벽이나 자기 자신으로 달려가서는 안 됩니다. 따라서 게임이 진행됨에 따라 뱀의 길이도 길어지고 플레이하기가 점점 더 어려워집니다.
이 JavaScript Snake Tutorial의 목표는 아래 게임을 구축하는 것입니다.
게임 코드는 내에서 확인할 수 있습니다. GitHub. 라이브 버전은 다음에서 호스팅됩니다. GitHub 페이지.
전제 조건
우리는 HTML, CSS, JavaScript를 사용하여 이 프로젝트를 구축할 것입니다. 우리는 기본적인 HTML과 CSS만 작성할 것입니다. 우리의 주요 초점은 JavaScript입니다. 그러므로 이 JavaScript Snake Tutorial을 따라가려면 이미 이를 이해하고 있어야 합니다. 그렇지 않다면 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>
위의 마크업은 기본적인 ‘Game Over’ 화면을 생성합니다. JavaScript를 사용하여 이 화면의 가시성을 전환하겠습니다. 또한 미로, 뱀, 음식을 그릴 캔버스 요소를 정의합니다. 마크업은 스타일시트와 JavaScript 코드도 연결합니다.
다음으로 스타일 지정을 위한 styles.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; }
‘*’ 규칙 세트에서는 모든 요소를 대상으로 하고 간격을 재설정합니다. 또한 모든 요소에 대한 글꼴 모음을 설정하고 테두리 상자라는 보다 예측 가능한 크기 조정 방법으로 요소의 크기를 설정합니다. 몸체의 경우 높이를 뷰포트의 전체 높이로 설정하고 모든 항목을 중앙에 정렬했습니다. 파란색 배경색도 지정했습니다.
마지막으로 ‘Game Over’ 화면의 스타일을 높이와 너비가 각각 200픽셀과 500픽셀로 지정했습니다. 또한 마젠타색 배경색과 검정색 테두리도 지정했습니다. 일반적인 문서 흐름에서 벗어나 화면 중앙에 정렬되도록 위치를 절대값으로 설정했습니다. 그런 다음 내용을 중앙에 배치했습니다. 표시를 없음으로 설정했으므로 기본적으로 숨겨집니다.
다음으로, 다음 몇 섹션에 걸쳐 작성할 snake.js 파일을 만듭니다.
전역 변수 생성
이 JavaScript Snake 튜토리얼의 다음 단계는 사용할 전역 변수를 정의하는 것입니다. snake.js 파일의 맨 위에 다음 변수 정의를 추가합니다.
// Creating references to HTML elements let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Creating context which will be used to draw on canvas let ctx = canvas.getContext("2d");
이 변수는 ‘Game Over’ 화면과 캔버스 요소에 대한 참조를 저장합니다. 다음으로 캔버스에 그림을 그리는 데 사용할 컨텍스트를 만들었습니다.
다음으로 첫 번째 세트 아래에 이러한 변수 정의를 추가합니다.
// Maze definitions let gridSize = 400; let unitLength = 10;
첫 번째는 그리드의 크기를 픽셀 단위로 정의합니다. 두 번째는 게임의 단위 길이를 정의합니다. 이 단위 길이는 여러 곳에서 사용될 예정입니다. 예를 들어, 미로 벽의 두께, 뱀의 두께, 음식의 높이와 너비, 뱀이 움직이는 증분을 정의하는 데 이를 사용합니다.
다음으로, 다음 게임플레이 변수를 추가합니다. 이러한 변수는 게임 상태를 추적하는 데 사용됩니다.
// Game play variables let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
snake 변수는 현재 뱀이 차지하고 있는 위치를 추적합니다. 뱀은 유닛으로 구성되며, 각 유닛은 캔버스에서 한 자리를 차지합니다. 각 유닛이 차지하는 위치는 스네이크 배열에 저장됩니다. 위치는 좌표로 x 및 y 값을 갖습니다. 배열의 첫 번째 요소는 꼬리를 나타내고 마지막 요소는 머리를 나타냅니다.
뱀이 움직이면 요소를 배열의 끝으로 밀어넣습니다. 그러면 머리가 앞으로 움직일 것입니다. 또한 길이가 동일하게 유지되도록 배열에서 첫 번째 요소나 꼬리를 제거합니다.
음식 위치 변수는 x, y 좌표를 사용하여 음식의 현재 위치를 저장합니다. 방향 변수는 뱀이 움직이는 방향을 저장하는 반면, 충돌 변수는 충돌이 감지되면 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 좌표로 정의된 위치를 가져와 뱀 몸의 일부가 해당 위치에 있는지 확인합니다. 이는 음식을 추가할 수 있는 자유로운 위치를 찾을 때 유용합니다.
이동 기능은 뱀이 가리키는 방향으로 뱀을 이동시키는 반면 회전 기능은 해당 방향을 변경합니다. 다음으로 onKeyDown 함수는 방향을 변경하는 데 사용되는 키 누르기를 수신합니다. gameLoop 기능은 뱀을 움직이고 충돌을 확인합니다.
기능 정의
이번 섹션에서는 앞서 선언한 함수를 정의하겠습니다. 또한 각 기능이 어떻게 작동하는지 논의하겠습니다. 코드 앞에는 함수에 대한 간단한 설명이 있고 필요한 경우 한 줄씩 설명하는 주석이 있습니다.
설정 기능
설정 기능은 다음 3가지 작업을 수행합니다.
따라서 해당 코드는 다음과 같습니다.
// Drawing borders on canvas // The canvas will be the size of the grid plus thickness of the two side border canvasSideLength = gridSize + unitLength * 2; // We draw a black square that covers the entire canvas ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // We erase the center of the black to create the game space // This leaves a black outline for the that represents the border ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Next, we will store the initial positions of the snake's head and tail // The initial length of the snake will be 60px or 6 units // The head of the snake will be 30 px or 3 units ahead of the midpoint const headPosition = Math.floor(gridSize / 2) + 30; // The tail of the snake will be 30 px or 3 units behind the midpoint const tailPosition = Math.floor(gridSize / 2) - 30; // Loop from tail to head in unitLength increments for (let i = tailPosition; i <= headPosition; i += unitLength) { // Store the position of the snake's body and drawing on the canvas snake.push({ x: i, y: Math.floor(gridSize / 2) }); // Draw a rectangle at that position of unitLength * unitLength ctx.fillRect(x, y, unitLength, unitLength); } // Generate food generateFood();
doesSnakeOccupyPosition
이 함수는 x 및 y 좌표를 위치로 사용합니다. 그런 다음 뱀의 몸에 그러한 위치가 있는지 확인합니다. JavaScript 배열 찾기 메소드를 사용하여 좌표가 일치하는 위치를 찾습니다.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
충돌 확인
이 함수는 뱀이 무엇과 충돌했는지 확인하고 충돌 변수를 true로 설정합니다. 먼저 왼쪽과 오른쪽 벽, 위쪽과 아래쪽 벽, 그리고 뱀 자체에 대한 충돌을 확인하는 것부터 시작하겠습니다.
왼쪽 및 오른쪽 벽에 대한 충돌을 확인하기 위해 뱀 머리의 x 좌표가 GridSize보다 크거나 0보다 작은지 확인합니다. 위쪽 및 아래쪽 벽에 대한 충돌을 확인하려면 동일한 확인을 수행하지만 y 좌표.
다음으로 뱀 자체와의 충돌을 확인하겠습니다. 신체의 다른 부분이 현재 머리가 차지하고 있는 위치를 차지하고 있는지 확인합니다. 이 모든 것을 결합하면 checkForCllision 함수의 본문은 다음과 같아야 합니다.
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Check for collisions against left and right walls if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Check for collisions against top and bottom walls if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Check for collisions against the snake itself const body = snake.slice(0, -2); if ( body.find( (position) => position.x == headPosition.x && position.y == headPosition.y ) ) { collided = true; } }
음식 생성
generateFood 함수는 do-while 루프를 사용하여 뱀이 차지하지 않는 음식을 놓을 위치를 찾습니다. 일단 발견되면 음식의 위치가 기록되어 캔버스에 그려집니다. generateFood 함수의 코드는 다음과 같습니다.
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); }
이동하다
이동 기능은 뱀 머리 위치의 복사본을 만드는 것으로 시작됩니다. 그런 다음 현재 방향을 기준으로 뱀의 x 또는 y 좌표 값을 늘리거나 줄입니다. 예를 들어 x 좌표를 늘리는 것은 오른쪽으로 이동하는 것과 같습니다.
이 작업이 완료되면 새 headPosition을 스네이크 배열에 푸시합니다. 또한 캔버스에 새로운 headPosition을 그립니다.
다음으로, 그 움직임에서 뱀이 음식을 먹었는지 확인합니다. headPosition이 foodPosition과 같은지 확인하여 이를 수행합니다. 뱀이 음식을 먹었다면 generateFood 함수를 호출합니다.
뱀이 음식을 먹지 않았다면 뱀 배열의 첫 번째 요소를 삭제합니다. 이 요소는 꼬리를 나타내며 이를 제거하면 뱀의 길이는 동일하게 유지되면서 움직이는 듯한 느낌을 줍니다.
function move() { // Create a copy of the object representing the position of the head 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; } // Add the new headPosition to the array snake.push(headPosition); ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength); // Check if snake is eating const isEating = foodPosition.x == headPosition.x && foodPosition.y == headPosition.y; if (isEating) { // Generate new food position generateFood(); } else { // Remove the tail if the snake is not eating tailPosition = snake.shift(); // Remove tail from grid ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
회전하다
마지막으로 다룰 주요 기능은 회전 기능입니다. 이 함수는 새로운 방향을 취하고 방향 변수를 새로운 방향으로 변경합니다. 그러나 뱀은 현재 움직이고 있는 방향과 수직인 방향으로만 회전할 수 있습니다.
따라서 뱀은 위쪽이나 아래쪽으로 움직일 때만 왼쪽이나 오른쪽으로 회전할 수 있습니다. 반대로 왼쪽이나 오른쪽으로 움직일 때만 위아래로 움직일 수 있습니다. 이러한 제약 조건을 염두에 두고 회전 기능은 다음과 같습니다.
function turn(newDirection) { switch (newDirection) { case "left": case "right": // Only allow turning left or right if they were originally moving up or down if (direction == "up" || direction == "down") { direction = newDirection; } break; case "up": case "down": // Only allow turning up or down if they were originally moving left or right if (direction == "left" || direction == "right") { direction = newDirection; } break; } }
onKeyDown
onKeyDown 함수는 누른 화살표 키에 해당하는 방향으로 회전 함수를 호출하는 이벤트 핸들러입니다. 따라서 함수는 다음과 같습니다.
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 함수를 사용하여 타이머를 시작합니다.
결론
이 시점에서 JavaScript 파일은 내 파일과 같아야 합니다. GitHub. 문제가 해결되지 않으면 저장소를 다시 확인하세요. 다음으로 JavaScript로 이미지 슬라이더를 만드는 방법을 배우고 싶을 수도 있습니다.