Performance Optimization and Profiling
Welcome to the essential guide for making your Unity games run smoothly! In this chapter, you'll learn how to identify performance bottlenecks, optimize your code and assets, and use Unity's powerful profiling tools to create games that run at 60 FPS on target devices.
What You'll Learn
By the end of this chapter, you'll know how to:
- Profile your game using Unity's built-in tools
- Identify performance bottlenecks in CPU, GPU, and memory
- Optimize scripts for better performance
- Optimize assets like textures, models, and audio
- Use best practices for mobile and desktop optimization
Why Performance Matters
Performance optimization is crucial for player experience. A game that runs smoothly feels professional and keeps players engaged, while a laggy game will frustrate users and hurt your reputation.
Pro Tip: Start optimizing early in development. It's much easier to build performance-friendly code from the start than to fix performance issues later.
Step 1: Understanding Unity's Profiler
Unity's Profiler is your best friend for identifying performance issues. Let's explore how to use it effectively.
Opening the Profiler
- Window → Analysis → Profiler (or press Ctrl+7)
- Play your game to start profiling
- Click "Record" to begin collecting data
Key Profiler Modules
CPU Usage
- Rendering: Time spent drawing objects
- Scripts: Your C# code execution time
- Physics: Physics calculations and collisions
- Garbage Collection: Memory allocation and cleanup
Memory Usage
- Used Memory: Current memory consumption
- GC Alloc: Garbage collection allocations
- Texture Memory: VRAM usage for textures
- Mesh Memory: 3D model data in RAM
GPU Usage
- Draw Calls: Number of objects drawn per frame
- Batches: Grouped draw calls for efficiency
- SetPass Calls: Shader state changes
Step 2: CPU Optimization Techniques
Script Optimization
Avoid Update() when possible:
// ❌ BAD: Called every frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Jump();
}
}
// ✅ GOOD: Use event-driven approach
void Start()
{
InputManager.OnJumpPressed += Jump;
}
Cache frequently accessed components:
public class PlayerController : MonoBehaviour
{
// ❌ BAD: Gets component every frame
void Update()
{
GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
}
// ✅ GOOD: Cache the component
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
rb.velocity = new Vector3(0, 0, 0);
}
}
Use object pooling for frequently created/destroyed objects:
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int poolSize = 50;
private Queue<GameObject> pool = new Queue<GameObject>();
void Start()
{
// Pre-instantiate objects
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject GetObject()
{
if (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab);
}
public void ReturnObject(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
Coroutine Optimization
Use coroutines instead of Update for timed events:
// ❌ BAD: Using Update with timer
public class BadTimer : MonoBehaviour
{
private float timer = 0f;
private float interval = 1f;
void Update()
{
timer += Time.deltaTime;
if (timer >= interval)
{
DoSomething();
timer = 0f;
}
}
}
// ✅ GOOD: Using coroutine
public class GoodTimer : MonoBehaviour
{
void Start()
{
StartCoroutine(DoSomethingRepeatedly());
}
IEnumerator DoSomethingRepeatedly()
{
while (true)
{
yield return new WaitForSeconds(1f);
DoSomething();
}
}
}
Step 3: Memory Optimization
Garbage Collection Best Practices
Avoid creating garbage in Update():
// ❌ BAD: Creates garbage every frame
void Update()
{
Vector3 position = transform.position;
position.y += 1f;
transform.position = position;
}
// ✅ GOOD: Reuse variables
private Vector3 position;
void Update()
{
position = transform.position;
position.y += 1f;
transform.position = position;
}
Use StringBuilder for string concatenation:
// ❌ BAD: Creates many string objects
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString();
}
// ✅ GOOD: Use StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i.ToString());
}
string result = sb.ToString();
Cache frequently used values:
public class OptimizedEnemy : MonoBehaviour
{
private Transform player;
private float updateInterval = 0.1f; // Update every 0.1 seconds
private float lastUpdateTime;
void Start()
{
player = GameObject.FindWithTag("Player").transform;
}
void Update()
{
if (Time.time - lastUpdateTime >= updateInterval)
{
UpdateAI();
lastUpdateTime = Time.time;
}
}
}
Step 4: Rendering Optimization
Draw Call Optimization
Use batching to reduce draw calls:
// Enable static batching for non-moving objects
[MenuItem("GameObject/Static Batching")]
static void EnableStaticBatching()
{
GameObject[] objects = Selection.gameObjects;
foreach (GameObject obj in objects)
{
obj.isStatic = true;
}
}
Use GPU Instancing for similar objects:
public class InstancedRendering : MonoBehaviour
{
public Mesh mesh;
public Material material;
public int instanceCount = 1000;
private Matrix4x4[] matrices;
private MaterialPropertyBlock propertyBlock;
void Start()
{
matrices = new Matrix4x4[instanceCount];
propertyBlock = new MaterialPropertyBlock();
// Generate random positions
for (int i = 0; i < instanceCount; i++)
{
matrices[i] = Matrix4x4.TRS(
Random.insideUnitSphere * 10f,
Quaternion.identity,
Vector3.one
);
}
}
void Update()
{
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, propertyBlock);
}
}
LOD (Level of Detail) System
Implement LOD for distant objects:
public class LODController : MonoBehaviour
{
[SerializeField] private GameObject[] lodLevels;
[SerializeField] private float[] distances;
[SerializeField] private Transform player;
private int currentLOD = 0;
void Update()
{
float distance = Vector3.Distance(transform.position, player.position);
for (int i = 0; i < distances.Length; i++)
{
if (distance <= distances[i])
{
SetLOD(i);
break;
}
}
}
void SetLOD(int lodLevel)
{
if (currentLOD != lodLevel)
{
// Deactivate all LOD levels
foreach (GameObject lod in lodLevels)
{
lod.SetActive(false);
}
// Activate current LOD level
if (lodLevel < lodLevels.Length)
{
lodLevels[lodLevel].SetActive(true);
}
currentLOD = lodLevel;
}
}
}
Step 5: Asset Optimization
Texture Optimization
Use appropriate texture formats:
// Set texture import settings
[MenuItem("Assets/Optimize Textures")]
static void OptimizeTextures()
{
Texture2D[] textures = Selection.GetFiltered<Texture2D>(SelectionMode.Assets);
foreach (Texture2D texture in textures)
{
TextureImporter importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture)) as TextureImporter;
if (importer != null)
{
importer.textureCompression = TextureImporterCompression.Compressed;
importer.maxTextureSize = 1024; // Adjust based on usage
importer.mipmapEnabled = true;
importer.wrapMode = TextureWrapMode.Clamp;
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(texture));
}
}
}
Audio Optimization
Use compressed audio formats:
public class AudioOptimizer : MonoBehaviour
{
[SerializeField] private AudioClip[] audioClips;
void Start()
{
// Set audio compression settings
foreach (AudioClip clip in audioClips)
{
AudioImporter importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(clip)) as AudioImporter;
if (importer != null)
{
importer.format = AudioImporterFormat.Compressed;
importer.compressionBitrate = 128; // 128 kbps for music, 64 for SFX
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(clip));
}
}
}
}
Step 6: Mobile Optimization
Mobile-Specific Optimizations
Optimize for mobile devices:
public class MobileOptimizer : MonoBehaviour
{
void Start()
{
// Set target frame rate
Application.targetFrameRate = 60;
// Optimize for mobile
if (Application.platform == RuntimePlatform.Android ||
Application.platform == RuntimePlatform.IPhonePlayer)
{
OptimizeForMobile();
}
}
void OptimizeForMobile()
{
// Reduce quality settings
QualitySettings.SetQualityLevel(1); // Medium quality
// Optimize physics
Physics.defaultSolverIterations = 4;
Physics.defaultSolverVelocityIterations = 1;
// Optimize rendering
QualitySettings.pixelLightCount = 1;
QualitySettings.shadowResolution = ShadowResolution.Low;
QualitySettings.shadowDistance = 20f;
}
}
Use occlusion culling for large scenes:
public class OcclusionCulling : MonoBehaviour
{
void Start()
{
// Enable occlusion culling
Camera.main.useOcclusionCulling = true;
// Set up occlusion culling data
if (OcclusionCullingSettings.occlusionCullingData == null)
{
Debug.LogWarning("Occlusion culling data not found. Please bake occlusion culling data.");
}
}
}
Step 7: Performance Monitoring
Runtime Performance Monitoring
Create a performance monitor:
public class PerformanceMonitor : MonoBehaviour
{
[SerializeField] private Text fpsText;
[SerializeField] private Text memoryText;
private float fps;
private float updateInterval = 0.5f;
private float lastUpdateTime;
void Update()
{
if (Time.time - lastUpdateTime >= updateInterval)
{
UpdatePerformanceStats();
lastUpdateTime = Time.time;
}
}
void UpdatePerformanceStats()
{
// Calculate FPS
fps = 1.0f / Time.deltaTime;
// Update UI
if (fpsText != null)
{
fpsText.text = $"FPS: {fps:F1}";
}
if (memoryText != null)
{
long memory = System.GC.GetTotalMemory(false);
memoryText.text = $"Memory: {memory / 1024 / 1024} MB";
}
}
}
Automated Performance Testing
Create performance tests:
public class PerformanceTest : MonoBehaviour
{
[SerializeField] private float targetFPS = 60f;
[SerializeField] private float testDuration = 10f;
private float startTime;
private List<float> fpsSamples = new List<float>();
void Start()
{
startTime = Time.time;
}
void Update()
{
if (Time.time - startTime < testDuration)
{
fpsSamples.Add(1.0f / Time.deltaTime);
}
else
{
AnalyzePerformance();
}
}
void AnalyzePerformance()
{
float averageFPS = fpsSamples.Average();
float minFPS = fpsSamples.Min();
Debug.Log($"Average FPS: {averageFPS:F1}");
Debug.Log($"Minimum FPS: {minFPS:F1}");
if (averageFPS < targetFPS)
{
Debug.LogWarning($"Performance below target! Average: {averageFPS:F1}, Target: {targetFPS}");
}
}
}
Step 8: Mini Challenge - Optimize Your Game
Your Task: Apply performance optimization techniques to your Unity project with these requirements:
- Profile your game using Unity's Profiler
- Identify bottlenecks in CPU, GPU, and memory usage
- Implement optimizations for scripts, assets, and rendering
- Test performance on target devices
- Achieve target FPS of 60 FPS on your target platform
Success Criteria:
- Game runs at 60 FPS on target hardware
- Memory usage is stable (no memory leaks)
- Draw calls are optimized (under 1000 for mobile, 2000 for desktop)
- Loading times are under 5 seconds
Pro Tips:
- Start with the biggest performance bottlenecks first
- Use Unity's built-in optimization tools
- Test on actual target devices, not just the editor
- Profile both in the editor and in builds
Troubleshooting Common Performance Issues
Issue 1: Low FPS
Problem: Game runs below target frame rate Solution:
- Check CPU usage in Profiler
- Optimize scripts and reduce Update() calls
- Use object pooling for frequently created objects
- Implement LOD system for distant objects
Issue 2: High Memory Usage
Problem: Game consumes too much RAM Solution:
- Check for memory leaks in Profiler
- Use object pooling instead of Instantiate/Destroy
- Optimize texture sizes and formats
- Unload unused assets
Issue 3: Long Loading Times
Problem: Game takes too long to load Solution:
- Use Addressables for asset loading
- Implement progressive loading
- Optimize asset sizes
- Use compressed formats
Issue 4: Stuttering and Frame Drops
Problem: Game stutters or has frame drops Solution:
- Check for garbage collection spikes
- Optimize physics calculations
- Use coroutines instead of Update for timed events
- Implement frame rate limiting
Pro Tips for Professional Optimization
-
Profile Early and Often
- Use the Profiler throughout development
- Don't wait until the end to optimize
- Set performance targets from the start
-
Optimize for Your Target Platform
- Mobile games need different optimizations than desktop
- Test on actual hardware, not just the editor
- Consider different quality settings for different devices
-
Use Unity's Built-in Tools
- Frame Debugger for rendering issues
- Memory Profiler for memory optimization
- Build Report for asset analysis
-
Monitor Performance in Production
- Implement runtime performance monitoring
- Collect performance data from players
- Use analytics to identify performance issues
What's Next?
Congratulations! You've learned how to optimize Unity games for peak performance. Your games will now run smoothly and provide an excellent player experience.
In the next chapter, Building and Publishing Your Game, you'll learn how to create builds for different platforms, optimize for distribution, and publish your game to app stores and game platforms.
Key Takeaways
- Use Unity's Profiler to identify performance bottlenecks
- Optimize scripts by caching components and avoiding garbage collection
- Implement object pooling for frequently created/destroyed objects
- Use LOD systems for distant objects
- Optimize assets with appropriate formats and compression
- Test on target hardware for accurate performance measurements
Your Unity games will now run at optimal performance, providing smooth gameplay experiences for players across all platforms!