I Optimized the Wrong Thing (A Profiling Story)
Six hours. That's how long I spent rewriting Sharpshooters' bullet system before realizing it wasn't the problem.
The Symptom
During wave 4, the frame rate would stutter every 3–4 seconds. Not a crash, not a freeze — a noticeable 20ms hike that made the game feel janky. I had 40 bullets on screen at peak. My first instinct: bullets are expensive.
What I Did Wrong
I immediately rewrote the bullet system to use object pooling. Bullets no longer instantiate and destroy — they activate and deactivate from a pool of 100. It took most of a weekend.
The stutter didn't go away.
What I Did Right (Eventually)
I opened Unity's Profiler. Five minutes after opening it, I found the actual culprit: the damage number UI.
Every hit spawned a TextMeshPro popup that displayed the damage value, animated upward, and then destroyed itself. At peak combat, that was 30–50 Instantiate calls per second — far more garbage than any bullet.
The fix took 45 minutes: a small pool of 20 text popups that get recycled instead of destroyed.
The Irony
The bullet pooling I spent the weekend on was still a good idea — it'll matter at higher bullet counts. But it was not the source of the problem I was trying to fix.
Rule I'm Taking Forward
Profile before you optimize. This is advice I'd heard a hundred times. Now I've paid the tuition.
My new process: reproduce the problem, open the Profiler, find the actual spike, then write code. Not before.
What the Profiler Showed
Frame time: 32ms (frame 1847)
GC.Collect 12.4ms ← here
TextMeshPro.Update 6.1ms
Physics.Simulate 4.8ms
Render 5.3ms
The GC.Collect call is the giveaway — that's the garbage collector running because something is allocating heavily. Bullets after pooling: 0 allocations. Text popups before pooling: ~800 bytes per popup × 50 popups/sec = ~40KB/sec of garbage.
Takeaway for Anyone New to Unity Performance
Three things generate GC pressure more than anything else in my experience:
- Instantiate/Destroy — use pooling for anything that spawns frequently
- String operations in
Update()—$"Score: {score}"creates a new string every frame - LINQ in hot paths —
.Where(),.Select()allocate enumerators
Profile first. Know which of the three you're actually hitting before you rewrite anything.
Sharpshooters dev log. Follow along as I build a 3D shooter from scratch.
