Платформер в стиле DOOM: портируем старую курсовую на чистый JavaScript
- суббота, 22 августа 2026 г. в 00:00:12

Несколько лет назад у нас с одногруппником был зачёт по C++, и вместо того чтобы сделать что-то скромное (например, «Калькулятор» или «Генератор паролей»), мы, конечно же, решили замахнуться на какую-то игру, и тогда нам попалась новость о 30-летнем юбилее игры DOOM. План был красивый, т. к. на тот момент командной работы у нас не было, мы решили, что мой одногруппник пишет псевдо-3D-движок в духе классического рейкастинга, а я делаю 2D-платформер, который в итоге должен был запускаться прямо внутри его мира, как игровой автомат, который запускался бы перед игрой, и сколько патронов ты собрал, столько у тебя и будет в игре.
Проблема появилась не в архитектуре и не в гейм-дизайне, а гораздо раньше — на этапе, который мы оба благополучно пропустили: мы не договорились, на чём пишем. Я по рекомендации преподавателя взял SFML — с ним я уже возился на парах, там всё human-readable, спрайты, окна, звук из коробки. Товарищ выбрал SDL, т. к. видел гайд про DOOM на SDL — она даёт прямой доступ к буферу пикселей, что для рейкастинга буквально то, что нужно: рисуешь вертикальные полоски стен вручную, без лишней абстракции между тобой и экраном.
По отдельности оба выбора были правильными, а вместе — нет. Слинковать в одном проекте SDL и SFML можно, но два разных event loop, два разных подхода к окну и таймингу — это уже не работа для автомата по зачёту, а отдельная курсовая работа, на которую у нас оставалось дня два-три. Мы посчитали и сдали как два независимых проекта. Препод посмотрел, оценил — и выдал ровно ту фразу, которую в такой ситуации выдал бы любой человек, который знал о проекте: «Идея классная, жалко, что не объединили», но нам это было неважно на тот момент, как говорится, зачёт получили — и на этом спасибо.
С тех пор мой платформер почти три года пролежал в архиве — .cpp-файл на 900 строк, ASCII-карта из символов прямо в коде и пачка DLL от SFML рядом (из минусов: я его улучшал для сдачи, однако сохранился только такой проект, но спасибо другу, который сохранил свой проект и мою часть в старом виде). Недавно я искал старый файл, там наткнулся как раз на репозиторий одногруппника и решил: раз объединять с чужим движком уже поздно (хотя, думаю, в будущем можно будет), портирую хотя бы свою часть — но не на C++, а на чистый JavaScript и Canvas, без единой библиотеки, так же, как и всегда в моих статьях. Заодно посмотрим, сколько багов честно дожили в коде, на которые я закрыл глаза и старался не показать при защите преподавателю.

DOOM выпустила id Software 10 декабря 1993 года — небольшая техасская студия, где движок и всю программную часть тянул Джон Кармак, левел-дизайном и общей постановкой занимался Джон Ромеро, монстров и графику рисовал Эдриан Кармак, а музыку и звук — Бобби Принс. Годом раньше та же команда выпустила Wolfenstein 3D, и DOOM задумывался не как ещё одна «стрелялка», а как демонстрация того, что персональный компьютер начала 90-х способен рисовать что-то похожее на настоящую трёхмерную архитектуру в реальном времени.
Сразу, думаю, нужно расставить точки над «ё», потому что «мы делали DOOM» — это красивое упрощение. То, что можно написать за семестр студенческой командой из двух человек, по механике ближе к Wolfenstein 3D, чем к самому DOOM, — и разница между ними как раз и есть главный технический подвиг id Software.

Wolfenstein 3D рисует мир рейкастингом: из каждого столбца экрана в мир выпускается луч, он идёт по строго прямоугольной сетке, пока не упрётся в стену, и по расстоянию до стены считается высота вертикальной полоски. Это дёшево и гениально просто, но с жёсткими ограничениями: все стены — только под 90 градусов, один уровень высоты, никаких лестниц, никаких комнат друг над другом.
DOOM обошёл эти ограничения не «более быстрым рейкастингом», а другой структурой данных — BSP-деревом (Binary Space Partitioning). Уровень заранее, ещё на этапе сборки карты, разбивается на выпуклые секторы, и во время рендера движок точно знает, в каком порядке отрисовывать полигоны, не сортируя их каждый кадр. Это дало произвольные углы стен, разную высоту потолка и пола в соседних секторах и ту самую иллюзию настоящей архитектуры, которую игроки 1993 года массово принимали за настоящее 3D — хотя пол и потолок оставались плоскими, а «этаж над этажом» в чистом виде так и не появился до Doom 3.
Наш студенческий рейкастинг был куда скромнее — но не такой игрушечный, как я думал, пока не наткнулся в архиве на репозиторий товарища. Он его, оказывается, так и не удалил. В README прямо описана механика, которую мы всё-таки реализовали, просто не так, как планировали изначально: два независимых исполняемых файла, Project7 (моя 2D-часть на SFML, старая) и ConsoleApplication4-pseudo-doom (его 2.5D-часть на SDL), а связаны они ровно так, как я и помнил, — «open 2d game, gain bullets there and after open 2.5D game and you will have the number of bullets from your previous game, multiplied by a factor depending on the number of lives you have left». То есть мой платформер реально писал число собранных патронов в текстовый файл, а движок товарища читал этот файл и стартовал с патронами, домноженными на количество оставшихся жизней, — самодельный SaveData через bullets.txt вместо общего движка.
Слинковать SDL и SFML в одном main() от всего этого не стало проще — по-прежнему два разных event loop и два разных подхода к окну и таймингу, — но теперь я точно знаю, что «жалко, что не объединили» было сказано не про халтуру: рабочая связка между играми там действительно была, просто через файл на диске, а не через общий движок.
Часть 1. Что вообще осталось от старого кода
Внутри Source.cpp пряталась ASCII-карта — буквально массив строк 67 на 150 символов, где каждый символ означал тайл.

z — стена, k — платформа, b/B — монеты, c — блок, из которого при ударе снизу вылетает сердце, p и 9 — порталы между секциями уровня. Коллизии считались не через физический движок, а тупо перебором тайлов под прямоугольником игрока — студенческий, но абсолютно рабочий подход, который я и перенёс в JS почти один в один. Забавно, что по духу эта сетка ближе не к самому DOOM, а опять же к Wolfenstein 3D: там уровень тоже был жёстко привязан к прямоугольной сетке тайлов фиксированного размера, просто зашитой в бинарный формат карты, а не в текстовые строки прямо в .cpp. DOOM от этой сетки как раз и ушёл — его уровни Ромеро собирал вручную в собственном редакторе DoomEd как произвольные многоугольники, без всякой привязки к клеткам.
Спрайты доставались из одного листа hero1.png через IntRect — прямоугольник вырезки с одной прекрасной особенностью SFML: если задать отрицательную ширину, спрайт отражается по горизонтали без единой дополнительной строчки кода. В Canvas такого трюка из коробки нет, drawImage с отрицательной шириной просто не работает, так что пришлось написать аналог руками. И вот здесь неожиданно всплывает настоящий DOOM: в его WAD-файлах спрайты монстров хранятся не на все стороны света, а только на часть ракурсов, а недостающие зеркалятся на лету по битому флагу прямо в таблице кадров — id Software экономила место на дискетах ровно тем же трюком.
Часть 2. Даём врагам характер
В оригинале все враги двигались одинаково: шли по прямой, при ударе о стену разворачивались — классическое поведение из учебника, без какого-либо намёка на характер. У настоящего DOOM с этим было в разы честнее: там на каждого монстра — своя таблица состояний (idle, sight, chase, missile, melee, pain, death), и, например, Lost Soul выцеливает игрока и буквально врезается в него на скорости по прямой, а Cacodemon вместо шага по земле спокойно висит в воздухе и плюёт файерболами. При портировании я решил, что раз враги теперь на 50% быстрее и умеют по-настоящему падать под гравитацией (в оригинале у них гравитации не было вообще, они просто «плавали» на фиксированной высоте), логично дать хотя бы двум типам собственное поведение — то есть подтянуть их поближе к DOOM, а не к Wolfenstein.
Летающий тип теперь замечает игрока и, если находится на земле, прыгает не просто так, а с горизонтальным рывком в сторону героя — по кулдауну, чтобы это читалось как осознанный скачок, а не дрожание от случайного числа каждый кадр.
Второй тип, покрупнее, при обнаружении игрока копит небольшую задержку и срывается в короткий рывок на учетверённой скорости — что-то среднее между поведением тарана и внезапной панической атакой, ну то есть практически мой личный маленький Lost Soul.
Часть 3. Генератор уровней вместо одной карты
Раз уж всё равно переписывать карту, стало жалко ограничиваться одной-единственной — той самой, нарисованной символами в коде три года назад. Здесь я, в отличие от id Software, пошёл ровно в обратную сторону: в оригинальном DOOM ни одного процедурно сгенерированного уровня нет и не было, каждый метр E1M1 Ромеро выкладывал вручную в DoomEd, — а я вместо этого написал генератор. Высота земли меняется не более чем на один тайл за раз, поэтому уровень гарантированно проходим целиком без единого «слепого» провала, а сверху редко и с минимальным расстоянием друг от друга разбросаны платформы с монетами — чтобы небо не превращалось в сплошной потолок из блоков, о который бьёшься головой.

От исходного C++/SFML-проекта в новой версии не осталось ни строчки кода — но ASCII-карта, тайловая физика и сама идея платформера внутри чужого движка живы один в один, только теперь без DLL и без необходимости объяснять однокурсникам, зачем в проекте два разных event loop.
DOOM за тридцать с лишним лет успели портировать буквально на всё, до чего дотянулись руки энтузиастов, — от принтеров и кассовых аппаратов до теста на беременность, — и на этом фоне «переписать свою студенческую работу на Canvas» выглядит скромно, но по духу это ровно та же традиция: взять движок, который давно должен был остаться в прошлом, и заставить его работать там, где никто не просил. Той истории с преподом было бы приятно поставить символическую точку — но объединять два движка спустя годы всё так же не хочется. Может быть, в следующий раз я вместо этого попробую портировать и рейкастинг товарища — тогда и «жалко, что не объединили» наконец-то перестанет быть актуальным, просто с опозданием на несколько лет.
Что я вынес из этого проекта?
Что грустно смотреть на старый код, т. к. смотрю на код товарища и на свой и вижу: у меня нет ни структуры, ни красоты, и сам проект выглядит очень просто. Удивлён, что преподаватель его засчитал, а так нужно будет всё же объединить код, чтобы была единая игра, а не два забытых проекта. Конечно, немного стыдно за этот проект, но он стал намного лучше, чем то, с чем я работал вначале, и его можно улучшать (сильно подводит дизайн, конечно, но рисунки не менял, чтобы был похож на прошлый проект).
Поиграть в мою версию можно здесь.
P.S. Если у вас есть идеи по улучшению проекта, вы нашли баг или просто хотите что-то рассказать — пишите в комментариях.
© 2026 ООО «МТ ФИНАНС»