Игра быстро показывает, понимаешь ли ты JavaScript на практике. Квадрат должен двигаться, но упирается в стену. Ключ меняет состояние мира, а дверь проверяет это состояние и решает, пропустить игрока или нет. Здесь переменные, функции, условия и массивы перестают быть отдельными темами из учебника: они начинают работать вместе. В этой статье мы разберём весь путь и соберём двухуровневую игру «Побег из лабиринта» на чистом JavaScript и Canvas.
Этот проект хорошо показывает, как устроены браузерные игры на практике: цикл кадра, состояние, рендеринг и правила.
Игра «Побег из лабиринта» без библиотек: два уровня, плавное движение, стены, ключ, закрытая дверь, победа, рестарт, управление стрелками, WASD и экранными кнопками.
Сначала попробуйте готовую игру
Кликните внутри поля, чтобы игра получила фокус, затем двигайте светлый квадрат стрелками или клавишами WASD. Выход отмечен дверью, но сразу пройти через неё нельзя. Сначала придётся найти ключ в одном из ответвлений лабиринта и вернуться к выходу. После первой двери загрузится новая карта, поэтому состояние уровня действительно меняется, а не просто появляется надпись о победе. На телефоне под полем есть экранный крест управления.
| Что видно в игре | Какая часть JavaScript за это отвечает |
|---|---|
| Квадрат едет по коридору | События клавиатуры, скорость, координаты и deltaTime |
| Стены не пропускают игрока | Проверка тайлов карты и столкновений прямоугольников |
| После ключа открывается дверь | Объект состояния, условия и изменение данных уровня |
| Загружается следующая карта | Массив уровней и функция инициализации |
Можно ли создавать игры на JavaScript
Да, и браузер для этого подходит лучше, чем кажется. JavaScript получает ввод с клавиатуры, мыши, сенсорного экрана и геймпада, меняет состояние игры и рисует следующий кадр через Canvas, SVG, DOM или WebGL. Игроку не нужно устанавливать клиент: достаточно открыть ссылку. Такой формат удобен для головоломок, карточных игр, платформеров, небольших стратегий, образовательных проектов и игровых рекламных механик. Ограничение не в самом языке, а в размере проекта, требованиях к графике и количестве систем, которые команда готова поддерживать.
Первую 2D-игру полезно написать без движка. Так видно, откуда берутся координаты, почему объект движется с разной скоростью на разных экранах и как проверяется стена. После этого Phaser уже не выглядит набором магических методов: он просто берёт повторяющиеся задачи на себя. Для крупного 3D-проекта, сложной физики или редактора уровней чистый Canvas быстро станет тесным. Но для лабиринта он даёт ровно тот уровень контроля, который нужен для обучения.
Книга по JavaScript для начинающих
«JavaScript с нуля: учимся программировать, создавая игры»Пошаговый формат с маленькими практическими задачами: от базового синтаксиса до первых игровых механик без перегруза теорией.
Читать книгу на ЛитРес →Что выбрать: DOM, Canvas или игровой движок
У JavaScript нет одного «правильного» способа рисовать игру. Выбор зависит от того, сколько объектов меняется на экране и какие инструменты понадобятся через неделю, а не только в первом прототипе. Крестики-нолики удобно собрать из HTML-кнопок, потому что браузер уже умеет работать с фокусом и текстом. Аркаду с постоянно движущимися объектами проще рисовать на Canvas. Если нужны сцены, загрузчик ресурсов, камера, анимации и физика, выгоднее начать с движка.
| Подход | Когда подходит | Что придётся делать самому |
|---|---|---|
| DOM + CSS | Карточные, пошаговые и интерфейсные игры | Игровые правила и состояние; доступность и раскладку браузер уже частично решает |
| Canvas 2D | Лабиринты, аркады, платформеры и частицы | Отрисовку, коллизии, камеру, сцены и работу с ресурсами |
| PixiJS | Визуально насыщенный 2D и быстрый рендеринг | Игровую архитектуру, физику и смену состояний: PixiJS прежде всего рендерер |
| Phaser 4 | Полноценные 2D-игры, где нужны сцены, физика, камера и ассеты | Правила своей игры; базовая инфраструктура уже есть |
| Three.js / Babylon.js | 3D-сцены и браузерные 3D-игры | В Three.js больше игровых систем собирается вручную; Babylon.js даёт более цельный набор |
Наш лабиринт будет на Canvas 2D без зависимостей. Это не попытка доказать, что движки не нужны. Наоборот, мы сначала разберём задачи, которые движок позже автоматизирует: цикл, ввод, карту, столкновения и переход между уровнями. Код останется достаточно коротким, чтобы его можно было прочитать целиком. При этом это будет настоящая игра, а не квадрат, который бесконечно летит вправо.
Что нужно знать до старта
Достаточно понимать переменные, функции, массивы, объекты и условные операторы. Классы не обязательны: в учебной игре простые объекты показывают состояние яснее и не прячут логику за большим количеством файлов. Из инструментов нужен современный браузер и любой редактор текста — Cursor, VS Code, Sublime Text или другой привычный вариант. Node.js для самой игры не требуется, потому что JavaScript выполняется в браузере. Локальный сервер пригодится только для удобного запуска и будущей загрузки изображений или модулей.
Если вы умеете прочитать объект и написать условие, начинайте. Незнакомые методы проще разобрать в месте, где от них зависит видимый результат.
Как устроен проект «Побег из лабиринта»
Для готовой версии хватит трёх файлов. HTML создаёт оболочку, панель статуса, Canvas и кнопки управления. CSS отвечает за размер игрового поля и мобильную раскладку. В game.js находятся карты, состояние, ввод, обновление и отрисовка. Такое разделение не перегружает первый проект, но уже не позволяет стилям и игровой логике смешаться в одном длинном документе.
maze-game/
├── game.html
├── style.css
└── game.js
Скачать ничего не нужно: готовые файлы этой статьи открываются прямо в браузере. Чтобы повторить проект локально, создайте папку, положите в неё три файла и запустите простой HTTP-сервер. В редакторах обычно есть расширение Live Server. Подойдёт и команда python3 -m http.server 8000, если Python уже установлен. После этого игра откроется по адресу http://localhost:8000.
Минимальная HTML-разметка
У Canvas есть два размера: внутреннее разрешение растрового буфера и видимый размер в CSS. Если менять только CSS, браузер растянет старую картинку, и линии станут размытыми. Поэтому внутренний размер мы настроим из JavaScript с учётом плотности экрана. В разметке оставим сам холст, статус и доступную текстовую подсказку. Кнопки мобильного управления будут обычными HTML-элементами, а не нарисованными зонами внутри Canvas.
<main class="maze" data-maze-game>
<header class="maze__header">
<button type="button" data-restart-level>Начать уровень заново</button>
</header>
<section class="maze__panel" aria-label="Состояние игры">
<strong data-level>Уровень 1 из 2</strong>
<span data-message aria-live="polite">Найдите ключ</span>
</section>
<div class="maze__stage">
<canvas data-canvas tabindex="0" aria-label="Игровое поле лабиринта"></canvas>
</div>
<div class="maze__controls" aria-label="Экранное управление">
<button type="button" data-direction="up" aria-label="Двигаться вверх">↑</button>
<button type="button" data-direction="left" aria-label="Двигаться влево">←</button>
<button type="button" data-direction="down" aria-label="Двигаться вниз">↓</button>
<button type="button" data-direction="right" aria-label="Двигаться вправо">→</button>
</div>
</main>
<script src="game.js" defer></script>
Canvas без магии: координаты и пиксели
Точка 0, 0 находится в левом верхнем углу Canvas. Координата x растёт вправо, а y — вниз. Прямоугольник игрока описывается четырьмя числами: позицией x, y, шириной и высотой. Метод fillRect(x, y, width, height) рисует его за один вызов. Когда координаты меняются и кадр рисуется заново, глаз воспринимает последовательность картинок как движение.
На обычном экране один CSS-пиксель часто совпадает с одним пикселем буфера. На HiDPI-экране физическая плотность выше, поэтому такой Canvas выглядит мягким. Значение window.devicePixelRatio показывает, во сколько раз стоит увеличить буфер. После увеличения мы масштабируем контекст и продолжаем рисовать в удобных логических координатах. Ограничение коэффициента до двух защищает слабые телефоны от лишней работы.
function resizeCanvas() {
if (!game.map) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(game.map.pixelWidth * dpr);
canvas.height = Math.round(game.map.pixelHeight * dpr);
canvas.style.aspectRatio = `${game.map.pixelWidth} / ${game.map.pixelHeight}`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
Интерактивный пример: координаты и управление
В этом поле ещё нет стен и цели. Оно изолирует одну задачу: получить ввод, изменить координаты и нарисовать квадрат на новом месте. Кликните по демо и нажмите стрелку или WASD. Счётчик покажет текущие x и y, поэтому связь между клавишей и числами видна сразу. Если удерживать две клавиши, квадрат пойдёт по диагонали с той же общей скоростью, а не ускорится в полтора раза.
Обработчик клавиатуры не должен сам двигать игрока. Он только запоминает, какие кнопки сейчас нажаты. Функция обновления читает это состояние на каждом кадре и вычисляет новое положение. Благодаря этому две кнопки работают одновременно, а удержание не зависит от частоты системного автоповтора клавиатуры. При потере фокуса набор очищается, иначе персонаж мог бы продолжить движение после переключения вкладки.
Уровень как данные, а не сотня вызовов рисования
Лабиринт удобно хранить массивом строк. Каждый символ описывает одну клетку: решётка означает стену, точка — пол, P — старт игрока, K — ключ, D — дверь. Так карту можно читать прямо в коде и менять без редактора уровней. Вторая карта имеет тот же формат, поэтому движок не знает, какой уровень сейчас рисует. Он получает данные и выполняет одинаковые правила.
| Символ | Объект | Поведение |
|---|---|---|
# | Стена | Не пропускает игрока |
. | Пол | Свободная клетка |
P | Старт | Начальная позиция игрока |
K | Ключ | Меняет hasKey на true |
D | Дверь | Пропускает только после ключа |
const LEVELS = [
[
"#########################",
"#P....#.............#...#",
"#####.#.###########.#.#.#",
"#.....#.....#.......#.#.#",
"#.#########.#.#######.#.#",
"#.........#.#.........#.#",
"#.#######.#.###########.#",
"#.#.....#.#.........#...#",
"#.#.###.#.#########.#.###",
"#...#...#.....#.....#...#",
"###.#.#######.#.#######.#",
"#...#.......#.#.......#K#",
"#.#########.#.#######.#.#",
"#...........#.........#D#",
"#########################"
],
// Вторая карта использует те же символы.
];
Карты должны быть прямоугольными: каждая строка содержит одинаковое число символов. В каждой карте нужен один старт, один ключ и одна дверь. Проверка этих условий при загрузке экономит время, потому что опечатка превращается в понятную ошибку, а не в невидимую стену. Парсер проходит по строкам, находит специальные клетки и сохраняет их координаты. После этого символы P, K и D можно считать полом, а объекты рисовать отдельными функциями.
function parseLevel(rows) {
const width = rows[0].length;
const cells = rows.map((row) => row.split(""));
const points = {};
rows.forEach((row, y) => {
if (row.length !== width) throw new Error("Карта должна быть прямоугольной");
[...row].forEach((cell, x) => {
if (cell === "P") points.player = { x, y };
if (cell === "K") points.key = { x, y };
if (cell === "D") points.door = { x, y };
if (cell === "P" || cell === "K" || cell === "D") cells[y][x] = ".";
});
});
if (!points.player || !points.key || !points.door) {
throw new Error("На карте нужны P, K и D");
}
return { cells, width, height: rows.length, ...points };
}
Состояние игры: что нужно помнить между кадрами
Canvas ничего не хранит. После очистки он не знает, где был квадрат и открыт ли выход. Эти сведения живут в обычном объекте JavaScript, который называют состоянием игры. Отрисовка только читает его и показывает текущий результат. Такой порядок позволяет отдельно проверять правила и отдельно менять внешний вид.
const game = {
levelIndex: 0,
map: null,
player: {
x: 0,
y: 0,
size: 16,
speed: 148
},
hasKey: false,
mode: "playing",
message: "Найдите ключ"
};
Поле levelIndex выбирает карту из массива LEVELS. В map лежит результат парсинга текущего уровня. Координаты игрока измеряются в пикселях, потому что движение плавное, а точки ключа и двери остаются привязанными к клеткам. Флаг hasKey отвечает на один вопрос и не пытается хранить весь инвентарь. Режим mode позволяет остановить правила после победы, не останавливая отрисовку интерфейса.
Управление стрелками и WASD
Для управления используем event.code, а не устаревший keyCode. Значение code описывает физическую кнопку, поэтому KeyW продолжает работать при русской раскладке. Стрелки и WASD сводятся к четырём внутренним направлениям. Событие keydown добавляет направление в набор, а keyup удаляет. Прокрутку страницы блокируем только для управляющих клавиш и только внутри сфокусированной игры.
const pressed = new Set();
const codeToDirection = {
ArrowUp: "up", KeyW: "up",
ArrowDown: "down", KeyS: "down",
ArrowLeft: "left", KeyA: "left",
ArrowRight: "right", KeyD: "right"
};
window.addEventListener("keydown", (event) => {
const direction = codeToDirection[event.code];
if (!direction || !game.hasFocus || game.mode !== "playing") return;
event.preventDefault();
pressed.add(direction);
});
window.addEventListener("keyup", (event) => {
const direction = codeToDirection[event.code];
if (direction) pressed.delete(direction);
});
window.addEventListener("blur", () => setGameFocus(false));
Набор Set удобен тем, что одна клавиша не может появиться в нём дважды. В функции обновления проверяем обе клавиши каждого направления. Если игрок одновременно держит вверх и вправо, получаем диагональный вектор. Перед умножением на скорость его нужно нормализовать, иначе диагональ окажется быстрее прямого движения. Это мелкая деталь, но она сразу отделяет управляемое движение от случайно работающего примера.
Игровой цикл: input, update, render
Игровой цикл повторяет три действия. Сначала код читает ввод, затем меняет состояние и после этого рисует новый кадр. Браузер вызывает функцию, переданную в requestAnimationFrame, перед очередной перерисовкой страницы. Частота не обязана быть равна 60: экран может работать на 90, 120 или 144 Гц, а вкладка может временно остановиться. Поэтому скорость нельзя задавать фразой «прибавь три пикселя за кадр».
Аргумент timestamp показывает время текущего кадра. Разность с предыдущим кадром даёт deltaTime, то есть длительность шага. Мы переводим миллисекунды в секунды и ограничиваем значение сверху. Ограничение нужно после возвращения во вкладку: персонаж не должен телепортироваться через стену из-за одного огромного кадра. Скорость в таком коде читается как «150 пикселей в секунду».
let previousTime = performance.now();
function loop(time) {
const deltaTime = Math.min((time - previousTime) / 1000, 0.05);
previousTime = time;
if (game.mode === "playing") {
update(deltaTime);
}
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
В клеточном лабиринте можно было двигаться ровно на одну клетку по нажатию. Мы выбрали плавное движение, чтобы на одном примере показать deltaTime и настоящую проверку стен. Карта при этом всё равно остаётся тайловой: стены легко редактировать строками. Сочетание непрерывных координат игрока и дискретной карты встречается во многих 2D-играх. Следующая задача — связать эти две системы без перебора каждой стены на каждом кадре.
Столкновения со стенами
Проверять весь лабиринт не нужно. По границам прямоугольника игрока можно вычислить несколько клеток, которых он касается сейчас. Если хотя бы одна из них содержит #, новое положение запрещено. Движение по X и Y проверяем отдельно: сначала пробуем горизонтальный шаг, затем вертикальный. Благодаря этому квадрат скользит вдоль стены, а не замирает при касании угла.
function obstacleAt(x, y) {
const inset = 2;
const left = Math.floor((x + inset) / CELL);
const right = Math.floor((x + game.player.size - inset) / CELL);
const top = Math.floor((y + inset) / CELL);
const bottom = Math.floor((y + game.player.size - inset) / CELL);
const corners = [
[left, top],
[right, top],
[left, bottom],
[right, bottom]
];
for (const [tileX, tileY] of corners) {
if (isWall(tileX, tileY)) return "wall";
if (!game.hasKey && isDoor(tileX, tileY)) return "door";
}
return "";
}
function movePlayer(dx, dy) {
const nextX = game.player.x + dx;
if (!obstacleAt(nextX, game.player.y)) game.player.x = nextX;
const nextY = game.player.y + dy;
if (!obstacleAt(game.player.x, nextY)) game.player.y = nextY;
}
Функция isWall должна считать стеной и выход за границу карты. Тогда отдельные проверки краёв Canvas не нужны: внешний мир просто состоит из непроходимых клеток. Небольшой inset уменьшает прямоугольник коллизии на пару пикселей и не даёт игроку цепляться за углы из-за округления. Для скоростного платформера такой проверки может быть мало, потому что объект способен перескочить тонкую стену за один шаг. В нашем лабиринте скорость ограничена, а deltaTime зажат, поэтому дискретная проверка работает предсказуемо.
Ключ, дверь и переход между уровнями
После движения смотрим, с какой специальной клеткой пересекается игрок. Контакт с ключом устанавливает hasKey = true и убирает предмет с карты. Дверь без ключа остаётся стеной и выводит короткую подсказку. Дверь с ключом вызывает загрузку следующего уровня. Если текущая карта последняя, режим меняется на won, а поверх поля появляется финальный экран.
function handleObjects() {
if (!game.hasKey && overlapsTile(game.map.key, 3)) {
game.hasKey = true;
setMessage("Ключ найден. Теперь ищите дверь");
updateStatus();
}
if (!overlapsTile(game.map.door, 3) || !game.hasKey) return;
if (game.levelIndex < LEVELS.length - 1) {
loadLevel(game.levelIndex + 1);
} else {
game.mode = "won";
setMessage("Лабиринт пройден");
overlay.hidden = false;
updateStatus();
}
}
Функция loadLevel должна полностью сбрасывать данные, относящиеся к карте. Она парсит новый массив строк, ставит игрока в стартовую клетку, возвращает hasKey в false и обновляет подпись уровня. Если забыть хотя бы один флаг, ключ с первой карты случайно откроет вторую дверь. Это типичная причина странных багов при смене сцен. Чёткая точка инициализации делает переход уровней обычной операцией, а не цепочкой исключений.
Состояния вместо случайных флагов
В маленькой игре легко добавить пять логических переменных: isStarted, isPaused, isWon, isLoading и isRestarting. Через несколько шагов некоторые из них начнут противоречить друг другу. Одно строковое поле mode не позволяет одновременно быть в режимах playing и won. Для большого проекта состояния обычно оформляют сценами или конечным автоматом. В лабиринте достаточно значений playing и won, но принцип уже виден.
Отрисовка карты и объектов
Функция render ничего не решает за игрока. Она очищает Canvas, рисует фон, стены, дверь, ключ, персонажа и интерфейс победы в фиксированном порядке. Если ключ уже собран, его просто не рисуют. Цвет двери зависит от hasKey, поэтому изменение состояния сразу видно без отдельной анимационной системы. Вся графика состоит из примитивов Canvas, и внешние изображения загружать не требуется.
function render() {
if (!game.map) return;
drawFloor();
drawWalls();
drawDoor();
if (!game.hasKey) drawKey();
drawPlayer();
}
Порядок важен: объект, нарисованный последним, окажется сверху. Для сложной сцены добавляют слои — фон, мир, эффекты и интерфейс. Здесь отдельные функции уже выполняют роль маленьких слоёв. Метод save() перед временной настройкой тени или прозрачности и restore() после неё не дают стилям одного объекта испортить следующий. Это особенно заметно у свечения ключа и затемнения финального экрана.
Управление на телефоне и доступность
У телефона нет WASD, поэтому под Canvas находятся четыре кнопки. События pointerdown и pointerup добавляют и удаляют те же внутренние направления, что клавиатура. Игровая логика не знает, откуда пришла команда. Свойство touch-action: none применяется только к кнопкам, а не ко всей странице, поэтому вертикальная прокрутка статьи остаётся нормальной. Отмена указателя и потеря фокуса тоже очищают направление, чтобы квадрат не ехал сам.
Canvas остаётся растровой картинкой и плохо передаёт смысл скринридеру. Поэтому номер уровня и сообщения о ключе или двери выводятся обычным HTML рядом с полем. Область сообщений имеет aria-live="polite", а кнопки получают понятные подписи. Клавиатурный фокус выделяется рамкой. Эти детали не превращают графическую игру в полностью доступную, но не оставляют управление и результат невидимыми за пределами картинки.
Как проверить игру, а не только посмотреть на неё
Рабочий первый запуск ещё не доказывает, что правила собраны правильно. Попробуйте пройти к двери без ключа и убедитесь, что она не пропускает. Затем возьмите ключ, вернитесь тем же путём и перейдите на второй уровень. Проверьте удержание двух клавиш, потерю фокуса вкладки и рестарт после победы. На телефоне нажмите направление, уведите палец за пределы кнопки и убедитесь, что движение прекращается.
| Симптом | Частая причина | Что проверить |
|---|---|---|
| Canvas пустой | Скрипт запущен до появления разметки или не найден контекст | defer, селектор Canvas и ошибки в консоли |
| Игра быстрее на другом мониторе | Скорость задана в пикселях за кадр | Умножение скорости на deltaTime |
| Игрок проходит сквозь стену | Слишком большой шаг или проверка только центра | Ограничение deltaTime и четыре угла коллизии |
| После смены уровня дверь уже открыта | Старое состояние не сброшено | hasKey = false внутри loadLevel |
| Стрелки прокручивают страницу | Игра не получила фокус или нет точечного preventDefault |
Клик по полю и обработку только управляющих клавиш |
| Квадрат продолжает ехать | Не очищен набор клавиш | Обработчики blur, pointercancel и visibilitychange |
Почему код разделён на update и render
Соблазнительно двигать игрока прямо в drawPlayer. Тогда одна функция сразу даёт видимый результат, но состояние начинает зависеть от способа отображения. Снимок для мини-карты или дополнительный рендер внезапно сдвинет персонажа второй раз. В чистой схеме update меняет числа, а render только читает их. Поэтому графику можно заменить, не переписывая правила ключа и двери.
Такое разделение упрощает отладку. Если игрок прошёл стену, проблему ищут в обновлении и коллизиях, а не среди градиентов. Если дверь стала неправильного цвета, состояние можно вывести в консоль и проверить отдельно от рисования. Этот принцип сохраняется и в движках: названия методов отличаются, но сцена всё равно обновляет мир и показывает его. Подробнее устройство цикла разобрано в материале «Игровой цикл на JavaScript».
Когда переходить с Canvas на Phaser
Чистый Canvas удобен, пока список собственных систем остаётся коротким. Когда появляются десятки спрайтов, несколько сцен, камера, физика, звук, загрузочный экран и редактор тайлов, вы начинаете писать маленький движок. В этот момент Phaser экономит время: он уже решает загрузку ресурсов, жизненный цикл сцен, ввод, камеры, анимации и типовые варианты физики. Знание текущего проекта не пропадёт, потому что карта, состояние, коллизии и разделение update/render останутся знакомыми. Поменяется уровень готовых инструментов.
Переходить стоит не после первой работающей функции, а после законченного маленького проекта. Иначе трудно понять, какую проблему решает новый метод движка. Лабиринт можно перенести в Phaser по частям: карту отдать tilemap-системе, игрока сделать игровым объектом, а уровни — отдельными сценами или данными сцены. Начальный маршрут есть в статье «Phaser JS для новичков». Для тренировки чистого Canvas продолжите с разбором движения персонажа и границ экрана.
Как опубликовать браузерную игру
Готовая версия состоит из статических файлов, поэтому ей не нужен отдельный серверный язык. Папку можно разместить на GitHub Pages, Cloudflare Pages, Netlify, обычном хостинге или внутри существующего сайта. Важно сохранить относительные пути между HTML, CSS, JavaScript и будущими изображениями. После загрузки откройте игру в приватном окне и проверьте консоль: локальный браузерный кэш иногда скрывает пропавшие файлы. Для игровых платформ позже понадобятся их SDK, правила загрузки и обработка рекламы, но ядро лабиринта останется тем же.
Откройте HTML, CSS и JavaScript во встроенном просмотрщике. Любой файл можно скопировать или скачать отдельно.
Куда развивать лабиринт дальше
Сначала добавьте одну механику, которая использует уже существующее состояние. Например, счётчик шагов потребует увеличивать число при движении и рисовать его в HTML-панели. Затем попробуйте несколько ключей разных цветов и двери, которые проверяют конкретный предмет. После этого можно ввести движущегося врага, но не начинать с поиска пути: пусть он ходит по заданному маршруту. Каждый такой шаг усложняет одно место и оставляет остальную архитектуру понятной.
Следующий полезный этап — вынести карты в отдельный JSON-файл и добавить проверку их корректности. Потом можно загрузить спрайты вместо геометрических фигур, подключить звук после первого пользовательского действия и сохранить пройденный уровень в localStorage. Если количество объектов вырастет, измерьте производительность до оптимизации. Пулы объектов, Web Workers и сложная физика не нужны лабиринту только потому, что встречаются в статьях о больших играх. Хорошая следующая версия решает заметную игроку задачу, а не демонстрирует ещё один термин.
Частые вопросы
Можно ли сделать игру на JavaScript без движка?
Да. Для небольшой 2D-игры достаточно Canvas и браузерных событий. Придётся самостоятельно написать цикл, ввод, отрисовку, столкновения и переходы состояний, зато каждая часть будет видна. Для обучения это преимущество. Для крупного проекта те же задачи выгоднее отдать Phaser или другому движку.
Нужно ли сначала полностью выучить JavaScript?
Нет. Нужна база: переменные, функции, условия, массивы и объекты. Игра связывает их в одну работающую систему и показывает, какие пробелы мешают прямо сейчас. Незнакомый API Canvas можно изучать по мере появления задачи. Полное знание языка не наступает до практики.
Почему используется Canvas, а не HTML-элементы?
Лабиринт постоянно перерисовывает карту и движущийся объект, поэтому Canvas здесь естественнее. Для шахмат, викторины или карточной игры DOM мог бы быть удобнее из-за готовых кнопок, текста и фокуса. Производительность не стоит сравнивать без конкретной сцены и измерений. Выбор определяется структурой игры, а не правилом «Canvas всегда быстрее».
Зачем нужен requestAnimationFrame?
Он просит браузер вызвать функцию перед следующим визуальным обновлением. Это связывает отрисовку с жизненным циклом страницы и обычно экономит ресурсы в неактивной вкладке. Метод не обещает ровно 60 кадров в секунду, поэтому движение считается через время между кадрами. Для пошаговой игры цикл может быть проще, но для плавного движения он подходит хорошо.
Можно ли добавить третий уровень?
Да. Добавьте ещё один массив строк в LEVELS с теми же символами и одинаковой длиной строк внутри карты. Код перехода уже сравнивает текущий индекс с длиной массива. Убедитесь, что у новой карты есть один старт, ключ и дверь. После этого движок загрузит её без отдельной функции.
Подойдёт ли этот код для мобильного браузера?
Да, готовая версия масштабирует Canvas и показывает экранные кнопки. Нужно учитывать, что маленький экран уменьшает клетки и делает длинный лабиринт визуально плотнее. Для самостоятельного мобильного релиза стоит увеличить кнопки, добавить полноэкранный режим и протестировать поворот устройства. Основные правила игры менять не потребуется.
Что вы теперь умеете
Создание игр на JavaScript начинается не с выбора самого мощного движка, а с понятной связи между вводом, состоянием и кадром. В нашем лабиринте карта хранится как данные, игрок движется независимо от частоты экрана, стены проверяются по тайлам, а ключ и дверь меняют ход игры через явные условия. Второй уровень загружается тем же кодом, поэтому проект уже можно расширять, а не переписывать. Сохраните материалы, измените одну карту и добавьте собственную механику. Именно в этот момент пример перестаёт быть чужим листингом и становится вашей игрой.
Книга по JavaScript для начинающих
«JavaScript с нуля: учимся программировать, создавая игры»Пошаговый формат с маленькими практическими задачами: от базового синтаксиса до первых игровых механик без перегруза теорией.
Читать книгу на ЛитРес →