Пояснення підручника про JavaScript Snake

У цій статті я поясню, як створити гру Snake за допомогою HTML, CSS і JavaScript.

Ми не будемо використовувати додаткові бібліотеки; гра буде працювати в браузері. Створення цієї гри — це весела вправа, яка допоможе вам розтягнути м’язи, які вирішують проблеми.

Схема проекту

Snake — це проста гра, у якій ви керуєте рухами змії до їжі, ухиляючись від перешкод. Коли змія дістається до їжі, вона її з’їдає і стає довшою. По ходу гри змія стає все більшою.

Змія не повинна натикатися на стіни або саму себе. Тому, у міру проходження гри, змія стає довшою, і грати стає все важче.

Метою цього підручника з JavaScript Snake є створення наступної гри:

Код гри доступний на моєму GitHub. Жива версія розміщена на Сторінки GitHub.

передумови

Ми створимо цей проект за допомогою HTML, CSS і JavaScript. Ми будемо писати лише базові HTML і CSS. Наша основна увага приділяється JavaScript. Таким чином, ви вже повинні це зрозуміти, щоб слідувати цьому підручнику з JavaScript Snake. Якщо ні, я настійно рекомендую вам переглянути нашу статтю про найкращі місця для вивчення 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 для стилів. Додайте до нього наступні стилі.

* {
    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;
}

У наборі правил «*» ми націлюємо всі елементи та скидаємо інтервали. Ми також встановлюємо сімейство шрифтів для кожного елемента та встановлюємо розмір елементів на більш передбачуваний метод розміру, який називається border-box. Для тіла ми встановили його висоту на повну висоту вікна перегляду та вирівняли всі елементи по центру. Ми також надали йому синій колір тла.

Нарешті, ми змінили стиль екрана «Гра закінчена», щоб надати йому висоту та ширину 200 та 500 пікселів відповідно. Ми також надали йому пурпуровий колір фону та чорну рамку. Ми встановлюємо його позицію як абсолютну, щоб вона була поза межами нормального потоку документів і була вирівняна по центру екрана. Потім ми центрували його зміст. Ми встановили для його відображення значення “none”, тому він прихований за замовчуванням.

Далі створіть файл 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");

Ці змінні зберігають посилання на екран «Гра закінчена» та елементи полотна. Далі ми створили контекст, який буде використовуватися для малювання на полотні.

Далі додайте ці визначення змінних під перший набір.

// 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;

Змінна змія відстежує позиції, які зараз займає змія. Змія складається з одиниць, і кожна одиниця займає певну позицію на полотні. Позиція, яку займає кожна одиниця, зберігається в масиві змійок. Позиція матиме значення x і y як координати. Перший елемент у масиві представляє хвіст, а останній – голову.

Під час руху змійки ми будемо штовхати елементи до кінця масиву. Це дозволить рухати голову вперед. Ми також видалимо перший елемент або хвіст з масиву, щоб довжина залишилася незмінною.

Змінна позиції їжі зберігає поточне розташування їжі за допомогою координат 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, і перевіряє, чи знаходиться будь-яка частина тіла змії в цьому положенні. Це стане в нагоді під час пошуку вільного місця для додавання їжі.

Функція переміщення переміщує змію в будь-якому напрямку, в якому вона вказує, а функція повороту змінює цей напрямок. Далі функція 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;
      });
    }

    checkForCollision

    Ця функція перевіряє, чи не зіткнулася змія з чимось, і встановлює для змінної collided значення true. Ми почнемо з перевірки зіткнень з лівою та правою стінками, верхньою та нижньою стінками, а потім із самою змією.

      Що таке інтерпретатор коду ChatGPT? Чому це так важливо?

    Щоб перевірити наявність зіткнень з лівою та правою стінами, ми перевіряємо, чи є координата 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

    Функція 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 еквівалентно переміщенню вправо.

      Як створити образ вашого USB-накопичувача

    Коли це буде зроблено, ми надсилаємо новий 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);
      }
    }

    поворот

    Остання основна функція, яку ми розглянемо, це функція turn. Ця функція прийме новий напрямок і змінить змінну direction на цей новий напрямок. Однак змія може повертатися лише в напрямку, перпендикулярному тому, у якому вона зараз рухається.

    Тому змія може повертатися вліво або вправо, лише якщо рухатися вгору або вниз. І навпаки, він може повертатися вгору або вниз, лише якщо рухатися вліво або вправо. З урахуванням цих обмежень функція turn виглядає так:

    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 — це обробник подій, який викликає функцію 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

    Функція gameLoop буде регулярно викликатися, щоб підтримувати роботу гри. Ця функція викличе функцію переміщення та функцію 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.