Physics Best Practices
Revisado con versión: 4.3
In this lesson we will be looking at some best practices for when using physics in a game and some evidence to demonstrate why they should be used.
Layers and Collision Matrix
All game objects, if not configured, are created on the Default layer where (by default) everything collides with everything. This is quite inefficient. Establish what should collide with what. For that you should define different Layers for each type of object. For each new layer, a new row and column is added on the Collision Matrix. This matrix is responsible for defining interactions between layers. By default, when adding a new layer, the Collision Matrix is set for that new layer to collide with every other existing one, so it’s the developer’s responsibility to access it and setup its interactions. By correctly setting layers and setting up your Collision Matrix, you will avoid unnecessary collisions and testing on collision listeners. For demonstration purposes I’ve created a simple demo where I instantiate 2000 objects (1000 red and 1000 green) inside a box container. Green objects should only interact with themselves and the container walls, the same with red objects. On one of the tests, all the instances belong to the Default layer and the interaction are done by string comparing the game objects tag on the collision listener. On another test, each object type is set on their own Layer, and I configure each layer’s interaction through the collision matrix. No string testing is needed in this case since only the right collisions occur.
Figure 1 : Collision Matrix config
The image below is taken from the demo itself. It has a simple manager which counts the amount of collisions and automatically pauses after 5 seconds. It’s quite impressive the amount of unnecessary extra collisions occurring when using a common layer.
Figure 2 : Collisions amount over 5 seconds
For more specific data I also captured profiler data on the physics engine.
Figure 3 : Common vs Separate Layers physics profiler data
There’s quite a difference on the amount of CPU spent on physics, as we can see from the profiler data, from using a single layer ( avg ~27.7 ms ) to separate layers ( avg ~17.6ms ).
Raycasting is a very useful and powerful tool available on the physics engine. It allows us to fire a ray on a certain direction with a certain length and it will let us know if it hit something. This however, is an expensive operation; its performance is highly influenced by the ray’s length and type of colliders on the scene.
Here are a couple of hints that can help on its usage.
- This one is obvious, but, use the least amount of rays that gets the job done.
- Don’t extend the ray’s length more than you need to. The greater the ray, the more objects need to be tested against it.
- Don’t use use Raycasts inside a FixedUpdate() function, sometimes even inside an Update() may be an overkill.
- Beware the type of colliders you are using. Raycasting against a mesh collider is really expensive.
- A good solution is creating children with primitive colliders and try to approximate the meshes shape. All the children colliders under a parent Rigidbody behave as a compound collider.
- If in dire need of using mesh colliders, then at least make them convex.
- Be specific on what the ray should hit and always try to specify a layer mask on the raycast function.
- This is well explained on the official documentation but what you specify on the raycast function is not the layer id but a bitmask.
- So if you want a ray to hit an object which is on layer which id is 10, what you should specify is 1<<10 ( bit shifting ‘1’ to the left 10x ) and not 10.
- If you want for the ray to hit everything except what is on layer 10 then simply use the bitwise complement operator (~) which reverses each bit on the the bitmask.
I’ve developed a simple demo where an object shoots rays which collide only with green boxes.
Figure 4 : Simple Raycast demo scene
From there I manipulate the number and length of the rays in order to get some profiler data to backup what I’ve written earlier. We can see from the graphics below the impact on performance that the number of rays and their length can have.
Figure 5 : Number of rays impact on performance
Figure 6 : Rays length impact on performance
Also for demonstration purposes, I decided to make it able to switch from a normal primitive collider into a mesh collider.
Figure 7 : Mesh Colliders scene ( 110 verts per collider )
Figure 8 : Primitive vs Mesh colliders physics profiler data
As you can see from the profile graph, raycasting against mesh colliders makes the physics engine do a bit more work per frame.
Physics 2D vs 3D
Choose what Physics engine is best for your project. If you are developing a 2D game or a 2.5D (3D game on a 2D plane), using the 3D Physics engine is an overkill. That extra dimension has unnecessary CPU costs for your project. You can check the performance differences between both engines on a previous article I wrote specifically on that subject:
The Rigidbody component is an essential component when adding physical interactions between objects. Even when working with colliders as triggers we need to add them to game objects for the OnTrigger events to work properly. Game objects which don’t have a RigidBody component are considered static colliders. This is important to be aware of because it’s extremely inefficient to attempt to move static colliders, as it forces the physics engine to recalculate the physical world all over again. Fortunately, the profiler will let you know if you are moving a static collider by adding a warning to the warning tab on the CPU Profiler. To better demonstrate the impact when moving a static collider, I removed the RigidBody of all the moving objects on the first demo I presented, and captured new profiler data on it.
Figure 9 : Moving static colliders warning
As you can see from the figure, a total amount of 2000 warnings are generated, one for each moving game object. Also the average amount of CPU spent on Physics increased from ~17.6ms to ~35.85ms, which is quite a bit. When moving a game object, it’s imperative to add a RigidBody to it. If you want to control its movement directly, you simply need to mark it as kinematic on its rigid body properties.
Tweak the Fixed Timestep value on the Time Manager, this directly impacts the FixedUpdate() and Physics update rate. By changing this value you can try to reach a good compromise between accuracy and CPU time spent on Physics.
All of the discussed topics are really easy to configure/implement and they will surely make a difference on your projects’ performance as almost every game you’ll develop will use the physics engine, even if it’s only for collision detection.