Статья обновлена с учётом полезных комментариев. Большое спасибо всем комментаторам за важные уточнения и дополнения.

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

Выходом является использование готовых универсальных UI библиотек. Текущее их поколение представлено такими «монстрами» как Scaleform и Coherent UI, хотя если вам так хочется писать UI на HTML, то можно и просто взять Awesomium.

К сожалению, у этой троицы, при всех её преимуществах, есть один существенный недостаток — жуткие тормоза, особенно на мобильных устройствах (несколько лет назад, я лично наблюдал, как практически пустой экран на Scaleform потреблял 50% от времени кадра на iPhone4).

На этом фоне, мне всегда было интересно, почему никто не использует в играх Qt — библиотеку, неплохо зарекомендовавшую себя в десктопных приложениях. На самом деле, это утверждение не совсем верно — в Wiki проекта Qt есть список игр, однако в нём почти нет современных профессиональных проектов.

Впрочем, причина, по которой именно привычные старые Qt Widgets не используются в играх, лежит на поверхности: они не рассчитаны на использование совместно с OpenGL или DirectX рендером. Попытки их скрестить дают довольно плохую производительность даже на десктопе, а про мобилки и говорить нечего.

Однако, уже довольно давно в Qt есть гораздо более подходящая для этой задачи библиотека: QtQuick. Её контролы по умолчанию рендерятся ускоренно, а возможность задавать описание UI в текстовом формате отлично подходит для быстрой настройки и изменения внешнего вида игры.

Тем не менее, я до сих пор не слышал об использовании Qt в профессиональном геймдеве. Статей на тему тоже не нашлось, поэтому я решил разобраться сам — то ли все что-то знают, чего не знаю я (но не рассказывают!), то ли просто не видят хорошую возможность сэкономить на времени разработки.

Аргументы против:



Начну с вещи, самой отдалённой от технических вопросов, а именно с лицензирования. Qt использует двойную лицензию — LGPL3 и коммерческую. Это означает, что если вас интересуют, в том числе, платформы, где динамическая линковка невозможна (iOS), то придётся раскошелится на 79$ в месяц за каждого работника «использующего» Qt. «Использовать», это, как я понимаю, хотя бы просто собирать проект с библиотеками, то есть, платить придётся за каждого программиста на проекте.

ChALkeRx уточняет, что для использования статической линковки не обязательно покупать коммерческую лицензию или выкладывать исходный код — достаточно выложить объектные файлы, набор которых позволит пересобрать ваше приложение с новой статической версией Qt.

Деньги не очень большие, но всё равно не бесплатно. И есть ещё один очень интересный момент: коммерческую лицензию Qt желательно получить как только вы начнёте использовать Qt в вашем проекте. В противном случае, при попытке получить лицензию вам предложат «связаться с нашими специалистами для обсуждения условий». Они и понятно: не только в нашей стране умные граждане догадались бы для всей разработки пять лет использовать бесплатную версию, и только для сборки финального билда купить лицензию на 1 месяц!

Пожалуй, самым важным техническим аргументом против Qt является её вес. Практически пустое десктопное приложение, использующее QML, занимает более 40Mb (при динамической линковке DLL). На Андроиде размеры будут несколько меньше, порядка 25Mb (в разжатом виде — APK будет заметно легче), но для мобильной платформы это просто ОЧЕНЬ много! Qt предлагают костыль, который позволяет установить библиотеки на телефон пользователя один раз, а использовать их из разных приложений (Ministro), но этот костыль, очевидно, доступен только на Андроиде, а нам бы хотелось ещё как-то решить вопрос с размерами на iOS и Windows Phone…

Впрочем, сокрушаясь по поводу разжиревших библиотек, не стоит забывать, что конкуренты — упомянутые выше Scaleform и Coherent — в этом плане не сильно лучше, оба выдают пустые приложения размерами в десятки мегабайт. Unity — немного легче, но всё равно, около 10Mb. Поэтому, здесь Qt сильно проигрывает только собственным, оптимизированным под задачу разработкам.

В заключение, упомяну ещё один потенциальный недостаток — Qt не готов к использованию под Web (Emscripten). Большей части разработчиков это не очень важно, но вот мы, например, занимаемся этим направлением, и тут использовать Qt пока нельзя, хотя работы в этом направлении ведутся.

Аргументы за:



Главным аргументом за использование QtQuick/QML является удобный формат описания UI, а также визуальный редактор для него. Плюс, большой готовый набор контролов.

Стоит упомянуть и возможность писать некоторую часть кода UI на JavaScript внутри QML, например, всякую простую арифметику, связывающую состояние полей разных объектов — возможность, очень редко доступная в самодельных UI библиотеках (и при этом часто необходимая).

Однако, стоит заметить, что Qt Designer — это не конструктор форм Visual Studio. Даже для базовых контролов, идущих в поставке Qt, он не даёт редактировать все возможные их свойства (например потому, что их можно добавлять динамически). В частности, вы не сможете через редактор назначить кнопке картинки для нажатого и отпущенного положения. И это только начало проблем. С другой стороны, совмещая использование визуального и текстового редактора, все эти проблемы можно преодолеть. Просто не надо рассчитывать, что можно будет отдать Qt Designer художнику, и он вам всё настроит мышкой, не залезая в текстовое представление.

Производительность, по моим ощущениям, у QtQuick допустимая. В свежем релизе Qt 5.7 её обещали ещё заметно улучшить с новыми QtQuick Controls 2.0, заточенными под мобильные платформы.

Технические особенности



Теперь перейдём к самому интересному — техническим особенностям использования Qt в игре.

Главный цикл



Первое, с чем предстоит столкнуться — Qt предпочитает быть хозяином главного цикла. В то же время, многие игровые движки так же претендуют на это. Кому-то придётся уступить. В моём случае, Nya engine, который мы используем на работе, без проблем расстаётся с main loop'ом, и, после минимальной инициализации, легко использует OpenGL контекст, созданный Qt. Но даже если ваш движок отказывается выпускать главный цикл из цепких лапок, то это не конец мира. Достаточно в вашем цикле вызывать у класса Qt приложения метод processEvents. Пример реализации приведён на StackOverflow, вместе с критикой.

DmitrySokolov указывает, что есть ещё как минимум два способа подружить рендер Qt и вашего движка: во-первых, можно рендерить вашу сцену в текстуру, которая будет рисоваться как один из компонентов сценического графа QtQuick, как описано в Mixing Scene Graph and OpenGL. Во-вторых, можно использовать объект QQuickRenderControl. По поводу последнего на Хабре есть полезная статья, которая, в частности, демонстрирует возможность использование двух (шаренных) контекстов для Qt и рендера игры, чтобы не заморачиваться так сильно с состояниями.

Если же вы пошли путём передачи главного цикла в руки Qt, то возникает вопрос — а когда же рендерить нашу игру? Объект QQuickView, в который грузится UI для отображения, предоставляет сигналы beforeRendering и afterRendering, на которые можно подписаться. Первый сработает до отрисовки UI — тут самое время отрендерить большую часть игровой сцены. Второй — после того, как UI нарисован, и тут можно нарисовать ещё какие-нибудь красивые партиклы, ну, или вдруг какие-то модельки, которым положено быть поверх UI (скажем, 3д-куклу персонажа в окне экипировки). ВАЖНО! При соединении сигналов, укажите тип соединения Qt::ConnectionType::DirectConnection, иначе вас ждёт ошибка из-за попытки доступа к контексту OpenGL из другого потока.

При этом, надо не забыть запретить Qt очищать экран перед рисованием UI — а то все наши труды будут затёрты (setClearBeforeRendering( false )).

Ещё, в afterRendering имеет смысл позвать у QQuickView функцию update. Дело в том, что обычно Qt экономит наше время и деньги, и пока в нём самом ничего не изменилось, перерисовывать UI не будет, и как следствие — не вызовет эти самые before/afterRendering, и мы тоже ничего нарисовать не сможем. Вызов update заставит на следующем же кадре всё нарисовать ещё раз. Если вам хочется ограничить количество кадров в секунду, то тут же можно и поспать.

Ещё кое-что об отрисовке



Нужно помнить про то, что у нас с Qt общий OpenGL контекст. Это значит, что обращаться с ним нужно осторожно. Во-первых, Qt будет с ним сам делать, что хочет. Поэтому когда нам надо будет отрисовать что-то самим (в before или в afterRendering), то во-первых, надо будет этот контекст сделать текущим (m_qt_wnd->openglContext()->makeCurrent( m_qt_wnd )), а во-вторых, установить ему все нужные нам настройки. В Nya engine это делается одним вызовом apply_state(true), но у вас в движке это может быть и сложнее.

Во-вторых, после того, как мы нарисовали своё, надо вернуть контекст в угодное Qt состояние, позвав m_qt_wnd->resetOpenGLState();

Кстати, стоит учесть, что поскольку OpenGL контекст создаёт Qt, а не ваш движок, то надо сделать так, чтобы ваш движок не делал ничего лишнего раньше, чем контекст будет создан. Для этого, можно подписаться на сигнал openglContextCreated, ну, или делать инициализацию в первом вызове beforeRendering.

Взаимодействий с QML



Итак, вот наша игра рисует свою сцену, поверх — Qt рисует свои контролы, но пока всё это друг с другом никак не общается. Так жить нельзя.

Если вы пишите свой код в QtCreator, либо же в другой IDE, к которой каким-то чудом прикручен вызов Qt-шного кодогенератора (MOC), то жизнь ваша будет проста. Достаточно связать между собой слоты и сигналы по именам, и QML будет получать вызовы от C++, и наоборот.

CodeRush указывает, что MOC достаточно легко прикрутить к любой из популярных IDE, поскольку в Qt есть средство генерации проектов из .pro-файлов:

Для VS проект из файла .PRO генерируется вот так:

qmake -tp vc path/to/project/file.pro

Для XCode — вот так:

qmake -spec macx-xcode path/to/project/file.pro

al_sh напоминает об Add-In'е для Visual Studio 2013 & 2015

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

Сюда (QML -> C++)



Qt нынче поддерживает два способа связывать сигналы и слоты — старый, по именам, и новый, по указателям. Так вот, с QML можно связываться только по именам. Это значит, во-первых, что нельзя на сигнал от QML повесить лямбду (хнык-хнык, а я так хотел C++11!), а во-вторых — что придётся иметь объект, в котором объявлен слот, и объект этот должен быть наследником QObject, и внутри себя иметь макрос Q_OBJECT, для кодогенерации. А у нас кодогенерации нету. Что делать? Правильно, брать объекты, у которых все слоты уже объявлены, и поэтому кодогенерация им не нужна.

На самом деле, это вообще очень полезный подход, который, с некоторой вероятностью, вам и так понадобится. Мы будем использовать вспомогательный класс QSignalMapper. У этого класса есть ровно один слот — map(). К нему можно привязать сколько угодно сигналов от сколько угодно объектов. В ответ, QSignalMapper для каждого принятого сигнала породит другой сигнал — mapped(), добавив к нему заранее зарегистрированный ID объекта, породившего сигнал, или даже указатель на него. Как это использовать? Очень просто.

Создаём отдельный QSignalMapper на каждый тип сигналов, которые могут исходить от QML (clicked — для кнопок, и т.п.). Далее, когда нам в C++ надо подписаться на сигнал от объекта в QML, мы связываем этот сигнал с нужным QSignalMapper'ом, а уже его сигнал mapped() связываем со своим классом, или даже лямбдой (на этом уровне C++11 уже работает, ура-ура). На вход нам придёт ID объекта, и по нему-то мы и поймём, что нам с ним делать:

QObject *b1 = m_qt_wnd->rootObject()->findChild<QObject*>( "b1" );
QObject::connect( b1, SIGNAL( clicked() ), &m_clickMapper, SLOT( map() ) );
QObject *b2 = m_qt_wnd->rootObject()->findChild<QObject*>( "b2" );
QObject::connect( b2, SIGNAL( clicked() ), &m_clickMapper, SLOT( map() ) );

m_clickMapper.setMapping( b1, "b1" );
m_clickMapper.setMapping( b2, "b2" );

QObject::connect( &m_clickMapper, static_cast<void(QSignalMapper::*)(const QString&)>(&QSignalMapper::mapped), [this]( const QString &sender ) {
    if ( sender == "b1" )
        m_speed *= 2.0f;
    else if ( sender == "b2" )
        m_speed /= 2.0f;
} );


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

Zifix указывает, что есть ещё один способ взаимодействия QML -> C++ — прокинуть C++ объект в QML, и дёргать его оттуда. Для этого нужно, чтобы объект был наследником QObject и содержал обработанный кодогенератором макрос Q_OBJECT, а также добавить его в контекст:

SignalsHub signal;
engine.rootContext()->setContextProperty(QLatin1String("signal"), &signal);



Туда (C++ -> QML)



Тут нас без кодогенерации ждёт засада — связать сигнал из C++ со слотом в QML не получится (точнее, способы есть, но на мой вкус, они слишком сложны). С другой стороны, а зачем?

На деле, у нас есть аж два (ну ОК, полтора) пути. Во-первых, можно напрямую менять свойства QML объектов из C++ кода, вызываю у них setProperty( «propName», value ). То есть, если вам просто нужно проставить новый текст какому-нибудь полю, то можно так. Очевидно, что этот метод взаимодействия достаточно ограничен во всех смыслах, но на самом деле вы себе даже не представляете, на сколько. Дело в том, что попытка потрогать свойства QML объектов из render-треда приведёт к ошибке. То есть, вот из этих самых before/afterRendering ничего трогать нельзя. А вы там уже, небось, игровую логику написали? :) Я — да.

Чего делать? Во-первых, можно завести в основном треде таймер, который будет срабатывать раз в N секунд и обрабатывать игровую логику. А рендер пусть рендерится отдельно. Придётся их как-то синхронизировать, но это решаемый вопрос.

Но если так делать не хочется, то выход есть! Сигналы QML мы посылать не можем, property писать не можем, а вот функции, внезапно, вызывать очень даже можем. Поэтому, если вам нужно повоздействовать на UI, то достаточно в нём объявить функцию, которая ваше воздействие будет осуществлять (скажем, setNewText), а потом позвать её из C++ через invokeMethod:

QVariant a1 = "NEW TEXT";
m_fps_label->metaObject()->invokeMethod( m_fps_label, "setText", Q_ARG(QVariant, a1) );


Важный момент: аргументы при таком вызове могут быть только типа QVariant, и надо использовать этот вот макрос, Q_ARG. Ещё, если метод чего-то может вернуть, то надо будет указать Q_RETURN_ARG( QVariant, referenceToReturnVariable ).

tzlom уточняет, что таким образом можно вызывать не только функции, но также и сигналы и слоты, объявленные в QML. При этом, если указать параметр Qt::QueuedConnection, то вызов будет произведён отложенно, в том потоке, в котором это точно можно делать

Zifix говорит, что описанную выше технику с прокидыванием C++-объекта в QML контекст можно применять и для связывания сигналов в направлении C++ -> QML. Это позволит избежать поиска QML-объектов в C++ коде, то есть, уменьшить нехорошую связность «по именам» между C++ и QML.

// Так
Connections {
    target: signal // signal - это имя нашего C++ - объекта, прокинутого в контекст

    onHello: {
        console.log("Hello from С++!");          
    });
}

// Или так
Component.onCompleted: {
    signal.onHello.connect(function() { // связываем сигнал C++-объекта с анонимной ф-ей в QML
        console.log("Hello from С++!");          
    });
}



Ресурсы



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

Возникает желание все ресурсы, связанные с UI запихнуть туда же, где лежат остальные ресурсы игры. Тем более, что их не всегда можно чётко разделить — порой одна и та же текстура может использоваться и в 3D сцене, и в UI. При этом, очень хочется, чтобы в QML-файле у нас по прежнему было написано «source: images/button_up.png», чтобы во время разработки, пока ресурсы у нас не упакованы, мы могли бы редактировать UI в Qt Designer, не занимаясь написанием плагинов к нему.

И вот в этот момент нас ждёт жесточайший, и очень обидный облом. Фактически, нам нужно подсунуть Qt свою ресурсную систему под видом файловой. Но поддержку виртуальных файловых систем в виде QAbstractFileEngine в версии 5.x благополучно выпилили «в связи с проблемами с производительностью» (обсуждение). Я не знаю, что и какой пяткой там было написано. Все наши игры прекрасно работают с VFS, сочетающий в себе несколько источников ресурсов, и на производительность не жалуются. Самое обидное, что замены авторы Qt не предложили.

Впрочем, пока что этот класс не выпилили до конца, а лишь «приватизировали», так что если вы любите жить рискованно, то можно использовать его, подключив приватную библиотеку и хидера.

Один костыль авторы оставили — в QMLEngine можно зарегистрировать QQuickImageProvider. С его помощью, вы сможете хотя бы текстуры грузить из вашей системы.

Чтобы QMLEngine использовал ваш QQuickImageProvider, а не лез напрямую в файл, надо указывать путь к изображению в QML-файле не просто «images/button_up.png», а «image:/my_provider/images/button_up.png» (где «my_provider» — имя, с которым вы зарегистрировали ваш наследник QQuickImageProvider в QMLEngine). Очевидно, что если так сделать, то вы тут же перестанете видеть картинки в Qt Designer, который о вашем кастомном провайдере ничего не знает, и знать не хочет.

Нет такого костыля, который нельзя было бы подпереть другим костылём! В QMLEngine можно зарегистрировать ещё одни класс — QQmlAbstractUrlInterceptor. Через этот самый Interceptor проходят все URLы, что грузятся в процессе обработки QML-файла. И тут же их можно подменить на что-нибудь. Что нам и требуется! Как только мы видим, что тип URLа UrlString, а, для надёжности, сам URL содержит текст ".png", то мы сразу же делаем:

QUrl result = path;
QString short_path = result.path().right( result.path().length() - m_base_url.length() );
result.setScheme( "image" );
result.setHost( "my_provider" );
result.setPath( short_path );
return result;


setScheme — это чтобы QML понял, что надо искать подходящий ImageProvider
setHost — имя нашего провайдера
setPath — а вот тут надо уточнить. Дело в том, что в Interceptor URLы приходят уже дополненные base url нашего QMLEngine. По умолчанию, это QDir::currentPath. Нам, очевидно, это совершенно неудобно, вот и приходится отрезать ненужный кусок пути, чтобы вместо какого-нибудь «file:///C:/Work/Test/images/button_up.png» получить, в результате, «image:/my_provider/images/button_up.png».

Ресурсы 2 — ложный след



Дабы повеселить публику, расскажу, как я пытался обмануть Qt, и грузить таки ВСЕ ресурсы из своей системы.

QMLEngine содержит ещё и третий тип классов, которые можно ему установить — это NetworkAccessManagerFactory. Неудобоваримое имя скрывает за собой возможность установить свой собственный обработчик http запросов. А что если, подумал я, мы будем в QQmlAbstractUrlInterceptor запросы к QML файлам подменять на http запросы, а в нашем NetworkAccessManagerFactory (а точнее, в NetworkAccessManager и NetworkReply) на деле открывать файлы из нашей ресурсной системы?

План сработал почти до самого конца :) URLы перехватываются, http-запросы подменяются, даже qml файлы успешно грузятся. Вот только при попытке чтения содержимого служебного файла qmldir с http QQMLTypeLoader делает assert :( И обойти это поведение мне не удалось. А без этого, вся затея бесполезна — мы не сможем импортировать свои QML-модули из нашей ресурсной системы.

Ресурсы Redux



Кстати, у Qt же есть своя собственная ресурсная система! Она позволяет скомпилировать ресурсы в rcc файл, и потом их оттуда использовать. Для этого, глубого в недрах Qt таки сделана своя виртуальная файловая система, которая, если у ресурса указан префикс qrc:/ или даже просто :/, грузит его не с диска, а откуда надо. К сожалению, «откуда надо» — это всё равно не из нашей ресурсной системы.

Есть два способа зарегистрировать источник ресурсов. Оба — вызовы разных перегрузок статической функции QResource::registerResource. Первый принимает на вход имя ресурсного файла на диске. Тут всё понятно — с диска прочитали, и используем. Второй — принимает голый указатель на некую rccData. Документация в этом месте лаконично заявляет, что эта функция регистрирует rccData в качестве ресурса. И дальше ещё мелет какую-то ерунду про файлы. Это — результат неудачной копипасты, кочующей из версии в версию без изменений.

Исследование исходного кода второй перегрузки registerResource показало, что она таки принимает на вход именно содержимое rcc-файла. Почему вместе с указателем не передаётся размер данных? Оказывается — потому, что Qt не хочет ничего проверять, а хочет read-read-read и access violation. В этом месте, библиотека ожидает получить качественные бинарные данные, у которых есть хотя бы заголовок (магические буквы «qres» и данные про размер и другие свойства оставшейся части блока памяти). До того момента, как будет прочитан валидный заголовок, Qt будет жизнерадостно читать любую память, которую вы ей подсунете. Не очень надёжно, но ладно.

Казалось бы, этот вариант нам подходит — можно прочитать rcc-файл из нашей ресурсной системы, засунуть его в QResource, и далее без проблем использовать все ресурсы с префиксом qrc:/. Отчасти, это так. Но помните, что прежде, чем регистрировать данные в ресурсной системе, вам придётся их полностью загрузить в память. Поэтому запихнуть в один rcc все UI-текстуры — скорее всего, плохая идея. Придётся либо готовить отдельный набор для каждого экрана, либо, например, положить в rcc только QML-файлы, а картинки грузить из своей ресурсной системы описанным выше методом через Interceptor+ImageProvider.

Подготовка к релизу



Если вы думаете, что после того, как вы побороли все программные проблемы Qt, написали свой код, нарисовали красивый UI и упаковали ресурсы, у вас всё готово к релизу — то это не совсем так.

Дело в том, что Qt — это много-много DLLей и QML-модулей. Для того, чтобы распространять вашу программу, всё это добро придётся таскать с собой. Но чтобы его таскать, его сначала найти надо, а оно попрятано по уголкам огромной установочной директории Qt. Qt Creator сам всё найдёт и положит куда надо, а вот если мы по прежнему пользуемся другой IDE… Руками вырезать все нужные DLL и прочие файлы — занятие сложное и нудное, а главное — легко допустить ошибку.

Здесь авторы Qt пошли навстречу простым программистам, и предоставили инструменты, такие как windeployqt и androiddeployqt. Под каждую платформу, такой инструмент свой, со своими ключами и ведёт себя по разному. Например, windeployqt принимает на вход путь к вашему главному исполняемому файлу и к директории с вашими QML-файлами, а на выходе — просто копирует все нужный DLL и прочая в указанное место. Дальше сами-сами-сами.

А вот androiddeployqt — это тот ещё комбайн, занимающийся и сборкой APK-пакета, и ещё чёрт знает чем. На iOS ситуация схожая.

Выводы



Итак, можно ли использовать QtQuick/QML для создания UI в играх? Мой короткий опыт интеграции и использования этой библиотеки показал, что в принципе можно. Но многое зависит от конкретных целей и ограничений.

Скажем, если вы готовы для разработки использовать QtCreator — значительная часть мелких неудобств автоматически пропадает, но если вам, по каким-то причинам, хочется остаться с любимым Visual Studio, XCode или vi, то надо готовится к некоторой боли.

Если вы разрабатываете игру под PC, или это очень крупный мобильный проект с сотнями мегабайтов ресурсов (встречаются ведь и такие), то 25-40Мб библиотек для вас не являются проблемой. Если же вы пишите очередную казуалку под Android, да ещё с прицелом на китайский или иранский рынки, с их рекомендованными 50Мб на приложение, то стоит три раза подумать, прежде, чем занимать большую их часть этой не слишком полезной нагрузкой.

Однако, если вам отчаянно не хочется писать свою UI библиотеку, то QtQuick/QML, как мне кажется, выигрывает у конкурентов по производительности, если не по размерам и не по удобству использования.

Интеграция Qt в проект не слишком сложна, но зато может вынудить изменить логику основного цикла и инициализации. В новом проекте это почти наверняка можно пережить, а вот сменить быстро UI с другого на QtQuick/QML вряд ли получится без долгих страданий.

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

Ещё одним минусом, по сравнению с Scaleform и Coherent, является то, что Scaleform позволяет создавать интерфейсы дизайнерам в привычных программах Adobe, а Coherent — нанять для разработки UI спеца по HTML. Разработка UI на QML потребует совместной работы программиста и дизайнера. Впрочем, в конце концов, всё равно к этому приходит, когда начинаются проблемы с производительностью и поведением UI внутри игры.

В общем, решать, как обычно, придётся вам самим!

Код примера интеграци Qt с Nya engine вы можете взять на GitHub MaxSavenkov/nya_qt.
Поделиться с друзьями
-->

Комментарии (30)


  1. tzlom
    21.06.2016 10:01
    +1

    Добавлю две вещи:
    При помощи invokeMethod можно вызывать и слоты и сигналы и методы Q_INVOKABLE, а параметр Qt::QueuedConnection позволяет это делать отложенно (т.е. из любого места в коде, сам вызов произойдёт когда это можно и в том потоке в котором можно), поэтому никаких проблем в коммуникации С++ -> QML на самом деле нет.

    Купившие лицензию могут статически собирать Qt и пытаться выиграть размер итоговой сборки.


    1. MaxEdZX
      21.06.2016 10:09

      Спасибо, это ценное замечание, учту.

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


      1. Antervis
        21.06.2016 10:16

        сложно сказать насколько будет выигрыш в большом проекте, использующем (около) полный набор API, но на одной из своих утилит получил 8 мб вместо 40 (и то потому, что не смог статически прилинковать libmysql и libwinpthread)


  1. Antervis
    21.06.2016 10:04

    Жить с Qt без MOC'a может быть и можно, но зачем? Неужели QtCreator — единственный IDE, поддерживающий компиляцию в несколько этапов?

    Предлагаю обратить внимание на статическую линковку. Всё равно лицензия нужна для любого более-менее серьезного проекта


    1. MaxEdZX
      21.06.2016 10:13

      Не единственная, очевидно. Полагаю, MOC можно прикрутить почти к чему угодно. К Студии так есть дополнение, которое это делает (правда, официальный add-in перестал работать в 2015ой, и до сих пор не починен, что печально).

      Вот с XCode придётся шаг добавлять руками. И автоматического подбора компонент для deployment тоже не будет.


      1. CodeRush
        21.06.2016 10:54
        +1

        Непонятно, почему не использовать qmake для генерации проекта для VS, XCode, Ninja и черта лысого, чтобы у вас был MOC и остальные плюшки автоматически.
        Для VS проект из файла .PRO генерируется вот так:

        qmake -tp vc path/to/project/file.pro
        Для XCode — вот так:
        qmake -spec macx-xcode path/to/project/file.pro


        1. MaxEdZX
          21.06.2016 10:58

          С одной стороны, я о таком не знал, поэтому видимо да, можно и нужно использовать.

          С другой стороны, с высокой вероятностью, у вас проект вашей игры под любимую(ые) IDE настроен руками, и просто так взять и перегенерировать его внешним генератором нельзя (к примеру, у нас активно используются prop-файлы). С третьей стороны, кто мешает вынести UI в отдельный проект, и генерировать его… Одно из этих решений можно выбрать.


      1. Antervis
        21.06.2016 12:38

        намного проще настроить алгоритм сборки/деплоя, чем каждый раз тянуть костыль из-за отсутствия MOC. Например, qmake определяет переменную $$[QT_INSTALL_LIBS] — директория с библиотеками Qt. Добавляем все нужные библиотеки в INSTALLS, определяем и добавляем шаг сборки make install. Запускаешь билд и всё лежит на своих местах. Архивируй и распространяй.


      1. al_sh
        21.06.2016 17:56

        В Visual Studio 2013 и 2015 прекрасно работает этот аддон visualstudiogallery.msdn.microsoft.com/c89ff880-8509-47a4-a262-e4fa07168408.
        Можно, еще создать Makefile проект и там все прописать. Я так для малины собираю, когда виртуалку лениво включать))


    1. crackedmind
      22.06.2016 16:35

      Жить без MOC можно. Не так давно ребята из Woboq header only библиотеку, которая через макросы генерирует бинарно совместимую мета объектную систему Qt.

      https://woboq.com/blog/verdigris-qt-without-moc.html
      https://github.com/woboq/verdigris


      1. MaxEdZX
        22.06.2016 22:49

        Да, я на неё натыкался, но забыл упомянуть в посте. Моя ошибка.


  1. Jester92
    21.06.2016 10:07
    +2

    Для небольших игр можно использовать CEGUI, у которого есть отрисовщики и для DirectX, и для OpenGL, и для OGRE — основное что использовал. Даже редактор свой есть (правда со своими особенностями). В сумме библиотек для релизной версии у меня получилось около 11 мб.
    А для больших игр не используются сторонние наработки по причине либо их закрытости и невозможности быстро допилить нужный функционал/исправить баг, либо из-за недостаточности функционала/производительности, либо кто-то в команде сказал — «Да сейчас сделаем, тут не сложно».


    1. MaxEdZX
      21.06.2016 10:18
      +1

      У нас в 2007ом году с CEGUI был очень грустный опыт в плане производительности и глючности. С тех пор, конечно, уже почти 10 лет прошло, но осадочек остался :) Вдобавок, я смотрю, они только в 2015ом году добавили поддержку OpenGL ES, то есть, что там творится с ним на мобилках — это ещё надо отдельно рассматривать.

      Для очень больших игр стороние наработки вовсю используются — но как правило это именно Scaleform или Coherent. Там закрытость — не проблема, поскольку платный саппорт — создаёшь запрос, и через некоторое время получаешь нужную функциональность в твоей персональной ветке. Время, правда, может быть от пары дней до недель… Я лично с этим двумя, правда, почти не работал (кроме неудачного опыта со Scaleform — и, кстати, сорцы от него у нас были), но вообще с коммерческими библиотеками мы жили на первой моей работе, и с их саппортом вот так общались.


  1. ChALkeRx
    21.06.2016 13:21
    +1

    Qt использует двойную лицензию — LGPL3 и коммерческую. Это означает, что если вас интересуют, в том числе, платформы, где динамическая линковка невозможна (iOS), то придётся раскошелится


    Зануда-mode: дело не в динамической/статической линковке, под LGPL вполне можно распространять библиотеку в статически слинкованном виде с вашей проприетарной программой (при наличии исходников самой библиотеки). Этого никто не запрещает.

    Что LGPL требует — так это чтобы у пользователя была возможность внести изменения в исходные коды библиотеки, собрать её, и использовать вашу программу с изменённой версией библиотеки. Чтобы это выполнить, достаточно приложить/выложить объектные файлы всего и предоставить инструкции по сборке этого в статический бинарь. Ну и не вшивать дополнительных проверок, например, чексуммы библиотеки.

    С iOS другая проблема — она, насколько я знаю, не удовлетворяет именно условию того, чтобы пользователь мог пересобрать программу с изменённой библиотекой и воспользоваться результатом. Так что покупать лицензию вам всё равно придётся, если вы распространяете программы через AppStore.


    1. MaxEdZX
      21.06.2016 13:33

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


    1. DmitrySokolov
      21.06.2016 15:45

      … под LGPL вполне можно распространять библиотеку в статически слинкованном виде ...

      Obligations of the LGPL

      In case of static linking of the library, the application itself may no longer be “work that uses the library” and thus become subject to LGPL. It is recommended to either link dynamically, or provide the application source code to the user under LGPL.

      В случае статического связывания библиотеки (линковки), приложение может перестать быть «произведением, использующим библиотеку», и т.о. стать объектом LGPL. Рекомендуется либо использовать динамическое связывание, либо предоставлять пользователям исходный код приложения по лицензии LGPL.

      Не определённо это «may no longer be», в каких случаях? Есть ли у вас что сказать по этому поводу?


      1. ChALkeRx
        21.06.2016 17:14

        Фантазии продаванов Qt Company о LGPL не имеют к LGPL никакого отношения. У них и давно ересь была на сайте в сравнительной табличке, и последний раз, когда я видел, в визарде выбора лицензии (основная задача которого — убедить тебя купить лицензию).


        https://www.gnu.org/licenses/gpl-faq.html#LGPLStaticVsDynamic


        P.S. Вот прямо сейчас проверил — этот визард на qt.io по вкладке Download открывается, по некоторым путям визарда спрашивает про тип линковки, спрашивает, готовы ли вы выложить исходники своего приложения, и на основе этого делает какие-то выводы.


  1. Zifix
    21.06.2016 14:43

    Не знаю специфики геймдева, но вот передача сигналов в обе стороны на самом деле сильно проще, вы очень усложняете. Берем C++ объект, прокидываем его в глобальный контекст (будет доступен из любого места в QML) и радуемся жизни:

    SignalsHub signal;
    engine.rootContext()->setContextProperty(QLatin1String("signal"), &signal);
    

    В этом объекте можно сделать сигналы, которые будут дергаться из QML, на них можно подписываться как угодно, в том числе через лямбды. Таким же образом, можно дергать в обратном направлении: сигналы дергаем из С++, а подписываемся на них из QML.


    1. MaxEdZX
      21.06.2016 15:14
      +1

      Это точно сработает с объектом, в котором нет Q_OBJECT? Я не очень понимаю, как QML найдёт в нём тогда слот. Сложности, которые я описал, они только для тех, кому, по какой-то причине, не хочется или нельзя использовать MOC. А так-то всё вообще просто.


      1. Zifix
        21.06.2016 18:51

        Вы правы, читал по диагонали и упустил этот момент.


  1. DmitrySokolov
    21.06.2016 16:02
    +1

    По поводу рендеринга контента: существует ещё два способа — Mixing Scene Graph and OpenGL. Один, тот что вы описали (рисование на контексте в момент вызова событий), второй — рисовать на текстуре/буфере, встроенной в граф сцены Qt Quick.
    Плюс есть еще QQuickRenderControl, который предназначен как раз для интеграции с 3rd party OpenGL renderers. И который, наоборот, отдает цикл отрисовки приложению, UI рендерится куда укажут.


    1. MaxEdZX
      21.06.2016 16:59

      О, про QQuickRenderControl даже есть статья на Хабре! Полезно.

      Тем временем, коллега подсказывает, что «рисовать на текстуре/буфере, встроенной в граф сцены Qt Quick» может быть не лучшей идеей на мобильных девайсах по причина того, что филлрейт там — узкое место.


  1. abagnale
    21.06.2016 18:46

    придётся раскошелиться на 79$ в месяц за каждого работника «использующего» Qt
    Это в случае лицензии «subscription», причём оплаченной сразу за год, и также если вы «qualify for Start-Up», иначе цена выше. К тому же, после истечения подписки вы теряете право на разработку, потому для долгосрочных проектов лучше приобрести «perpetual» лицензию

    «использовать», это, как я понимаю, хотя бы просто собирать проект с библиотеками
    Для процесса сборки лицензия не требуется. Например, вы можете иметь несколько десятков билд-машин и лицензия для каждой из них не нужна

    важным техническим аргументом против Qt является её вес
    Относительно скоро появится новый «компонент» — Qt Light, который существенно снизит вес таскаемых библиотек. На самом деле, «компонент» — неудачный термин, это будет что-то вроде системы конфигурации Qt, позволяющей исключить неиспользуемые модули

    просто не надо рассчитывать, что можно будет отдать Qt Designer художнику, и он вам всё настроит мышкой
    Ну теоретически так и предполагается, что можно разделить работу на дизайнера и погроммиста. Первый «нарисует» UI мышью, а второй, получив от первого сгенерированный QML, напишет логику. В теории. Не знаю, применяется ли это реально в живых проектах, может у кого-то есть опыт?

    обещали ещё заметно улучшить с новыми QtQuick Controls 2.0
    В свежайшем релизе 5.7 они уже есть (до этого также были в 5.6, как Technical Preview). Вот видео, где человек рассказывает, за счёт чего произошли улучшения в производительности, и недавний пост

    для того, чтобы распространять вашу программу, всё это добро придётся таскать с собой
    Но ведь если вы приобрели лицензию, то вы можете слинковать всё статически. Кстати, можно также использовать Qt Quick Compiler, чтобы и QML файлы не лежали открыто и не компилились дополнительно


    1. MaxEdZX
      21.06.2016 18:48

      если вы приобрели лицензию, то вы можете слинковать всё статически.


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

      Кстати, можно также использовать Qt Quick Compiler, чтобы и QML файлы не лежали открыто и не компилились дополнительно


      Полезно, хотя у нас вот свой формат пакета ресурсов, уже и так шифрованный.

      (Если можно, поправьте форматирование комментария, чтобы была цитата цитатой)


      1. abagnale
        21.06.2016 20:42

        Как минимум в размере бинарников экономия, и ощутимая, выше писали 8 метров против 40 — это вполне реальный пример. И лицензия это не только статическая линковка, но и также дополнительные компоненты, которые доступны только в коммерческой лицензии, а также тех.поддержка с доступом к RnD команде, которая разрабатывает собственно Qt.

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


        1. MaxEdZX
          21.06.2016 20:53

          Компоненты начиная с 5.7 стали одинаковые в платной и бесплатной версии после тотального перехода на LGPL3, как я понял (отказа от лицензирования части компонентов по LGPL2.1).


          1. abagnale
            21.06.2016 21:15

            Это не совсем так. Часть компонентов (графики, виртуальная клавиатура, Qt Quick 2D renderer и другие) теперь стала доступна под GPL, но не под LGPL. А некоторые (Boot to Qt и тот же Qt Quick Compiler, например) по-прежнему есть только в коммерческой лицензии.


            1. Antervis
              22.06.2016 06:01

              Qt Quick Compiler планируют добавить в публичную лицензию в Qt 5.8


              1. abagnale
                22.06.2016 09:22

                Планируют, но это ещё в процессе обсуждения. И несмотря на то, что это инструмент, а не библиотека, пока нет ясности с его лицензированием. Ну то есть должно быть GPL, чтобы было как с Qt Creator (можно использовать и не открывать свои исходники), но вопрос ещё обсуждается.


  1. RSATom
    22.06.2016 09:49

    По поводу ресурсов. Мне однажды доводилось реализовывать генерацию и загрузку небольшого файла ресурсов в run-time — только вот подробности, к сожалению, припоминаются смутно, но вдруг кому окажется полезным…