Spotlight Team Best Practices: Collision Performance Optimization
On the Spotlight Team, we work with the most ambitious Unity developers to try to push the boundary of what a Unity game can be. We see all sorts of innovative and brilliant solutions for complex graphics, performance, and design problems. We also see the same set of issues and solutions coming up again and again.
This blog series is going to look at some of the most frequent problems we encounter while working with our clients. These are lessons hard won by the teams we have worked with, and we are proud to be able to share their wisdom with all our users.
Many of these problems only become obvious once you are working on a console or a phone, or are dealing with huge amounts of game content. If you take these lessons into consideration earlier in the development cycle, you can make your life easier and your game much more ambitious.
Occasionally, we can track down a physics performance problem to a single problem asset or setup. The best way to notice these is to regularly run the Profiler and compare to a previous run. If you catch a performance regression early, it is pretty easy to go through the recent changes and spot the problem.
While a simple physics joint can be quite fast, the underlying math is very complex. In essence, a joint is setting up a system of equations for your Rigidbody’s position, velocity, acceleration, rotation, etc. If you set up something with lots of different Rigidbodies with lots of different joints –all of them colliding, all of them needing to meet the requirements of their joints and not penetrate collision — it can get extremely expensive very quickly.
When setting up complicated joint schemes, think critically about how many joints you need, what sort of collision you require, and how many Rigidbodies are necessary. You can use Layers to mask out unnecessary collisions, and joints have an Allow Collision checkbox that should be used very sparingly. You can reduce the amount of collision you need to detect by constraining the range of motion of your joints. Tune the joints such that collision is unlikely or impossible and you no longer need to detect it. You can reduce the number of joints and Rigidbodies by using them as control points into an interpolation method.
The Profiler screen will show you how many Rigidbodies are active at any given time. Keep a close eye on this number. Rigidbody count, especially if they are near each other, can have a big impact on performance. It is very easy to inflate this number more than you expect when placing objects or spawning things at runtime. While a single can of soda that moves around is no problem, trying to make a supermarket display pyramid with that same Prefab is going to cause issues.
Be aware of any MeshColliders you add to the game. It is very easy to simply use the visual mesh for collision, but that can cause significant performance degradation, and not always in obvious ways. PhysX does a very good job of only testing against what it absolutely has to. So if you add high-poly collision to an object that is small, or out of the way, it might work just fine. However, when you scale up that same MeshCollider and place it somewhere that RayCasts are common, you can see your performance suddenly plummet. As a general rule, make custom, low-poly collision meshes for any object that is going to be in the default layer, or can collide against most things. If you find a specific mesh to be a problem and do not want to or cannot take the time to make a custom mesh, you can make that MeshCollider convex and tune the SkinWidth to get an automatically generated lower poly collision mesh.
Our users are very clever and generally avoid the big, obviously slow things. It is much more common to see a project that made a series of rational decisions, each of which slowed down the PhysX update a small amount. When you are working on a game at scale, it is very easy to have this happen. Your AI test level runs fine with 5 AIs in an empty box. When you put those same AI in your real level, suddenly your framerate tanks, Physics.Update spikes, and you don’t really know why. Which of the hundreds or thousands of GameObjects with collision are the culprit?
Are you doing more tests than you need? Layers can be used to control which GameObjects collide with each other using the grid of checkboxes in ProjectSettings->Physics. This gets evaluated before any expensive tests against your specific collision geometry. Use this to aggressively cull any collisions that do not need to happen. Many games will use large Colliders with ‘Is Trigger’ set to true to detect characters or other specific game objects, commonly referred to as trigger volumes. Very often these trigger volumes are set to collide with the Default layer or everything. By having characters in specific Layers, and these trigger volumes in a Layer that only collides with your character layers, you can avoid testing large volume colliders against your complex world mesh collision or terrain collision.
Are triggers slowing everything down? A trigger volume, a Collider with ‘Is Trigger’ set to true, is still collision geometry that has to be tested against at the end of the day. If you are moving a trigger volume is just as expensive as moving any other collision geometry, and can cause a lot of work sending collision and overlap events. If you are going to have trigger collision moving every frame, make sure you are colliding against only the objects you need to. Try to keep the trigger as small as possible, and group up similar or overlapping triggers.
A common pattern I see is to have NPCs use multiple large trigger volumes to detect interactive targets. Each kind of game object that the NPC is looking for will have its own Collider with a bunch of code in the OnCollision callback to make sure it has found the sort of thing of it is looking for. It is generally faster to merge multiple trigger volumes into a single trigger, and then filter based on Tag or Layer or distance inside the OnCollision callback. In many cases, you can get even better performance by bypassing collision entirely. Rather than check a sphere every frame against your collision world, register any objects you want to find with a shared manager, then have the NPC’s do a simple distance check against all registered objects. If you have a small list of potential targets, this will be far more efficient than testing against all the collision in the world.
Perhaps the Hierarchy is causing PhysX to do far more work than is needed? On Recore, we found several places where rotating rings of platforms were constructed with each square of the ring having its own Rigidbody. This causes each segment to test for collision against all the other segments. By grouping all of the platforms under a shared parent with a Rigidbody and rotating that parent instead, we were able to save significant frame time inside of the Physics.Update. Be aware, combining Colliders under a shared Rigidbody does increase the cost of doing any Raycast or shape cast tests against it.
Coming from the other direction, if you already have several colliders underneath a shared Rigidbody parent, you need to be very careful not to move them relative to the parent. Any time a Rigidbody changes shape, the center of mass and Inertial Tensor must be recalculated. This takes significant frame time. This most often comes up when a Rigidbody gets attached to the limbs of an animating character. You can turn this off by setting your own center of mass. As long as the GameObject’s shape doesn’t change too much, this shouldn’t be noticeable.
These small, systemic issues add up quickly, so keep them in mind as you develop and plan ahead to avoid them. If you suddenly see a big spike in Physics time, look to the common causes of large performance problems and see if any of those apply.