Isometry, z-indices in mobile games and their optimization


Hi, Habr! Recently we came out with our game , which was long and hard prepared and in the process of which a considerable number of interesting topics accumulated that should be shared with the community. The topic will be far from being interesting not only to iOS and other mobile developers, but also to all those who are interested in how all sorts of graphical things work under the hood, as well as all fans of 2D-strategies, which I am myself for the third decade.

Today we will talk about the nuances of such an important topic as z-indices on an isometric surface (yes, not everything is as simple as it seems to some clever people). Strangely enough, in the 3d world, we have three coordinates — x, y, z — that completely determine the position of an object in space. The task of determining the proximity to the camera objects there also stands, but falls entirely on the shoulders of OpenGL. The developer only operates with high-level parameters such as the depth of the z-buffer, which affect performance, but otherwise you can trust OpenGL as a black box - it has enough information.

A completely different situation is observed in our “pseudo-3D” world - each object has only (x, y) - the coordinates and size of the sprite. The first task, which becomes for the programmer at the time of writing the engine, is the task of determining which objects should overlap each other in front of our virtual “camera”.

Synopsis


The coordinates of SpriteKit (where (0; 0) is the center of the “world”, and Y goes up) in this case are not at all of interest to us, since they mean nothing in our isometric “world” with you, so let's make a reservation - we have a diamond-shaped field like Age of Empires.



The tile with the coordinates (0; 0) is in the left corner of the rhombus, the abscissa X increases “down” and “right”, i.e. grows closer to the observer, the ordinate Y increases “up” and “to the right”, i.e. decreases as you approach the observer.

Also, the rails should be “under” the train, the smoke from the chimney should be “above” the train. But let's not bother with the “layers of being” - obviously, nothing prevents us from making as many isometric “slices” that work according to the same rules. Let us assume that in the same tile there is always one object - for clarity, there is no need for a larger one.



Consider the two trains above. Obviously, from an observer's point of view, wagons should be located “below” the train, i.e. their z-index should be less. At the same time, the “upper” train must “overlap” its neighbors, be “further”. Can we, having only coordinates (x; y), construct a map of z-indices for each tile?

Obviously, yes, using the following formula (pseudocode a la swift):

zIndex = pos.x * field.size.width - pos.y 

Thus, we guarantee that as the ordinate grows, objects move away (-pos.y), and as the abscissa grows, objects approach (pos.x) and, importantly, any object that has an abscissa, say, 44, will be “closer ”Than any object that has an abscissa 43. In order to add“ puffiness ”(remember, rails under the train, smoke above the pipe), it is enough to add some constant“ height ”of the layer:

 zIndex = layerZIndex + pos.x * field.size.width - pos.y 

Everything, you can finish the article, and praise yourself for the basics of stereometry learned in 10th grade and get down to the logic of the game. Not? If! I would write about the obvious things! (well, as the obvious, a couple of days and it is ruined on it)

We just get down to the most interesting, go ahead.

Fight for performance


Everyone, at least once running a test project under SpriteKit (or coconut, or any other engine), saw magic numbers - fps and nodes.



Obviously, fps is the number of frames per second, and nodes are the number of nodes, mainly sprites. But in practice, most of all, fps is not the number of nodes, but a different parameter, which by default is not displayed, but which can also be displayed in one line - the number of redraws of draws.



In the same scene, as you can see, the number of nodes is about 6000, and the number of drawings is about 120. This is at the minimum zoom (the camera is as close as possible to the surface), 1: 1.

And now we give the camera a maximum distance (in our game it is 2.5: 1)



We changed the scale only 2.5 times (this is still not all objects that are drawn in the example), and the number of draws increased 5-6 times with unchanged nodes count!

Of course, the number of drawings affects fps disproportionately more than the abstract number of nodes. SpriteKit simply does not draw nodes that do not fall on the viewport (in the camera). The only exception I have found so far is the emitters of particles, which are always drawn, regardless of whether they are visible or not.

Now let's talk about what this “draw” draw means. The video card has all the nodes “layers”, guided by their z-indices. And the whole picture passes over and over again, starting from the lowest and ending at the highest. The number of such cycles of drawing - and there are draws.

Now you understand that if each tiny object (and we have a large map, approximately 6000 x 3000) is drawn with its own z-index, it will ruin the performance of any phone.

The problems are best seen on the old 5 and 5s, but the presence of the iPhone 10 does not guarantee anything - with the wrong approach, you can ruin any powerful hardware. In our game one of the cornerstones was extreme clarity. At the closest zoom, one-to-one sprites correspond to retina pixels. I must say that in most mobile games, the resolution is orders of magnitude smaller, so the requirements are not so huge, but we did it qualitatively as for ourselves ...

So you have to go on tricks.


All this allows you to reduce the number of draws at times, correcting fps, even on an old iPhone. I had to severely limit some effects on them, but Apple has not released updates for them for a year already - sin will complain!

Elevation height


That's all, the engine is ready, you can already start something interesting? It is possible for someone, but it is too early for us. After all, the train should leave the tunnel beautifully, and everything is not as simple as it may seem.



The train should be “higher” than the “far” wall of the tunnel, and “lower” than the roof of the tunnel and the mountains following it. It's beautiful, because when the map is so multilevel, with elevation differences - again, we do not do heartless nonsense, but what we like!

But back to the details - for this, the map was “cut” as follows.



The inner wall of the tunnel and everything else to the left-below and



the top of the tunnel, along with the mountains into which it flows. At this point no procedural generation of z-indices will help, only the harsh Belarusian hardcode.

Attentive habrayuser noticed in the screenshot from the game that near the tunnels the trees are neatly “mowed out”, exposing the pristine beach sand. This seemingly flaw comes from the fundamental impossibility of implementing such tree planting in 2D. A train leaving the tunnel must obviously be “above” the trees it covers, closing them with itself. But these same trees should block the roof of the tunnel, under which the train should come! And the roof should be higher than the train, and so in a circle, we have a logical contradiction ...

Approximately for a similar reason, due to the imperfection of the graphics engine, in old games like Duke Nukem and Doom2 there are no large differences in height and number of storeys of buildings.

That is why trees do not grow near the tunnels.

I hope it was interesting, the toy is alive here (free to play) , the next article of the cycle will be about beautiful realistic 2D water, do not miss it!

PS By the way, the video for attracting attention can be viewed on youtube in normal quality.

PPS The game is currently available only in the CIS, Canada and Ireland, if someone wants to watch from other countries, send in a personal email with appleId - add to TestFlight

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


All Articles