Struggle with 2D physics in Unity as an example of an endless game

  • Tutorial


My strange creative path brought me to the development of games. Thanks to an excellent student program from an IT company, the name of which consists of one Greek Small Letter, collaborating with our university, we managed to assemble a team, give birth to documentation and establish Agile development of the game under the supervision of a high-quality QA engineer (hello Anna!)

Without much thought , Unity was chosen as the engine. This is a wonderful engine on which you can really quickly and easily make a very bad game, which, in your right mind, no one will ever play. To create a good game, you still have to shovel the documentation, delve into some of the features and gain development experience.

Our game used a physical engine in an unexpected way for him, which gave rise to many performance problems on mobile platforms. This article, as an example of our game, describes my struggle with the physics engine and all the features of its work that were noticed on the way to a viable beta version.

The game


Gif with a game

A few words about how it is made.
Made using Blender and a couple of python scripts. At the time of shooting, there were 16 squares in the corner of the screen, the color of which encoded 32 bits of the floating-point number - the rotation of the phone at a given time. R, G - data, B - parity. 0 - 0, 255 - 1. The video taken on the computer was divided into frames using ffmpeg, each decoded frame was assigned a decrypted angle. This format made it possible to survive any compression during the shooting process and overcame the fact that all programs have slightly different ideas about the passage of time. In reality, the game is played in the same way as in the render.

The plane flies through an endless and unpredictable cave, in which there are bonuses, all sorts of coins and enemies in which you can shoot homing missiles. Crashed into a wall - immediately lost.
A distinctive feature of the game is that the level is nailed to the horizon and the control in it is gyroscopic, moreover, absolute. I tipped the phone 45 degrees - the plane flew at an angle of 45 degrees. You need to make a dead loop - you have to twist the tablet. There is no sensitivity, only hardcore.
There are two main and obvious problems for the developer:

Problem 1: Infinity

Unity stores and processes the coordinates of objects in the form of ordinary 32-bit floats, with an accuracy of up to 6 decimal places. The problem is that our game is endless and if we fly long enough, various crazy bugs will start, up to teleporting through the walls. There are several approaches to solving this problem:

  1. Ignoring In Minecraft, for example, rounding errors only made the game more interesting, giving rise to the phenomenon of "Distant Lands . "
  2. Teleport to (0; 0; 0) if the airplane is too far from the origin.
  3. Change of reference point. It is not the plane that moves, but the level around it.

In our case, the only acceptable option is the third, which was implemented. About implementation - a little later.
The first - ignoring - is absolutely unacceptable. Creating a robot that can play our game forever is an interesting (and very simple) task that someone will solve. Yes, and ordinary Korean players should not be underestimated - the plane is fast, the level is generated unpredictably. And if you fly and fly before passing through the walls, then much more accurate shooting will obviously begin to fail after 5 minutes of flight.
The second - teleportation of the player and the whole world - puts mobile devices on their knees, in some cases - somewhere for half a second. This is very noticeable, and therefore unacceptable. But this is a perfectly acceptable option for simple endless PC games.

Problem 2: Level Generation



There are several basic approaches to building endless runners:

  1. The use of ready-made level segments that are joined randomly. This is done, for example, in Subway Surfers. It's easy to implement, but the player quickly gets used to it and knows what to prepare for, which is boring.
  2. A level is simply a line on which obstacles are randomly placed. This is done at Joypack Joyride and Temple Run. In our case, this would greatly limit the number of maneuvers.
  3. Everything is randomly generated. The most difficult, unpredictable and interesting option for the player.

Of course, we chose the most difficult option. In his heart is a very complex machine of states, which performs random transitions through them. But within the framework of this article, it is not the mechanism that is interesting, but the level generation process and its organization, taking into account the chosen reference point.

Level structure



We fly in a cave, it has a floor and a ceiling - a couple of blocks, elementary building units. Blocks are combined into segments that seamlessly fit together. Segments, as a whole, revolve around the aircraft and move along its velocity vector, creating the illusion of flight. If a segment leaves the field of view of the camera, it is cleared of blocks, docked to the last segment of the level and filled with new blocks, according to the instructions of the generator. The totality of such segments is the level.

Experienced Unity-developers could justifiably frown, considering the amount of work and all possible pitfalls. But in words everything is simple, but I did not have development experience ...

Basic Laws of Physics in Unity


For a month of development, experimentation and reading documentation, I identified three basic laws of physics in Unity. They can be violated, but the violation fee is performance. The engine will not warn you about a mistake, and without a profiler you can never know about them. Failure to comply with these laws can slow your game dozens of times. As I understand it, violation of any law leads to the fact that the physics engine marks the offending collider as incorrect and recreates it on the object, followed by recalculation of physics:

1. Colliders should not move, rotate, turn on / off and change size.

As soon as you add a collider to an object - forget about any impact on it or the objects in which it is contained. A regular collider is an exceptionally static object. A tree, for example, can be with one collider. If a tree can fall on a player, the tree will fall along with performance. If this tree grows from a magical nutritious cloud, which the collider does not have, but can move, this will be accompanied by a drop in productivity.

2. If the object moves or rotates - it must be a solid body ie to have a Rigidbody component.

This is written in the documentation, yes. Which is not necessary to read thoughtfully in order to start making the game, because Unity is very simple and intuitive.
Rigidbody change the attitude of the physical engine to the object. External forces begin to influence it, it can have linear and angular velocities, and most importantly, a solid body can move and rotate by means of a physical engine, without causing a complete recalculation of physics.
There are two types of solids - ordinary and kinematic. Ordinary bodies interact with each other and ordinary colliders - one body cannot pass through another. Kinematic bodies follow the simplified rules of simulation - they are not affected by any external forces, including gravity. They can freely pass through each other and colliders, but they repel ordinary solid bodies, as if having an infinite mass.
If objects are not a pity to give under the control of the physical engine - use ordinary solids. For example, if you need to beautifully roll stones off a cliff. If your scripts or animators control the object directly - use kinematic bodies, so you do not have to constantly struggle with the engine and random collisions of objects. For example, if you have an animated character or guided missile exploding when in contact with something.

3. If the object is a solid - it must move and rotate through the methods of a solid.

Forget about direct access to the Transform object immediately after adding a collider to it. Now and forever, Transform is your enemy and productivity killer. Before you write transform.position = ... or transform.eulerAngles = ..., say the phrase "I now absolutely clearly understand what I'm doing, I am satisfied with the brakes that will be caused by this line." Do not forget about hierarchical relationships: if you suddenly move an object containing solids, physics will be recalculated.

There are three levels of solid state control:

- The highest and, therefore, natural, level is through strength. These are the AddForce and AddTorque methods. The physics engine will take into account body mass and correctly calculate the resulting speed. All body interactions occur at this level.
- Average level - change in speeds. These are the properties of velocity and angularVelocity. Based on them, the forces that affect the bodies during their interaction are calculated, as well as, obviously, their positions at the next moment in time. If a solid body has a very low speed, it “falls asleep” to save resources.
- The lowest level is directly the coordinates of the object and its orientation in space. These are the MovePosition and MoveRotation methods. At the next iteration of the physics calculation (this is important, since each subsequent method call within one frame replaces the previous one) they teleport the object to a new position, after which it lives as before. Our game uses exactly this level, and only it, because it provides full control over the object.

What is left overboard? Turn on / off the object and scale. I do not know if there is a way to resize an object without confusing the engine. It is possible that not. Turning off the object is painless, and turning it on ... yes, it recalculates the physics in the vicinity of the turned on object. Therefore, try not to include too many objects at the same time, stretch this process in time so that the user does not notice.

There is a law that does not affect performance, but affects performance: a solid cannot be part of a solid. The parent object will dominate, so the child will either stand still relative to the parent, or behave unpredictably and incorrectly.

There is another feature of Unity that is not related to physics, but worthy of mention: the dynamic creation and deletion of objects using the Instantiate / Destroy methods is an insanely slow process. I’m afraid to even imagine what is happening under the hood during the creation of the object. If you need to create and delete something dynamically - use factories and refuel them with the necessary objects during game loading. Instantiate should be called as a last resort - if the factory suddenly runs out of free objects, but forget about Destroy forever - everything created should be reused.

Putting laws into practice


(in this section there is a line of reasoning when creating a game and its features)



The level, obviously, should rotate and move.
Make life easier for yourself by placing the axis of rotation of the level - the airplane - at the origin. Now we can calculate the distance from the point to it by calculating the length of the coordinate vector of the point. A trifle, but nice.
Joint movement of objects is easily implemented through the hierarchy of objects in Unity, because children are part of the parent. For example, the described level structure is logically implemented as follows:
- Ось вращения
- - \ Уровень
- - - \ Сегмент 1
- - - - \ Блок 1 (Collider)
- - - - \ ...
- - - - \ Блок N
- - - \ Сегмент 2 ...
- - - \ Сегмент 3 ...
- - - \ Сегмент 4 ...
(You can even do without a level object)

The script on the axis receives data from the gyroscope and sets the appropriate angle for it ... And it violates many rules at once, because the rotation will be transmitted along the hierarchy to the colliders, which will drive the physics engine crazy. You have to make the axis a solid body and rotate it through the appropriate method. But what about level movement? Obviously, the axis of rotation and the level object will not move, each segment needs to be moved individually, otherwise we are faced with the problem of infinity. So, the solids should be segments. But we already have a solid body higher in the hierarchy and a solid body cannot be part of a solid body. A logical and elegant hierarchy does not fit, you have to do everything with your hands - both rotation and movement, without using an object for the axis of rotation. Be prepared for this if you have unique gameplay features.

If segments would have to be moved directly, then they would have to be rotated. The main difficulty is that in the Unity physics engine there is no “rotate an object around an arbitrary point” method (Transform has it, but do not be tempted). There is only "rotate around its center." This is logical, because rotation around an arbitrary axis is both rotation and movement, and these are two different operations. But it can be imitated. First, rotate the segment around its axis, then rotate the coordinates of "its axis" around the plane. Due to the fact that we have the plane at the origin, we don’t even have to remember school geometry and climb into Wikipedia, Unity already has everything. It is enough to translate the angle of rotation into the quaternion and multiply it by the coordinates of the point. By the way, I found out about this right at the time of writing the article, before that the rotation matrix was used.

We have enemies who push the plane into the wall, hoping to kill. There is a shield that pushes the plane away from the walls, helping to survive. This is implemented trivially - there is a displacement vector that each frame is added to the coordinates of each segment and discarded after that. Anyone who wants to kick an airplane, through a special method, can leave the vector of their kick, which will be added to this displacement vector.

Ultimately, the real coordinates of the segment, each frame, are calculated by the level control center of the level somehow like this:
Vector3 position = segment.CachedRigidbody.position;
Vector3 deltaPos = Time.deltaTime * Vector3.left * settings.Speed;
segment.truePosition = Quaternion.Euler( 0, 0, deltaAngle ) * ( position + deltaPos + movementOffset );

After all the calculations and crutches necessary for the precise joining of segments during regeneration, segment.truePosition is sent to the MovePosition method of the segment solid.

conclusions


How fast does it all work? On the old flagships - Nexus 5 and LG G2 - the game flies at 60 FPS, with a barely noticeable drawdown during the inclusion of new colliders during the generation of the segment (this is inevitable and does not cost) and the worms are pulled out of the ground (you can heal some kind of hell, to get around this, but now there is a deliberate violation of the third law). 40 stable FPS gives out any device with a gyroscope that we came across. Without knowledge and consideration of all laws, the performance was, to put it mildly, unsatisfactory and the phones overheated. So much so that I was thinking of writing my own unpretentious specialized engine for 2D physics. Fortunately, the physics in Unity turned out to be flexible enough so that all problems could be circumvented and a unique game created, only a couple of weeks of experiments was enough.

Now, knowing all the main pitfalls of the Unity physics engine, you can quickly clone our game by destroying the dreams, lives and faith of three poor students in humanity. I hope this article saves you a lot of time in the future and helps you find not-so-obvious violations of the laws of productive physics in your projects.

Read the documentation and experiment, even if you use simple and intuitive tools.

Also popular now: