📜 ⬆️ ⬇️

Experience of developing an arcade for Android in C ++ and Qt


Space does not impose itself

Prerequisites


I, like many programmers, chose this profession because I used to play computer games and dreamed of developing them. As soon as I learned to write more or less code that could be compiled without syntax errors, I, of course, began to make all sorts of silly games that I showed to all my friends and acquaintances. But time went on, and it made us engage in completely different things, work on projects that, to put it mildly, are more serious than games. And so it went the last few years. And the original desires did not disappear, only free time disappeared.

I have long wanted to do any project for Android, and, as you know, the majority of projects are developed on the Android SDK and Java, and the NDK is recommended to be used only in “critical in speed” places and not to do everything in it.
')
But who needs all these recommendations and rules when there is Qt? I don’t know Java to the extent that I consider sufficient for the high-quality development of the game, and I didn’t want to study it, but I have in my knowledge C ++. After several test projects on Qt for Android, I realized that it is possible to develop a full-fledged application on it, and even transfer it to other platforms. Also, after watching the Shia LaBeouf video - Just Do it, it became clear that I was doomed to do it.

So, I want to tell you about the experience of developing games 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 sensors of the phone in an attempt to make a toy “snitch” (temporary name), the meaning of which was - the harder 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 drawing 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. Originally intended to make a two-dimensional game with graphics in a drawing style. In the future, all flowed into a three-dimensional game with a top view. A cave generator was written that allowed the creation of layers with contours. How to count the collisions with the walls of the cave 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 a voxel geometry of the world. The collision calculation and screen output have become noticeably simpler, but at the cost of the fact that the game has become similar to minecraft. I was not taken aback and made her even more like him, adding a character model from him. So the form and concept of the game was formed.


The first version of the cave generator (archival photo)

Starting to develop the game, I did not set myself the goal to earn 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, for three-dimensional graphics, OpenGL ES 2.0 can be used; the following classes can help us with this:


Experimenting with these widgets, I made several conclusions:


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

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

To calculate the animation I needed to calculate the frame time. To do this, I first used QTime , which was a bad idea, since this class counts non-monotonous time, and on the mobile device time adjustments are often made, which lead to getting into the past or future, depending on the clock leaving on the device. Also, the resolution of changing this class is limited to milliseconds, which is not enough for smooth animation with unstable FPS.

After thinking about it and turning 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's skin. The first version of the interface was a gray square buttons and was made in the evening, and processing took a whole month.

The first screenshot of the game


My friend was chosen as a designer. The advantage of this choice is that this designer can be trusted, and the downside is that he is not a designer - he is an auto mechanic.

Later we drew the original textures for several levels and made the textures of the characters.
The process of drawing a “beautiful” interface and characters was quite long. While the designer was trying to squeeze a new character out of himself or a button, I had nothing to do but to continue to optimize the code.

Screenshot from the penultimate version


The level texture is a large atlas of all blocks and holds 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 several calls.

The problem occurred in the extreme pixels of the 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 a single pixel-wide border to each block that duplicates the pixel edge of the block. Such a kind of CLAMP_TO_EDGE.

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

I also wanted to note that although there were 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 texture of the block, which allowed to diversify the visual component of the game. Although, I applied the animation only on one of the levels to create the effect of rotating 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. By itself, a class is simply a wrapper over a set of OpenGL calls and, accordingly, is not a full-fledged object, in the sense that the work of a 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 fine 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

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


Bar hp \ 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 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); } 


Separately, I would like to say about the variety of android devices and their graphics accelerators. I ran into problems:


The result of all this - if you plan to use shaders in a mobile game, check them on the most common models of graphics accelerators. If the shader compiles and runs 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 for the future, and today we have what we have.

Sounds


Search

Sounds is generally a separate song. You can record them yourself, which is quite laborious and requires a normal microphone and hearing (not our case). And you can find the desired sounds on the Internet.
Search for sounds, preferably with a Creative Commons 0 license, in order not to indicate authorship for each sound, of which there may be several dozen. It may seem that finding a normal free sound is quite difficult, and it is. The problem is not that there are few of them, on the contrary, there are a lot of them, most of which are terrible. The search for sound is a process in which it is necessary to listen to very, very many sounds and choose the most suitable of them.

Facilities

In Qt, for the output of sounds, there are classes QSound , QSoundEffect , QAudioOutput and QMediaPlayer . 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 sound files, but this class, more precisely, its implementation for Android has some unpleasant moments.

QSound and QSoundEffect can work only with uncompressed wav files. QSoundEffect is designed to output sounds without delay 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 format of the sound file 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 up 44KHz, some need 48KHz, and the object itself transmits sound as it is recorded in the wav file. The size of the 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, significant FPS subsidence has appeared when playing sounds and music.

The solution was to reject these classes and use the SFML library for sounds. 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-shny native classes are suitable. For game development, not at all. Better and easier to use SFML.

Advertising in the application


For advertising, I used the ready-made implementation of AdMob under Qt - QtAdMob .

At first, only one small banner was added to the menu, but in the future an interscreen advertisement was formed.
It is interesting that the inter-screen ad, even in the loaded state, appears with some delay. That is, there is a need to block the user interface at the time of the appearance of advertising and restore after its closure. At the same time, the library did not allow to catch the moments when the ad is shown and closed. I finished this functionality of the library in the version for Android. I have not yet touched the version for ios, for lack of the opportunity to test the performance.

Analytics and statistics


Laying out the first version on the Play Market, I hoped for statistics in the developer console. But, as it turned out, the statistics of active users is tied to Google Analytics and does not work until you enable the analytics tracker in the application. And the statistics that are available, comes with a delay of more than a day and is calculated according to Pacific time. This situation does not allow you to understand how your actions affect the download. Therefore, I supplemented the activity class from QtAdMob, which is inherited from QtActivity by analytics initialization functions and functions for sending game events. I do not provide code examples, since everything is perfectly described in the documentation.

In the event I made a press of all the buttons of 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 a laptop and watch in real time how in Brazil someone launched the game, could not complete the first mission, went out and probably deleted the game.

Still, according to statistics, we are now champions in our game.

About Google Play


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

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

I somehow magically so that the tech support employee could not explain what happened, created 2 AdWords accounts and tied them to one mail, while tying one account to the developer console and the other to AdMob (I did not know about this).

On one account, I threw 500r to check the advertising campaign. In an attempt to deal with this and following the advice of tech support, I transferred one of the accounts to the “left” mail and closed myself access to it. All this led first to the inoperability of both accounts from my mail, then, with repeated disconnections and self-connections, the performance returned. But as it turned out, AdMob stopped working. Since AdMob was more important to me than those 500r, I had to spend the whole procedure all over again, at the same time praying that I would not lose access to everything at all, to get AdMob back. And of course, those 500r remained hanging on a non-connected account.

So, be careful with this.

Transfer


In Game

The game menu we translated 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.

To implement textual translations in Qt, there is a built-in mechanism implemented by the class
  QTranslator myTranslator; myTranslator.load(":/translations/neverfall_" + QLocale::system().name()); a.installTranslator(&myTranslator); 

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

But this is only half the battle. You need to create the translations themselves, which is done by the lupdate and lrelease programs. The first collects information from source code containing these functions and macros, and creates an Xml file with information for translation.
The second one collects a 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 done so that there was no need to change the source code, to write something differently, and then also change all the translations, because for lupdate this is another line.

On the market

On Google Play, we chose the easiest way: we wrote the text in Russian - we threw it into a Google translator - we translated it into English, corrected it. And then the English text was translated into the most common languages ​​by the same Google translator. It's funny that one of the description options contained the phrase “Dip in a dungeon with a head”, which, having passed through all these translations in Chinese, meant “Jumping in a cave on the head”. We left it that way, because since they give us pearls on AliExpress, we will not be left behind.

Conclusion and plans


Result Video


In conclusion, I want to say that the process of developing this game was and is one of the most interesting activities, which 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, since August of this year. The money was spent only on the developer’s account and a couple of thousand on advertising, so I’m not upset if this game will bring us a little less than nothing.

Further plans depend on how people will react to our creation. Judging by the reviews, it is pretty good, but judging by the downloads and deletions, I want to go for the rope to the nearest household goods store. Probably, the game was rather complicated, and we have a clear lack of promotional activities.

We plan to move the game under ios, but this is hampered by the absence of apple technology and $ 100 a year, and the prospect of communication on the elven Objective C is also scary.

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

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

Source: https://habr.com/ru/post/272423/


All Articles