ПрограммированиеФорумОбщее

Как вообще может выглядеть структура мультиплатформенного движка? Прошу совета и наставления :)

Страницы: 1 2 3 Следующая »
#0
21:38, 27 мая 2014

Приветствую!

Пришло время как следует обновить свой движок (пока это C++, DirectX), полностью переписав его под несколько платформ.
И тут встал вопрос о том, как теперь оформлять код?
Тут я в совершенно неизведанной для меня области и с помощью нашего пёстрого сообщества надеюсь получить более чёткую картину о мультиплатформенном программировании.

Движок у меня в формате ООП, где имеется главный объект, где через менеджеры, мы и получаем доступ ко всем подсистемам: графике, звуку, управлению и т.д.
Снаружи видно только другие классы, несколько enum'ов, да константы.
Хотелось бы примерно так всё и оставить: аккуратненько и закрыто.
То есть подхода, как в SDL, когда всё торчит снаружи изо всех дыр, хотелось по возможности бы избежать.

На ум пришла только пара вариантов (не считая вариантов с полным разделением проекта на несколько, под разные платформы):

Вариант 1. Через встроенные в код дефайны. Аля:

unsigned int CEngineTools::getProcessorsCount() // static
{
  unsigned int uiProcessorsCount = 0;

#ifdef ENGINE_PLATFORM_WIN
  SYSTEM_INFO rSystemInfo;
  GetSystemInfo(&rSystemInfo);
  uiProcessorsCount = (unsigned int)rSystemInfo.dwNumberOfProcessors;

#elif ENGINE_PLATFORM_OSX
  int arName[2];
  arName[0] = CTL_HW;
  arName[1] = HW_AVAILCPU;
  size_t stLength = sizeof(uiProcessorsCount);

  sysctl(arName, 2, &uiProcessorsCount, &stLength, NULL, 0);

  if (uiProcessorsCount < 1)
  {
    arName[1] = HW_NCPU;
    sysctl(arName, 2, &uiProcessorsCount, &stLength, NULL, 0);

    if (uiProcessorsCount < 1)
      uiProcessorsCount = 1;
  }

  uiProcessorsCount = (unsigned int)sysconf(_SC_NPROCESSORS_ONLN);
#endif

  return uiProcessorsCount;
}

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

Вариант 2. Это подключение соответствующих заголовков на стадии компиляции. Код в .cpp файлах, в таком случае, так же заключается в дефайны, выкидывающие его, если он добавлен в проект, но относится к другой платформе.
Что-то вроде такого:

EngineWindow.h:

#ifndef _ENGINE_WINDOW_H_
#define _ENGINE_WINDOW_H_

#include "EnginePrecompiled.h"

#include "EngineWindow_WIN.h"
#include "EngineWindow_OSX.h"

#endif // _ENGINE_WINDOW_H_

EngineWindow_WIN.h и EngineWindow_OSX.h (каждый, соответственно, содержит свою реализацию):

#ifndef _ENGINE_WINDOW_WIN_H_
#define _ENGINE_WINDOW_WIN_H_

#include "EnginePrecompiled.h"

#ifdef ENGINE_PLATFORM_WIN

class CEngineWindow
{
  // Тут у нас идут универсализированные для пользователя методы (под всеми платформами выглядят одинаково и имеют тот же набор параметров)
  ...
  // А тут можно добавить и каких-то OS-специфик методов
}
#endif // ENGINE_PLATFORM_WIN

#endif // _ENGINE_WINDOW_WIN_H_

В данном случае проверки занесены внутрь .h и .cpp файлов. Можно, конечно, эти проверки вынести наружу, но тогда их надо будет делать рядом с каждым includ'ом. Но не это главное.
Суть второго способа - это полная замена пары файлов .h и .cpp, в зависимости от выбранной платформы сборки.

Тогда у нас появляются файлы вроде:
EngineWindow.h
EngineWindow_WIN.h
EngineWindow_OSX.h
EngineWindowManager.h
EngineWindowManager_WIN.h
EngineWindowManager_OSX.h

а в последствии начнёт зарастать и всякими:
EngineButton.h
EngineButton_WIN.h
EngineButton_OSX.h
и т.д.

Вооот. И теперь получается, что в файлах EngineWindow_WIN.h и EngineWindow_OSX.h мы вынуждены повторять описание одних и тех же

методов, с теми же самыми параметрами (для универсализации) и только в дополнительных, приватных, методах уже описывать какие-либо

os-специфик методы...
Не слишком ли это избыточно? И вообще верное ли направление?

Честно говоря, за несколько последних лет заработал себе Lua мозга и вернуться и думать по C++'ному пока не очень хорошо получается.
В связи с этим буду рад любой критике, комментариям и советам, как можно аккуратно это всё разрулить.

Технически, конечно, рабочую организацию проекта я могу сварганить, но хотелось бы сделать что-то более или менее понятное и другим.
На сях приходилось кодить, в основном, в некотором одиночестве и потому требуется определённый feedback от мира, чтобы в последствии этим добром мог пользоваться не только я :-)

На всякий случай вот схема движка, который надобно мультиплатформизировать:

+ Схемко

Правка: добавил схему для наглядности.

#1
21:52, 27 мая 2014

MoonStone
В идеале - интерфейсы и реализации

#2
21:56, 27 мая 2014

Первый вариант вполне нормален, почему бы нет?

#3
22:18, 27 мая 2014

-Eugene-, больно много проблем возникает с разбором всяких менеджеров и ресурсов без знания их типов.

#4
22:19, 27 мая 2014

-Eugene-, спасибо большое за напоминание! До этого, признаться, совершенно без них обходился. Вроде и не к месту были. Оставлю этот вариант напоследок - эстетически кажется не очень, но обязательно подумаю над этим и не исключено, что поменяю своё мнение.

Бунтарчик, первый вариант хорош для достаточно низкоуровневых методов. И для них он, возможно, останется. А вот если переписывается уже целый комплекс классов, то начинаются проблемы.
Я неспроста как пример привёл менеджер окошек (хотя правильней назвать его GUIManager, конечно, но это всё в работе).
Смысл в том, что тогда дефайны придётся пихать не только в сам код, но и в описания классов, убирая или добавляя соответствующие методы, поля... Что сомнительно в плане удобства кодинга (да и всевозможные VisualAssist'ы и IntelliSens'ы с ума сойдут)) ).

#5
22:26, 27 мая 2014

ArchiDevil
Пфф... По-хорошему движке из платформозависимого кода - только создание окна.

MoonStone
Платфоромозависимость сильная, как я уже сказал, в главном классе окна/окон и главном же цикле. Делаешь IEngineWindow с нужными функциями и от него наследуешься.
Утилитные платформозависимости (потоки, сокеты, файловая система) стоит свалить на библиотеки (boost, qt), или, опять же, обернуть интерфейсами.

#6
23:37, 27 мая 2014

MoonStone
> А вот если переписывается уже целый комплекс классов
Откуда взялся "целый комплекс"?
По хорошему 1-2 класса связанных с особенностями ОС, остальное уже платформенно не зависимое.

Можешь на мой двиг глянуть:
https://github.com/Try/Tempest#
4 класса: SystemAPI(abstract) WindowsAPI LinuxAPI AndroidAPI - больше не надо.
Есть еще в ogl ряд дефайнов, но это от глупости - совместить opengl2 и opengles2 в одном классе.

#7
0:30, 28 мая 2014

-Eugene-
> ArchiDevil
> Пфф... По-хорошему движке из платформозависимого кода - только создание окна.
Мультрендер DX, OGL как предлагаешь разделять? Так, чтобы не приходилось в типы ресурсов лазить, например.

#8
0:56, 28 мая 2014

-Eugene-
> Пфф... По-хорошему движке из платформозависимого кода - только создание окна.
Создание поток, синхронизация через планировщик потоков ОС, работа с файловой системой, работа с сетью, работа со звуком... может ещё что...

#9
2:30, 28 мая 2014

Try, преогромнейшая благодарность за код! Буду изучать.
Я пока просто полистал и не успел понять, там уже готова связка "c++" -> "java"?
Я её проглядел или её ещё нет? Если она есть, то как выглядит и откуда копать?

> Откуда взялся "целый комплекс"?
Ну как правильно уже отметили, кроме окон там ещё работа с файлами, сетью, устройствами ввода... Ощем набирается. И всё это со своим собственным немаленьким функционалом под каждую из платформ.
То есть парой исключений из правил оформления кода и местной инъекцией дефайнов не обойтись - это всё надо уже закладывать в общую структуру движка, чтобы расширяемость была.

И да, пока по теме рядом: я предполагал сделать какой-то унифицированный набор функционала, который одинаков для любой из платформ. Но при необходимости можно напрямую получить доступ к подситеме платформы и работать уже с ней и её нативными методами. Ну, видимо так оно в целом и делается, не всеми, но в основном (просто отметил это, так как вычитал сегодня на хабре, что в той же Corona такой возможности вроде как и нет, но то был пост от 2011 года и сам я не проверял :) ).

Ещё вот обновил примерную схемку движка, которую мне предстоит замультиплатформить.
Так как изначально предполагалось развитие двига и покорение других платформ, то ещё тогда постарался более или менее изолировать всё по своим классам. В частности работу с файлами и с графическими устройствами (хоть пока оно у меня и одно - DX8).

+ Схемко
#10
3:46, 28 мая 2014

Вот мой вариант (То есть второй из твоих).

+ Показать

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

При этом я специально поместил в класс Platform - мой следующий пост

#11
3:51, 28 мая 2014

MoonStone
> а в последствии начнёт зарастать и всякими:
> EngineButton.h
> EngineButton_WIN.h
> EngineButton_OSX.h
Этот подход неверен.

Вообще например лично я инкапсулирую весь внешний код в одном или нескольких местах. Весь остальной движок не должен вообще никакого внешнего кода использовать.

То есть я бы написал мультиплатформенную обертку в которой бы скрыл весь платформозависимый код, и пользовался только этой оберткой

#12
10:01, 28 мая 2014

Bishop
> Создание поток
> синхронизация через планировщик потоков ОС
> работа с файловой системой
> работа с сетью
Qt/boost
> работа со звуком
Al

ArchiDevil
> Мультрендер DX, OGL как предлагаешь разделять?
У ТСа все равно DX. Так что либо переписывать на GL... Либо кроссплатформа и кучи интерфейсов, да.

#13
10:33, 28 мая 2014

MoonStone
> Я пока просто полистал и не успел понять, там уже готова связка "c++" ->
> "java"?
> Я её проглядел или её ещё нет? Если она есть, то как выглядит и откуда копать?
https://github.com/Try/Tempest/blob/master/android/ - заготовка для android-base проекта

Класс для activity:
https://github.com/Try/Tempest/blob/master/android/src/com/tempes… vityBase.java
(при использовании придется поправить список загружаемых *.so - по вкусу)

Также понадобится подлинковать android_dummy.cpp к модулю где располагается функция int main(). В целом стандартный набор android-костылей)

При работе с android не нужно парится с проблемами синхронизации - как и в других ОС события окна, метод render вызываются синхронно, из одного и того-же потока.

#14
10:40, 28 мая 2014

MoonStone
> Вариант 1. Через встроенные в код дефайны.
Я использую такой вариант в единичных случаях. Когда большая часть кода в cpp платформо-независима и только малая часть платформо-зависима.

Я остановился на другом варианте. Есть заголовочный файл, не содержащий никаких полей, зависимых от платформы. Таким образом заголовочный файл класса окон EngineWindow.h не тянет за собой windows.h и другие. Для каждой платформы есть свой .h файл реализации (EngineWindow_WinAPI.impl.h, EngineWindow_), который оформлен, именно как файл реализации, а не заголовочный. В соответствующем .cpp файле проверяется платформа, и в зависимости от неё включается соответствующий EngineWindow*.impl.h файл.

-Eugene-
> Пфф... По-хорошему движке из платформозависимого кода - только создание окна.
Это какой-то промежуточный вариант. Если изначально использовать только кроссплатформенные библиотеки (например, SDL, OpenAL), то платформозависимого кода вообще не будет. Либо другая крайность - использование только того, что есть в ОС. Тогда вообще много платформозависимого кода будет.

ArchiDevil
> Мультрендер DX, OGL как предлагаешь разделять?
Ну вообще-то кроссплатформенность != мультирендер.

>Так, чтобы не приходилось в типы ресурсов лазить, например.
Ты имеешь в виду, что многие создают TextureGL и TextureDX и подобные и мучаются с ними?
У меня вообще весь API в одном классе. Обёртка над API такая плотная, что можно считать, что я сделал свой собственный универсальный API. Сейчас он реализован только через OpenGL. Чтобы добавить в движок DirectX, надо только один класс создать, унаследовав его от абстрактного AGraphics и переопределив его чистые виртуальные методы. Единственная проблема - это разные языки шейдеров у DX и OpenGL. В принципе решается прикруткой Cg, но это сторонняя библиотека, а я не люблю таскать сторонние библиотеки. Да и вообще мне Cg не нравится. Так что видимо ещё и свой язык шейдеров делать придётся.
Весь код реализации OpenGL у меня занимает 120 КБ, но это уже с учётом кода работы с расширениями. Соответствующий код с DX11 займёт наверное ~ 70 КБ.

Страницы: 1 2 3 Следующая »
ПрограммированиеФорумОбщее

Тема в архиве.