
Полная версия
MINI DOOM современный С++ через игру том 2 окно в ад

Алексей Примак
MINI DOOM современный С++ через игру том 2 окно в ад
Вместо предисловия
Это второй том книги «MINI DOOM. Современный C++ через игру». В первом томе ты прошёл основы C++ в консоли: переменные, ветвления, функции, контейнеры, свои типы, классы и память — и закончил консольным рогаликом, который проверяет AddressSanitizer.
Теперь из консоли выходим в окно: пиксели, игровой цикл, управление и текст на экране. Номера глав, листингов и рисунков в серии сквозные, поэтому ссылка «см. главу 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. Каждая глава начинается с проекта предыдущей, так что можно начать читать этот том, взяв готовый код последней главы прошлого тома.
Часть II. Окно в ад
Консоль своё отслужила. Чёрный экран с буквами годится для рогалика, но Doom — это пиксели: текстуры, огонь, кровь. В этой части мы откроем настоящее окно Windows, научимся красить в нём каждую точку, заведём игровой цикл, который крутится сотни раз в секунду, подключим клавиатуру и мышь и напишем свой шрифт. К концу части по окну будет бегать квадратный «игрок», которого ты ведёшь мышью и клавишами, а в углу будет тикать счётчик кадров.
Одно важное устройство этой части. Код, который общается с Windows, мы с самого начала будем держать отдельно от кода, который рисует и считает. Так сделан и настоящий MINI DOOM: вся игра не знает о Windows ничего, а маленький файл main_win32.cpp передаёт ей нажатия клавиш и показывает готовые кадры. Поэтому код игры можно проверять автотестами без всякого окна — и мы будем это делать.

Глава 9. Своё окно
Окно в Windows — не просто прямоугольник на экране. Это объект, который живёт своей жизнью: его двигают, сворачивают, закрывают, перекрывают другими окнами, и обо всём этом Windows сообщает твоей программе. Программа должна внимательно слушать и вовремя отвечать, иначе Windows решит, что она зависла, и напишет в заголовке «Не отвечает».
В этой главе мы напишем минимальную программу с окном на чистом Win32 API — без библиотек и мастеров проектов. Строк будет немного, но каждая важна: на этом скелете будет висеть весь MINI DOOM.
Как Windows разговаривает с программой
Представь окно ресторана выдачи. Windows — кухня, которая непрерывно подсовывает в окошко записки: «по окну щёлкнули мышью», «нажали клавишу», «окно надо перерисовать», «окно закрывают». Каждая записка называется сообщением. Записки складываются в очередь сообщений твоей программы, и программа должна по одной вынимать их и обрабатывать.
Вынимает записки цикл сообщений — обычный while в конце wWinMain. А обрабатывает их оконная процедура — функция, которую ты пишешь сам и сообщаешь Windows её адрес. Windows вызывает её для каждого сообщения, а ты решаешь: ответить самому или передать стандартному обработчику. Вся схема — на рисунке 9.1.

Рисунок 9.1 — Цикл сообщений: Windows кладёт записки в очередь, программа вынимает их и отдаёт оконной процедуре

Первое окно
Вот программа целиком — листинг 9.1. Она открывает окно 1280 × 800 с заголовком MINI DOOM и заливает его чёрным. Файл назван main_win32.cpp: в нём живёт всё, что связано с Windows, и в следующих главах этот файл будет только расти.

Листинг 9.1 — Минимальная программа с окном

Листинг 9.1 (продолжение) — Минимальная программа с окном
Собираем, запускаем — и вот оно, рисунок 9.2. Пустое чёрное окно, но это наше окно: его можно двигать, сворачивать, разворачивать на весь экран и закрывать крестиком.

Рисунок 9.2 — Первое окно MINI DOOM — настоящий запуск программы из листинга 9.1
Разберём три главных шага, они отмечены в комментариях цифрами.
Шаг 1, строки 34–42 — класс окна. Прежде чем создать окно, Windows хочет знать его «породу»: какая у окон этого вида оконная процедура, какой курсор, как называется порода. Это описание — структура WNDCLASSEXW — регистрируется функцией RegisterClassExW. Пустые фигурные скобки в объявлении wc{} обнуляют все поля, а мы заполняем только нужные. Поле cbSize — размер самой структуры: так Windows понимает, с какой версией структуры имеет дело.
Шаг 2, строки 44–53 — само окно. Мы хотим, чтобы картинка внутри окна была ровно 1280 × 800, но окно — это ещё рамка и заголовок. AdjustWindowRect берёт желаемый размер картинки и увеличивает его на толщину рамки и заголовка. Потом CreateWindowExW создаёт окно по зарегистрированному классу, а ShowWindow показывает его. Чем окно отличается от его клиентской области, видно на рисунке 9.3.

Рисунок 9.3 — Окно — это заголовок, рамка и клиентская область; AdjustWindowRect добавляет рамку к нужному размеру картинки
Шаг 3, строки 55–59 — цикл сообщений. GetMessageW ждёт, пока в очереди появится записка, и кладёт её в msg. TranslateMessage превращает нажатия клавиш в символы — для игры это не важно, но так принято. DispatchMessageW отдаёт записку нашей оконной процедуре. Цикл крутится, пока GetMessageW возвращает положительное число. Когда приходит сообщение о выходе, она вернёт ноль — и программа завершится.
Строки 9–28 — оконная процедура. Мы обрабатываем всего два сообщения. WM_PAINT приходит, когда окну нужно перерисоваться — например, его вытащили из-за другого окна. Мы заливаем всё чёрным между BeginPaint и EndPaint. WM_DESTROY приходит, когда окно уничтожено, и PostQuitMessage(0) кладёт в очередь то самое сообщение о выходе, которое остановит цикл. Всё остальное — передвижение, изменение размера, клик по крестику — отдаём стандартному обработчику DefWindowProcW на строке 27.
Обрати внимание на букву W на конце почти всех функций и на букву L перед строками: L"MINI DOOM". Это «широкие» версии функций и строк для Unicode. Обычная строка хранит символы в char, по байту на символ, а широкая — в wchar_t. Сколько места это занимает в Windows, покажет листинг 9.2: я собрал его компилятором под Windows и запустил как настоящую Windows-программу.

Листинг 9.2 — Размеры обычных и широких строк в Windows
В Windows wchar_t занимает два байта, и строка L"DOOM" весит вдвое больше обычной. Этими двухбайтовыми символами — кодировкой UTF-16 — Windows и говорит с программами начиная с Windows NT. Поэтому мы пишем W-функции и L-строки: так русские, японские и любые другие буквы в заголовках окон работают без кракозябр.
А последняя строка вывода — привет из главы 2. Помнишь грабли про long, который в Windows занимает 4 байта, а в Linux — 8? Вот они, живьём: Windows-программа честно сообщает 4. Если собрать тот же файл под Linux, получится wchar_t в 4 байта и long в 8. Типы в C++ не обязаны иметь одинаковый размер на разных системах, и об этом полезно помнить.


Таблица 9.1 — Функции Win32, из которых собрано первое окно
Сборка
В Visual Studio создай пустой проект C++ — не «Консольное приложение» и не «Классическое приложение Windows», последнее генерирует кучу лишнего кода. Добавь файл main_win32.cpp, выставь настройки строгости из таблицы 1.2 и ещё две из таблицы 9.2.

Таблица 9.2 — Дополнительные настройки проекта с окном
Я собираю тот же файл компилятором clang под Windows — листинг 9.3. Первая попытка специально без режима Unicode, вторая — правильная.

Листинг 9.3 — Сборка под Windows: без Unicode и с ним
Без Unicode сборка падает на строке 38. Дело в том, что у большинства функций Windows по две версии: LoadCursorA для старых однобайтовых строк и LoadCursorW для Unicode. Константа IDC_ARROW тоже бывает двух видов, и какой именно — решает макрос UNICODE. Мы вызвали W-функцию, а IDC_ARROW оказался A-вида — типы не сошлись. Флаг -municode включает Unicode и заодно говорит, что точка входа — wWinMain. Флаг -mwindows — что это оконная программа, а не консольная. Во второй раз компилятор промолчал, а команда file подтвердила: получился исполняемый файл Windows с графическим интерфейсом, GUI.



Итоги главы
— Windows общается с программой сообщениями, которые копятся в очереди.
— Цикл сообщений вынимает их GetMessageW и отдаёт DispatchMessageW оконной процедуре.
— Оконная процедура обрабатывает нужные сообщения, остальные отдаёт DefWindowProcW.
— Окно создаётся по зарегистрированному классу; AdjustWindowRect считает размер с рамкой.
— WM_DESTROY → PostQuitMessage → GetMessageW возвращает 0 → программа выходит.
— Функции и строки — W-версии и L"..."; подсистема — Windows, точка входа — wWinMain.
Контрольная точка: у тебя есть чёрное окно MINI DOOM, которое честно отвечает Windows и закрывается крестиком. В главе 10 мы перестанем заливать его чёрной кистью и начнём красить каждый пиксель сами.
Упражнения
Упражнение 9.1. Что выведет? Программа из листинга 9.4 собрана под Windows. Что она напечатает? Будь внимателен с последним числом.

Листинг 9.4 — Упражнение 9.1
Упражнение 9.2. Найди баг. В программе из листинга 9.5 окно открывается и закрывается крестиком, но если посмотреть в диспетчер задач, процесс doom.exe остаётся висеть. Что сломано?

Листинг 9.5 — Упражнение 9.2

Листинг 9.5 (продолжение) — Упражнение 9.2
Упражнение 9.3. Напиши сам. Сделай так, чтобы окно закрывалось по клавише Esc. Подсказка: нажатие клавиши приходит сообщением WM_KEYDOWN, а код клавиши лежит в параметре wp; у Esc это VK_ESCAPE.
Упражнение 9.4. Подумай. Что случится, если в обработчике WM_PAINT вместо пары BeginPaint и EndPaint просто ничего не делать и вернуть 0?
Задачи
Задача 9.1. Окно фиксированного размера. Сделай окно, у которого нельзя менять размер мышью и которое нельзя развернуть на весь экран. Подсказка: стиль WS_OVERLAPPEDWINDOW — это комбинация нескольких флагов; убери из неё лишние оператором & и ~.
Задача 9.2. Подтверждение выхода. При нажатии крестика Windows присылает WM_CLOSE. Перехвати его и спроси игрока через MessageBoxW, точно ли он хочет выйти. Закрой окно, только если ответ «Да».
Задача 9.3. Счётчик сообщений. Заведи в оконной процедуре статический счётчик и выводи число полученных сообщений в заголовок окна через SetWindowTextW. Подвигай окно мышью и посмотри, сколько сообщений приходит за секунду.
Глава 10. Пиксели
Чёрное окно из прошлой главы залито кистью Windows. Это удобно, но для игры бесполезно: кисть умеет красить прямоугольники, а нам нужны огонь, текстуры и рожа импа. Поэтому мы поступим, как настоящие игры девяностых: заведём в памяти свой кусок экрана, будем красить в нём каждую точку сами, а Windows попросим только показать готовую картинку.
В этой главе разберёмся, как устроен цвет, как двумерная картинка лежит в одномерной памяти и как одним вызовом растянуть её на всё окно. Попутно наступим на три настоящих грабли — и все три снимем на скриншоты.
Пиксель и цвет
Экран — это сетка крошечных лампочек, пикселей. Каждый пиксель светится смесью трёх цветов: красного, зелёного и синего — отсюда сокращение RGB. Яркость каждого канала — число от 0 до 255, ровно один байт. Все три по нулям — чёрный, все по 255 — белый, красный 255 при нулевых остальных — чистый красный. Всего так можно смешать 256³, то есть почти 17 миллионов цветов.
Три байта удобно хранить в одном 32-битном числе std::uint32_t. Мы будем раскладывать их так: старший байт пустой, дальше красный, зелёный и синий. В шестнадцатеричной записи это выглядит как 0x00RRGGBB — каждые две цифры ровно байт, поэтому цвет читается прямо глазами. Как собрать такое число из трёх каналов и разобрать обратно, показывает листинг 10.1.

Листинг 10.1 — Упаковка и распаковка цвета сдвигами и масками
Строка 8 собирает цвет. Оператор << сдвигает биты числа влево: r << 16 переносит красный на 16 бит выше, в третий байт; g << 8 — во второй. Синий остаётся на месте. Оператор | («или» для битов) складывает три сдвинутых числа в одно, не мешая друг другу, — ведь каждое занимает свой байт. Как биты разъезжаются по местам, показано на рисунке 10.1.

Рисунок 10.1 — Цвет в одном 32-битном числе: по байту на канал
Строки 16–18 разбирают цвет обратно. Сдвиг вправо опускает нужный байт в самый низ, а маска & 0xFF отрезает всё, что выше: оставляет только младшие восемь бит. В выводе видно, что из 0x00FF7828 вернулись те же 255, 120 и 40.
Строка 22 затемняет цвет вдвое — каждый канал пополам. Этим приёмом в главе 18 мы будем погружать дальние стены во тьму. А формат {:08X} в std::format печатает число шестнадцатеричными цифрами, восемь знаков с ведущими нулями: так байты цвета видны по отдельности.

Картинка в памяти
Наша картинка — 640 × 400 пикселей. Как хранить двумерную сетку? Можно вектором векторов, но это четыреста отдельных кусков памяти. Игры поступают проще: один длинный ряд из 640 × 400 = 256 000 чисел, строки картинки лежат друг за другом, как вагоны поезда. Пиксель (x, y) живёт по номеру y × width + x: пропусти y целых строк и отсчитай x в нужной. Проверим на крошечном экране 4 × 3 — листинг 10.2.

Листинг 10.2 — Двумерный экран в одномерном векторе
Первая строка вывода — память как она есть: двенадцать символов подряд. Остальные три — тот же вектор, нарезанный по четыре: так его видим мы. Буква C в точке (1, 2) лежит под номером 2 × 4 + 1 = 9. Схема — на рисунке 10.2.

Рисунок 10.2 — Экран 4 × 3 и тот же экран в памяти: строки идут друг за другом
У такой укладки есть ловушка, и она в листинге 10.3. Программа хочет обвести экран 640 × 400 рамкой: левый край — пиксель x = 0, правый — x = width. Собрано с санитайзером.

Листинг 10.3 — Правый край на пиксель дальше, чем нужно
Санитайзер нашёл запись за концом вектора на строке 13. Но посмотри внимательнее, когда он её нашёл: на последней, четырёхсотой строке. Правый край — x = width, а последний пиксель строки — width − 1. Для всех строк, кроме последней, пиксель «x = width» — это на самом деле первый пиксель следующей строки. Программа молча красила левый край строкой ниже, и ни один сторож этого не заметил. Только на последней строке «следующей строки» уже нет, и запись ушла за конец памяти.

Холст: класс Canvas
Обернём кусок памяти в класс — как учились в главе 7. Холст знает свою ширину и высоту, умеет залить всё одним цветом, поставить точку и закрасить прямоугольник. И ничего не знает про Windows: это обычный C++. Заголовок — листинг 10.4.
Конец ознакомительного фрагмента.
Текст предоставлен ООО «Литрес».
Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.









