Creation and promotion of Symbian-hit. Part 1 Development
In this post, a story is published about how the Magic Brush app was created, and about the difficulties encountered during its development. Currently Magic Brush is one of the most popular applications in the Ovi Store, which provides a steady income. In good times, the number of app sales only in the Ovi Store reaches 4K copies per month. And the Lite version was downloaded about 1.7 M times. But the fun thing that was originally conceived completely different application. Everything in order under the cut.
It all started in February 2010. Then I bought my first smartphone - Nokia 5800 Express Music. Having played around for about a month with a new device, I remembered my childhood dream and decided to write a game for a touch-phone. And all I wanted then was this:
gain new experience;
get fan out of development;
maybe make some money. :)
I chose the genre of physical 2D puzzles, but I wanted my toy to stand out against the background of already existing Symbian games. After studying the content from the Ovi Store, I decided to make a game with vector graphics, anti-aliasing and sub-pixel positioning of objects in the game. And as soon as the decision was made, I took up the implementation. The first step was to create an engine.
Getting to know the Symbian SDK first shocked me. First, Symbian C ++ seemed to me extremely strange and unusual. Secondly, Symbian ^ 1 smartphones (5800, N97, etc.) were built on a rather weak hardware platform without a graphics accelerator on board. The SDK had OpenGL libraries, but they were implemented programmatically. Thirdly, despite the support of smartphone CPUs for floating-point operations, the SDK compiler did not know how to generate a code that performs floating-point operations in hardware.
The first problem was solved rather quickly. Fortunately, you can write code not only on Symbian C ++, but also on standard C ++. And since my game with the Symbian API practically didn’t work, only initialization was written on Symbian C ++, interaction with sensors and a timer. Everything else was successfully written in classic C ++ using STL.
Having dealt a bit with the charms of Symbian C ++, I started working on displaying graphics on the screen. I had no experience with OpenGL, besides, on Symbian ^ 1 OpenGL would not give a performance boost (since there is no accelerator). But in Symbian, a very nice feature was discovered - Direct Screen Access (DSA). With DSA, I was able to directly access the video memory of the device.
Ok, the access to the video buffer has been obtained, now you can programmatically render everything that your heart desires at the maximum possible speed. Quite quickly, methods for outputting sprites to the buffer were written, and Anti Grain Geometry (AGG) library was included for smoothed vector graphics. Rather, its very lite version: AGG Lite. Using AGG, methods were quickly written for displaying simple graphic primitives (line, circle, etc.) on the screen. Everything worked quite fast, and the picture was pleasing to the eye.
To store information about complex vector objects consisting of many colorful figures, container classes were used. In them, I also implemented methods for displaying these objects on the screen in certain coordinates, at a certain angle and with a certain scale. After that, Box2D was connected to the project, and the picture on the screen began to obey the laws of physics.
And then the problems started. The picture on the screen looked great, all the lines were smoothed, but due to the sub-pixel accuracy of the output, all the movements of the objects seemed very smooth ... While there were few moving objects on the screen. If 10–12 game vector objects were added to the screen, then fps fell disastrously. It was necessary to do something to save the situation.
First of all, all floating point arithmetic was rewritten to fixed-point arithmetic. This was helped by the Fixed class from Box2D, then pre-calculated tables were added to calculate the trigonometric functions, optimized methods for calculating the square root and random numbers. In the project, there is not a single use of floating-point numbers. This gave a good performance boost, but still not enough. It was possible to find several places in AGG that are optimizable. For example, turning off color mixing and turning on simple copying to display opaque areas of objects. This added a couple more fps, but still failed to raise fps above 15. It was possible to play at 15 fps, but ... I had to think over another gameplay, in which there would be less dynamics and more static objects.
In parallel with thinking about new gameplay, I continued working on the engine: added support for bitmap fonts from the AngelCode converter, self-made localization support and a window system, as well as a small set of widgets.
They are called when you touch the screen, move your finger and tear it away from the screen surface, respectively. In addition, the base class contains attributes that are responsible for color, transparency and thickness of the lines:
agg::rgba8 color; Fixed opacity; Fixed width;
and a pointer to the current output buffer:
BufferGraphics* graphics;
To implement the brush, you will need Smoke from thin lines - “streams” of smoke - to build a thicker line. To do this, you need to know the current position of each “jet” relative to the current position of the user's finger. For this purpose, SmokeBrush uses the vector of points:
std::vector<Point> points;
It is also necessary to memorize the previous position of the finger in order to draw lines from it to the current position:
Point previousFingerPosition;
The logic of the brush is described in the StrokeStart and Stroke methods:
void SmokeBrush::StrokeStart(Fixed x, Fixed y) { points.clear(); for (int i = 0; i < (int)(width * Fixed(3)) + 16; ++i) { Fixed r = (fixrand() * Fixed(0.4f) + Fixed(0.1f)) * width; Fixed a = fixrand() * Fixed::PI2; points.push_back(Point(r * cos(a), r * sin(a))); } previousFingerPosition = Point(x, y); }
When you touch the screen, you need to clear the vector points from the values ​​that remained in it after the previous use of the brush. Then a new point cloud is written to this vector. And the greater the thickness of the brush, the more “jets” will be needed to display the brush on the screen. Each individual “jet” is located at a random distance from the touch point of the screen, but not more than half the thickness of the brush. You also need to remember the current touch point for later use.
Earlier, I wrote that in the project there was not a single use of floating point numbers, but in the text you can see float. Fortunately, the compiler is smart enough to properly handle the inline constructor of the Fixed class. As a result, at the execution stage, the work actually occurs only with integer values.
In the loop, all points in the cloud are traversed. For each point, a small random offset from its current position is calculated. If the point has shifted too far from the point of touch of the screen (more than half the thickness of the brush), then it remains in its old place. Then a separate “jet” is drawn on the screen, and the thickness of the individual “jet” depends on the thickness of the brush as a whole. At the end of the cycle, the offset point is written back to points. This allows you to get wavy thin lines in the composition of a thicker brush.
In such a simple way you can make a brush, which in skillful hands can become a useful tool for drawing: