Skip to content

Game Optimization: Performance Tuning for Games

DodaTech Updated 2026-06-20 8 min read

In this tutorial, you'll learn about Game Optimization: Performance Tuning for Games. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

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
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 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
1000 tris
0-10m] --> B[Medium
500 tris
10-30m] B --> C[Low
200 tris
30-60m] C --> D[Culled
Invisible
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

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:

  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

Game Audio Guide
Multiplayer Networking
Unreal Blueprints

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro