В очередной раз решил поэкспериментировать с модульной архитектурой движка. Для начала немного теории о том как я вижу реализацию такой архитектуры. После завершения разработки движка (или прототипа) напишу как все получилось в итоге.
Преимущества.
Расширяемость - новые модули легко добавляются, подписываются на события, отправляют свои или подменяют другие модули, полностью меняя их функционал.
Абстракция - снаружи видны только идентификаторы модулей, сообщения/запросы/комманды, базовый модуль и фабрика, все остальное полностью скрыто.
Недостатки.
Главный недостаток - сложность разработки подобной системы, многое выполняется и проверяется только в рантайме, из-за большей абстрации (чем интерфесы например) тяжелее отлаживать.
Проблемы расшияемости - иногда приходится добавлять новые типы сообщений и событий, чтоб поддерживать новые модули.
Очередность обработки сообщений/событий - это когда модули зависят друг от друга, но обновляются в обратной последовательности, тогда получится запаздывание на кадр, возможны и другие варианты .
Циклические зависимости - это касается как цикличных ссылок в RC так и зацикливания отправки сообщений между модулями.
Дублирование кода - пока не подтвержено на (моей) практике, но случаи, когда придется в разных модулях писать похожий код с минимальными изменениями вполне реальны. Кстати, сама тема дублирования кода заслуживает отдельной статьи и я не считаю, что дублирование кода это всегда плохо.
Модуль.
Итак, каждый модуль содержит в себе:
- список присоединенных модулей.
- ссылку на модуль, к которому присоединен.
- ссылку на модуль, который управляет этим модулем (аналог system в ECS).
Пользователю доступны методы:
- отправка сообщение/команды/запроса.
- подписка на события.
- список поддерживаемых типов сообщений.
- список поддерживаемых типов событий.
- идентификатор (тип) модуля.
Добавление/удаление модулей идет через отправку сообщений, как и все прочие взаимодействия.
Абстракция.
Модуль сам по себе является хорошей абстракцией, но это приводит к другой проблеме - как подключать только нужные модули? Например: как недопустить подключение текстуры к менеджеру потоков? Для этого используются слои.
Слой Global предназначен для систем, это менеджер потоков, контекст графического API (opengl, vulkan, directx), менеджер окон, менеджер задач (асинхронных) и тд. Всех их отличает то, что они не привязаны к какому-либо потоку, а наоборот, сами создают и управляют своими потоками. Например: менеджер задач используется для коммуникаций между потоками, менеджер потоков создает потоки и управляет их жизнью, графический контекст сам выбирает время для синхронизации своих потоков.
Все модули из слоя Global присоединяются к классу Main, с создания этого класса и начинается работа с движком. Единственное что отличает Main от других модулей - через него можено обратиться к фабрике (потокобезопасно) и создать любой модуль.
Слой Thread предназначен для работы с модулями внутри одного потока, за взаимодействиями между потоками они обращаются к модулям из слоя Global. Поток создается следующим образом: из фабрики вызывается функция создания потока, менеджер потоков создает новый поток и модуль потока (ThreadModule), дальше к модулю потока цепляется модуль обработки задач (TasksModule) и, если это потор рисования, то добавляется модуль очереди рисования (OpenGLThread или OpenGLRenderQueue).
Слой Object предназначен для объектов/ресурсов, они работают внутри одного потока и никак не могут (не должны) сами взаимодействовать с другими потоками, только через своего менеджера (из слоя Thread). Пример: создается модуль GLTexture, его менеджер - OpenGLThread, но при этом текстуры может присоединяться к любому другому модулю, например Scene или Material. К модулю текстуры цепляется источник данных (DataSource), это может быть файл (FileDataSource), память (MemoryDataSource), выдеопамять (GpuMemoryDataSource), также можно прицепить расшаренный объект из OpenCL (CLImage).
Если брать аналигию с Entity-Component-System архитектурой, то получается Thread это Entity, слой Object содержит Component, а OpenGLThread, Window и тд это System. Но строгое применение ECS для архитектуры приложения/движка не всегда удобно, поэтому в модульной архитектуре я хочу позволить группе модулей (например группа OpenGL модулей) самим решать какую архитектуру реализовывать.
Связывание модулей.
Возьмем слой Thread: тут есть модуль потока к которому присоединены модули: Window, KeyInput, OpenGLThread. Порядок добавления модулей значения не имеет, сначала все модули находятся в состоянии инициализации, в это время к ним можно присоединять любые поддерживаемые модули.
Завершает инициализацию сообщение Link, в этот момент каждый модуль перебирает своих соседей и подписывается на их события. Так OpenGLThread и KeyInput подпишутся на событие создания окна, далее придет сообщение Initialize, модули OpenGLThread и KeyInput его проигнорируют, а модуль Window не имеет зависимостей, поэтому создаст окно и отправит событие в OpenGLThread и KeyInput.
Проверка ошибок.
С одной стороны модульная архитектура более расположена к добавлению багов, но при этом позволяет подключать модули валидации. Насколько такой подход окажется лучше я пока не знаю.
Зачем?
Несмотря на всю сложность модульной архитектуры, это еще один шаг к универсальности движка. После написания движков под разные платформы: Windows, Android, VR и разные графические API: OpenGL 4.x, OpenGL ES 2, OpenCL, Vulkan, DirectX 11, я понял насколько сложно все предусмотреть заранее, поэтому возможность быстро перекомпоновать модули для поддержки чего-то нового значительно упростит портирование.
Еще модульная архитектура позволяет применить модный сейчас Data-Driven-Design и в дальнейшем переделывать на другие архитектуры внутри группы модулей.
Комментариев нет:
Отправить комментарий