The article has been updated with useful comments. Many thanks to all the commentators for important clarifications and additions.In games with a complicated UI, creating your own library for its display and tools for easy editing can be a very long and difficult task. Which I really want to decide once and for all, and not to do it again in each new project, and even in each new company.
The solution is to use ready-made universal UI libraries. Their current generation is represented by such “monsters” as
Scaleform and
Coherent UI , although if you so want to write UI in HTML, then you can just take
Awesomium .
')
Unfortunately, this trio, with all its advantages, has one major drawback - terrible brakes, especially on mobile devices (a few years ago, I personally watched the Scaleform’s almost empty screen consume 50% of the frame time on iPhone4).
Against this background, I was always wondering why no one uses
Qt in games, a library that has worked well in desktop applications. In fact, this statement is not quite right - in the Qt project Wiki there is a
list of games, however there are almost no modern professional projects in it.
However, the reason why the familiar old
Qt Widgets are not used in games lies on the surface: they are not designed for use with the OpenGL or DirectX render. Attempts to cross them give pretty bad performance
even on the desktop , but there's nothing to say about mobile phones.
However, Qt has a much more suitable library for this task for quite some time:
QtQuick . Its controls by default are rendered at an accelerated rate, and the ability to set the UI description in text format is great for quick customization and changing the look of the game.
However, I still have not heard about using Qt in professional game development. There were no articles on the topic either, so I decided to figure it out myself - whether everyone knows something, which I don’t know (but don’t tell!), Or simply don’t see a good opportunity to save on development time.
Arguments against:
I'll start with the thing that is the most distant from technical issues, namely
licensing . Qt uses a dual license - LGPL3 and commercial. This means that if you are interested in, including platforms, where dynamic linking is not possible (iOS), then you have to fork out $ 79 a month for each employee of “using” Qt. “Use” is, as I understand it, at least just to build a project with libraries, that is, you have to pay for each programmer on the project.
ChALkeRx clarifies that to use static linking, it is not necessary to buy a commercial license or upload the source code - just lay out the object files, a set of which will allow you to rebuild your application with a new static version of Qt.Money is not very big, but still not free. And there is another very interesting point: it is desirable to get a commercial Qt license as soon as you start using Qt in your project. Otherwise, when you try to get a license, you will be offered to "contact our specialists to discuss the conditions." They are understandable: not only in our country, smart citizens would have guessed using the free version for the whole development for five years, and only to build a license for 1 month to build the final build!
Perhaps the most important technical argument against Qt is its weight. A practically empty desktop application using QML takes up more than 40Mb (with dynamic DLL linking). On Android, the sizes will be slightly smaller, about 25Mb (in the uncompressed form - the APK will be noticeably lighter), but for a mobile platform this is just VERY much! Qt offer a crutch that allows you to install libraries on a user's phone once, and use them from different applications (
Ministro ), but this crutch is obviously available only on Android, but we would like to somehow solve the issue with the sizes on iOS and Windows Phone ...
However, lamenting over the fattened libraries, do not forget that the competitors - the Scaleform and Coherent mentioned above - are not much better in this respect, both of them give out empty applications in tens of megabytes. Unity is a little easier, but still, about 10Mb. Therefore, here Qt strongly loses only its own, optimized for the task of development.
Finally, I mention another potential flaw - Qt is not ready for use under the Web (Emscripten). Most developers are not very important, but here we are, for example, engaged in this direction, and here it is impossible to use Qt, although work is
being done in this direction.
Arguments for:
The main argument for using QtQuick / QML is a convenient UI description format, as well as a visual editor for it. Plus, a large ready-made set of controls.
It is worth mentioning the possibility to write some part of the UI code in JavaScript inside QML, for example, any simple arithmetic connecting the state of the fields of different objects - an opportunity that is very rarely available in self-made UI libraries (and often necessary).
However, it is worth noting that Qt Designer is not a Visual Studio form designer. Even for the basic controls that come with Qt, it does not allow editing all their possible properties (for example, because they can be added dynamically). In particular, you will not be able to assign a
picture to the
button for the pressed and released position through the editor. And this is just the beginning of the problems. On the other hand, by combining the use of a visual and text editor, all these problems can be overcome. Just do not expect that you can give Qt Designer to the artist, and he will set everything up for you without getting into the textual representation.
Performance, according to my feelings, is acceptable for QtQuick. In the latest release of Qt 5.7, it was promised that it would be noticeably improved with new QtQuick Controls 2.0, sharpened for mobile platforms.
Technical features
We now turn to the most interesting - the technical features of the use of Qt in the game.
Main loop
The first thing to face is that Qt prefers to be the master of the main loop. At the same time, many game engines also claim this. Someone will have to give up. In my case, the
Nya engine that we use at work breaks up without any problems with the main loop, and, after minimal initialization, easily uses the OpenGL context created by Qt. But even if your engine refuses to release the main loop of tenacious legs, this is not the end of the world. It is enough in your loop to call the processEvents method on the Qt application class. An example implementation is shown on
StackOverflow , along with criticism.
DmitrySokolov points out that there are at least two ways to make friends with the Qt render and your engine: first, you can render your scene into a texture that will be drawn as one of the components of the QtQuick stage graph, as described in Mixing Scene Graph and OpenGL . Secondly, you can use the QQuickRenderControl object. Regarding the latter, there is a useful article on Habré, which, in particular, demonstrates the possibility of using two (spherical) contexts for Qt and the game render, so as not to bother so much with states.If you went by handing the main loop to Qt, then the question arises - when will we render our game? The QQuickView object, into which the UI is loaded for display, provides the
beforeRendering and
afterRendering signals to which you can subscribe. The first one will work to render the UI - here it’s time to render most of the game scene. The second is after the UI is drawn, and here you can draw some more beautiful particle, well, or all of a sudden some modelki, which are supposed to be on top of the UI (say, the character's 3d doll in the equipment window). IMPORTANT! When connecting signals, specify the type of connection
Qt :: ConnectionType :: DirectConnection , otherwise you will get an error due to an attempt to access the OpenGL context from another thread.
At the same time, you should not forget to forbid Qt to clear the screen before drawing the UI - otherwise all our works will be
erased (setClearBeforeRendering (false) ).
Also, in afterRendering, it makes sense to call the update function from QQuickView. The fact is that usually Qt saves our time and money, and as long as nothing has changed in it, there will be no redrawing of the UI, and as a result it will not cause these same before / afterRendering, and we will not be able to draw anything either. Calling update will force everything to be drawn again at the next frame. If you want to limit the number of frames per second, then you can immediately sleep.
Something else about drawing
It is necessary to remember about the fact that we have a common OpenGL context with Qt. This means that you need to handle it with care. First, Qt will do whatever it wants with it. Therefore, when we have to draw something ourselves (in before or in afterRendering), then first, we need to make this context current (
m_qt_wnd-> openglContext () -> makeCurrent (m_qt_wnd) ), and second, set He needs all the settings we need. In Nya engine, this is done with a single call to apply_state (true), but in your engine it can be more difficult.
Secondly, after we have drawn our
own , it is necessary to return the context to the state Qt
satisfies , calling
m_qt_wnd-> resetOpenGLState ();By the way, it is worth considering that since OpenGL context creates Qt and not your engine, you need to make sure that your engine does not do anything extra before the context is created. To do this, you can subscribe to the
openglContextCreated signal, well, or do initialization in the first call to beforeRendering.
QML Interactions
So, here our game draws its own scene, on top - Qt draws its controls, but so far all this does not communicate with each other. So you can not live.
If you write your code in QtCreator, or in another IDE, to which the call to the Qt-shnogo code generator (MOC) is miraculously bolted, then your life will be simple. It is enough to connect the slots and signals by names, and QML will receive calls from C ++, and vice versa.
CodeRush indicates that MOC is fairly easy to tie to any of the popular IDEs, since Qt has a means of generating projects from .pro files:
For VS, the project from the .PRO file is generated like this:
qmake -tp vc path / to / project / file.pro
For Xcode, like this:
qmake -spec macx-xcode path / to / project / file.pro
al_sh reminds of Add-In for Visual Studio 2013 & 2015However, you may want to live without MOC. It is possible! But you have to get some crutches out of the cushion.
Here (QML -> C ++)
Qt now supports two ways to connect signals and slots - the old, by name, and the new, by pointers. So, with QML you can contact only by name. This means, firstly, that it is impossible to hang a lambda on the signal from QML (sob-sob, but I so wanted C ++ 11!), And secondly - that you have to have an object in which the slot is declared, and this object should to be a QObject successor, and inside yourself to have a Q_OBJECT macro, for code generation. And we have no code generation. What to do? That's right, taking objects in which all slots have already been declared, and therefore they do not need code generation.
In fact, this is generally a very useful approach, which, with some probability, you will already need. We will use the
QSignalMapper helper class. This class has exactly one slot - map (). You can attach as many signals as you want from as many objects as you like. In response, QSignalMapper for each received signal will generate another signal - mapped (), adding to it the previously registered ID of the object that generated the signal, or even a pointer to it. How to use it? Very simple.
Create a separate QSignalMapper for each type of signals that can come from QML (clicked for buttons, etc.). Further, when we in C ++ need to subscribe to a signal from an object in QML, we associate this signal with the desired QSignalMapper, and already its mapped () signal is associated with its class, or even lambda (at this level C ++ 11 already works , cheers cheers). At the entrance we will receive the object ID, and according to it we will understand what we should do with it:
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; } );
In the test project code, a link to which you will find at the end of the article, there is an example of wrapping this mechanism for a little more convenient use.
Zifix indicates that there is another way for QML -> C ++ to interact - throw a C ++ object into QML, and pull it from there. To do this, the object must be a QObject descendant and contain the Q_OBJECT macro processed by the code generator, and also add it to the context:
SignalsHub signal; engine.rootContext()->setContextProperty(QLatin1String("signal"), &signal);
There (C ++ -> QML)
Here, an ambush awaits us without code generation - there is no way to link a signal from C ++ to a slot in QML (more precisely, there are ways, but for my taste, they are too complicated). On the other hand, why?
In fact, we have as many as two (well, OK, one and a half) ways. First, you can directly change the properties of QML objects from C ++ code,
calling them
setProperty ("propName", value) . That is, if you just need to affix a new text to some field, then you can. Obviously, this method of interaction is rather limited in all senses, but in fact you cannot even imagine for yourself how much. The fact is that an attempt to touch the properties of QML objects from the render thread will lead to an error. That is, here from these most before / afterRendering it is impossible to touch anything. And you there already, probably, the game logic was written? :) I - yes.
What to do? Firstly, you can start a timer in the main thread, which will be triggered once every N seconds and process the game logic. And let render render separately. We'll have to synchronize them somehow, but this is a solvable question.
But if you do not want to do that, then there is a way out! We cannot send QML signals, we cannot write property, but suddenly we can call functions. Therefore, if you need to pause the UI, then it is enough to declare a function in it that will effect your impact (say, setNewText), and then call it from C ++ via invokeMethod:
QVariant a1 = "NEW TEXT"; m_fps_label->metaObject()->invokeMethod( m_fps_label, "setText", Q_ARG(QVariant, a1) );
An important point: the arguments for such a call can only be of type QVariant, and you need to use this macro, Q_ARG. Also, if a method can return something, you will need to specify Q_RETURN_ARG (QVariant, referenceToReturnVariable).
tzlom clarifies that this way you can call not only functions, but also signals and slots declared in QML. In this case, if you specify the Qt :: QueuedConnection parameter, the call will be made deferred, in the stream in which this can be done exactlyZifix says that the technique described above with procide a C ++ object in QML context can also be used to bind signals in the direction of C ++ -> QML. This will avoid searching for QML objects in C ++ code, that is, reducing the poor connectivity “by name” between C ++ and QML.
Resources
In principle, we are already almost all good. And if for all games all resources would simply lie in clear form next to the executable file, an article on this could be completed. Unfortunately, life is a bit wrong: resources in games are often packed, and even in some special format of their own. Which game engine is able to efficiently stream from disk to memory and all that.
There is a desire to push all the resources associated with the UI to the same place where the other resources of the game lie. Moreover, they can not always be clearly divided - sometimes the same texture can be used in the 3D scene and in the UI. At the same time, I really want to see in the QML file that we still have written “source: images / button_up.png”, so that during development, while the resources are not packed, we could edit the UI in Qt Designer, without writing plugins to it.
And at this moment we are waiting for the most severe, and very offensive bummer. In fact, we need to slip Qt our resource system under the guise of a file system. But the support of virtual file systems in the form of
QAbstractFileEngine in version 5.x was safely
cut out "due to performance problems" (
discussion ). I do not know what and what heel was written there. All our games work great with VFS, combining several sources of resources, and do not complain about performance. The most annoying is that Qt authors didn’t suggest replacements.
However, so far this class has not been cut out to the end, but only “privatized”, so if you like to live risky, you can use it by connecting the private library and the hider.
The authors have left one crutch - in QMLEngine you can register
QQuickImageProvider . With it, you can at least load textures from your system.
In order for QMLEngine to use your QQuickImageProvider, rather than climbing directly to a file, you must specify the path to the image in the QML file not just “images / button_up.png”, but “image: /my_provider/images/button_up.png” (where “my_provider” - the name with which you registered your successor to the QQuickImageProvider in QMLEngine). Obviously, if you do this, you will immediately stop seeing the pictures in Qt Designer, which knows nothing about your custom provider and does not want to know.
There is no such crutch that could not be supported with another crutch! In QMLEngine, you can register another class -
QQmlAbstractUrlInterceptor. All the URLs that are loaded during the processing of a QML file pass through this same Interceptor. And then they can be replaced by something. What we need! As soon as we see that the URL type is UrlString, and, for reliability, the URL itself contains the text ".png", we immediately do:
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 is for QML to understand that you need to look for a suitable ImageProvider
setHost is the name of our provider
setPath - and here it is necessary to clarify. The fact is that the Interceptor URLs come already supplemented with the base url of our QMLEngine. By default, this is QDir :: currentPath. Obviously, this is completely inconvenient for us, so we have to cut an unnecessary piece of the path in order to get, as a result, “image: / my_provider” instead of some “file: /// C: /Work/Test/images/button_up.png” /images/button_up.png. "
Resources 2 - False Footprint
In order to amuse the public, I will tell you how I tried to deceive Qt and load ALL resources from my system.
QMLEngine also contains a third type of class that can be set for it — this is
NetworkAccessManagerFactory . The indigestible name hides the ability to set up its own http request handler. What if, I thought, we’ll replace QML files with QQmlAbstractUrlInterceptor with http requests, and in our NetworkAccessManagerFactory (or rather NetworkAccessManager and NetworkReply) actually open files from our resource system?
The plan worked almost to the very end :) URLs are intercepted, http-requests are replaced, even qml files are successfully loaded. That's just when trying to read the contents of the qmldir service file with http QQMLTypeLoader makes assert :( And I could not get around this behavior. And without this, the whole idea is useless - we cannot import our QML modules from our resource system.
Redux Resources
By the way, Qt also has its own resource system! It allows you to compile resources into an rcc file, and then use them from there. For this, deep in the depths of Qt, its own virtual file system is made, which, if the resource has the prefix qrc: / or even simply: /, loads it not from disk, but from where it is necessary. Unfortunately, “from where it is necessary” is still not from our resource system.
There are two ways to register a source of resources. Both are calls to different overloads of the QResource :: registerResource static function. The first one takes as input the name of the resource file on disk. Everything is clear - read from the disk and use it. The second - takes a bare pointer to some rccData. The documentation at this point concisely states that this function registers rccData as a resource. And further still grinds some nonsense about files. This is the result of an unsuccessful copy-paste, wandering from version to version without changes.
An investigation of the source code of the second registerResource overload showed that it did take the contents of the rcc file as input. Why is the data size not transmitted along with the pointer? It turns out - because Qt does not want to check anything, but wants to read-read-read and access violation. In this place, the library expects to receive high-quality binary data that has at least a header (the magic letters "qres" and data about the size and other properties of the remaining part of the memory block). Until a valid header is read, Qt will cheerfully read any memory you give it. Not very reliable, but okay.
It would seem that this option suits us - you can read the rcc file from our resource system, stick it into the QResource, and then use all the resources with the prefix qrc: / without any problems. In part, it is. But remember that before registering data in a resource system, you will have to completely load it into memory. Therefore, stuffing all UI textures into one rcc is probably a bad idea. You have to either prepare a separate set for each screen, or, for example, put only QML files in rcc, and load pictures from your resource system using the method described above via Interceptor + ImageProvider.
Release preparation
If you think that after you have overcome all the Qt software problems, wrote your code, drew a beautiful UI and packed resources, everything is ready for release - this is not entirely true.
The fact is that Qt is a lot of DLLs and QML modules. In order to distribute your program, all this good will have to carry with you. But in order to carry it, you first need to find it, but it is hidden by the corners of the huge Qt installation directory. Qt Creator will find everything and put it where it should be, but if we still use another IDE ... Hands to cut out all the necessary DLLs and other files - the task is difficult and tedious, and most importantly - it is easy to make a mistake.
Here, the Qt authors went along with simple programmers, and provided tools such as windeployqt and androiddeployqt. Under each platform, such a tool is yours, with its keys and behaves differently. For example, windeployqt accepts a path to your main executable file and a directory with your QML files, and at the output it simply copies all the necessary DLL and other files to the specified location. Then do it yourself.
But androiddeployqt - this is the same harvester, engaged in the assembly of the APK package, and the devil knows what. On iOS, the situation is similar.
findings
So, can QtQuick / QML be used to create UI in games? My short experience of integration and use of this library showed that, in principle, it is possible. But a lot depends on specific goals and restrictions.
Let's say if you are ready to use QtCreator for development - a significant part of the minor inconvenience disappears automatically, but if for some reason you want to stay with your beloved Visual Studio, XCode or vi, then you need to get ready for some pain.
If you are developing a game for PC, or it is a very large mobile project with hundreds of megabytes of resources (there are such things), then 25-40 MB of libraries are not a problem for you. If you write another kazualka for Android, and even with an eye on the Chinese or Iranian markets, with their recommended 50MB per application, you should think about it three times before taking up most of this not too payload.
However, if you desperately don’t want to write your UI library, QtQuick / QML, it seems to me, outperforms competitors in performance, if not in size and not in usability.
Integration of Qt into the project is not too complicated, but it can force to change the logic of the main loop and initialization. In the new project, this is almost certainly possible to survive, but changing quickly the UI from another to QtQuick / QML is unlikely to happen without much suffering.
Qt's documentation is pretty good, but sometimes it's lying or incomplete. In these cases, you have to go into the source code - and it’s very good that it is completely open! Its volumes are solid, but in fact it’s quite possible to figure out how something loads or works.
Compared to Scaleform and Coherent, another disadvantage is that Scaleform allows designers to create interfaces in familiar Adobe programs, and Coherent can hire an HTML specialist to develop the UI. Developing a QML UI will require collaboration between a programmer and a designer. However, in the end, it still comes to this when problems begin with the performance and behavior of the UI inside the game.
In general, you have to decide, as usual, by yourself!
You can get the Qt integration example code with the Nya engine on GitHub
MaxSavenkov / nya_qt .