📜 ⬆️ ⬇️

How we optimized our Theme Hospital for different platforms

image

Project Hospital is a game about managing a hospital building with all the standard aspects of the genre: dynamic scenes created by the player, a variety of active characters and objects, a UI system. To make the game work on different equipment, we had to make a lot of effort, and this was an excellent example of the notorious "death from a thousand cuts" - many small steps that solve a lot of very specific problems and a lot of time spent on profiling.

Performance level: what we wanted to achieve


At an early stage of development, we decided on the main parameters: the maximum size of the scenes, the level of performance and system requirements.

We set ourselves the task of providing support for at least a hundred active and fully animated characters on one screen, three hundred active characters in total, tile maps of about 100x100 in size and up to four floors in the building.
')
We were firmly convinced that the game should work in 1080p with a decent frame rate even on integrated graphics cards, and this goal in itself was not so difficult to achieve: the main limiting factor is the CPU, especially with increasing hospital volumes. Modern integrated video cards begin to experience problems only at resolutions of approximately 2560 x 1440.

To simplify support for mods, most of the data was made open, that is, we had to sacrifice the performance achieved by packing files, but this had a particularly strong effect, with the exception of a slightly increased load time.

Graphics


Project Hospital is a “classic” isometric 2D game, so you can see that everything is drawn from behind to the front — in Unity this is accomplished by setting the corresponding values ​​along the Z axis (or the distance to the camera) for individual graphical objects. If possible, objects that do not interact with each other are ordered in layers, for example, the floors are independent of objects and characters.


All geometry in an isometrically rendered scene is dynamically created in C #, so one of the two most important aspects for graphics performance is the frequency of rebuilding the geometry. The second aspect is the number of draw calls.

Draw calls


The number of individual objects drawn in one frame, regardless of their simplicity, is the main limitation, especially on weak hardware (besides, the Unity engine itself adds excessive consumption of resources). The obvious solution is to group (batching) as many graphic objects as possible into a single draw call. So you can get some pretty interesting results, for example, to group objects that are at the same distance from the camera so that the rest of the graphics is correctly rendered for or in front of them.


Here are some numbers: on a 96 x 96 tile map, theoretically, you can fit 9216 objects, which will require 9216 draw calls. After batching, this number drops to 192.

However, in real life, everything is a bit more complicated, because it is possible to group only objects with the same texture, which is why the results are slightly less optimal, but the system still works quite well.


Most of the batching is done manually in order to have control over the results. In addition, as a last resort, we also use Unity's dynamic batching, but this is a double-edged sword - it actually helps to reduce the number of draw calls, but leads to waste of resources in each frame, and in some cases can be unpredictable. For example, two superimposed sprites at the same distance from the camera in different frames can be rendered in a different order, which causes flicker, which does not appear manually during batching.

Multistory


Players can build buildings with several floors, and this increases complexity, but, surprisingly, performance helps. Rendering and animating need only characters on the active floor and on the street, and everything on the other floors of the hospital can be hidden.

Shaders


Project Hospital uses relatively simple custom-made shaders with a few tricks, such as color swapping. Suppose a character shader can replace up to five colors (depending on the conditions in the shader code), and therefore is quite expensive, but this does not seem to cause problems, because characters rarely take up much space on the screen. The shader justified the efforts put into it, because the possibility of using an infinite number of colors of clothes allows us to greatly increase the variability of characters and the environment.

In addition, we quickly learned how to avoid specifying shader parameters and instead used vertex colors whenever possible.

Texture quality


An interesting fact is that in Project Hospital we do not use any texture compression: the graphics are made in a vector style, and on some textures the compression looks very bad.

To save CPU memory in systems with less than 1 GB, we automatically reduce the size of the in-game textures to half the resolution (except for the user interface textures) - this can be understood by seeing the option “texture quality: low” in the options. For UI textures, the original resolution is preserved.

Optimized CPU performance - multithreading


Although the Unity script logic is essentially single-threaded, we always have the ability to run multiple threads directly in C #. Perhaps this approach is not suitable for game logic, but often there are not time-critical tasks that can be performed in separate streams by organizing a system of tasks. In our case, the threads were used for two functions:

1. Pathfinding tasks, especially on large maps with confusing locations, can take up to hundreds of milliseconds, so this was the main candidate for transfer from the mainstream. Parallel tasks take into account the number of hardware threads of the machine.

2. Lighting maps can also be updated in a separate stream, but only one floor at a time - this is not a critical system, but automatic lamps in the rooms go out at such a rate for which a rare update is enough.

Animations


Almost at the very beginning of development, we decided to use a two-dimensional skeletal animation system. Having studied various modern animation programs, we ultimately decided to modify a simple system that I created several years ago (essentially as a hobby project), tailoring it to the needs of Project Hospital - it resembles a simplified Spine with direct support for creating character variations. Similar to Spine, it uses the C # runtime, which is obviously more expensive than the native code, so during the development process we performed a couple of optimization cycles. Fortunately, our rigs are pretty simple, only about 20 bones per character.

A curious fact: the most useful improvement in optimizing access to the transform of individual bones was the transition from map search to simple indexing of arrays.


In addition, the characters are not animated outside the camera, there is another trick: the characters hidden behind the windows of the main UI, too, do not need to animate. Unfortunately, in the final version of the game we switched to a translucent UI, so we could not use it.

Caching


Whenever possible, we try to perform the most costly calculations only with changes that affect their values. The best example of this is rooms and elevators: when a player places an elevator or builds walls, we run a fill algorithm that marks the tiles from which elevators and rooms are accessible. This speeds up the subsequent search for paths and can be used to show the player which rooms are currently unavailable.

Scattered and deferred updates


In some cases, it is logical to perform certain updates only partially. Here are some examples:

Some updates can be performed in each frame only for a part of the characters, for example, the scripts of the behavior of half of the patients are updated only in odd frames, and for the second half - in even frames (although the animation and movement are performed smoothly).

In certain states, especially when the characters are in standby mode, but cause expensive parts of the code (for example, employees who check what needs to be filled and who are looking for unoccupied equipment), operations are performed only at certain intervals, say, once a second.

One of the most costly, and at the same time common, calls is to check which tests are available for each patient. At the same time, many factors need to be assessed - for example, which of the department staff is currently busy and what equipment is reserved. In addition, this information is not common to all patients, because it is affected, for example, by the doctor appointed by him and their ability to speak. It is necessary to check dozens of available types of analyzes, therefore, in one frame, the update is performed only for some, and proceeds as follows.


Conclusion


Optimizing a game manager with many interacting parts has proven to be a long process. I regularly had to work with the Unity profiler and fix the most obvious problems; it became an integral part of the development process.

Of course, there is always room for improvement, but we are quite pleased with the results. The game copes with the tasks set by us, and the players constantly create fashion for it, significantly exceeding the initial limit on the number of characters.

It is also worth saying that even in comparison with some AAA games I worked on at Project Hospital I encountered the most complex game logic in my practice, therefore many of the problems were specific to this project. Nevertheless, I still recommend leaving enough time in any project for optimization in accordance with the complexity of the game.

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


All Articles