суббота, 15 апреля 2017 г.

Модульная архитектура

  В очередной раз решил поэкспериментировать с модульной архитектурой движка. Для начала немного теории о том как я вижу реализацию такой архитектуры. После завершения разработки движка (или прототипа) напишу как все получилось в итоге.


Преимущества.

  Расширяемость - новые модули легко добавляются, подписываются на события, отправляют свои или подменяют другие модули, полностью меняя их функционал.

  Абстракция - снаружи видны только идентификаторы модулей, сообщения/запросы/комманды, базовый модуль и фабрика, все остальное полностью скрыто.


Недостатки.

  Главный недостаток - сложность разработки подобной системы, многое выполняется и проверяется только в рантайме, из-за большей абстрации (чем интерфесы например) тяжелее отлаживать.

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

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

  Циклические зависимости - это касается как цикличных ссылок в 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 и в дальнейшем переделывать на другие архитектуры внутри группы модулей.

Комментариев нет:

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