Experience in developing arcades for Android in C ++ and Qt


    Space will not impose itself

    Background


    I, like many programmers, chose this profession because in my childhood I played computer games and dreamed of developing them. As soon as I learned to write more or less code that could compile without syntax errors, I, of course, began to make all sorts of stupid games that I showed to all my friends and acquaintances. But time passed, and it forced us to do completely different things, to work on projects that, to put it mildly, are more serious than games. And so it went on for the past few years. And the original desires have not gone away, only free time has disappeared.

    I have long wanted to make some kind of project for Android, and, as you know, the bulk of projects are developed on the Android SDK and Java, and NDK is recommended to be used only in “speed-critical” places and not to do everything entirely on it.

    But who needs all these recommendations and rules when there is Qt? I don’t know Java to the extent that I think is sufficient for high-quality development of the game, and I didn’t want to learn it, but I have C ++ knowledge in store. After several test projects on Qt for Android, I realized that it is quite possible to develop a full-fledged application on it, and even transfer it to other platforms. Also, after watching the video Shia LaBeouf - Just Do it, it became clear that I was doomed to do this.

    So, I want to talk about the experience of developing a game for Android on Qt 5.5.1 and C ++.


    Idea


    Oddly enough, the idea of ​​the game came pretty quickly and in a very unusual way. We experimented with the phone’s sensors in an attempt to make a toy “snitch” (temporary name), the meaning of which was - the more and more often you hit the phone, the more points you get. To do this, it was necessary to distinguish the blow from the phone from the swing, and, fortunately, we did not succeed, but when displaying the graphs on the screen, interesting pictures were drawn, which prompted us to the idea of ​​the game, which will be discussed.

    Suddenly, I wanted to make a game about something falling into a cave, the contours of which change over time. It was originally supposed to make a two-dimensional game with graphics in a drawn style. In the future, everything flowed into a three-dimensional game with a top view. A cave generator was written that allowed you to create layers with contours. How to calculate collisions with cave walls and how to turn them into a three-dimensional model? It was possible to think and invent something incredibly cool for a long time, but I took a detour and made the voxel geometry of the world. Calculation of collisions and display on the screen have become noticeably easier, but at the cost of the fact that the game has become similar to minecraft. I was not taken aback and made her look even more like him, adding a character model from him. So the look and concept of the game was formed.


    The first version of the cave generator (archive photo)

    Starting to develop the game, I did not set myself the goal of making money on it, like many, but I just wanted to start my project and bring it to the end. Therefore, the game should be free (who will buy it from me?), But still with a minimal amount of advertising.

    Graphics


    Facilities

    In Qt, you can use OpenGL ES 2.0 for 3D graphics, the following classes can help us with this:
    • QGLWidget is an obsolete class, a descendant of QWidget. Used before Qt 5.0
    • QOpenGLWidget - a new class, performs the same functions as QGLWidget, but uses an intermediate framebuffer in its implementation
    • QOpenGLWindow - designed to display OpenGL graphics, is not a widget and does not pull dependencies widget module


    Experimenting with these widgets, I made several conclusions:
    • QGLWidget is much faster than QOpenGLWidget on weak smartphones
    • QOpenGLWidget does not change the size of the internal framebuffer when resizing the window, it is probably a bug or feature, or I'm just an idiot, but nevertheless, when using this widget, the window resolution did not match the viewport size, which led to either a stretched image or a traveled one projection matrix. For all that, the performance using this widget was extremely small.
    • QOpenGLWindow is the most suitable candidate. It cannot be used as a viewport for a graphic scene and you cannot add child widgets, which makes it impossible to create a user interface using Qt. But he is the most productive of all three


    To implement the game, I chose QOpenGLWindow, and I had to cycle the user interface myself.

    The frame update was implemented by the frameSwapped () signal, which is generated after swapBuffers. Using this signal allows you to achieve a smoother frame change than when using timers.

    To calculate the animation, I needed to calculate the frame time. To do this, I used QTime first , which was a bad idea, since this class considers non-uniform time, and on the mobile device, time adjustments often occur, which led to getting into the past or future, depending on the clock’s departure on the device. Also, permission to change this class is limited to milliseconds, which is not enough for smooth animation with unstable FPS.

    After thinking and referring to the documentation, I decided to use the QElapsedTimer class , which tries to use monotonous time and has a resolution of up to nanoseconds.

    Textures and interface

    For the first versions and debugging, the textures were borrowed temporarily from minecraft, as well as some character skin. The first version of the interface was a gray square button and was made in the evening, and the processing took a whole month.

    The first screenshot of the game


    My friend was chosen as the designer. The advantages of this choice is that you can trust this designer, and the disadvantages are that he is not a designer - he is an auto mechanic.

    In the future, we drew the original textures for several levels and made the textures of the characters.
    The process of drawing a “beautiful” interface and characters turned out to be quite lengthy. While the designer was trying to squeeze a new character or button out of me, I had no choice but to continue optimizing the code.

    Screenshot from the penultimate version


    The level texture is a large atlas of all blocks and accommodates 32 cubes with a side of 16 pixels. With this approach, when drawing layers, you do not need to constantly reinstall the current texture, but you can draw the entire level using vertex buffers in a few calls.

    A problem arose in the extreme pixels of blocks that are adjacent to other blocks. When rounding the texture coordinates, artifacts began to appear in the form of stripes at the edges of the blocks. I solved this problem by adding to each block a border of one pixel wide duplicating the pixel of the edge of the block. Such a peculiar CLAMP_TO_EDGE.

    Unfortunately, with this approach, I could not use mipmap for the level, because the texture coordinates in the vertex buffer must take into account this offset by one pixel, and when creating a texture of reduced detail this offset becomes smaller, and I would have to use different level geometry depending on the degree of detail. I decided not to bother with this, because the very texture level is only 256x256 pixels, and bilinear filtering would not bring much performance gain, but would bring only additional difficulties in implementation.

    I also wanted to note that although there are only 32 blocks, I set the level properties to set the blocks rotated 90, 180 and 270 degrees, as well as the animation of switching between the block texture, which allowed us to diversify the visual component of the game. Although, I applied animation only at one of the levels to create the effect of rotation of the fans.

    Shaders

    Qt has convenient classes for working with shaders. I used QOpenGLShaderProgram . This class allows you to add vertex and fragment shaders, compile them, link, set uniform and attrubute. The class itself is just a wrapper over many OpenGL calls and, accordingly, is not a full-fledged object, in the understanding that the class can be broken by using GL calls directly between calls to this class.

    Conveniently, the class automatically adds define in such a way that the ES shader compiles normally on both the desktop and the mobile device. This applies primarily to precision specifiers, which on the desktop turn into nothing.

    I had to write separate shaders for the game world, including lighting and animation of some blocks, a shader for the character and interface shaders.

    Interface shaders include
    • simple texture shader
    • shader bar xp \ shield
    • circle shader

    In the first version, I implemented a progress bar with a set of rectangles forming the indicator bulb and the indicator itself, hidden in the bulb. But, when I saw the layout of the new bar, I had to refuse this method.


    Bar xn \ shield

    I decided to implement this bar with a shader, at first it seemed to me a very strange idea, but in the end I accepted it as inevitable and implemented it in this way:
    Shader code
    #ifdef GL_ES
    precision highp int;
    #endif
    varying highp vec4 v_position;
    varying highp vec2 v_dim;
    uniform lowp vec4 u_color;
    uniform highp float u_tg;
    uniform highp float u_value_a;
    uniform highp float u_value_b;
    uniform highp int u_step;
    uniform lowp vec3 u_pallete[3];
    void main()
    {
        int hstep = u_step/2;
        int w = int(v_dim.x);
        int h = int(v_dim.y);
        int wd = u_step*3;
        int pos_bl = int(v_position.x - u_tg*v_position.y + v_dim.y);
        int pos_br = int(v_position.x + u_tg*v_position.y);
        int pos_l = (pos_bl - wd*(pos_bl/wd))/u_step;
        lowp float b0 =   float(pos_l == 0) * float(pos_bl <= int(u_value_a*v_dim.x));
        lowp float b1 =   float(pos_l == 1) * float(pos_bl >= int((1.0-u_value_b)*v_dim.x));
        lowp float b2  =  clamp(float(pos_l == 2) + 1.0-(b0+b1), 0.0, 1.0);
        highp float p = abs(2.0*(v_position.w - 0.5));
        highp float out_p = (1.0 - 0.25*p);
        lowp float i = float(int(v_position.y) > hstep) * float(int(v_position.y) < h - hstep);
        lowp float o = (1.0-i)*float(pos_br >= h) * float(pos_bl <=w);
        lowp float a = i*float(pos_br >= h+u_step)  * float(pos_bl <=w - u_step);
        lowp float b = i*float(pos_br < h+u_step)   * float(pos_br >= h);
        lowp float c = i*float(pos_bl > w - u_step) * float(pos_bl <= w);
        highp float pr = (1.0 - p)*a;
        gl_FragColor = vec4(  u_pallete[0]*pr*b0
                            + u_pallete[1]*pr*b1
                            + u_pallete[2]*pr*b2
                            + u_pallete[0]*out_p * b
                            + u_pallete[1]*out_p * c
                            + mix(u_pallete[0], u_pallete[1], v_position.z)*out_p*o,
                            clamp(a+b+c+o, 0.0, 1.0)*u_color.a);
    }
    


    I would also like to say about the variety of android devices and their graphics accelerators. I ran into problems:
    • The shader code compiles without problems on one device, and errors occur on another
    • The shader compiles normally, but it doesn’t work correctly on some devices. For example, on PowerVR, if the local variable is called the same as the uniform or attribute, this does not lead to errors, but the shader itself stops working correctly
    • On some devices, errors occur if the variable or function is called the same as the built-in function. For example, the variable mix or clamp
    • There were problems with float accuracy on some devices


    The result of all this - if you plan to use shaders in a mobile game, test them on the most common models of graphics accelerators. If the shader compiles and works on your computer, this does not mean that it will work on your neighbor’s phone. The new Vulkan API should solve the problem with various shader compilers and bring order to this crazy world, but this is a matter of the future, and today we have what we have.

    Sounds


    Search

    Sounds are generally another song. You can record them yourself, which is rather laborious and requires a normal microphone and hearing (not our case). And you can find the desired sounds on the Internet.
    It is desirable to search for sounds with a Creative Commons 0 license, so as not to indicate authorship for each sound, of which there may be several dozen. It may seem that finding a normal free sound is pretty hard, and it is. The problem is not that they are few, on the contrary, there are a lot of them, most of which are terrible. Sound search is a process in which you need to listen to a very, very many sounds and choose the most suitable ones from them.

    Facilities

    In Qt, there are QSound , QSoundEffect , QAudioOutput, and QMediaPlayer classes for outputting sounds . In the first versions, I used QSoundEffect to output effects and QMediaPlayer to output sounds. But, as it turned out, they all do not fit.

    Only QMediaPlayer can work with compressed audio files, but this class, more precisely, its implementation under Android has several unpleasant moments.
    • Friezes at the start of sound. When the music plays in a looped state, when repeated, there is a noticeable delay in the entire application.
    • When reading a media file from resources, it creates a temporary file in the application data folder and does not always delete it after itself, which leads to an increase in the size of the application. It was discovered by chance and well, that time.

    QSound and QSoundEffect can only work with uncompressed wav files. QSoundEffect is designed to output sounds without delays and can loop sounds itself, but when using it on Android, the message AUDIO_OUTPUT_FLAG_FAST denied by client ”often appears in the logs, which means that the sound file format cannot be output by the media server without delays. This is due to an incorrect sampling rate, which is different on different devices. Some devices swallow 44KHz, some need 48KHz, and the object itself transmits the sound as it is recorded in the wav file. The size of sound resources was a significant percentage of the size of the application.

    All these shortcomings have led to the fact that after adding sounds to the game, tangible FPS subsidence appeared when playing sounds and music.

    The solution was to abandon these classes and use the SFML library for sounds . A very simple and lightweight library similar to SDL. Convenient classes for working with graphics, sound, input devices. This library does not know how to work with mp3 (license, all things), but it does much more. I used the ogg format for effects and music.

    Sound output

    For use in an application where sounds are optional, Qt native classes are suitable. For game development - not at all. Better and easier to use SFML.

    In-app advertising


    For advertising, I used a ready-made AdMob implementation for Qt - QtAdMob .

    At first, only one small banner was added to the menu, but later an inter-screen ad was formed.
    It is interesting that an inter-screen ad, even when it is in a busy state, appears with some delay. That is, there was a need to block the user interface at the time the advertisement appeared and recover after it was closed. At the same time, the library did not allow to catch the moments when the ad was shown and closed. I added this library functionality to the version for Android. The version for ios has not yet been touched, for lack of the ability to check operation.

    Analytics and statistics


    By posting the first version on the Play Market, I hoped for statistics in the developer's console. But, as it turned out, the statistics of active users is tied to Google Analytics and does not work until you turn on the analyzer tracker in the application. And the statistics that are available come with a delay of more than a day and are calculated according to Pacific time. This state of affairs does not allow us to understand how your actions affect downloads. Therefore, I added the activity class from QtAdMob, which is inherited from QtActivity by analytic initialization functions and game event sending functions. I don’t give examples of the code, because everything is beautifully described in the documentation.

    In the events I made pressing all the buttons on the interface, the occurrence of some game situations, opening and unlocking characters with levels.

    Thanks to the collection of all these statistics, I can sit at the laptop and watch in real time how in Brazil someone started the game, could not complete the first mission, left and probably deleted the game.

    Also, according to statistics, we are now champions of our game.

    About Google Play


    The developer console itself is a fairly convenient tool, but the functionality of statistics and advertising is tied to other accounts.

    To fully develop the application yourself, you must at least have an AdWords, AdSense, AdMob, Google Analytics account. In this case, a connection is established between them. All these accounts are separate Google products and have various technical support and settings. It’s also worth noting that an AdMob account requires an AdWords and AdSense account. However, all these accounts can be linked in a single copy to the main Gmail account. But, as practice has shown, you can get confused in all of this from the very beginning, because you open one service, it offers you to create a new account in another, one in the third and so on.

    In some magical way, so that the technical support officer could not explain what happened, created 2 AdWords accounts and linked them to one mail, while linking one account to the developer's console and the other to AdMob (I did not know about this).

    I threw 500r on one account in order to check the advertising campaign. In an attempt to deal with this and following the technical support tips, I transferred one of the accounts to the “left” mail and closed my access to it. All this led first to the inoperability of both accounts from my mail, then, with repeated disconnections and connections of myself, the performance returned. But, as it turned out, AdMob stopped working. Since AdMob was more important to me than those 500r, I had to carry out this entire procedure anew, praying along the way that I would not lose access to everything at all, to return AdMob to work. And of course, those 500r were left hanging on an unconnected account.

    So, be careful with this.

    Transfer


    In Game

    We translated the game menu into 2 languages ​​- Russian and English. The choice of the game language is carried out by the system locale. If the system is Russian, then the games are in Russian, in all other cases - in English.

    For translating textual information in Qt, there is a built-in mechanism that is executed by the class
     QTranslator myTranslator;
     myTranslator.load(":/translations/neverfall_" + QLocale::system().name());
     a.installTranslator(&myTranslator);
    

    All strings that need to be translated are passed to the QObject :: tr () function, for classes that are not QObject descendants, you can use the QApplication :: translate function and for strings declared in arrays the macros QT_TRANSLATE_NOOP, QT_TR_NOOP.

    But this is only half the battle. It is necessary to create the translations themselves, which is done by the lupdate and lrelease programs. The first collects information from the source code containing these functions and macros, and creates an Xml file with information for translation.
    The second one collects the qm binary file from the xml file, which is loaded directly into QTranslator .

    We used something like tags as translatable strings, which were then translated into English and Russian. For example, “#GameOverText” translates to “Game over.” It was made so that there was no need to change the source code, to write something different, and then also change all the translations, because for lupdate this is another line.

    On the market

    In Google Play, we went the simplest way: wrote the text in Russian - threw it into a Google translator - translated into English, corrected it. And then the English text was translated into the most common languages ​​by the same Google translator. It is funny that one of the options for the description contained the phrase “Plunge into the dungeon with your head”, which, having gone through all this translations in Chinese, meant “Jumping in a cave on your head”. So we left, because since they give us pearls at AliExperse, we won’t be left behind.

    Conclusion and plans


    Result Video


    In conclusion, I want to say that the development process of this game was and is one of the most interesting activities, it brought us great pleasure and experience, which I modestly share in this article. Development was carried out in free time, after work and at night, from August of this year. The money was spent only on the developer’s account and a couple of thousand on advertising, so I won’t be upset if this game brings us a little less than nothing.

    Future plans depend on how people will respond to our creation. Judging by the reviews, it’s pretty good, but judging by the downloads and deletions, I want to go behind the rope to the nearest household goods store. The game was probably quite complicated, and we have a clear lack of advertising

    We plan to transfer the game to ios, but this is hindered by the lack of apple technology and $ 100 a year, as well as the prospect of communicating on the elven Objective C.,

    I also hope that this is not our last game, there are many new ideas that I will try to implement in life, given the experience gained. This game - the first pancake, whether it is lumpy or not - is not for me to judge.

    I anticipate comments about the fact that this could be done easier and faster, using ready-made engines, attracting designers and publishers. I will answer this that we wanted to go the way of the Jedi developers from beginning to end, on our own, in order to plunge into it headlong.

    Also popular now: