Dagon 10 лет!

Проект стартовал 9 октября 2016 года, ровно 10 лет назад. Конечно, это не первая моя разработка в сфере геймдева, и я начал писать движок далеко не с полного нуля — к тому моменту у меня был DGL, набор классов, упрощающий работу с OpenGL в D (а до этого я работал с OpenGL и писал разнообразные сопутствующие библиотеки на C++). Однако именно Dagon стал наиболее успешным.

Создание игрового движка — наверное, одна из самых интересных и нетривиальных задач в программировании. Есть устойчивое убеждение, что свой движок писать вообще не нужно, и я как-то раз написал статью о том, почему это скорее миф. Существуют примеры успешных проектов как на лицензированных движках, так и на эксклюзивных, оба варианта имеют свои плюсы и минусы. На мой взгляд, главное преимущество собственной технологии — независимость от чужих лицензионных условий и архитектурных решений. В современном мире большинство проектов делается на Unity и UE, в результате чего игры слишком похожи друг на друга. Собственный движок — это ваш уникальный почерк. Понятно, конечно, что с UE конкурировать практически невозможно — продукты, в которые вложены миллионы долларов, будут всегда впереди. Но UE — это движок-монстр, все возможности которого нужны далеко не каждому. Если речь об инди, то нелепо, когда рантайм игры весит, как все ее ресурсы (если не больше), а системные требования — как у AAA. Должны же быть и маленькие движки! Ну и, наконец, психологический фактор: игра на чужом движке воспринимается как не совсем твоя.

Я начал интересоваться геймдевом еще в далеком 2005 году, когда открыл для себя Game Maker, Blitz3D и другие популярные любительские технологии того времени. Затем я пробовал работать с Irrlicht, OGRE, Blender Game Engine, но с точки зрения архитектуры меня очень впечатлила GLScene, графическая библиотека для Delphi. Мои собственные разработки во многом вдохновлены классами GLScene, хотя напрямую я оттуда ничего не копировал. Я вообще почти не заглядываю в исходники чужих движков — не знаю, хорошо это или плохо, но у меня свое понимание того, что удобно, а что нет. Боже упаси создавать клон Unity или что-то подобное. Не мыслите шаблонами индустрии, изобретайте свои технологии!

Я никогда не писал на DirectX, как-то почти сразу освоил OpenGL и всю жизнь сидел на нем. В нулевых это были версии 1.x-2.x, еще с фиксированным конвейером, где шейдеры были необязательной частью. Потом — короткий период 3.x, и, наконец, к концу десятых — 4.x. Я проследил практически всю эволюцию OpenGL и использовал его до последнего, когда стало уже очевидно, что пора двигаться дальше и осваивать эксплицитные API. Я, однако, не фанат чистого Vulkan. На нем не надо писать приложения напрямую по той же причине, по которой не пишут напрямую на ассемблере. Vulkan — это машинно-ориентированный бэкенд, поверх которого нужна абстракция, удобная для человека. Я терпеливо ждал появления таких абстракций, они появились, и вот теперь можно со спокойной душой уходить с OpenGL.

Что еще могу сказать на основе десятилетнего опыта? Разрабатывая большую систему, такую, как игровой движок, рано или поздно приходишь к идее неизбежности компромисса между двумя силами, тянущими проект в две разные стороны — абстракцией и специализацией. Абстракция — это сокрытие деталей под высокоуровневым интерфейсом. Абстракция повышает удобство на стороне пользователя, но снижает гибкость и возможности оптимизации. Специализация, напротив, подразумевает, что пользователь сам должен предоставить реализацию некоего интерфейса — то есть, углубиться в детали и интегрировать в движок свое решение специфической задачи. Это дает гибкость и возможности оптимизации, но снижает удобство. Хорошая система включает в себя и то, и другое. Дзен в балансе: нужно постоянно решать, в какой степени система должна быть абстрактной, и в какой — специализированной. Например, один из первых 3D-движков, с которыми я работал — Xtreme3D для Game Maker 6 — имел очевидный приоритет абстракции над специализацией: в нем были готовые декодеры разных форматов 3D-моделей, но полное отсутствие возможности создать свой собственный (то есть, собрать модель вручную из вершин и индексов). А вот встроенный 3D-рендер Game Maker, наоборот, не включал практически никаких абстракций и вынуждал программировать графику на низком уровне, составляя списки отрисовки из отдельных полигонов. В первом случае удобно, но не гибко, во втором — гибко, но неудобно. Учтя эти особенности, я решил делать движок, который охватывает оба сценария использования. Если вам нужно просто загрузить glTF, то это легко. А если вы хотите создать свой формат сцен, то это, пусть и с оговорками и требованиями, но реализуемо. Я всегда подчеркиваю, что Dagon — это фреймворк, то есть, каркас игрового приложения. Его ядро включает только создание окна и контекста OpenGL, управление таймером главного цикла и очередью событий. Рендер — это уже надстройка; в Dagon можно не использовать встроенный рендер и написать свой. То же касается абсолютного большинства всех подсистем — они не прибиты гвоздями. Принципиальной основой работы в Dagon является инверсия контроля, как и во всех фреймворках, но ядро минималистично — оно не отвечает за все процессы, и вы можете заменить почти все, что угодно, на самописные компоненты.

Разработка не стоит на месте, я постоянно что-то улучшаю и переписываю, чему, главным образом, и посвящен этот блог. Не хочется загадывать, но надеюсь, что в следующем году доделаю Dagon 2.0 — начинается новая большая страница в истории движка.

Статья по теории рендеринга в Dagon

Наконец-то дошли руки написать теоретическую статью о модели освещения, которую я использую в движке, со всеми формулами и ссылками на научную литературу. Это GGX/Trowbridge-Reitz, на которую опирается освещение в Unreal Engine 4 и многих других движках — физически обоснованная модель зеркально отраженного света для изотропных шероховатых поверхностей. Ее многие сейчас используют, одни и те же уравнения можно встретить в самых разных движках, но мало кто задумывается, откуда они взялись.

Поскольку в тексте много математики, я оформил статью в виде PDF — можно скачать файл или читать при помощи встроенного в браузер просмотрщика.

Обновления

Dagon 1.6.0

Добавлено расширение dagon:server для создания игровых серверов.
Исправлен баг в методе Application.takeScreenshot. Исправлен вывод в лог первой записи (версии Dagon) при сохранении лога в файл.
Движок теперь использует dlib 1.6.0.

Minicrypto 0.5.0

Добавлены модули minicrypto.base64 (реализация кодировки Base64) и minicrypto.hex (конвертер байтовых массивов в шестнадцатиричный текст и обратно). Эти фичи не имеют прямого отношения к криптографии, но полезны для сохранения ключей шифрования и прочих бинарных данных в текстовом виде, что необходимо для копипаста, при использовании в JSON, в конфигах и т.д.

Обновления

Dagon 1.5.0

Добавлены расширения dagon:network и dagon:security, реализующие, соответственно, сетевой интерфейс на базе UDP/ENet и безопасный транспортный протокол.
Функции для работы с кубическими картами из dagon.graphics.texproc вынесены в отдельный модуль dagon.graphics.cubemap.
Метод Application.takeScreenshot теперь не использует сборщик мусора.

dlib 1.6.0

Добавлен новый пакет dlib.time с реализацией кроссплатформенного интерфейса к системным функциям времени-даты. Временные метки (DateTime) имеют точность до целых секунд и предназначены для относительно редких событий (логирование, онлайн-переписка, календарные события и т.д.). Для более точного измерения времени следует использовать наносекундный счетчик в std.datetime.stopwatch, либо специализированные таймеры внешних библиотек (например, таймер SDL).
Добавлен новый модуль dlib.container.sorting с реализациями алгоритмов сортировки выбором и вставками. Они лучше всего подходят для сортировки небольших массивов нечасто обновляющихся данных в реальном времени. Например, если массив уже отсортирован, то функция сортировки вставками insertionSort будет в сто раз (!) быстрее, чем std.algorithm.sort, что идеально для игр.
Методы встроенного мьютекса Mutex помечены как @nogc.

STP-D — защищенный протокол для игр на D

Идея с шифрованием из коробки реализовалась буквально за неделю: я решил портировать Monocypher на D, что вылилось в создание пакета Minicrypto и расширения dagon:security, о которых сегодня и расскажу. Вкратце — это позволит играм на Dagon (да и вообще практически любым приложениям) использовать защищенное UDP-соединение клиента и сервера, причем без зависимости от тяжелых протоколов типа DTLS или SRTP.

(далее…)

dagon:network — асинхронный UDP-клиент для игр

В Dagon грядет расширение для создания сетевых игр! Я решил интегрировать библиотеку ENet и написать поверх нее легковесный клиент, при помощи которого игра сможет взаимодействовать с UDP-сервером. Причем это работает через менеджер событий и асинхронную систему обмена сообщениями (dagon.core.messaging), о которой я писал в соответствующем посте. Это дает сразу два преимущества: сетевой клиент не блокирует игровую логику (поскольку работает в собственном потоке), и гарантируется порядок отправки сообщений (сообщения добавляются в SPSC-очередь).

Выглядит это так:

import dagon.ext.network;

class TestScene: Scene
{
    MyGame game;
    NetworkClient netClient;

    this(MyGame game)
    {
        super(game);
        this.game = game;
    }

    override void afterLoad()
    {
        // Инициализация игры...

        netClient = New!NetworkClient("GameClient",
            game.networkManager, "XXX.XXX.XXX.XXX", 1234, this);
        netClient.run();

        eventManager.messageBroker.enabled = true;
    }

    override void onMessage(
        int domain, string sender, string message, void* payload)
    {
        if (sender == "GameClient")
        {
            logInfo("[Server] ", message);
        }
    }

    override void onKeyDown(int key)
    {
        send("GameClient", "Hello, World!");
    }
}

class MyGame: Game
{
    NetworkManager networkManager;

    this(uint w, uint h, bool fullscreen, string title, string[] args)
    {
        super(w, h, fullscreen, title, args);
        networkManager = New!NetworkManager(this);
        currentScene = New!TestScene(this);
    }
}

Реализация полностью потоко-безопасна — хост и пир ENet, ассоциированные с NetworkClient, изолированы в рамках потока, и цикл enet_host_service у него тоже свой собственный.

На данный момент NetworkClient работает только со строками (как, собственно, и сам брокер сообщений). Для поддержки бинарных данных, видимо, придется чуть изменить архитектуру. Кроме того, хочется, чтобы было из коробки шифрование, так что буду копать в сторону AEAD-алгоритмов.

Обновления

Dagon 1.4.0

Добавлена поддержка сжатия текстур в BC4 и BC7, бэкпортированная из Dagon 2. Компрессор BC7 является D-портом библиотеки bc7enc Рича Гелдриха, компрессор BC4 написан с нуля.
Модуль dagon.resource.sdlimage объединен с dagon.resource.texture.
prefilterCubemap теперь принимает опциональный параметр lumaScale, коэффициент яркости для входных значений кубмапа.
Исправлен баг, из-за которого свойство Application.fullscreen не обновлялось по конфигу.

dlib 1.5.1

Небольшой багфикс-релиз dlib 1.5, в котором был исправлен неприятнейший баг декодера JSON — пустые объекты и массивы интерпретировались как узлы JSONType.Null.

Обновления

Dagon 1.3.0

Выпустил новую версию движка, до этого также вышло несколько багфиков для 1.2. Метод Application.takeScreenshot теперь совершает полную отрисовку кадра, чтобы все изменения в рендере отразились на скриншоте. Добавлены методы Material.copyTo и Material.copyFrom для копирования свойств одного материала в другой. Добавлено свойство Entity.blurOnlyRotation, предназначенное для ограничения motion-векторов угловой скоростью, игнорируя изменение позиции — это важно для объектов, которые вращаются, но не меняют своего положения в экранном пространстве, таких, как колеса машины.

Улучшен префильтр яркости для эффекта glow: добавлен верхний порог яркости, а нижний теперь не срезает пиксели жестко, используя технику soft-knee для плавной градации. Разница заметна, например, при свечении неба, которое имеет неравномерную яркость. Соответственно, добавлены новые опции render.conf: glow.softKnee, glow.maxEnergy.

Исправлены баги рендеринга декалей. Я использую диффузные декали для размытых теней под днищем машин, но это приводило к тому, что тень некрасиво наползала на вертикальную стенку, когда машина приближалась к ней вплотную. Это вызвано вырожденным случаем при расчете текстурных координат проекции. Проблема исправлена тем, что прозрачность декали зависит от ее копланарности поверхности, на которую она проецируется. Если поверхность перпендикулярна, то прозрачность равна нулю.

dlib 1.5.0

Вышло небольшое обновление dlib. Добавлена функция trsMatrix для создания матрицы трансформации из смещения/поворота/масштаба за один шаг, без перемножений. Появилась функция lerpAngle для линейной интерполяции углов по кратчайшей дуге, а также shortestAngleDelta для вычисления угловой дельты по кратчайшей дуге. Добавлена новая изинг-функция easeOutElastic. Функция wrapAngle теперь работает с любыми угловыми единицами при помощи опционального параметра периода (по умолчанию период равен 360, что означает градусы).

Chillwave Drive

Игра была значительно обновлена: добавлен подсчет кругов и гоночная логика, главное меню, экран загрузки, уникальная покраска кузова для каждого участника заезда, поддержка различных материалов покрытия, влияющих на физику, интерфейс в режиме паузы, поддержка вибрации контроллера, поддержка суперсэмплинга при снятии скриншотов и многое другое. Симулятор приближается к полнофункциональной альфа-версии.

Dagon 1.2.0

Выпустил новую версию Dagon. Основным поводом к этому релизу стало исправление серьезного бага, связанного с горячим подключением геймпадов (в качестве индексов для GameInputDevice использовались неправильные ID). Кроме того, игра заметно подтормаживала при вызове InputManager.getAxis из-за использования SDL_GameControllerGetAxis — эта проблема была решена использованием значений осей из событий SDL_CONTROLLERAXISMOTION. Соответственно, GameInputDevice теперь имеет поле axisValues.

Исправлена утечка памяти в методе Application.takeScreenshot.

Добавлена поддержка суперсэмплинга. Он управляется при помощи опции ss.sampleRatio в render.conf. Если выставить в нее 2, то игра будет рендериться в двухкратном разрешении.

Шейдер FXAA заменен на более качественную версию 3.11. Добавлена поддержка независимого от разрешения радиуса размытия для фильтра свечения (glow). Если в render.conf присутствует опция glow.normalizedRadius, то обычный glow.radius не используется — радиус вычисляется динамически как доля высоты экрана. Например, если выставить значение 0.005, то при 1080p радиус свечения будет 6 пикселей.

Обновления по итогам июня

Вышел Dagon 1.1.1. Добавлено свойство GLTFMeshPrimitive.materialIndex, индекс материала в файле glTF. Этот индекс записывается в структуру Triangle при обходе по граней с помощью GLTFAsset.opApply, что позволяет передавать данные о материалах в различные обработчики мешей. Основное применение этой фичи — учет материалов при обнаружении столкновений и рейкастинге. Соответственно, в dagon:newton добавлена поддержка индексов материалов при создании NewtonMeshShape. В интерфейсный метод NewtonRaycaster.onRayHit добавлен параметр long id, через который коллбек получает искомый индекс материала при попадании луча в меш.

Пошла серьезная работа над Chillwave Drive. Я уже рассказывал про ИИ, но, помимо этого, на днях улучшил модель колеса и наконец-то добавил поддержку материалов, чтобы машина учитывала свойства покрытия. Например, когда машина вылетает с трассы, то трава на обочине сильно ее замедляет и не дает разгоняться. Также добавил HUD-тахометр, обновил текстуры тестовой трассы, реализовал эффекты погоды — дождь и динамически появляющееся и исчезающее за облаками солнце. У игры появилась страничка на GameJolt, где я регулярно выкладываю альфа-версии.

Привел в порядок блог, объединил старую версию и новую, подкорректировал неактуальную информацию.