Как я написал «Обратную змейку» на чистом Canvas
- среда, 17 июня 2026 г. в 00:00:04

Начнём с небольшой предыстории: пару недель назад я ждал друга в кафе. Как обычно, он опаздывал примерно минут на сорок. Ноутбук с собой, интернет есть, а игр нет — чисто рабочая машина, ничего лишнего, хотя нужно будет что-то скачать, чтобы играть на работе xd. От скуки я открыл браузер и набрал в поиске «змейка». Google выдал свою фирменную змейку прямо в результатах поиска.
Я кликнул, и пошла игра. Потом прошло пять минут, десять, пятнадцать, и я не мог оторваться, так как немного азартный и, пока не выиграю, не захочу заканчивать. После многих поражений, примерно на двадцать пятой минуте игры, я поймал себя на мысли: а почему яблоко всегда жертва и почему вообще змейка — наш главный герой?
Друг пришёл на тридцать пятой минуте. Я закрыл ноутбук, но мысль засела. И тем же вечером я уже начал писать код: вот поле, вот красное яблоко, вот змейка, вот ещё одна, и все они пытаются съесть яблоко.
Так моё желание доказать, что яблоко не просто ждун, а тоже личность, пошло дальше, и я решил написать минимально простую игру: просто посмотреть логику и подумать, может, поменять правила или как-то усложнить игру.
За два плотных вечера проект вырос из прототипа в полноценную игру с тремя типами змей, интерфейсом для телефонов и компьютеров. В этой статье я расскажу обо всех этапах: от зелёного квадрата на чёрном фоне (отсылка на прошлую статью) до пиксельного яблока с листиком, которое отчаянно пытается выжить.
Начнём, думаю, с классики — как устроена обычная змейка и с чего все началось. Если говорить кратко про историю то, первая версия появилась аж в 1976 году под названием Blockade — её запилили ребята из Gremlin Industries для игровых автоматов. И самое удивительное, что для меня стало новостью: там никаких яблок вообще не было, и змея была не одна и вообще не росла.
Смысл игры был таким, что два игрока управляли змеями, которые оставляли за собой стену, и условием победы было просто загнать соперника в тупик. Если вспоминать аналоги, то мне сразу же напомнили эту игру события из фильма и одноимённой игры Tron Light Cycles. В конце концов всё оказалось ещё проще, чем в той игре, которую все знают: никакой еды и очков, просто стены и змейки. Потом, в 1978-м, игра перебралась на Atari 2600 под названием Surround — там уже добавили цвета и разные режимы, но суть та же: стены и тупики.
А настоящий бум случился в 1997-м, когда Nokia взяла и предустановила Snake на свой 6110, и здесь понеслось — сотни миллионов телефонов, и змейка стала народной игрой, в которую рубились вообще все — от школьников до банкиров. В 2013-м Nokia переродилась под Microsoft, игру переписали под смартфоны, но та самая чёрно-белая версия уже навсегда в истории как символ мобильных игр девяностых (статья, в которой хорошо прописана история).

Теперь разберём, как работает классическая змейка. В этой игре простое поле, которое поделено на клетки, где в каждом кадре голова сдвигается в текущем направлении, новый сегмент добавляется в начало массива, а последний удаляется. Если змея съела яблоко, последний сегмент не удаляется, и змея растёт.
let head = { x: snake[0].x + dx, y: snake[0].y + dy }; snake.unshift(head); if (!ateApple) snake.pop();
Вся магия — в трёх строчках. Направление меняется по нажатию клавиши. Как в теории, так и во всех играх мы проверяем коллизию простым сравнением координат головы и остального тела: находятся ли они в границах поля. Яблоко появляется в случайной свободной клетке.
Если подумать, «Змейка» — очень понятная и простая игра, которая стала мировым феноменом благодаря своей универсальной доступности в те годы и увлекательному геймплею. Будучи предустановленной на миллионах легендарных телефонов Nokia, она предложила людям простой способ скоротать время и превратила мобильный телефон из аппарата для звонков в персональное игровое устройство, что было новинкой для того времени.
Первое, что я сделал, — поменял роли. Игрок управляет не змеёй, а яблоком. Цель — не собрать очки, а просто выжить как можно дольше. Счёт растёт сам по себе, каждое мгновение жизни — уже победа.
Второе — змеи больше не растут. Они появляются из-за краёв поля, проходят сквозь экран и исчезают с другой стороны, если игрок сумел от них уйти. Никакого поедания, никакого увеличения — только преследование. Вот тут, кстати, я думаю, можно поразмышлять, т. к. если, например, игра будет начинаться с маленьких змей, а потом, чем больше у тебя будет очков, тем больше будут становиться змейки (не пробовал, но эта идея пришла уже при написании статьи).
Третье — змеи бывают разные. Почти как пелось в песне: чёрные, белые, красные. Ладно, тут обычные зелёные, жёлтые и красные, как светофор. И это не просто внешний вид, а различия в их поведении в игре. Я хотел, чтобы игрок постоянно адаптировался, и поэтому их три вида, а появляются они случайным образом (почти). Так родились три типа:

Зелёные — обычные линейные крипы, которые идут прямо по линии без поворотов или каких-нибудь усложнений. Главное только то, что они ходят поочерёдно: то по горизонтали, то по вертикали.

Жёлтые — это змеи настроения, как я их называю, т. к. они быстрые и в любой момент могут поменять направление. (Примечание: поворот каждые десять ходов в сторону игрока. Пока оставил так, но, может, лучше поставить рандом.)

Красные — толстые и хитрые. Первые тридцать ходов агрессивно преследуют игрока, а потом внезапно разворачиваются и уходят к ближайшему краю. Игрок расслабляется — и тут появляется новая красная змея.
Четвёртое — сложность самой игры. Здесь были проблемы: баланс с нуля всегда сложно придумать, и поэтому его нужно будет, скорее всего, менять. Сейчас змеи появляются по таймерам, и чем дольше живёшь, тем короче становятся интервалы между спавнами. Тем самым на экране просто становится больше змей, и выжить почти невозможно.
Я решил не сваливать всё в один файл, а разбить проект на модули. А так как проект простой, можно было и в один, но это некрасиво. Никаких сборщиков, никаких import и export — просто четыре файла, которые подключаются в правильном порядке через теги <script>. Открываешь index.html — и всё работает.
Вот как выглядит структура:
reverse-snake/ ├── index.html ├── style.css ├── snake.js ├── renderer.js └── game.js
Я решил не мудрить с архитектурой и написал всё на классах. Как обычно, и в прошлых статьях, я не использую никаких фреймворков, сборщиков или готовых решений из интернета. Главное, чтобы я мог открывать веб-программу и спокойно поиграть.
Теперь пойдём по классам. Например, класс Snake отвечает за одну змею. Он хранит массив сегментов тела, текущее направление, тип и коэффициент скорости. Главный метод update() получает позицию игрока и решает, куда двигаться дальше. Логика разная для каждого типа:
if (this.type === 'yellow' && this.state === 'normal' && this.timer === 10) { this.state = 'turned'; if (this.dx !== 0) { this.dx = 0; this.dy = player.y > head.y ? 1 : -1; } else { this.dy = 0; this.dx = player.x > head.x ? 1 : -1; } }
Класс Renderer занимается отрисовкой и ничем больше, использует только fillRect() и никаких спрайтов. Сетка рисуется как шахматная доска: чётные клетки темнее, нечётные светлее. Змеи отрисовываются как квадраты с внутренней текстурой и каждой змеи свой внешний вид: сначала внешний цвет, потом тёмный прямоугольник внутри, потом два чёрных квадратика-глаза у головы, чтобы у человека появлялось чувство антропоморфизма. Такое же действие делаем с яблоком, которое состоит из восьми пикселей на клетку: красное тело, белый блик, коричневый черешок и зелёный листик.
Класс Game управляет всем: запускает игровой цикл, обрабатывает коллизии, считает очки, спавнит змей по таймерам. Отдельно я вынес логику интервалов спавна в метод updateSpawnTimers(), который пересчитывает частоту появления змей в зависимости от текущего счёта.
Разделение на три файла (snake.js, renderer.js, game.js) спасло меня от хаоса. Когда жёлтая змея странно поворачивала, я знал, что копать нужно в Snake. Когда пиксели съезжали — проблема была в Renderer. Когда интервалы спавна работали не по плану — в Game. И так можно перечислять до бесконечности.
Я люблю, когда проект можно открыть с любого устройства — и на компе, и на телефоне. Но с телефонами всегда морока. А, т. к. многие смотрят с телефона, то лучше придумать решение этих проблем.
С клавиатурой всё просто: её нет — значит, сделаем свою. Я добавил четыре кнопки-стрелки прямо на экране, под игровым полем. Они расположены крестом: верх, низ, лево, право. Выглядят как обычные серые квадраты с треугольниками внутри, ничего лишнего. На компе эти кнопки спрятаны, они появляются только на узких экранах, где ширина меньше 768 пикселей.
С размером экрана было сложнее всего, т. к. я привык, что на десктопе сетка 24×24 клетки смотрится нормально, но на телефоне она превращается в мелкую мозаику — пальцем не попасть, глаза сломаешь. Поэтому я сделал так, чтобы игра сама подстраивалась под ширину экрана. Если экран узкий, как у телефона, сетка становится 20×20 клеток — каждая клетка крупнее, и играть удобнее. Если планшет — 22×22. А если нормальный монитор — классические 24×24.
Ещё одна фишка — пауза. На компе я, как и обычно в играх, нажимаю Esc и спокойно ставлю игру на паузу, но вот здесь и появляется проблема, на телефоне Escape просто нет. Сначала аналогично мобильным играм, я хотел сделать отдельную кнопку паузы, но потом подумал и нашёл в интернете решение: а что, если просто встряхнуть телефон? Оказалось, в браузере есть событие devicemotion — оно считывает показания акселерометра. Я беру ускорение по трём осям, считаю общую величину, и если она больше 25 (это значит, что телефон хорошо тряхнули), игра встаёт на паузу. Работает как магия: тряхнул — меню вылезло. Интересное решение, которое нашёл в интернете. До этого проекта не знал, что так можно.
const magnitude = Math.sqrt( acceleration.x ** 2 + acceleration.y ** 2 + acceleration.z ** 2 ); if (magnitude > 25) pauseGame();
Самым сложным оказался баланс, что, конечно, логично, но всё же было тяжело. Первая версия спавнила слишком мало змей — можно было бегать минутами без всякой угрозы, тем самым игра была прям очень скучной. Потом я решил увеличил частоту — игра превратилась в хаос и уже было невозможно просто уворачиваться от змей на двадцатом очке, когда поле заполнялось десятком змей одновременно, яблоко съедалось моментально, и ты проигрывал.
Решение нашлось не сразу, но благодаря методу проб и ошибок. Я ввёл лимиты на максимальное количество змей каждого типа: не больше шести зелёных, пяти жёлтых и трёх красных, затем подогнал интервалы спавна под разные этапы игры, чтобы чем дольше ты играешь, тем была сложнее игра. Первые пятьдесят очков — расслабленный режим, только зелёные, к ста двадцати очкам появляются жёлтые и уже к двумстам пятидесяти — красные не отстают, и зелёные сыплются каждые восемнадцать ходов.
Могу сказать одно: это было неожиданно весело, я всего лишь хотел убить время в кафе, а в итоге получил великолепную идею, которую я смог реализовать и поиграть. Яблоко убегает, змеи наступают, счёт тикает — всё, как я задумал, и больше считаю, что добавлять ничего не надо.


Я доволен результатом, проект напомнил мне, что не всегда нужны сложные технологии, чтобы сделать что-то интересное. Иногда достаточно перевернуть старую идею с ног на голову — и получить новую игру.
Из минусов пока не знаю, что делать дальше. Надо подумать, за что браться следующим. Есть мысль сделать что-то вроде тамагочи: пиксельного питомца на чистом Canvas, которого нужно кормить и укладывать спать. Звучит как вызов, но после змейки как раз есть силы и интерес.
Если же возвращаться к «Обратной змейке», проект мне понравился, идея простая, но цепляет. Реализация вышла минималистичной и стильной, как я и хотел. Но, конечно, есть что улучшить, хотя ничего критичного я пока не вижу.
Код проекта находится здесь, поиграть в мою версию «Змейки» можно здесь.
P.S. Если у вас есть идеи по улучшению, вы нашли баг или просто хотите похвастаться своим рекордом — пишите в комментариях.
© 2026 ООО «МТ ФИНАНС»