E1M0

Брифинг

Что такое движок DOOM и как пользоваться этим сайтом

Игра, которая научила компьютеры притворяться трёхмерными

10 декабря 1993 года маленькая техасская студия id Software выложила в сеть первый эпизод DOOM. Игроку достался морпех на базе Фобоса, а компьютерам того времени — задача, которую они, казалось, не могли решить: рисовать 35 раз в секунду лабиринт с комнатами разной высоты, лестницами, лифтами, мерцающим светом и толпами монстров. Обычный домашний ПК тогда — это процессор 386 или 486 без отдельного графического ускорителя и экран 320×200 точек в 256 цветах.

Движок написал в основном Джон Кармак. Разработка шла на рабочих станциях NeXT, там же жили редактор карт DoomEd и утилита, которая готовила карты к игре. Готовая игра собиралась для MS-DOS. В 1997 году id опубликовала исходный код версии для Linux, а в 1999 перевела его под лицензию GPL. С тех пор устройство DOOM можно изучать не по слухам, а по файлам r_bsp.c, p_enemy.c и am_map.c. Эта энциклопедия опирается именно на них.

Заставка Freedoom: морпех и змееподобный монстр на красном фоне
Заставка Freedoom — свободной замены графики DOOM. Все игровые картинки на этом сайте взяты из неё.

Главная хитрость движка — он не трёхмерный. Мир DOOM — это плоская карта, как план этажа, где у каждой комнаты записаны высота пола и потолка. Стены всегда вертикальные, полы всегда горизонтальные, комнату нельзя поставить над другой комнатой, а посмотреть вверх или вниз игрок не может. Такую схему называют «2,5D». Каждое из этих ограничений — сознательная цена за скорость, и почти все приёмы движка становятся понятными, если помнить: внутри DOOM — двумерный мир, которому досталась одна лишняя координата.

E1M1

Ангар данных

Из чего сделана карта: вершины, линии, стороны, секторы и вещи

WAD: «где все данные?»

Всё содержимое игры — графика, звуки, музыка и уровни — лежит в одном файле с расширением .wad (от «Where's All the Data?»). Внутри это простой архив: заголовок, набор «лампов» (кусков данных) и каталог с их именами. Движок не знает, откуда взялась конкретная картинка или карта, он просто ищет лампы по имени. Поэтому с первых дней игроки делали свои WAD-файлы с новыми уровнями: такой файл, загруженный поверх основного, подменял лампы с совпадающими именами.

Уровень в WAD — это лампы, которые идут строго в одном порядке после маркера с именем карты, например E1M1. Порядок задан в исходнике перечислением ML_THINGS … ML_BLOCKMAP:

  • VERTEXES — вершины, пары координат x, y;
  • LINEDEFS — линии между двумя вершинами: стены, проходы, края ступенек;
  • SIDEDEFS — стороны линий: какие на них текстуры и к какому сектору они обращены;
  • SECTORS — участки пола: высота пола и потолка, их текстуры, яркость, особый тип;
  • THINGS — вещи: точка старта игрока, монстры, аптечки, бочки;
  • SEGS, SSECTORS, NODES — BSP-дерево, которое строят заранее (глава E1M3);
  • REJECT и BLOCKMAP — таблицы-подсказки для проверки видимости и столкновений (главы E1M8 и E1M7).

Первые пять ламп рисует автор уровня в редакторе. Остальные пять за него вычисляет отдельная программа — построитель узлов (node builder). Сама игра при загрузке ничего не считает: она получает готовые таблицы, и поэтому уровень загружается почти мгновенно.

Автокарта уровня E1M1 из Freedoom: красные стены, жёлтые и коричневые перепады высот
Настоящая карта E1M1 из freedoom1.wad, нарисованная по правилам автокарты: красное — сплошные стены, коричневое — перепад пола, жёлтое — перепад потолка. Белая стрелка — старт игрока.

Линия и две её стороны

Самая важная запись карты — linedef. Это 14 байт: номера двух вершин, флаги, номер спецэффекта, метка и ссылки на две стороны. Если у линии одна сторона, это сплошная стена. Если две, линия разделяет два сектора, и сквозь неё можно смотреть и проходить.

На двусторонней линии могут быть три текстуры: верхняя закрывает перепад потолков, нижняя — перепад полов, средняя обычно пуста или служит решёткой. Так получаются ступеньки, окна и дверные проёмы, хотя в данных нет ни «двери», ни «окна» — только два сектора с разными высотами.

  • 1196вершин в E1M1 Freedoom
  • 1175линий
  • 182сектора
  • 292вещи

Сектор — это не комната

Сектор — набор линий, ограничивающих участок с одинаковыми полом, потолком и светом. Он не обязан быть выпуклым и даже связным: колонна посреди зала — это дырка в секторе, обведённая односторонними линиями. Дверь в DOOM — отдельный крошечный сектор, у которого потолок опущен до пола. Когда игрок нажимает «использовать», движок заводит «мыслителя», который каждый такт поднимает этот потолок, пока проём не откроется. Лифт делает то же самое с полом.

Высоты хранятся в целых единицах карты. Морпех имеет радиус 16 единиц и рост 56, глаза у него на высоте 41, а ступенька выше 24 единиц для него уже стена. Комнаты на картах обычно занимают от нескольких сотен до пары тысяч единиц. На этом сайте один CSS-пиксель равен одной единице карты.

Схема: разобранный уровень

Наведите курсор на вершину, линию или сектор.

Красные линии — односторонние (стены), жёлтая — перепад потолка (проём двери), коричневые — перепад пола (ступенька). Внизу — реальные размеры ламп карты E1M1 из Freedoom.

Выход с уровня E1M1 → слой «Автокарта»

Движок превратит каждый блок страницы в сектор, а его края — в линии карты, и нарисует их цветами автокарты. Картинки и схемы станут сплошными колоннами (красные), плашки с текстом — приподнятыми площадками (коричневые).

выключено
E1M2

Такт и точка

Игровое время 35 Гц, числа с фиксированной точкой и углы в «двоичных градусах»

35 тактов в секунду

Игровой мир DOOM живёт тактами — «тиками». Константа TICRATE равна 35: ровно столько раз в секунду движок опрашивает клавиатуру и мышь, двигает игрока и монстров, открывает двери и мигает лампами. Отрисовка подстраивается: если компьютер медленный, кадров меньше, но за один кадр прогоняется несколько тактов, и мир движется с той же скоростью. Больше 35 кадров в секунду оригинальная игра не рисует — новых тактов просто нет.

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

typedef struct {
    char  forwardmove;  // вперёд/назад
    char  sidemove;     // шаг вбок
    short angleturn;    // поворот
    short consistancy;  // проверка сети
    byte  chatchar;
    byte  buttons;      // огонь, «использовать»
} ticcmd_t;

Дробные числа без дробей

У процессора 386 не было быстрой арифметики с плавающей точкой, а у многих машин её не было вовсе. Поэтому все координаты, скорости и масштабы в DOOM — 32-битные целые числа с фиксированной точкой 16.16: старшие 16 бит — целая часть, младшие — дробная. Константа FRACUNIT = 65536 означает единицу. Сложение остаётся обычным сложением, а умножение — это умножение с последующим сдвигом на 16 бит (функция FixedMul). Так 1,5 единицы хранится как 98304, а 0x00018000 в шестнадцатеричном виде сразу показывает целую и дробную части.

Круг из 232 частей

Углы хранятся так же хитро: тип angle_t — беззнаковое 32-битное число, и полный оборот равен 232. Тогда ANG90 = 0x40000000, ANG180 = 0x80000000, а переход через 360° происходит сам собой при переполнении. Такая запись называется BAM (binary angle measure). Для синусов движок сдвигает угол на 19 бит вправо и получает номер одного из 8192 «тонких углов» в таблице finesine. Косинус берётся из той же таблицы со сдвигом на четверть круга.

  • 35тактов в секунду
  • 16.16формат координат
  • 8192тонких углов на круг

Схема: кадры, такты и числа

Водите курсором над шкалой: верхняя дорожка — кадры экрана, нижняя — такты игры. Красная линия — движение мыши, которое игра «увидит» только в моменты тактов. Справа — циферблат BAM: потяните стрелку.

Статус-бар Freedoom: патроны, здоровье, броня Восемь поворотов монстра — по одному на каждые 45 градусов
Статус-бар и восемь поворотов спрайта. Номер поворота — это старшие три бита угла BAM между монстром и игроком: по 45° на картинку.

Выход с уровня E1M2 → слой «Игрок»

Курсор станет стрелкой игрока с автокарты. Её положение обновляется 35 раз в секунду, а направление — по движению. В панели слоёв будут видны координаты и угол в формате BAM.

выключено
E1M3

Разделяй и рисуй

BSP-дерево: как разрезать уровень, чтобы стены сами выстроились по удалённости

Задача художника

Чтобы нарисовать сцену, надо знать, какая стена ближе. Самый прямолинейный способ — «алгоритм художника»: отсортировать стены по расстоянию и рисовать от дальних к ближним, затирая лишнее. Но сортировать сотни стен каждый кадр дорого, а многое при этом рисуется зря. Весной 1993 года ранние версии DOOM ещё работали без BSP и тормозили на сложных участках.

Решение Кармак нашёл в научных статьях о двоичном разбиении пространства (binary space partitioning). Идею BSP-деревьев описали Генри Фукс, Цви Кедем и Брюс Нейлор в 1980 году. До DOOM Кармак уже пробовал её при переносе Wolfenstein 3D на Super Nintendo. Суть: один раз заранее разрезать уровень прямыми на выпуклые куски и записать разрезы в дерево. После этого порядок «от ближнего к дальнему» для любой точки обзора получается простым обходом дерева, без всякой сортировки.

Карта E1M1, раскрашенная по подсекторам: сотни выпуклых многоугольников
E1M1, разрезанная на 682 выпуклых подсектора — каждый своим цветом.

Построитель узлов выбирает одну из линий карты и продлевает её в бесконечную разделяющую прямую. Всё, что лежит справа, уходит в «переднее» поддерево, всё, что слева, — в «заднее». Линии, которые прямая пересекает, режутся надвое — так из linedef получаются сегменты (segs). Потом процесс повторяется для каждой половины, пока кусочки не станут выпуклыми. Такие выпуклые кусочки называются подсекторами (subsectors). В выпуклом многоугольнике стены не могут загораживать друг друга, поэтому внутри подсектора порядок отрисовки уже не важен.

Узел дерева

Каждый узел в ламбе NODES — это 28 байт: начало прямой и её направление, два прямоугольника-границы (для переднего и заднего поддерева) и два номера детей. Если в номере ребёнка установлен старший бит NF_SUBSECTOR (0x8000), это не узел, а лист — подсектор.

void R_RenderBSPNode (int bspnum)
{
    if (bspnum & NF_SUBSECTOR) {
        R_Subsector (bspnum & ~NF_SUBSECTOR);
        return;
    }
    bsp = &nodes[bspnum];
    // с какой стороны прямой игрок?
    side = R_PointOnSide (viewx, viewy, bsp);
    // сначала ближняя половина
    R_RenderBSPNode (bsp->children[side]);
    // дальняя — только если видна
    if (R_CheckBBox (bsp->bbox[side^1]))
        R_RenderBSPNode (bsp->children[side^1]);
}

Вот и весь секрет: с той стороны прямой, где стоит игрок, всё заведомо ближе, чем на другой. Обходя сначала «свою» половину, движок получает стены строго спереди назад.

Разделяющие прямые BSP-дерева карты E1M1
Разделяющие прямые всех 681 узлов E1M1. Жёлтые и толстые — у корня дерева, синие и тонкие — в глубине (до 29 уровней).

Цена готового дерева

BSP-дерево отлично работает, пока стены неподвижны. Поэтому в DOOM двигаются только полы и потолки: дверь поднимает потолок, но её линии остаются на месте. Вращающихся или ездящих стен в движке нет — это прямое следствие заранее построенного дерева.

Построение — небыстрая задача. Хороший построитель старается выбрать такие разрезы, чтобы дерево было сбалансированным, а разрезанных линий — поменьше, ведь каждая новая сегментная линия — это лишняя работа при отрисовке. В 1993 году узлы для больших уровней могли считаться минутами. Позже сообщество написало десятки построителей: BSP, ZenNode, ZDBSP и другие.

Дерево годится не только для отрисовки. Функция R_PointInSubsector спускается по нему, чтобы узнать, в каком секторе стоит игрок или монстр. По дереву же, начиная с версии 1.2, проверяется прямая видимость между монстром и игроком.

Схема: строим BSP-дерево по шагам

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

Выход с уровня E1M3 → слой «BSP»

Мини-движок построит BSP-дерево по блокам этой страницы прямо в браузере, нарисует разделяющие прямые и подсветит подсектор, в котором вы сейчас стоите. Путь по дереву появится в панели слоёв.

выключено
E1M4

Поле зрения

Отсечение: углы, отбрасывание задних сторон и список закрытых столбцов solidsegs

Девяносто градусов

Обход дерева выдаёт сегменты спереди назад, но большинство из них игрок всё равно не видит. Функция R_AddLine отсеивает их за несколько целочисленных операций. Сначала она считает углы от глаз игрока до обоих концов сегмента. Если угловой размах больше или равен 180°, сегмент повёрнут к игроку спиной, и его можно забыть: у сплошной стены видна только одна сторона. Затем углы поворачиваются на угол взгляда и обрезаются по краям поля зрения. Константа FIELDOFVIEW = 2048 «тонких углов» — это ровно 90°.

Обрезанные углы через таблицу viewangletox превращаются в номера столбцов экрана — от 0 до 319. С этого момента сегмент для движка — просто отрезок [x1, x2] на одномерной полосе экрана. Вся дальнейшая работа идёт с этой полосой, и это вторая главная идея движка после BSP.

Кадр, отрисованный мини-движком сайта по алгоритму DOOM
Кадр, нарисованный мини-движком этого сайта: стены в нём отсекаются точно так же — по столбцам.

Список закрытых столбцов

Массив solidsegs (не больше 32 элементов) хранит диапазоны столбцов, которые уже закрыты сплошными стенами. В начале кадра в нём только два «стража» — всё левее нуля и всё правее 319. Когда приходит новая сплошная стена, R_ClipSolidWallSegment находит в её отрезке ещё не закрытые куски, отправляет на отрисовку только их и объединяет диапазоны. Двусторонняя линия-«окно» (например, край ступеньки) рисуется в свободных кусках, но столбцы не закрывает — сквозь неё видно дальше. Это делает R_ClipPassWallSegment.

Ранний выход

Когда вся полоса от 0 до 319 закрыта, дальние ветви дерева можно не обходить. Перед тем как спуститься в дальнюю половину узла, R_CheckBBox проецирует на экран её прямоугольник-границу и проверяет, остался ли в нём хоть один открытый столбец. Если нет, пропускается целая половина уровня.

Закрытая дверь для отсечения — тоже сплошная стена: если потолок сектора за линией опустился до пола, R_AddLine отправляет её в R_ClipSolidWallSegment. А пустая линия, у которой по обе стороны одинаковые пол, потолок и свет, не рисуется вовсе: такие линии ставят как невидимые «спусковые крючки» для скриптов уровня.

Карта открывается взглядом

У отсечения есть неожиданный побочный эффект. В r_segs.c при каждой отрисовке стены выполняется linedef->flags |= ML_MAPPED. Автокарта показывает только линии с этим флагом. Поэтому карта уровня открывается не там, где вы прошли, а там, куда вы посмотрели.

Схема: заполнение solidsegs

Перетащите игрока по карте, колесом или клавишами Q/E поверните его. Сегменты приходят в порядке обхода BSP. Серые отброшены (спиной к игроку или вне поля зрения), зелёные дали новые столбцы, синие — «окна». Справа — сама полоса из 320 столбцов и текущее содержимое solidsegs.

Выход с уровня E1M4 → слой «Поле зрения»

У игрока появится конус обзора 90°. Всё, что заслонено колоннами-картинками, уйдёт в тень, а автокарта начнёт забывать линии: цветными останутся только те, что вы уже видели, остальные станут серыми, как при «компьютерной карте».

выключено
E1M5

Свет и тень

Яркость сектора, таблица COLORMAP и тьма, которая сгущается с расстоянием

256 цветов на всё

Экран DOOM работает в режиме с палитрой: каждый пиксель — один байт, номер цвета из 256. Палитру хранит ламп PLAYPAL. Как сделать стену темнее, если нельзя просто умножить цвет на коэффициент? Заранее приготовить таблицу. Ламп COLORMAP — это 34 таблицы по 256 байт. Таблица номер N отвечает на вопрос: «Каким номером палитры нарисовать цвет X на уровне темноты N?» Таблица 0 — полная яркость, 31 — почти чернота, 32-я делает картинку инверсно-серой для неуязвимости, 33-я полностью чёрная.

Таблица COLORMAP: 34 строки по 256 цветов, от ярких к тёмным
COLORMAP из Freedoom: сверху вниз — 34 таблицы, слева направо — 256 цветов палитры.

Затемнение пикселя стоит одно обращение к памяти: colormap[texel]. Внутренний цикл рисования колонки так и выглядит: взять байт текстуры, пропустить через таблицу света, записать на экран. Никакой арифметики с цветом.

У каждого сектора есть яркость от 0 до 255. Движок делит её на 16 и получает один из 16 уровней LIGHTLEVELS. Чтобы прямоугольные комнаты не выглядели плоскими, есть «ложный контраст»: стены, идущие строго с запада на восток, рисуются на уровень темнее, а с севера на юг — на уровень светлее.

Тьма растёт с расстоянием

Самый узнаваемый эффект DOOM — дальние стены уходят в темноту, даже если комната ярко освещена. Для стен движок использует таблицу scalelight[16][48]: чем меньше масштаб стены на экране (то есть чем дальше она), тем темнее выбранная таблица COLORMAP. Для полов и потолков — таблицу zlight[16][128] по расстоянию до строки пола. Обе считаются один раз при запуске, по формуле из R_InitLightTables.

Таблиц света всего 32, поэтому затемнение идёт ступенями — на полу видны полосы-«кольца». Это не ошибка, а характерный вид DOOM, который в современных портах иногда воспроизводят нарочно.

Мигающий свет — это мыслитель

Особые типы секторов оживают при загрузке уровня: P_SpawnSpecials заводит для них маленьких «мыслителей». T_LightFlash случайно гасит свет, T_StrobeFlash мигает с периодом 15 или 35 тактов, T_Glow плавно меняет яркость на 8 единиц за такт, T_FireFlicker дрожит, как огонь. Вспышку выстрела делает переменная extralight: на пару тактов весь мир становится на одну-две ступени светлее.

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

Схема: какой будет COLORMAP

Слева — вся таблица zlight: 16 уровней света сверху вниз и 128 шагов расстояния слева направо, каждая клетка — номер таблицы COLORMAP. Справа — текстура STARTAN3, пропущенная через выбранную таблицу, как это делает R_DrawColumn.

Выход с уровня E1M5 → слой «Свет»

Вокруг игрока появятся кольца света с теми же ступенями, что в zlight: чем дальше от курсора, тем темнее. Некоторые блоки начнут мерцать и пульсировать по правилам T_Glow и T_StrobeFlash. Текст при этом не погаснет до нечитаемого.

выключено
E1M6

Кадр по столбцам

Стены, визплейны и спрайты: в каком порядке на экране появляются пиксели

Стена — это пачка вертикальных полосок

Каждый видимый кусок сегмента попадает в R_StoreWallRange. Для каждого столбца экрана движок считает масштаб (насколько близко стена), верх и низ стены и номер столбца текстуры, после чего рисует вертикальную полоску функцией R_DrawColumn. Ключевое свойство: вдоль вертикальной полоски стены расстояние до глаз не меняется. Значит, шаг по текстуре постоянный, и внутренний цикл — это несколько сложений без единого деления. Поэтому у DOOM нельзя наклонить голову: стены перестали бы быть вертикальными на экране, и трюк бы сломался.

Ради того же трюка текстуры стен хранятся по столбцам. Текстура собирается из «патчей» — картинок, описанных в лампах TEXTURE1 и PNAMES. Сами патчи записаны как наборы вертикальных «столбиков» с прозрачными промежутками, и рисовать их по столбцам удобнее всего.

Текстура STARTAN3, собранная из двух патчей, границы патчей обведены
Текстура STARTAN3 собрана из двух патчей по 64 пикселя — рамки показывают, где кончается один и начинается другой.

Пол рисуется строками

С полами и потолками всё наоборот: у них постоянна глубина вдоль горизонтальной строки экрана. Пока рисуются стены, движок только запоминает, какие пиксели каждого столбца остались над стеной (потолок) и под ней (пол). Эти отметки собираются в визплейны (visplanes): у визплейна одна высота, одна текстура, одна яркость и два массива top[320] и bottom[320]. После стен R_DrawPlanes превращает столбцы визплейна в горизонтальные отрезки — спаны — и рисует их функцией R_DrawSpan.

Знаменитые лимиты

Все массивы рендера в DOOM статические, размеры выбраны раз и навсегда:

  • 128визплейнов (MAXVISPLANES)
  • 256отрисованных сегментов (MAXDRAWSEGS)
  • 128спрайтов в кадре (MAXVISSPRITES)
  • 32диапазона в solidsegs

Если на экране больше 128 разных участков пола и потолка, игра падает с ошибкой «R_FindPlane: no more visplanes». Авторы карт годами учились её обходить: упрощали узоры пола, уменьшали число разноцветных секторов. Порты вроде Boom сняли ограничение, заменив массив хеш-таблицей списков.

Спрайты — последними

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

Тот же кадр, где каждый визплейн залит своим цветом
Тот же кадр, что и выше, но каждый визплейн залит своим цветом. Полосы одного цвета склеиваются из столбцов разных стен.
Кадры анимации монстра: ходьба, атака, боль и смерть
Все кадры «змеешипа» из Freedoom (замена беса): четыре кадра ходьбы, три атаки, боль и пять кадров смерти. Каждый кадр — отдельный патч со своими смещениями.

Схема: кадр собирается на глазах

Ползунок проходит стадии кадра в порядке DOOM: сначала столбцы стен спереди назад, потом визплейны, потом спрайты. Справа можно перетащить игрока и повернуть его колесом.

Выход с уровня E1M6 → слой «Вид от первого лица»

В углу экрана откроется окно 320×200 — эта страница, отрисованная как уровень DOOM с того места, где стоит курсор. Плашки с текстом станут приподнятыми площадками, картинки — компьютерными колоннами, а над ними будет небо.

выключено
E1M7

Стены не пускают

BLOCKMAP, проверка положения и скольжение вдоль стен

Сетка 128 × 128

Чтобы понять, может ли игрок шагнуть в новую точку, нужно проверить все линии рядом с ним. Перебирать каждый такт тысячу линий уровня для каждого монстра слишком дорого. Поэтому построитель узлов накрывает карту сеткой из квадратов 128×128 единиц (MAPBLOCKUNITS) и для каждого квадрата записывает в ламп BLOCKMAP список линий, которые его задевают. Вещи тоже «прописываются» в квадраты, в которых стоят их центры.

Сетка BLOCKMAP поверх карты E1M1: чем ярче клетка, тем больше в ней линий
BLOCKMAP карты E1M1: 32×27 клеток. Чем ярче клетка, тем больше линий в её списке — до 26 в самой плотной.

Когда вещь хочет сдвинуться, P_TryMove вызывает P_CheckPosition. Та строит вокруг новой точки квадрат со стороной в два радиуса (у морпеха радиус 16 — квадрат 32×32) и смотрит только клетки, которые он задевает. Каждая линия из их списков проверяется на пересечение с квадратом: сплошная стена останавливает сразу, двусторонняя — если за ней ступенька выше 24 единиц или слишком низкий потолок. Вещи из тех же клеток проверяются на перекрытие квадратов. Клетки берутся с запасом MAXRADIUS = 32, ведь центр соседа может лежать в соседней клетке.

Скольжение

Если упереться в стену под углом, морпех не застревает, а скользит вдоль неё. Это работа P_SlideMove: она ищет ближайшую линию, в которую упёрлось движение, и оставляет от вектора скорости только часть, параллельную стене. Если найти линию не удаётся, срабатывает запасной вариант «ступенька»: сначала попробовать сдвинуться только по X, потом только по Y.

Бесконечно высокие монстры

Столкновения в оригинальном DOOM почти двумерные. Высота вещей учитывается только при проверке пола и потолка, а друг для друга вещи бесконечно высокие. Поэтому нельзя пройти над головой монстра или запрыгнуть на него: в плане они просто квадраты. Это тоже экономия — и ещё одно напоминание, что мир DOOM плоский.

Квадрат вместо круга даёт забавный эффект: по диагонали морпех «толще», чем по прямой, и в узкий диагональный коридор пролезает хуже.

Схема: P_TryMove вживую

Квадрат-вещь (радиус 16) догоняет курсор со скоростью до 30 единиц за такт. Подсвечены клетки, которые проверяет P_CheckPosition, и линии из их списков. Упритесь в стену под углом — увидите скольжение.

Выход с уровня E1M7 → слой «Столкновения»

У игрока появится тело. Стрелка будет догонять курсор, упираться в колонны-картинки и скользить вдоль них. Застрянет дольше секунды — телепортируется к курсору, как будто вы набрали IDCLIP.

выключено
E1M8

Мыслители

Список «мыслителей», таблица состояний, слух и зрение монстров

Всё живое — в одном списке

Всё, что меняется со временем, — монстр, летящий шар огня, открывающаяся дверь, мигающая лампа — в DOOM называется «мыслителем» (thinker). Это структура с двумя указателями (на соседей по двусвязному списку) и указателем на функцию. Каждый такт P_RunThinkers проходит по списку и вызывает у каждого его функцию. Чтобы удалить мыслителя, ему ставят функцию -1, и на следующем проходе он выбрасывается из списка.

Монстры поведение не программируют. Каждый живёт по таблице состояний states[] из info.c. Состояние — это кадр спрайта, длительность в тактах, функция-действие и номер следующего состояния. Бес, например, стоит в состояниях S_TROO_STND и S_TROO_STND2 по 10 тактов, вызывая A_Look. Проснувшись, он бежит по кругу из восьми состояний по 3 такта с A_Chase. Атака — два раза по 8 тактов A_FaceTarget и бросок A_TroopAttack.

Матрица REJECT карты E1M1: 182 на 182 сектора
REJECT карты E1M1: 182×182 бита. Тёмная точка — из сектора-строки точно не видно сектор-столбец, и трассировку можно не делать.

Как монстр слышит

Выстрел игрока вызывает P_NoiseAlert. Та запускает P_RecursiveSound — заливку по секторам: из сектора игрока звук переходит в соседние через двусторонние линии, если проём между ними открыт (закрытая дверь звук глушит). Линия с флагом ML_SOUNDBLOCK пропускает звук один раз, а через вторую такую линию он уже не пройдёт. Каждый сектор, куда дошёл звук, запоминает источник в поле soundtarget. Спящий монстр в A_Look первым делом смотрит, не записан ли там игрок.

Как монстр видит

Если не услышал, монстр ищет игрока глазами (P_LookForPlayers): смотрит только вперёд, в полукруге 180°, если игрок не подошёл вплотную. Сама проверка видимости P_CheckSight сначала заглядывает в таблицу REJECT — битовую матрицу «сектор на сектор». Бит говорит, что из одного сектора другой не виден никогда, и тогда считать ничего не надо. Иначе по BSP-дереву трассируется луч от глаз монстра к игроку.

Восемь направлений

Бегает монстр не к игроку напрямую, а по одному из восьми направлений, выбирая в P_NewChaseDir то, что ближе к цели. Скорости по диагонали записаны в таблице как 47000 — это 65536 × cos 45°. Каждые несколько шагов (счётчик movecount берётся из P_Random) направление пересчитывается. Если путь закрыт, монстр пробует соседние направления и даже разворот. Отсюда «дёрганая» походка и умение монстров застревать за колоннами.

Схема: состояния беса и заливка звука

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

Выход с уровня E1M8 → слой «Монстры»

По странице начнут бродить змеешипы. Они спят, пока не услышат выстрел. Клик мышью — выстрел из дробовика (7 дробин, автоприцел), звук растечётся по главам. Проснувшиеся монстры погонятся за курсором и будут бросаться огнём. Урон только для вида: умереть здесь нельзя.

выключено
E1M9

Секретный уровень: детерминизм

Вы нашли секрет! Таблица случайных чисел, демо-записи и коды разработчиков

Случайность из таблицы

В DOOM нет генератора случайных чисел в привычном смысле. В m_random.c лежит массив rndtable из 256 заранее перемешанных байтов, а функция P_Random просто увеличивает индекс на единицу и возвращает следующее число. Через 256 вызовов последовательность повторяется. Для игровой логики (урон, решения монстров) используется P_Random, для «косметики» вроде экранного перехода — M_Random со своим индексом, чтобы украшения не сбивали игру.

Зачем так примитивно? Ради детерминизма. Если в начале уровня сбросить индекс и подать на вход ту же последовательность команд ticcmd_t, мир повторится до последнего пикселя. На этом держатся две вещи. Демо — это 4 байта на такт, а не видео. Сетевая игра на четверых передаёт по сети только команды игроков, и каждый компьютер сам считает одинаковый мир.

Обратная сторона — «рассинхрон». Стоит игре в другой версии один раз вызвать P_Random не в том месте, и демо идёт вразнос: игрок бежит в стену и стреляет в пустоту, ведь его команды рассчитаны на другой мир.

Коды из 1993 года

Разработчикам нужно было быстро проверять уровни, и в игру встроены коды, которые набирают прямо во время игры. Они работают и на этом сайте — наберите на клавиатуре:

  • iddqd — режим бога: лицо в статус-баре светится, здоровье не убывает;
  • idkfa — «всё оружие и ключи»: включает все слои движка разом;
  • iddt — полная автокарта, повторный ввод показывает монстров;
  • idclip — прохождение сквозь стены;
  • idbeholdl — светоусилитель: гасит затемнение;
  • idclev и две цифры — переход на уровень, например idclev13;
  • idmypos — показать координаты и угол.

Схема: запишите демо

Нажмите «Запись» и поводите мышью по песочнице 5 секунд: записываются только команды — смещение за такт. Повтор пересчитает мир заново из тех же команд, и три шара будут вести себя точно так же. Сдвиньте индекс случайных чисел на единицу — и повтор разойдётся.

Эпизод пройден?

Слои
0 / 8
Убито
0
Секреты
0 / 1
Время
0:00

Если секретов 0 из 1, вернитесь к главе E1M3: одна из её плашек ведёт себя не как остальные. Кликните по ней.

Что осталось за кадром

Мини-движок этого сайта — упрощение. Его BSP строится из прямоугольников страницы, сектора здесь только трёх видов, а монстры видят только один поворот спрайта. Но порядок работы в нём тот же, что в DOOM: обход дерева спереди назад, отсечение по столбцам, таблицы света, сетка столкновений и мыслители, которые 35 раз в секунду делают свой шаг.

Остальные работы курса — в меню «Работы» вверху страницы и в слотах «Задания» на статус-баре.

Источники и лицензии

  • Исходный код DOOM (linuxdoom-1.10), id Software, 1997; лицензия GNU GPL — github.com/id-Software/DOOM. Файлы r_bsp.c, r_segs.c, r_plane.c, r_main.c, p_enemy.c, p_map.c, p_sight.c, p_lights.c, m_random.c, f_wipe.c, am_map.c. Таблица rndtable на этом сайте взята оттуда.
  • Fabien Sanglard. Game Engine Black Book: DOOM. 2018.
  • David Kushner. Masters of Doom. 2003 — история id Software.
  • H. Fuchs, Z. Kedem, B. Naylor. On Visible Surface Generation by A Priori Tree Structures. SIGGRAPH, 1980.
  • DoomWiki.org — статьи «Doom rendering engine», «Visplane overflow», «Reject», «Blockmap».
  • Графика, палитра, текстуры и данные карты E1M1: Freedoom 0.13.0, © 2001–2024 Contributors to the Freedoom project, лицензия BSD 3-Clause — freedoom.github.io. Картинки извлечены из freedoom1.wad собственным скриптом, схемы и кадры отрисованы мини-движком сайта.