Quick answer: Deep hierarchies force Unity to recompute world matrices for every descendant on every change. Flatten pools to root, cache world positions per frame, and keep dynamic object depth under 5 levels.
Your game runs at 120 FPS with 50 enemies. You add 500 bullet pool objects parented under each weapon and the frame rate drops to 30. You have not added any Update methods. The Profiler shows TransformChangeDispatch eating 15 ms per frame. The bug is hierarchy shape.
Why Depth Costs So Much
Every Transform caches its local-to-world matrix. When a parent changes, every descendant’s cached matrix is invalidated and recomputed on the next access. A 10-deep chain with 100 children at each level has 1010 potential descendants affected by a single root move. Real games are not that pathological, but depth 10 with breadth 50 is enough to tank a frame.
The Fix
Step 1: Flatten bullet/particle/FX pools to root.
// Bad: bullet parented under weapon
bullet.transform.SetParent(weapon.transform);
// Good: bullet at root, no parent
bullet.transform.SetParent(null);
bullet.transform.position = weapon.transform.position;
bullet.transform.rotation = weapon.transform.rotation;
Unrooting pooled objects means their world transform does not change when the shooter moves. You lose automatic position inheritance but gain massive savings at scale.
Step 2: Cache world positions during hot loops.
void Update()
{
Vector3 myPos = transform.position; // one property access
foreach (var enemy in _nearbyEnemies)
{
if ((enemy.transform.position - myPos).sqrMagnitude < 25f)
AttackEnemy(enemy);
}
}
Step 3: Use Transform.hasChanged to gate expensive work.
void LateUpdate()
{
if (transform.hasChanged)
{
RebuildSpatialHashEntry();
transform.hasChanged = false;
}
}
When to Keep Depth
Characters with bones (5-10 level skeletons) are fine. UI hierarchies under a Canvas are optimized separately. The problem case is dynamic gameplay objects in deep chains that change every frame.
Verifying the Fix
Open the Profiler, record a frame, and look at the Transforms entry under Scripts. Before the fix, it is a double-digit millisecond bar. After flattening pools and caching positions, it drops to sub-millisecond.
“Parenting is for logical grouping, not for performance. Dynamic pools belong at root.”
Related Issues
For broader performance work, see how to profile frame rate drops in Unity. For object pool state bugs, see Unity object pooling returning wrong state.
Bullet pools, particles, and VFX should never be parented to their source. Always spawn them at root.