Mini Doom современный C++ через игру том 4 здесь кто то есть

Алексей Примак
MINI DOOM современный C++ через игру том 4 здесь кто то есть
Вместо предисловия
Это четвёртый том книги «MINI DOOM. Современный C++ через игру». К концу третьего тома у тебя трёхмерный лабиринт: лучи DDA, стены с текстурами без единого файла картинок, пол и потолок, затухание света и двери, которые открываются по E.
Теперь лабиринт перестаёт быть пустым. Номера глав, листингов и рисунков в серии сквозные, поэтому ссылка «см. главу 12» ведёт туда же, куда и в однотомнике.
Серия целиком
— Том 1. Учимся держать оружие — главы 1–8: основы C++ в консоли, от первой программы до памяти и санитайзеров.
— Том 2. Окно в ад — главы 9–13: своё окно Windows, пиксели, игровой цикл, клавиатура и мышь, текст.
— Том 3. Третье измерение — главы 14–19: рейкастинг, стены, текстуры, пол и свет, двери.
— Том 4. Здесь кто-то есть — главы 20–25: спрайты, монстры, оружие, искусственный интеллект, смерть.
— Том 5. Шум, свет и финальные титры — главы 26–29: звук, экраны, уровни, CMake и тесты.
Все исходники серии — на GitHub: https://github.com/RushSex/MiniDoom. Папка examples/chNN для каждой главы, итоговая игра — examples/ch29/doom. Там же README с инструкцией по сборке в Visual Studio 2026. Каждая глава начинается с проекта предыдущей, так что можно начать читать этот том, взяв готовый код последней главы прошлого тома.
Часть IV. Здесь кто-то есть
Лабиринт готов: стены, текстуры, свет, двери. Но бродить по пустому лабиринту — всё равно что ходить по ночному офису: красиво, жутковато и совершенно бессмысленно. Doom — это аптечки за углом, патроны в последний момент и, конечно, те, кто ждёт тебя в темноте.
В этой части мир оживёт. Сначала мы научимся ставить в него плоские картинки — спрайты — так, чтобы стены честно их закрывали. Потом нарисуем кодом монстров с анимацией, выдадим игроку пистолет и дробовик, научим монстров замечать игрока, гоняться за ним по коридорам и стрелять в ответ. В конце части в игре можно будет умереть — а значит, это уже игра.

Глава 20. Спрайты
Спрайт — это плоская картинка, которая стоит в трёхмерном мире и всегда смотрит на игрока. Аптечка, коробка патронов, бочка, монстр — всё это спрайты. Чтобы нарисовать спрайт, нужно ответить на три вопроса: где он на экране, какого он размера и какие его части закрыты стенами. Первые два — это та же перспектива, что у стен. Третий — новая идея, Z-буфер.
К концу главы на уровне будут лежать аптечки и патроны, игрок будет их подбирать, а вверху экрана, как в Doom, появится надпись «PICKED UP A MEDIKIT».
Предметы на карте
Предметы, как и всё на уровне, рисуем прямо в карте: h — аптечка (health), a — патроны (ammo). Map::isWall считает стенами только цифры, поэтому буквы для движения — пустой пол. Сам предмет — маленькая структура, листинг 20.1.

Листинг 20.1 — Предмет: что за вещь, где лежит, подобрана ли
enum class с указанием типа : std::uint8_t — перечисление занимает один байт, а не четыре, как int. Для двух значений это мелочь, но это привычка: в Doom предметов, монстров и состояний сотни.
В конструкторе Game пробегаем по карте и для каждой буквы h или a кладём предмет в центр клетки. Подобранный предмет не удаляется из вектора, а только помечается taken: так проще, а позже, в главе 25, по этому флагу мы вернём все предметы на место при перезапуске уровня.
Картинка с дырками
Стены — прямоугольники, их текстура заполнена целиком. А аптечка — не прямоугольник: вокруг неё должно просвечивать то, что сзади. Значит, у картинки спрайта должны быть прозрачные пиксели. Самый простой способ — занять под это старший байт цвета. Мы до сих пор писали цвет как 0x00RRGGBB, и старший байт всегда был нулём. Теперь договоримся: в спрайтах этот байт — альфа, «непрозрачность». 0xFF — пиксель есть, 0x00 — пикселя нет, сквозь него видно фон. Листинг 20.2 и 20.3.

Листинг 20.2 — Непрозрачный цвет и картинки предметов

Листинг 20.3 — Аптечка и патроны из прямоугольников

Листинг 20.3 (продолжение) — Аптечка и патроны из прямоугольников
Новая картинка Image заполнена нулями — то есть полностью прозрачна. Функция box закрашивает прямоугольник непрозрачным цветом. Аптечка — белая коробка с красным крестом в нижней части картинки 64 × 64: спрайт, как и стена, занимает одну клетку в высоту, а маленький предмет должен лежать на полу, а не висеть посередине.

Где спрайт на экране
Спрайт стоит в точке (sx, sy). Чтобы понять, куда его рисовать, переведём эту точку в систему координат камеры: сколько до неё вперёд по взгляду и сколько вбок, вправо. Вперёд — это проекция вектора «от игрока к спрайту» на направление взгляда, а вбок — на направление «вправо» из главы 14, рисунок 20.1.

Рисунок 20.1 — Спрайт в системе камеры: depth — вперёд по взгляду, side — вбок
Дальше всё уже знакомо. depth — это ровно то расстояние вдоль взгляда, которое мы мерили для стен в главе 16, поэтому и размер спрайта считается так же: h / depth. Положение по горизонтали — из окна камеры: окно на расстоянии 1 имеет полуширину 0,8, на расстоянии depth — 0,8 × depth, и спрайт, сдвинутый вбок на side, попадает в точку side / (0,8 × depth) от −1 до +1. Низ спрайта — на полу, а пол на расстоянии depth виден на (h/2) / depth ниже горизонта, это формула из главы 18.
Вся отрисовка спрайтов — функция drawSprites, листинги 20.4 и 20.5.

Листинг 20.4 — Спрайт и камера

Листинг 20.5 — drawSprites: проекция, сортировка, Z-буфер, прозрачность

Листинг 20.5 (продолжение) — drawSprites: проекция, сортировка, Z-буфер, прозрачность
Строки 19–21 — depth для каждого спрайта. Строки 31–39 — формулы с рисунка 20.1: side, середина на экране, размер, левый и верхний края, шаг по картинке и свет по расстоянию, как у стен.
Строки 41–46 — прямоугольник спрайта на экране, обрезанный по краям. Лямбда px — просто короткое имя для static_cast
Строки 47–59 — рисование. Внешний цикл по столбцам, внутренний по строкам. Пиксель картинки рисуем, только если его альфа не ноль. А на строке 48 — главное в этой главе.
Z-буфер: кто кого закрывает
Стены мы рисуем раньше спрайтов. Если просто нарисовать спрайт поверх, он окажется виден сквозь стены — аптечка из соседней комнаты будет висеть на кирпичах. Нужно знать, где стена ближе спрайта, а где дальше. И эта информация у нас уже есть: для каждого столбца экрана мы знаем расстояние до стены. Раньше мы его выбрасывали, а теперь drawView записывает его в массив, листинг 20.6.

Листинг 20.6 — drawView теперь заполняет zbuf
Этот массив — по одному числу на каждый из 640 столбцов — и называется Z-буфером, буфером глубины: «z» по традиции обозначает глубину сцены. Спрайт в столбце col рисуется, только если он ближе стены в этом столбце: depth < zbuf[col]. Рисунок 20.2.

Рисунок 20.2 — Z-буфер: спрайт на глубине 3 рисуется только в столбцах, где стена дальше
Посмотрим, что будет без этой проверки. Уберём строки 48–50 из листинга 20.5 — листинг 20.7 — и снимем кадр со стартовой позиции, рисунок 20.3.

Листинг 20.7 — Ошибка: Z-буфер не проверяется

Рисунок 20.3 — Без Z-буфера: на кирпичной стене висят призрачные патроны и аптечка из соседней комнаты
Присмотрись к кирпичной стене и к металлической колонне справа: на них нарисованы тёмные силуэты — коробка патронов и аптечка из соседней комнаты. Они далеко, поэтому маленькие и тёмные, но они есть и просвечивают сквозь стену. С проверкой их нет.

Спрайты закрывают друг друга
Стены от спрайтов отгородили. Но спрайты могут перекрывать друг друга: аптечка перед коробкой патронов. Z-буфер тут не поможет, в нём только стены. Поможет старый «алгоритм художника» из главы 18: рисуем от дальних к ближним, и ближние ложатся поверх. Для этого спрайты сортируются по depth по убыванию — строка 23 листинга 20.5:
std::ranges::sort(sprites, std::greater{}, &Sprite::depth);
Это сортировка из C++20 с двумя дополнительными аргументами. Второй — сравнение: std::greater{} — «больше», значит, по убыванию. Третий — проекция: указатель на поле &Sprite::depth говорит, что сравнивать надо не спрайты целиком, а только их глубины. Без проекции пришлось бы писать лямбду [](const Sprite& a, const Sprite& b) { return a.depth > b.depth; } — то же самое, только длиннее и с шансом перепутать знак.
Обрати внимание: drawSprites получает вектор по значению, то есть копию. Сортировка переставляет копию, а исходный порядок у вызывающего не меняется. Копия пары десятков спрайтов ничего не стоит, а функция становится честнее: она ничего не портит снаружи.
Подбираем
Остаётся подбирать предметы. Каждый кадр смотрим, не подошёл ли игрок к предмету ближе чем на полклетки, листинг 20.8.

Листинг 20.8 — Подбор предметов и сборка спрайтов для кадра

Листинг 20.8 (продолжение) — Подбор предметов и сборка спрайтов для кадра
Строки 161–165. Расстояние сравниваем в квадрате: dx² + dy² > 0,5². Корень не нужен — если расстояние больше, то и его квадрат больше. Мелкая, но приятная экономия: эту проверку мы будем делать для каждого предмета и монстра каждый кадр.
Строки 166–169 — правило из Doom: если здоровье уже 100%, аптечка остаётся лежать. Её можно вернуться и взять потом, когда понадобится. Патроны берутся всегда.
Строки 181–197 — сборка кадра. Z-буфер — локальный вектор: render константный и не может менять поля Game, да и не нужно — буфер живёт один кадр. Для каждого неподобранного предмета кладём спрайт с указателем на его картинку. Картинки живут в static-массиве itemImage до конца программы, поэтому указатель на них безопасен.
HUD теперь показывает здоровье, патроны и сообщение — по примеру Doom, оно висит вверху слева две секунды. Робот проверяет — листинг 20.9.

Листинг 20.9 — Автотест: идём вдоль комнаты и подбираем

Листинг 20.9 (продолжение) — Автотест: идём вдоль комнаты и подбираем
— start — на уровне 8 предметов, здоровье 80%.
— forward 0.9 s — игрок прошёл через первую аптечку: здоровье 80 + 25 = 105, но не больше 100. Предметов осталось 7, сообщение висит.
— forward 0.7 s — прошли через вторую аптечку, но здоровье 100%, и она осталась лежать: всё ещё 7. Сообщение — от первой, оно висит две секунды.
— waited 2 s — сообщение исчезло.

Рисунок 20.4 — Кадр со старта: две аптечки, ближняя закрывает дальнюю
На рисунке 20.4 обе аптечки — и ближняя честно закрывает дальнюю. В окне робот делает то же: идёт вперёд и подбирает аптечку — рисунок 20.5.

Рисунок 20.5 — Контрольная точка: аптечка подобрана, сообщение вверху, здоровье 100%

Итоги главы
— Спрайт — плоская картинка, всегда повёрнутая к игроку.
— Позиция в системе камеры: depth — проекция на взгляд, side — проекция на «вправо».
— На экране: x = w/2 × (1 + side / (0,8 × depth)), размер h / depth, низ на полу.
— Прозрачность — альфа в старшем байте цвета: 0 — пикселя нет.
— Z-буфер — расстояние до стены в каждом столбце; спрайт рисуется, где он ближе стены.
— Спрайты между собой — от дальних к ближним: std::ranges::sort(v, std::greater{}, &Sprite::depth).
Контрольная точка: аптечки и патроны лежат в мире и подбираются. Но предметы скучные — коробки из прямоугольников. В следующей главе мы научимся рисовать кодом настоящих монстров: с объёмом, контуром и анимацией.
Упражнения
Упражнение 20.1. Что выведет? Листинг 20.10 сортирует предметы по глубине.

Листинг 20.10 — Что выведет программа?
Упражнение 20.2. Найди баг. В листинге 20.11 программист сортирует спрайты и рисует их по порядку. Ближний спрайт A на экране оказывается закрыт дальними. Почему?

Листинг 20.11 — Порядок рисования спрайтов
Упражнение 20.3. Напиши сам. Добавь на карту бочку — букву b. Бочку нельзя подобрать, она просто стоит. Нарисуй её кодом: зелёный цилиндр с тёмными обручами.
Упражнение 20.4. Подумай. Почему у спрайта и у стены на одном расстоянии одинаковая высота на экране? Как сделать маленький спрайт, например гильзу, если размер всегда h / depth?
Задачи
Задача 20.1. Сообщения по очереди. Если подобрать два предмета подряд за полсекунды, второе сообщение затирает первое. Сделай очередь сообщений: каждое показывается хотя бы секунду.
Задача 20.2. Бочки мешают проходить. Сделай, чтобы бочка из упражнения 20.3 была препятствием, как стена. Подумай: бочка — круглая, а стены — клетки.
Задача 20.3. Свечение. В Doom некоторые предметы светятся сами и не темнеют вдали. Добавь в Sprite флаг fullbright и для таких спрайтов используй яркость 256 вместо lightAt.
Глава 21. Рисуем монстров кодом
Аптечку можно собрать из трёх прямоугольников. С монстром так не выйдет: прямоугольный имп выглядит как шкаф с рогами. Живому существу нужны округлые формы, объём, контур — то, что художник делает кистью. Мы будем рисовать кодом, и наша кисть — функции: эллипс с объёмом, многоугольник, обводка.
Попутно разберём две вещи из C++, без которых такие кисти пишутся тяжело. Лямбды — маленькие функции прямо внутри кода, которые могут видеть переменные вокруг себя. И шаблоны функций — функции, которые работают с любым подходящим типом. Мы ими уже пользовались, не называя: std::ranges::sort из главы 20 — шаблон, а [](int x, int y) {…} в главе 14 — лямбда. Теперь разберёмся по-честному.
Лямбды и шаблоны на пальцах
Почти каждая кисть устроена одинаково: пройти по всем пикселям картинки и для каждого решить, красить ли его и каким цветом. Обход один и тот же, отличается только «решение». Значит, обход можно написать один раз, а решение передавать внутрь. Посмотрим на этот приём в консоли — листинг 21.1.

Листинг 21.1 — Эллипс и треугольник буквами: шаблон обхода и лямбды

Листинг 21.1 (продолжение) — Эллипс и треугольник буквами: шаблон обхода и лямбды
Строки 6–14 — шаблон функции. Слово template
Строки 21–24 — лямбда. Квадратные скобки — список захвата: что лямбда берёт из окружения. [&pic] — взять переменную pic по ссылке, чтобы лямбда могла её менять. Круглые скобки — параметры, как у обычной функции. Тело — в фигурных. Лямбду можно сохранить в переменную (её тип знает только компилятор, поэтому пишем auto) и вызывать как функцию: put(x, y, '@').
Строки 27–35 — лямбда с захватом [&]: «всё, что упомянуто, — по ссылке». Она видит cx, cy, rx, ry и put. Внутри — формула эллипса: точка внутри, если (x − cx)² / rx² + (y − cy)² / ry² ≤ 1. И сразу «объём»: чем ближе к левому верхнему краю, тем светлее, к краям — темнее. Буквы @, # и + — три уровня яркости.
Строки 36–56 — треугольник. Как понять, что точка внутри? Возьмём ребро AB и посмотрим, с какой стороны от него лежит точка. Это говорит знак векторного произведения — формула в лямбде cross. Внутри выпуклой фигуры точка лежит с одной и той же стороны от всех рёбер, обойдённых по кругу. Если для одних рёбер знак плюс, а для других минус — точка снаружи. Рисунок 21.1.

Рисунок 21.1 — Точка внутри выпуклого многоугольника — по одну сторону от всех рёбер

Кисти для спрайтов
Те же идеи, только на настоящей картинке Image с альфой. Набор кистей — листинги 21.2 и 21.3.

Листинг 21.2 — Кисти: обход, эллипс, многоугольник, обводка, подкраска

Листинг 21.2 (продолжение) — Кисти: обход, эллипс, многоугольник, обводка, подкраска
forEachPixel — тот же шаблон, что в листинге 21.1, но для Image. Шаблоны пишут целиком в заголовке: компилятор должен видеть их тело в каждом файле, где их вызывают, чтобы сделать нужную версию. Остальные функции — обычные, их тела в paint.cpp.

Листинг 21.3 — Кисти: реализация

Листинг 21.3 (продолжение) — Кисти: реализация

Листинг 21.3 (продолжение) — Кисти: реализация
lit (строки 11–20) умножает каждый канал цвета на яркость k и зажимает в 0…255 — так, в отличие от shade из главы 18, можно и осветлять, k больше единицы. Внутренняя лямбда ch захватывает k по значению: [k] — ей нужно только прочитать число.
fillEllipse (строки 22–36) — эллипс из листинга 21.1, только вместо трёх букв — плавная яркость.
fillPolygon (строки 38–57) — проверка знаков векторного произведения для любого числа вершин. Вершины передаются через std::span
outline (строки 59–74) обводит фигуру: прозрачный пиксель, рядом с которым есть непрозрачный, красится тёмным. Важная деталь — строка 60: смотрим мы на копию картинки, сделанную до обводки, а пишем в оригинал. Почему это важно, покажет упражнение 21.2.
tint (строки 76–89) подмешивает цвет ко всей фигуре — пригодится, когда монстр получит пулю и покраснеет.
Имп и зомби
Теперь можно рисовать. Имп и зомби — листинг 21.4. Каждый — пара десятков вызовов кистей, слой за слоем: сначала то, что сзади, потом то, что спереди. Ноги, туловище, руки, голова, глаза, обводка.

Листинг 21.4 — Монстры из эллипсов и многоугольников

Листинг 21.4 (продолжение) — Монстры из эллипсов и многоугольников

Листинг 21.4 (продолжение) — Монстры из эллипсов и многоугольников
Строки 13–24 — две мелкие кисти поверх fillPolygon: прямоугольник и треугольник. Обрати внимание на std::array
Строки 26–29 — анимация. В двух позах ноги сдвинуты на 3 пикселя в разные стороны, а руки — на полтора. Чередуем позы — и монстр переминается с ноги на ногу. В Doom у каждого монстра было по четыре кадра ходьбы, и смотрелось это ненамного богаче.
Строки 78–89 — все четыре картинки строятся один раз, при первом вызове. Здесь новый для нас приём: лямбда, которую тут же вызывают, — [] { … }(). Она строит массив кадров и возвращает его, а static инициализируется результатом. Удобно, когда для инициализации нужно несколько строк кода, а не одно выражение.
Конец ознакомительного фрагмента.
Текст предоставлен ООО «Литрес».
Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.
Приобретайте полный текст книги у нашего партнера:
