У цьому матеріалі я розкрию процес розробки гри “Змійка” за допомогою HTML, CSS та JavaScript.
Ми обійдемося без додаткових бібліотек; гра функціонуватиме безпосередньо у веб-браузері. Створення цієї гри є чудовою вправою, що допоможе вам покращити навички вирішення задач.
Структура проєкту
“Змійка” – це нескладна гра, де ви керуєте рухом змії, щоб вона дісталася до їжі, уникаючи перешкод. Коли змія досягає їжі, вона її з’їдає і збільшується в розмірі. З часом змія стає дедалі довшою.
Змія не повинна натрапляти на межі ігрового поля або на саму себе. Тому, по мірі проходження гри, змія стає більшою, і гра стає складнішою.
Мета цієї інструкції по створенню “Змійки” на JavaScript полягає у відтворенні наступної гри:
Код гри розміщено на моєму GitHub. Активну версію можна знайти на GitHub Pages.
Необхідні знання
Ми створимо цей проєкт використовуючи HTML, CSS та JavaScript. Ми застосуємо тільки базові HTML і CSS. Головний акцент буде на JavaScript. Отже, ви повинні вже мати уявлення про це, щоб слідкувати за цим посібником зі створення “Змійки” на JavaScript. Якщо ні, настійно рекомендую ознайомитися з нашим матеріалом про найкращі ресурси для вивчення JavaScript.
Крім того, вам потрібен редактор коду для написання програмного коду. І, звичайно, веб-браузер, який ви, ймовірно, вже маєте, якщо ви читаєте цей текст.
Налаштування проєкту
Насамперед налаштуємо файли проєкту. У порожній теці створіть файл index.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>Змійка</title> </head> <body> <div id="game-over-screen"> <h1>Гра закінчена</h1> </div> <canvas id="canvas" width="420" height="420"> </canvas> <script src="./snake.js"></script> </body> </html>
Ця розмітка створює початковий екран “Гра закінчена”. Ми будемо керувати його видимістю за допомогою JavaScript. Також визначається елемент canvas, де ми будемо відображати ігрове поле, змію та їжу. Розмітка підключає файл стилів та 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. Для тега body ми встановили висоту на повну висоту екрана та вирівняли всі елементи по центру. Колір тла встановлено на світло-блакитний.
Нарешті, ми відредагували стиль екрана “Гра закінчена”, встановивши його висоту та ширину 200 і 500 пікселів відповідно. Також надали йому фіолетовий колір фону та чорну рамку. Ми встановили для нього абсолютне позиціонування, щоб він знаходився поза основним потоком документа і був вирівняний по центру екрана. Далі ми вирівняли його вміст. Для його відображення встановлено значення “none”, тож він прихований за замовчуванням.
Наступним кроком буде створення файлу snake.js, код якого ми напишемо в наступних розділах.
Створення глобальних змінних
Наступним кроком у цьому посібнику зі створення “Змійки” на JavaScript є оголошення глобальних змінних, які ми будемо використовувати. У файлі snake.js додайте наступні визначення змінних у верхній частині:
// Отримання посилань на HTML елементи let gameOverScreen = document.getElementById("game-over-screen"); let canvas = document.getElementById("canvas"); // Створення контексту, який використовуватиметься для малювання на полотні let ctx = canvas.getContext("2d");
Ці змінні зберігають посилання на екран “Гра закінчена” та елемент canvas. Далі ми створили контекст, який буде використовуватися для малювання на полотні.
Потім додайте ці визначення змінних під першим набором.
// Визначення розмірів ігрового поля let gridSize = 400; let unitLength = 10;
Перша змінна задає розмір ігрового поля у пікселях. Друга визначає довжину однієї клітинки в грі. Ця довжина буде застосовуватися в кількох місцях. Наприклад, ми будемо її використовувати для визначення товщини стін ігрового поля, товщини змії, висоти та ширини їжі, а також кроків, на які переміщається змія.
Далі додайте наступні змінні ігрового процесу. Ці змінні використовуються для відстеження стану гри.
// Змінні ігрового процесу let snake = []; let foodPosition = { x: 0, y: 0 }; let direction = "right"; let collided = false;
Змінна “snake” відслідковує позиції, які наразі займає змія. Змія складається з окремих клітинок, кожна з яких займає певну позицію на полотні. Позиція, яку займає кожна клітинка, зберігається у масиві “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”
Функція “setUp” виконує 3 дії:
Тому код для цього виглядатиме так:
// Малювання меж ігрового поля на полотні // Полотно буде розміром з ігрове поле плюс товщина двох бічних меж canvasSideLength = gridSize + unitLength * 2; // Малюємо чорний квадрат, що покриває все полотно ctx.fillRect(0, 0, canvasSideLength, canvasSideLength); // Видаляємо середину чорного, щоб створити ігровий простір // Залишається чорний контур, що представляє собою межу ctx.clearRect(unitLength, unitLength, gridSize, gridSize); // Далі зберігаємо початкові позиції голови та хвоста змії // Початкова довжина змії буде 60px або 6 клітинок // Голова змії буде на 30 пікселів або 3 клітинки попереду середини const headPosition = Math.floor(gridSize / 2) + 30; // Хвіст змії буде на 30 пікселів або 3 клітинки позаду середини 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) }); // Малюємо прямокутник у цій позиції розміром unitLength * unitLength ctx.fillRect(x, y, unitLength, unitLength); } // Створюємо їжу generateFood();
Функція “doesSnakeOccupyPosition”
Ця функція приймає координати x та y як позицію. Потім вона перевіряє, чи існує така позиція в тілі змії. Вона використовує метод пошуку масиву JavaScript для пошуку позиції з відповідними координатами.
function doesSnakeOccupyPosition(x, y) { return !!snake.find((position) => { return position.x == x && y == foodPosition.y; }); }
Функція “checkForCollision”
Ця функція перевіряє, чи не зіткнулася змія з чимось, і встановлює змінну “collided” у значення “true”. Ми починаємо з перевірки зіткнень з лівою та правою стінками, верхньою та нижньою стінками, а потім із самою змією.
Щоб перевірити наявність зіткнень з лівою та правою стінами, ми перевіряємо, чи координата x голови змії більша за “gridSize” або менша за 0. Щоб перевірити наявність зіткнень з верхньою та нижньою стінами, ми виконуємо ту саму перевірку, але з y-координатою.
Далі ми перевіримо наявність зіткнень з самою змією; ми перевіримо, чи якась інша частина його тіла займає положення, яке зараз займає голова. Поєднуючи все це, тіло функції “checkForCllision” має виглядати так:
function checkForCollision() { const headPosition = snake.slice(-1)[0]; // Перевірка на зіткнення з лівою та правою стінами if (headPosition.x < 0 || headPosition.x >= gridSize - 1) { collided = true; } // Перевірка на зіткнення з верхньою та нижньою стінами if (headPosition.y < 0 || headPosition.y >= gridSize - 1) { collided = true; } // Перевірка на зіткнення з самою змією 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); }
Функція “move”
Функція “move” починає з створення копії позиції голови змії. Потім, в залежності від поточного напрямку, вона збільшує або зменшує значення координати x або y змійки. Наприклад, збільшення координати x еквівалентно переміщенню вправо.
Коли це зроблено, ми додаємо нову “headPosition” до масиву “snake”. Також малюємо нову позицію голови на полотні.
Далі перевіряємо, чи з’їла змія їжу під час цього кроку. Це робиться шляхом порівняння “headPosition” з “foodPosition”. Якщо змія з’їла їжу, викликаємо функцію “generateFood”.
Якщо змія не з’їла їжу, ми видаляємо перший елемент масиву “snake”. Цей елемент символізує хвіст, і його видалення зберігає довжину змії незмінною, створюючи ілюзію руху.
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; } // Додаємо нову "headPosition" до масиву 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 { // Видалення хвоста, якщо змія не їсть tailPosition = snake.shift(); // Видалення хвоста з ігрового поля ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength); } }
Функція “turn”
Остання головна функція, яку ми розглянемо, це функція “turn”. Ця функція приймає новий напрямок і змінює значення змінної “direction” на цей новий напрямок. Однак змія може повертатися лише в напрямку, перпендикулярному до того, в якому вона зараз рухається.
Тому змія може повертатися вліво або вправо лише тоді, коли рухається вгору або вниз. І навпаки, вона може повертатися вгору або вниз лише тоді, коли рухається вліво або вправо. З урахуванням цих обмежень, функція “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”
Функція “gameLoop” буде регулярно викликатися для забезпечення роботи гри. Ця функція викличе функцію “move” та функцію “checkForCollision”. Вона також перевіряє, чи “collided” є “true”. Якщо так, то вона зупиняє таймер, який ми використовуємо для запуску гри, і відображає екран “Гра закінчена”. Функція буде виглядати так:
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.