Как самому создать 3D engine типа DOOM 1995
С наступившим 2021 годом вас форумчане.
Я хотел написать 3D движок как типа DOOM 1995 года
не совсем такой без врагов но со стенами и текстурами
и потолком то есть совсем простой движок
Если кто-то знает как помочь мне хотя бы намекните каким должен быть код движка
пишу на SFML 2.4.2 & C++ (CLion)
С Уважением, Сергей
Использование библиотек в doom engine (id tech 1)
Здравствуйте! Прошу помощи у тех, кто хотя бы несколько знает об движке doom. Использовались ли в.
Как создать самому конструктор сайтов?
Всем привет. Имея только знания HTML и CSS, как создать конструктор сайтов самому? Может есть.
Как создать engine js?
Всем доброго времен суток. Есть такая ссылка на сайте: https://wmglobus.com/engine_js/?loc Как.
GoshaM, Я знаю про этот урок и он мне не понравился там нет
полного кода движка за 10 минут, а извините меня модераторы просто разглагольствования
я же хочу увидеть полный код самого простого движка
Добавлено через 6 минут
я вижу одну ошибку.
Решение
Консольную программу на 3D engine файл называется CommandLineFPS.cpp с GITHUB ссылка выше
FPS зашкаливает за 1000 кадров
Добавлено через 2 часа 14 минут
Как Вы думаете игровые движки типа Dunia Engine после компилевки в каком формате используются? в dll?
Вообще прочитал что такое игровой движок на википедии. но на самом деле интересно в каком он.
Как самому создать сайт по недвижимости — за неделю, без знаний?
Добрый вечер! Хочу за неделю создать свой сайт по купле-продаже недвижимости. Никаких знаний у.
Как создать динамический двумерный массив и самому его заполнить?
Как создать динамический двумерный массив и самому его заполнить?
Как самому создать живой CD Windows XP SP3 с нужным мне ПО
Как самому создать живой CD Windows XP SP3 с нужным мне ПО, при помощи какой проги это можно.
Как самому создать цикл, который будет работать по моей технологии
Я хочу разработать цикл, который будет работать автономно (т.е. тоже генерировать итерации) без.
Исходный код Doom 3 – будь проще
Если вы забьете в поисковик запрос «лучший исходный код на C++», чаще всего в выдаче будет встречаться упоминание исходного кода Doom 3 вкупе с отзывами наподобие этого:
«Я провел немало времени, изучая исходный код Doom3. Думаю, это самый чистый и приятный код из всех виденных мною».
Doom 3 – это видеоигра, разработанная студией id Software и выпущенная компанией Activision. Игра принесла id Software коммерческий успех, разойдясь тиражом более 3,5 миллионов копий.
Спустя 7 лет после выхода игры, 23 ноября 2011 года студия id Software представила исходный код своего старого движка Doom 3. Код был тщательно изучен разработчиками, и вот, на примере CoderGears Blog, какого мнения придерживается большинство из них:
Doom 3 написан на C++, таком разнообразном языке программирования, что с его помощью можно создать как безупречно изящный код, так и жутких поганищ, от одного вида которых режет глаза. К счастью, id Software решила придерживаться паттернов, близких к парадигме «Си с классами», поэтому мозг легко воспринимает этот код:
Многие эксперты по C++ больше не рекомендуют подход «Си с классами», однако Doom3 создавали в период с 2000 до 2004 года, так что разработчики просто не имели под рукой современных механизмов программирования на C++.
Давайте углубимся в код игры при помощи CppDepend (инструмент, который помогает разработчику получить нужную информацию из исходного кода, проверка правильности иерархической структуры проекта, отслеживание внесенных изменений со времени последней ревизии или оценка качества кода) и узнаем, что делает его таким особенным.
Doom 3 поделен на модули при помощи нескольких проектов. Ниже представлен список этих проектов и немного статистических данных об их типах:
А вот график зависимостей, показывающий отношения между ними:
Doom 3 использует много глобальных функций. Тем не менее, основную работу берут на себя классы.
Модель данных определяется при помощи структур. Для того, чтобы составить представление об использовании структур в исходном коде, взгляните на представленное ниже изображение, где структуры отображены в виде синих прямоугольников. Основание кода здесь представлено посредством метода Treemap. Тримапинг – это метод отображения древовидной структуры данных в виде сцепленных прямоугольников. Представленная древовидная структура – это стандартная иерархия кода:
Как видите, многие структуры явно определены. К примеру, более 40% типов DoomDLL – это структуры. Они систематически используются для определения модели данных. Эта практика используется во многих проектах, однако подобных подход чреват проблемами в случае с многопотоковыми приложениями. И в самом деле, структуры с общедоступными областями нельзя назвать неизменяемыми.
У неизменяемых объектов есть одно большое преимущество: они значительно упрощают параллельное программирование. Задумайтесь: почему написание качественной многопотоковой программы – такая сложная задача? Дело в том, что существует проблема синхронизации доступа потоков к ресурсам (объектам или другим ресурсам ОС). Почему так сложно настроить синхронизацию доступа? Потому что сложно гарантировать отсутствие режима конкуренции между множественными доступами к чтению и записи, которые возникают при обращении множества потоков к множеству объектов. А что, если доступы к записи исчезнут? Другими словами, что, если состояние объектов, к которым получают доступ потоки, не будет меняться? В таком случае необходимость в синхронизации отпадает!
Давайте найдем классы, в которых есть как минимум один базовый класс:
Почти 40% структур и классов имеют базовый класс. Как правило, одним из преимуществ наследования в объектно-ориентированном программировании является полиморфизм. Синим цветом здесь обозначены виртуальные методы, заданные в исходном коде:
Более 30% методов – виртуальные. Правда, лишь немногие из них полностью виртуальные. Вот список всех заданных абстрактных классов:
Только 52 из них – абстрактные классы; чистых интерфейсов здесь 35, то есть все их виртуальные методы чистые.
Давайте поищем методы с использованием RTTI.
Только несколько методов используют RTTI.
Подведем итоги: только базовые концепции ООП, никаких продвинутых шаблонов разработки, никакого злоупотребления интерфейсами и абстрактными классами, ограниченное использование RTTI, определение данных в виде структур.
Пока наш код ничем особо не отличается от многих других проектов на басе «Си с классами», которые так критикуют многие программисты на C++.
Вот несколько интересных решений от разработчиков Doom 3, которые помогут нам понять их секрет:
Используйте общий базовый класс с полезными сервисами
Многие классы наследуют от idClass:
idClass предоставляет следующие сервисы:
Облегчите построчную обработку
Как правило, string – наиболее часто используемый тип данных в проекте. Зачастую обработка ведется именно при помощи string, а для управления ими нам потребуются функции.
Doom 3 использует класс idstr, который содержит почти все нужные методы управления строками. Вам не нужно задавать собственный метод, как часто бывает со многими строковыми классами, которые предоставляют другие фреймворки.
Исходный код отделен от GUI-фреймворка (MFC)
Во многих проектах, где задействуется MFC, код тесно сплетен с их типами, и вы находите типы MFC в самых разных частях кода.
Что же касается Doom3, код здесь отделен от MFC, и только GUI-классы и MFC имеют прямую зависимость. Взгляните на этот запрос CQLinq:
Подобное решение значительно влияет на производительность. И в самом деле, фреймворк MFC – это вотчина разработчиков GUI, поэтому другие разработчики не должны тратить свое время на MFC.
Очень хорошая вспомогательная библиотека idlib
Почти во всех проектах очень часто используются вспомогательные классы. Это показано в результатах следующего запроса:
Как видите, больше всего здесь именно вспомогательных классов. Если разработчики не используют качественный фреймворк для вспомогательных классов, они тратят большую часть времени на борьбу с техническим слоем.
Idlib обеспечивает вас полезными классами со всеми необходимыми методами для управления строками, контейнерами и памятью. Это облегчает работу разработчикам и позволяет им уделять больше внимания логике.
Реализация проста для понимания
В Doom 3 используется встроенный компилятор, а программисты на C++ знают, что написание парсеров и компиляторов – весьма трудоемкая задача. С другой стороны, реализация Doom 3 проста для понимания, а код игры чист и изящен.
Вот график зависимости используемых компилятором классов:
А это фрагмент программы из исходного кода компилятора:
Мы изучили исходный код многих парсеров и компиляторов, но нам впервые попадается компилятор с таким легким для понимания кодом, что можно сказать обо всем исходном коде Doom 3. Это настоящая магия. Исследуя исходный код Doom 3, так и хочется воскликнуть: «Как же он прекрасен!»
Выводы
Несмотря на то, что в Doom3 используются самые базовые дизайнерские решения, разработчики постарались сделать так, чтобы они могли сосредоточиться на игровой логике.
С другой стороны, при использовании метода «Си с классами» нужно четко понимать, что вы делаете. Нужно быть настоящим профессионалом не хуже разработчиков Doom 3. Начинающим программистам не рекомендуется рисковать и игнорировать современные рекомендации по C++.
Пишем клон движка Doom: чтение информации карт
Введение
Цель этого проекта — создание клона движка DOOM, использующего ресурсы, выпущенные вместе с Ultimate DOOM (версия со Steam).
Он будет представлен в виде туториала — я не хочу добиваться в коде максимальной производительности, а просто создам работающую версию, и позже начну её улучшать и оптимизировать.
У меня нет опыта создания игр или игровых движков, и мало опыта в написании статей, поэтому можете предлагать свои изменения или даже полностью переписать код.
Вот список ресурсов и ссылок.
Книга Game Engine Black Book: DOOM Фабьена Санглара. Одна из лучших книг по внутреннему устройству DOOM.
Требования
Необязательное
Мысли
Не знаю, смогу и я завершить этот проект, но приложу для этого все силы.
Моей целевой платформой будет Windows, но поскольку я использую SDL, будет просто заставить движок работать под любой другой платформой.
А пока установим Visual Studio!
Проект был переименован из Handmade DOOM в Do It Yourself Doom with SLD (DIY Doom), чтобы его не путали с другими проектами под названием «Handmade». В туториале есть несколько скриншотов, на которых он всё ещё называется Handmade DOOM.
Файлы WAD
Прежде чем приступать к кодингу, давайте поставим перед собой цели и продумаем, чего мы хотим достигнуть.
Для начала давайте проверим, сможем ли мы считывать файлы ресурсов DOOM. Все ресурсы DOOM находятся в файле WAD.
Что такое файл WAD?
«Where is All my Data»? («Где все мои данные»?) Они в WAD! WAD — это архив всех ресурсов DOOM (и игр на основе DOOM), находящийся в одном файле.
Разработчики Doom придумали этот формат, чтобы упростить создание модификаций игры.
Анатомия файла WAD
Файл WAD состоит из трёх основных частей: заголовка (header), «кусков» (lumps), и каталогов (directories).
Формат заголовка
Размер поля | Тип данных | Содержимое |
---|---|---|
0x00-0x03 | 4 символа ASCII | Строка ASCII (со значениями «IWAD» или «PWAD»). |
0x04-0x07 | unsigned int | Номер элемента каталогов. |
0x08-0x0b | unsigned int | Значение смещения на каталог в файле WAD. |
Формат каталогов
Размер поля | Тип данных | Содержимое |
---|---|---|
0x00-0x03 | unsigned int | Значение смещения на начало lump-данных в файле WAD. |
0x04-0x07 | unsigned int | Размер «куска» (lump) в байтах. |
0x08-0x0f | 8 символов ASCII | ASCII, содержащие название «куска». |
Архитектура
Примечание: такая архитектура может быть и неоптимальной, и при необходимости мы будем её изменять.
Приступаем к коду
Давайте добавим два новых класса: WADLoader и WADReader. Начнём с реализации WADLoader.
Реализовать конструктор будет просто: инициализируем указатель данных и храним копию передаваемого пути к файлу WAD.
Теперь давайте приступим к реализации вспомогательной функции загрузки OpenAndLoad : просто попробуем файл открыть как двоичный и в случае неудачи выведем ошибку.
Если всё будет хорошо, и мы сможем находить и открывать файл, то нам понадобится знать размер файла, чтобы выделить память для копирования в неё файла.
Теперь мы знаем, сколько места занимает полный WAD, и выделим нужное количество памяти.
Скопируем содержимое файла в эту память.
Давайте вкратце проверим код и убедимся, что всё работает. Но прежде нам нужно реализовать LoadWAD. Пока LoadWAD будет вызывать «OpenAndLoad»
И давайте добавим в функцию main код, создающий экземпляр класса и пытающийся загрузить WAD
Нужно будет ввести правильный путь к вашему файлу WAD. Давайте запустим!
Ой! Мы получили консольное окно, которое просто открывается на несколько секунд! Особо ничего полезного… работает ли программа? Идея! Давайте взглянем на память, и посмотрим, что в ней! Возможно, там мы найдём что-нибудь особенное! Для начала разместим точку останова, дважды щёлкнув слева от номера строки. Вы должны увидеть нечто подобное:
Я поместил точку останова сразу после чтения всех данных из файла, чтобы посмотреть на массив памяти и увидеть, что в него загружено. Теперь снова запустим код! В автоматическом окне я вижу несколько первых байтов. В первых 4 байтах написано «IWAD»! Отлично, работает! Никогда не думал, что этот день настанет! Так, ладно, нужно успокоиться, впереди ещё много работы!
Считывание заголовка
Общий размер заголовка составляет 12 байт (от 0x00 до 0x0b), эти 12 байт разделены на 3 группы. Первые 4 байта — это тип WAD, обычно «IWAD» или «PWAD». IWAD должен быть официальным WAD, выпущенным ID Software, «PWAD» должен использоваться для модов. Другими словами, это просто способ определить, является ли файл WAD официальным релизом, или выпущен моддерами. Заметьте, что строка не NULL terminated, поэтому внимательнее! Следующие 4 байта являются unsigned int, в котором содержится общее количество каталогов в конце файла. Следующие 4 байта обозначают смещение первого каталога.
Давайте добавим структуру, которая будет хранить информацию. Я добавлю новый файл заголовка и назову его «DataTypes.h». В нём мы будем описывать все нужные нам struct.
Теперь нам нужно реализовать класс WADReader, который будет считывать данные из загруженного массива байтов WAD. Ой! Тут есть хитрость — файлы WAD имеют формат big-endian, то есть нам нужно будет сдвинуть байты, чтобы сделать их little-endian (сегодня в большинстве систем используется little endian). Для этого мы добавим две функции, одну для обработки 2 байт (16 бит), другую — для обработки 4 байт (32 бит); если нам нужно считать только 1 байт, то делать ничего не надо.
Теперь мы готовы к считыванию заголовка: считаем первые четыре байта как char, а затем добавим к ним NULL, чтобы упростить себе работу. В случае с количеством каталогов и их смещением можно просто использовать вспомогательные функции для преобразования их в правильный формат.
Давайте соединим всё вместе, вызовем эти функции и выведем результаты
Запустим программу и посмотрим, всё ли работает!
Отлично! Строку IWAD хорошо заметно, но правильны ли другие два числа? Попробуем считать каталоги при помощи этих смещений и посмотрим, получится ли!
Нам нужно добавить новую struct для обработки каталога, соответствующего представленным выше параметрам.
Теперь давайте дополним функцию ReadDirectories: считаем смещение и выведем их!
В каждой итерации мы умножаем i * 16, чтобы перейти к инкременту смещения следующего каталога.
Запустим код и посмотрим, что получится. Ого! Большой список каталогов.
Судя по названию lump, можно предположить, что нам удалось правильно считать данные, но возможно есть способ получше, чтобы проверить это. Мы взглянем на записи WAD Directory при помощи Slade3.
Похоже, что название и размер lump соответствуют данным, полученным при помощи нашего кода. Сегодня мы проделали отличную работу!
Другие примечания
DOOMReboot: полностью несогласен. 15 МБ ОЗУ в наши дни — совершенная мелочь, и считывание из памяти будет значительно быстрее, чем объёмные fseek, которые придётся использовать после загрузки всего, необходимого для уровня. Это увеличит время загрузки не меньше, чем на одну-две секунды (у меня всё время загрузки занимает меньше 20 мс). fseek задействуют ОС. У которой файл скорее всего находится в кэше ОЗУ, но может быть и нет. Но даже если он там, это большая трата ресурсов и эти операции запутают множество считываний WAD с точки зрения кэша ЦП. Самое лучшее, что можно создать гибридные методы загрузки и хранить данные WAD для уровня, которые помещаются в кэш L3 современных процессоров, где экономия окажется потрясающей.
Исходный код
Базовые данные карт
Научившись считывать файл WAD, давайте попробуем использовать прочитанные данные. Будет здорово научиться считывать данные миссии (мира/уровня) и применять их. «Куски» данных миссий (Mission Lumps) должны быть чем-то сложным и хитрым. Поэтому нам нужно будет двигаться и нарабатывать знания постепенно. В качестве первого небольшого шага давайте создадим нечто наподобие функции автокарты (Automap): двухмерного плана карты с видом сверху. Для начала посмотрим, что находится внутри Mission Lump.
Анатомия карты
Начнём сначала: описание уровней DOOM очень похоже на 2D-чертёж, на котором стены обозначены линиями. Однако для получения 3D-координат каждая стена берёт высоты пола и потолка (XY — это плоскость, по которой мы движемся горизонтально, а Z — это высота, позволяющая двигаться вверх и вниз, например, поднимаясь на лифте или спрыгнув вниз с платформы. Эти три компоненты координаты используются для рендеринга миссии как 3D-мира. Однако для обеспечения хорошей производительности движок имеет определённые ограничения: на уровнях нет расположенных одна над другой комнат и игрок не может смотреть вверх-вниз. Ещё одна интересная особенность: снаряды игрока, например, ракеты, поднимаются по вертикали, чтобы попасть в цель, расположенную на более высокой платформе.
Эти любопытные особенности стали причиной бесконечных холиваров по поводу того, является ли DOOM 2D- или 3D-движком. Постепенно был достигнут дипломатический компромисс, спасший множество жизней: стороны сошлись на приемлемом для обеих обозначении «2.5D».
Чтобы упростить задачу и вернуться к теме, давайте просто попробуем считать эти 2D-данные и посмотреть, можно ли их как-то использовать. Позже мы попробуем отрендерить их в 3D, а пока нам нужно разобраться в том, как работают совместно отдельные части движка.
Проведя исследования, я выяснил, что каждая миссия составлена из набора «кусков». Эти «куски» (Lumps) всегда представлены в файле WAD игры DOOM в одинаковом порядке.
Показанная выше демо-карта имеет следующие характеристики:
Формат вершин
Как и можно ожидать, данные вершин очень просты — всего лишь x и y (точка) каких-то координат.
Размер поля | Тип данных | Содержимое |
---|---|---|
0x00-0x01 | Signed short | Позиция X |
0x02-0x03 | Signed short | Позиция Y |
Формат Linedef
В Linedef содержится больше информации, он описывает линию, соединяющую две вершины, и свойства этой линии (которая позже станет стеной).
Размер поля | Тип данных | Содержимое |
---|---|---|
0x00-0x01 | Unsigned short | Начальная вершина |
0x02-0x03 | Unsigned short | Конечная вершина |
0x04-0x05 | Unsigned short | Флаги (подробнее см. ниже) |
0x06-0x07 | Unsigned short | Тип линии/действие |
0x08-0x09 | Unsigned short | Метка сектора |
0x10-0x11 | Unsigned short | Передний sidedef (0xFFFF — стороны нет) |
0x12-0x13 | Unsigned short | Задний sidedef (0xFFFF — стороны нет) |
Значения флагов Linedef
Не все линии (стены) отрисовываются. Некоторые из них имеют особое поведение.
Бит | Описание |
---|---|
0 | Преграждает путь игрокам и монстрам |
1 | Преграждает путь монстрам |
2 | Двусторонняя |
3 | Верхняя текстура отключена (об этом мы поговорим позже) |
4 | Нижняя текстура отключена (об этом мы поговорим позже) |
5 | Секрет (на автокарте показывается как односторонняя стена) |
6 | Препятствует звуку |
7 | Никогда не показывается на автокарте |
8 | Всегда показывается на автокарте |
Архитектура
Для начала давайте создадим класс и назовём его map. В нём мы будем хранить все данные, связанные с картой.
Пока я планирую только хранить как вектор вершины и linedefs, чтобы применить их позже.
Также давайте дополним WADLoader и WADReader, чтобы мы могли считывать эти два новых элемента информации.
Кодинг
Код будет похож на на код чтения WAD, мы только добавим ещё несколько структур, а затем заполним их данными из WAD. Начнём с добавления нового класса и передачи названия карты.
Теперь добавим структуры, чтобы считывать эти новые поля. Поскольку мы уже несколько раз это делали, просто добавим их все сразу.
Далее нам понадобится функция для считывания их из WADReader, она будет близка к тому, что мы делали ранее.
Думаю, для вас здесь нет ничего нового. А теперь нам нужно вызвать эти функции из класса WADLoader. Позвольте изложить факты: здесь важна последовательность lumps, мы найдём название карты в lump каталога, за которым в заданном порядке будут следовать все lumps, связанные с картами. Чтобы упростить себе задачу и не отслеживать индексы lumps по отдельности, мы добавим перечисление, позволяющее избавиться от магических чисел.
Также я добавлю функцию для поиска карты по её названию в списке каталогов. Позже мы скорее всего повысим производительность этого шага, использовав структуру данных карт, потому что здесь присутствует значительное количество записей, и нам придётся довольно часто проходить по ним, особенно в начале загрузки таких ресурсов, как текстуры, спрайты, звуки и т.д.
Ого, мы почти закончили! Теперь давайте просто считаем VERTEXES! Повторюсь, мы уже делали такое раньше, теперь вы должны разбираться в этом.
Хм, похоже, что мы постоянно копипастим один и тот же код; возможно, в дальнейшем его придётся оптимизировать, но пока вы реализуете ReadMapLinedef самостоятельно (или посмотрите на исходный код по ссылке).
Финальные штрихи — нам нужно вызвать эту функцию и передать ей объект карты.
Теперь давайте изменим функцию main и посмотрим, всё ли будет работать. Я хочу загрузить карту «E1M1», которую передам в объект карты.
Теперь давайте всё это запустим. Ого, куча интересных чисел, но верны ли они? Давайте проверим!
Посмотрим, сможет ли slade помочь нам и в этом.
Мы можем найти карту в меню slade и посмотреть на подробности lumps. Давайте сравним числа.
А как насчёт Linedef?
Также я добавил это перечисление, которое мы попробуем использовать при отрисовке карты.
Другие примечания
В процессе написания кода я ошибочно считывал больше байтов, чем нужно, и получал неверные значения. Для отладки я начал смотреть на смещение WAD в памяти, чтобы понять, нахожусь ли я на нужном смещении. Это можно сделать при помощи окна памяти Visual Studio, которые оказываются очень полезным инструментом при отслеживании байтов или памяти (также в этом окне можно устанавливать точки останова).
Если вы не видите окно памяти, то перейдите в Debug > Memory > Memory.
Теперь мы видим значения в памяти в шестнадцатеричном виде. Эти значения можно сравнить с hex-отображением в slade, нажав правой клавишей на любой lump и отобразив его как hex.
Сравниваем их с адресом загруженного в память WAD.
И последнее на сегодня: мы увидели все эти значения вершин, но есть ли простой способ визуализировать их без написания кода? Я не хочу тратить время на это, просто чтобы выяснить, что мы движемся не в том направлении.
Наверняка уже кто-то создал графопостроитель. Я загуглил «draw points on a graph» и первым результатом оказался веб-сайт Plot Points — Desmos. На нём можно вставить числа из буфера обмена, и он нарисует их. Они должны быть в формате «(x, y)». Чтобы получить его, достаточно немного изменить функцию вывода на экран.
Ого! Это уже похоже на E1M1! Мы чего-то добились!
Если вам лениво это делать, то вот ссылка на заполненный точками график: Plot Vertex.
Но давайте сделаем ещё один шаг: немного потрудившись, мы можем соединить эти точки на основании linedefs.