Skip to content
Game Optimization: Performance Tuning for Games

Game Optimization: Performance Tuning for Games

DodaTech Updated Jun 20, 2026 7 min read

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.

OptimizationDraw Call ReductionEffort
Static batchingCombines static objectsLow
Dynamic batchingCombines moving objects (< 300 verts each)Low
GPU instancingRenders same mesh multiple timesLow
Texture atlasingReduces material swapsMedium
// 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 call

Enable: 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 skinned

Level 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 visible

In 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 rendered

Walls 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

ToolPlatformWhat It Measures
Unity ProfilerEditor, deviceCPU, GPU, memory, rendering, physics, audio
Unreal InsightsEditor, deviceTiming traces, rendering, asset loading
RenderDocPC, consoleGPU frame capture, draw calls, shaders
Xcode InstrumentsiOSCPU, GPU, memory, energy
Android Studio ProfilerAndroidCPU, 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:

AllocationImpact
new inside Update()Creates garbage every frame
String concatenationCreates intermediate strings
LINQ queriesBoxes value types
foreach on non-arrayAllocates 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

PlatformTargetWhy
PC (high-end)60-144 FPSSmooth, responsive
Console30-60 FPSStable, cinematic
Mobile (iOS)60 FPSProMotion displays
Mobile (Android)30-60 FPSBattery, thermal limits
VR72-120 FPSPrevents 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:

  1. Profile with Unity Profiler (note CPU, GPU, draw calls)
  2. Enable static batching on non-moving objects
  3. Add LOD Group to high-poly objects
  4. Bake occlusion culling
  5. Profile again and compare results

FAQ

Should I optimize for PC or console first?
Optimize for the lowest target platform first. If it runs well on mobile, it will fly on PC. Then add PC-specific features.
What’s the difference between static and dynamic batching?
Static batching merges objects at build time. Dynamic batching batches at runtime but has CPU overhead and vertex limits.
Is occlusion culling automatic?
No. You must bake occlusion data after placing Occlusion Areas in your scene. Objects in non-visible cells are culled.

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