Game Optimization: Performance Tuning for Games
Game optimization ensures your game runs smoothly on target hardware. A beautiful game that runs at 15 FPS is unplayable. Optimization is not about making a game look worse — it’s about spending rendering budget where it matters most.
In this tutorial, you’ll learn draw calls and batching, LODs (Level of Detail), occlusion culling, texture atlasing, profiling tools (Unity Profiler, Unreal Insights), garbage collection, memory management, and frame rate targeting for different platforms.
What You’ll Learn
- Draw calls: what they are and how to reduce them
- Static and dynamic batching in Unity
- Level of Detail (LOD) systems
- Occlusion culling to skip invisible objects
- Texture atlasing for fewer material swaps
- Profiling tools: Unity Profiler, Unreal Insights
- Garbage collection and memory management
- Frame rate targets per platform
Why Optimization Matters
Players notice poor performance immediately. A 60 FPS game feels “smooth”; a 30 FPS game feels “laggy.” On mobile, every millisecond of GPU time counts. At DodaTech, we apply these techniques to keep Doda Browser running at 120 FPS on high-refresh-rate displays.
Learning Path
flowchart LR
A[Game Audio] --> B[Game Optimization<br/>You are here]
B --> C[Build & Ship]
style B fill:#f90,color:#fff
Understanding Draw Calls
A draw call is a command from CPU to GPU telling it to render something. Each draw call has overhead — 1000 draw calls at 60 FPS means 60,000 calls per second.
| Optimization | Draw Call Reduction | Effort |
|---|---|---|
| Static batching | Combines static objects | Low |
| Dynamic batching | Combines moving objects (< 300 verts each) | Low |
| GPU instancing | Renders same mesh multiple times | Low |
| Texture atlasing | Reduces material swaps | Medium |
// Enable GPU instancing from code
public class EnableInstancing : MonoBehaviour
{
void Start()
{
var renderer = GetComponent<MeshRenderer>();
var mat = renderer.material;
mat.enableInstancing = true; // Allows GPU instancing
}
}Expected behavior: Objects using the same material and mesh are batched into a single draw call, dramatically reducing CPU overhead.
Static and Dynamic Batching
Static batching merges all static GameObjects sharing the same material into one mesh at build time:
Without batching: 100 houses × 1 draw call each = 100 draw calls
With batching: 100 houses merged = 1 draw callEnable: Check “Static” in the Inspector top-right.
Dynamic batching automatically batches moving objects (less than 300 vertices each). It has CPU overhead, so profile before relying on it.
// Dynamic batching requirements:
// - Under 300 vertices
// - Same material
// - Not scaled (1,1,1)
// - Objects must be mesh renderers, not skinnedLevel of Detail (LOD)
LOD systems swap high-detail meshes for simpler versions as distance increases:
flowchart LR
A[Full Detail<br/>1000 tris<br/>0-10m] --> B[Medium<br/>500 tris<br/>10-30m]
B --> C[Low<br/>200 tris<br/>30-60m]
C --> D[Culled<br/>Invisible<br/>60m+]
// Unity LOD Group setup (done in editor, but configurable in code)
using UnityEngine;
public class LODSetup : MonoBehaviour
{
void Start()
{
var lodGroup = gameObject.AddComponent<LODGroup>();
// Create LOD levels (best quality to lowest)
var lods = new LOD[]
{
new LOD(0.3f, new Renderer[] { highResMesh }), // 0-30% screen size
new LOD(0.15f, new Renderer[] { medResMesh }), // 15-30%
new LOD(0.05f, new Renderer[] { lowResMesh }), // 5-15%
new LOD(0f, new Renderer[] { culledRenderer }) // Below 5% = invisible
};
lodGroup.SetLODs(lods);
lodGroup.RecalculateBounds();
}
}Expected behavior: As the camera moves away, the mesh automatically swaps to lower-detail versions. Players rarely notice the transition.
Occlusion Culling
Occlusion culling prevents rendering objects hidden behind walls or other geometry:
Without occlusion culling: Render everything in camera frustum
With occlusion culling: Only render what's directly visibleIn Unity: Window > Rendering > Occlusion Culling > Bake.
// Occlusion areas define where the player can be
// Bake cells that are visible from each area
// Objects in non-visible cells are not renderedWalls as occluders: Large static objects act as occluders. Small objects (trees, chairs) are poor occluders.
Texture Atlasing
A texture atlas combines multiple small textures into one large texture:
4 separate textures (256×256 each) → 1 atlas (512×512)
4 draw calls → 1 draw call// When using an atlas, all objects reference the same material
// One draw call for all atlased objects
public class AtlasUser : MonoBehaviour
{
// UV coordinates are offset to point to the correct sub-region
// Atlas (0,0) = top-left tile, (0.5,0) = top-right tile, etc.
}Tools: Unity Sprite Atlas, TexturePacker, custom tools.
Profiling Tools
| Tool | Platform | What It Measures |
|---|---|---|
| Unity Profiler | Editor, device | CPU, GPU, memory, rendering, physics, audio |
| Unreal Insights | Editor, device | Timing traces, rendering, asset loading |
| RenderDoc | PC, console | GPU frame capture, draw calls, shaders |
| Xcode Instruments | iOS | CPU, GPU, memory, energy |
| Android Studio Profiler | Android | CPU, memory, network, energy |
// Unity Profiler scripting
using UnityEngine.Profiling;
void Update()
{
Profiler.BeginSample("HeavyCalculation");
// ... expensive code ...
Profiler.EndSample();
}Expected behavior: Custom profiling samples appear in the Unity Profiler timeline, helping you identify bottlenecks.
Garbage Collection and Memory
C# garbage collection (GC) can cause frame rate spikes. Every allocation adds to GC pressure:
| Allocation | Impact |
|---|---|
new inside Update() | Creates garbage every frame |
| String concatenation | Creates intermediate strings |
| LINQ queries | Boxes value types |
| foreach on non-array | Allocates enumerator |
// Bad: allocates every frame
void Update()
{
Debug.Log("Position: " + transform.position);
}
// Good: reuse StringBuilder
private System.Text.StringBuilder sb = new();
void Update()
{
sb.Clear();
sb.Append("Position: ");
sb.Append(transform.position);
Debug.Log(sb.ToString());
}Use object pooling to reuse frequently spawned/destroyed objects:
public class BulletPool : MonoBehaviour
{
public GameObject bulletPrefab;
public int poolSize = 30;
private Queue<GameObject> pool = new();
void Start()
{
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(bulletPrefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject GetBullet()
{
if (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return null; // Pool exhausted
}
}Frame Rate Targets
| Platform | Target | Why |
|---|---|---|
| PC (high-end) | 60-144 FPS | Smooth, responsive |
| Console | 30-60 FPS | Stable, cinematic |
| Mobile (iOS) | 60 FPS | ProMotion displays |
| Mobile (Android) | 30-60 FPS | Battery, thermal limits |
| VR | 72-120 FPS | Prevents motion sickness |
Set the target:
void Start()
{
Application.targetFrameRate = 60;
QualitySettings.vSyncCount = 1; // Sync to monitor refresh
}Common Mistakes
1. Premature Optimization
Optimize after profiling, not before. A 1ms save in a non-bottleneck area is worthless. Profile first, then optimize the bottleneck.
2. Realtime Lights Without Baking
Every realtime light adds rendering cost. Bake static lighting into lightmaps. Use realtime lights only for dynamic objects.
3. Single-Threaded CPU Bottlenecks
Unity’s main thread handles most game logic. Offload heavy computations to jobs (Unity Jobs System) or compute shaders.
4. Shader Complexity
Complex shaders (tessellation, subsurface scattering) are expensive on mobile. Use simpler shader variants per platform.
5. Loading Everything at Startup
Stream assets as needed. Use Addressables or Asset Bundles for level-based loading. Split large levels into chunks.
Practice Questions
1. What is a draw call and why does it matter?
A draw call sends render data from CPU to GPU. Each call has overhead. Reducing draw calls is the #1 optimization for rendering.
2. How does static batching work?
It combines static GameObjects with the same material into a single mesh at build time, reducing multiple draw calls to one.
3. What problem does LOD solve?
It reduces triangle count for distant objects, saving GPU time without visible quality loss.
4. Why does garbage collection cause frame drops?
GC pauses execution to collect unused memory. Frequent allocations cause more GC collections. Object pooling reduces allocations.
5. Challenge: Profile and optimize a scene.
Take a scene with 500 objects. Profile it. Add static batching, LODs, occlusion culling, and texture atlasing. Measure the improvement.
Mini Project: Optimization Pass
Take any existing scene and:
- Profile with Unity Profiler (note CPU, GPU, draw calls)
- Enable static batching on non-moving objects
- Add LOD Group to high-poly objects
- Bake occlusion culling
- Profile again and compare results
FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro