Движок моей мечты на Си

 Я уже давно болею идеей собственного движка. В этом нет какого-либо практического смысла. Просто я получаю удовольствие от подобной разработки. Последнюю неделю мысли про создание движка вернулись и я не знаю как от них избавиться, так как это вредит разработке моего основного проекта, RTS на Unity3d. Времени браться за разработку у меня на данный момент к сожалению нет. Поэтому я просто запишу ключевые идеи по ключевым моментам.

Буду постепенно расширять статью по мере увеличения количества идей.

1. Технология

Я хотел бы использоваться для разработки Си, так как С++ склоняет разработчиков к оверинженерингу. Си же напротив склоняет к простым решениям. Я хочу использовать C11 + расширения GNU (--std=gnu11), эту пару поддерживают самые распространенные компиляторы gcc и clang. Но, увы msvc не поддерживает требующиеся расширения GNU. Но, поддержка clang в Visual Studio улучшается с каждой версией, так что на Windows тоже можно использовать clang.

Из GNU мне нужны следующие вещи: #pragma once и typeof

Пример как можно реализовать типо-безопасные контейнеры на Си с помощью typeof:

2. Структура данных

ECS набирает популярность в последние годы. И эта парадигма прекрасно ложится на Си, в отличие от объектной модели. Поэтому, выбор очевиден. Мне очень хочется попробовать реализовать свою схему. Которая довольно примитивная, но я рассчитываю что должна работать довольно быстро.

Scene

- - Component Dictionary

- - - - Entity Indexes[] - Для поиска по индексу просто ищем нужный индекс в массиве

- - - - Components[] - Данные компонентов в порядке добавления

Компоненты добавляются в первое свободное место в массиве Componenents, в соседнем массиве Entity Indexes хранятся соответствующие компонентам id Entity. Если мы хотим достать компонент, соответствующий Entity, мы можем найти id Entity в Entity Indexes и вернуть данные из массива Components по тому же индексу. Разделять Components и Indexes необходимо для сache-оптимизации. Более того, при поиске по id необходимо кэшировать предыдущий найденный id, и начинать поиск не сначала, а с последнего найденного id. Так как компоненты добавляются последовательно, очевидно, что вероятнее всего, следующий нужный компонент будет находиться радом с предыдущим.
Таким образом, если вам нужно получать компоненты последовательных Entity (самый частый случай), код будет быстро работать. Если нужно доставать случайные Entity, это будет приводить каждый раз к перебору массива и будет работать значительно медленнее.

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

Рабочий пример:

3. Рефлексия

Если мы хотим сделать редактор, или сохранять сцены в файлы, нам не обойтись без рефлексии. Я хотел бы оформлять всю рефлексию структур в функции. Например:

 
typedef struct
{
    int x, y, width, height;
} Bounds2Int;

void Bounds2Int_ref(Operation* operation)
{
    FIELD(operation, Bounds2Int, x);
    FIELD(operation, Bounds2Int, y);
    FIELD(operation, Bounds2Int, width);
    FIELD(operation, Bounds2Int, height);
}
  

В каждом макросе FIELD вся необходимая информация о полях передается в операцию. Операция решает, что с этой информацией делать, она может записывать данные при загрузке игры, или наоборот сохранять данные в файл. Либо в операции может содержаться код ImGUI, который будет отображать поля структуры в редакторе. Это самый минималистичный способ представления рефлексии в Си из известных мне. 

Примеры кода:

4. Формат файлов уровней

На игровых проектах, в которых я работал повсеместно использовались yaml и json это очень гибкие форматы, идеально подходящие для сохранения иерархических данных. Но в новом движке я хотел бы попробовать использовать для сохранения сцен формат и для других случаев табличный формат csv. Так как бинарные форматы неудобно мержить системами контроля версий, кроме того, есть нюансы из-за разных процессоров little-endian и big-endian . А json и yaml дублируют названия полей многократно. Кроме того, при парсинге названия полей нужно парсить каждый раз, что очень медленно. 

В csv же мы можем хранить компоненты одного типа в виде отдельных таблиц. Например

Transform
id, x, y, z, rotation.w, rotation.x, rotation.y, rotation.z
23512, 10, 20, 10, 0, 0, 0, 1
23513, 50, 30, 60, 0, 1, 0, 1
23514, 80, 60, 30, 0, 0, 1, 1
...

Этот формат плохо поддается чтению в текстовом виде, зато может быть открыт в табличном процессоре и редактироваться в удобном и более безопасном виде, чем json или yaml. Более того, наличие парсера csv в движке сразу позволяет использовать его для записи характеристик юнитов и оружия, которые удобно хранить в табличном виде. Локализацию кстати тоже часто хранят в csv. 
Такой формат так же хорошо поддается merge через git.

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

Для хранения вложенных структур и вложенных списков структур я предлагаю записывать эти типы в таком же виде и делать ссылки по id.


5. Ресурсы

Первая обязательная на мой взгляд вещь - это id. Ресурсы должны иметь заранее известные id чтобы на них можно было ссылаться не только из кода. Раньше я старался везде использовать голые указатели, но с опытом пришел к тому, что все что может быть использовано из скриптов или сохранено в файлы должно иметь id. То же самое и с ресурсами.

Второе. По моему мнению, ресурсы должны загружаться заранее. До первого их использования, так как загрузка занимает время и ей не место в середине кадра. Ресурсы лучше объединять в паки и грузить в нужные моменты. Если же игра пытается получить доступ к незагруженному ресурсу, просто выдавать ошибку.

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

7. Рендеринг

Так как все стремятся к унификации и Vulkan начинает поддерживаться на множестве платформ, стоит брать его за основу высокоуровневого графического интерфейса.

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

Комментарии

  1. Хоть я в этом не че не понимаю, но желаю удачи

    ОтветитьУдалить
  2. А ты планируешь сделать движек кроссплатформенным? Если да, то возможно лучше было бы использовать какую нибудь виртуальную среду (java/.net), в таком случае уже не нужно о кроссплатформенности задумываться. В противном случае нужно будет заниматься поддержкой своего движка под разные платформы

    ОтветитьУдалить
    Ответы
    1. Виртуальную середу сначала нужно запустить и еще нужно сделать множество мостов для функционала. Это все в любом случае придется делать на Си/С++ и задумываться о кроссплатформенности придется еще как. Для поддержания адекватной производительности большую часть важного функционала в любом случае придется писать на каком-то компилируемом языке типа Си/С++/Rust.

      Удалить

Отправить комментарий

Популярные сообщения из этого блога

Siege Up! Editor (beta)

STM32F4 и программный выход в DFU