Spotlight Team Best Practices: Optimizing the Hierarchy
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 early in the development cycle, you can make your life easier and your game much more ambitious.
Transform Changed Messages
Every time a GameObject moves, altering its transform, we need to let every game system that might care about this know. Rendering, physics, and every parent and child of the GameObject needs to be informed of this movement in order to match. As the scope of a game increases, and the number of game objects skyrockets, just the overhead of sending out all these messages can become a performance problem.
Take an example from one our recent projects, Shadow Tactics.
This is one of their NPCs with all of its component parts. This screenshot was taken after they had already moved all their rigs over to use Optimize Game Objects, so the original version had all of the NPC model’s bones in addition to all of the gameplay objects and model structure.
This is a completely normal, expected setup for a game. Level designers are placing NPC spawners. Those spawners then, at runtime, create NPC instances as their own children. Enemy_normal is the root of the NPC. This contains the NavAgent that controls the NPC’s movement. Each NPC has a bunch of GameObject children that define their abilities and tuning. Nothing about this is at all wrong.
What this all means, though, is that every frame, when the NPC moves, it must inform all of its children and all of its parents that its transform has changed. Every frame, every NPC is sending hundreds of Transform Changed messages and taking up a good amount of the frame doing so.
Once we saw how much frame time Transform Changed messages were taking, we talked with Mimimi Productions and they altered their spawning in some simple ways. In addition to turning on Optimize Game Objects, they started spawning their NPCs at the root level of their scene. They also moved all of the NPC Ability Game Objects up to the root of the NPC, so they would not be a child of the NavAgent. This left only the visuals and the physics of the NPC under the Nav Agent. On their target hardware this improved performance by ~10 FPS.
Let’s say that again, for those in the back.
10 frames per second.
Without majorly impacting workflow. Without needing to go back and redo a ton of content. Just by taking the content they already had and moving it around a little bit.
Transform Change Dispatch
Staring with Unity 5.4, we have been heavily optimizing all of the code having to do with transforms and the Transform Changed message. We have optimized memory layout and provided the .SetPositionAndRotation API to avoid extraneous changes. We are now allowing systems to register as caring about a specific transform, rather than needing to broadcast the Transform Changed message to every system in the engine.
One of the largest changes, that is still ongoing, is moving over to a delayed and threaded TransformChangeDispatch system. This lets us queue up all of the Transform Change notifications in a bit of the hierarchy that is completely self-contained and then resolve them in a job off of the main thread. We are moving as many systems as we can over to dispatching these notifications, rather than making them on the main thread synchronously.
Even with all these improvements, it is very important that you think about the structure of your hierarchy as you develop your game. With a bit of forethought, you can save yourself frames of execution time, and use that for things your players will care about.
Hierarchy Structure Guidelines
- If something moves every frame, make sure all its children care about position. Only rendering, physics, audio, or core systems like that should be there.
- When dynamically creating game objects at runtime, if they do not need to be children of the spawner for the above reasons, spawn things at the root of the scene.
- You can easily register everything you spawn and pass along the ActiveInHeirarchy state of the spawner using OnEnable and OnDisable.
- Try to group your moving transforms such that you have around 50 or so GameObjects per root. This lets the underlying system group your TransformChangeDispatch jobs into a fairly optimal amount of work per thread. Not so few that the thread overhead dominates; not so many that you are waiting on thread execution.
Thanks to Mimimi Productions for letting us use their game Shadow Tactics for our examples. We will be back next time with lessons learned working with Realtime Global Illumination and multi-scene editing.