Unity Memory Optimization Part 3: Advanced Techniques and Production Strategies
Unity Memory Optimization Part 3: Advanced Techniques and Production Strategies
These techniques go beyond fixing problems - they're about architecting memory-efficient applications from the ground up and maintaining optimal performance in production environments.
Garbage Collection Optimization
The garbage collector (GC) in Unity can cause noticeable frame rate hitches, especially on mobile devices. The key to smooth performance is minimizing garbage collection frequency and duration by reducing allocations.
Understanding Allocation Hotspots
Before optimizing, you need to identify where allocations occur. Some common culprits are often overlooked:
1public class AllocationHotspots : MonoBehaviour
2{
3 void Update()
4 {
5 // ALLOCATION: String concatenation creates garbage
6 string debugInfo = "Position: " + transform.position.ToString();
7
8 // ALLOCATION: GetComponent<T>() when called repeatedly
9 Rigidbody rb = GetComponent<Rigidbody>();
10
11 // ALLOCATION: Physics queries return new arrays each time
12 Collider[] nearbyObjects = Physics.OverlapSphere(transform.position, 5f);
13
14 // ALLOCATION: LINQ operations create temporary objects
15 var activeEnemies = enemies.Where(e => e.isActive).ToList();
16
17 // ALLOCATION: Boxing when using generic collections with value types
18 objectList.Add(transform.position); // Vector3 gets boxed if objectList is List<object>
19 }
20
21 private List<Enemy> enemies = new List<Enemy>();
22 private List<object> objectList = new List<object>();
23}Zero-Allocation Alternatives
Here's how to eliminate common allocation sources:
1public class ZeroAllocationOptimized : MonoBehaviour
2{
3 // Pre-allocate frequently used objects
4 private StringBuilder stringBuilder = new StringBuilder(256);
5 private Collider[] colliderBuffer = new Collider[50];
6 private List<Enemy> activeEnemiesList = new List<Enemy>();
7
8 // Cache component references
9 private Rigidbody cachedRigidbody;
10
11 void Start()
12 {
13 cachedRigidbody = GetComponent<Rigidbody>();
14 }
15
16 void Update()
17 {
18 // NO ALLOCATION: Use StringBuilder for string construction
19 stringBuilder.Clear();
20 stringBuilder.Append("Position: ");
21 AppendVector3ToString(stringBuilder, transform.position);
22 string debugInfo = stringBuilder.ToString();
23
24 // NO ALLOCATION: Use cached component reference
25 if (cachedRigidbody != null)
26 {
27 // Work with cached rigidbody
28 }
29
30 // NO ALLOCATION: Use non-allocating physics queries
31 int hitCount = Physics.OverlapSphereNonAlloc(
32 transform.position, 5f, colliderBuffer);
33
34 // Process hits without allocation
35 for (int i = 0; i < hitCount; i++)
36 {
37 ProcessNearbyObject(colliderBuffer[i]);
38 }
39
40 // NO ALLOCATION: Manual filtering instead of LINQ
41 GetActiveEnemies(enemies, activeEnemiesList);
42 }
43
44 void AppendVector3ToString(StringBuilder sb, Vector3 vector)
45 {
46 sb.Append("(");
47 sb.Append(vector.x.ToString("F2"));
48 sb.Append(", ");
49 sb.Append(vector.y.ToString("F2"));
50 sb.Append(", ");
51 sb.Append(vector.z.ToString("F2"));
52 sb.Append(")");
53 }
54
55 void GetActiveEnemies(List<Enemy> source, List<Enemy> result)
56 {
57 result.Clear();
58 for (int i = 0; i < source.Count; i++)
59 {
60 if (source[i].isActive)
61 {
62 result.Add(source[i]);
63 }
64 }
65 }
66
67 void ProcessNearbyObject(Collider collider)
68 {
69 // Process the collider without creating strings or temporary objects
70 }
71}
72
73public class Enemy
74{
75 public bool isActive;
76}Object Pooling Implementation
Object pooling eliminates allocation/deallocation cycles for frequently created objects:
1public class GenericObjectPool<T> : MonoBehaviour where T : Component
2{
3 [Header("Pool Settings")]
4 [SerializeField] private T prefab;
5 [SerializeField] private int initialPoolSize = 20;
6 [SerializeField] private bool allowGrowth = true;
7 [SerializeField] private int maxPoolSize = 100;
8
9 private Queue<T> availableObjects = new Queue<T>();
10 private HashSet<T> activeObjects = new HashSet<T>();
11 private Transform poolParent;
12
13 void Start()
14 {
15 InitializePool();
16 }
17
18 void InitializePool()
19 {
20 // Create a parent object to organize pooled objects
21 poolParent = new GameObject($"{typeof(T).Name}_Pool").transform;
22 poolParent.SetParent(transform);
23
24 // Pre-instantiate initial pool objects
25 for (int i = 0; i < initialPoolSize; i++)
26 {
27 CreateNewPoolObject();
28 }
29 }
30
31 T CreateNewPoolObject()
32 {
33 T newObj = Instantiate(prefab, poolParent);
34 newObj.gameObject.SetActive(false);
35 availableObjects.Enqueue(newObj);
36 return newObj;
37 }
38
39 public T GetObject()
40 {
41 T obj = null;
42
43 if (availableObjects.Count > 0)
44 {
45 obj = availableObjects.Dequeue();
46 }
47 else if (allowGrowth && (activeObjects.Count + availableObjects.Count) < maxPoolSize)
48 {
49 obj = CreateNewPoolObject();
50 availableObjects.Dequeue(); // Remove from available since we're about to use it
51 }
52 else
53 {
54 Debug.LogWarning($"Pool for {typeof(T).Name} exhausted and cannot grow!");
55 return null;
56 }
57
58 obj.gameObject.SetActive(true);
59 obj.transform.SetParent(null); // Remove from pool parent when active
60 activeObjects.Add(obj);
61
62 return obj;
63 }
64
65 public void ReturnObject(T obj)
66 {
67 if (obj != null && activeObjects.Remove(obj))
68 {
69 obj.gameObject.SetActive(false);
70 obj.transform.SetParent(poolParent);
71
72 // Reset object to default state
73 ResetPooledObject(obj);
74
75 availableObjects.Enqueue(obj);
76 }
77 }
78
79 protected virtual void ResetPooledObject(T obj)
80 {
81 // Override this method to reset object-specific properties
82 // For example, reset position, rotation, velocity, etc.
83 obj.transform.position = Vector3.zero;
84 obj.transform.rotation = Quaternion.identity;
85 }
86
87 // Monitoring and debugging
88 public void LogPoolStats()
89 {
90 Debug.Log($"Pool {typeof(T).Name} - Available: {availableObjects.Count}, " +
91 $"Active: {activeObjects.Count}, " +
92 $"Total: {availableObjects.Count + activeObjects.Count}");
93 }
94
95 void OnDestroy()
96 {
97 availableObjects.Clear();
98 activeObjects.Clear();
99 }
100}
101
102// Example usage for bullets
103public class BulletPool : GenericObjectPool<Bullet>
104{
105 protected override void ResetPooledObject(Bullet bullet)
106 {
107 base.ResetPooledObject(bullet);
108
109 // Reset bullet-specific properties
110 bullet.damage = bullet.baseDamage;
111 bullet.velocity = Vector3.zero;
112 bullet.hasHitTarget = false;
113 }
114}
115
116// Example Bullet component
117public class Bullet : MonoBehaviour
118{
119 [Header("Bullet Properties")]
120 public float baseDamage = 10f;
121 public float lifetime = 5f;
122
123 [HideInInspector] public float damage;
124 [HideInInspector] public Vector3 velocity;
125 [HideInInspector] public bool hasHitTarget;
126
127 private BulletPool parentPool;
128
129 public void Initialize(BulletPool pool, Vector3 startVelocity)
130 {
131 parentPool = pool;
132 velocity = startVelocity;
133 damage = baseDamage;
134 hasHitTarget = false;
135
136 // Auto-return to pool after lifetime
137 StartCoroutine(ReturnToPoolAfterDelay());
138 }
139
140 System.Collections.IEnumerator ReturnToPoolAfterDelay()
141 {
142 yield return new WaitForSeconds(lifetime);
143
144 if (!hasHitTarget && parentPool != null)
145 {
146 parentPool.ReturnObject(this);
147 }
148 }
149
150 void OnTriggerEnter(Collider other)
151 {
152 if (!hasHitTarget)
153 {
154 hasHitTarget = true;
155 // Handle collision logic
156
157 // Return to pool
158 if (parentPool != null)
159 {
160 parentPool.ReturnObject(this);
161 }
162 }
163 }
164}Platform-Specific Memory Optimization
Different platforms have unique memory constraints and optimization opportunities. Understanding these differences is crucial for multi-platform Unity applications.
Mobile Optimization Strategies
Mobile devices have the most stringent memory limitations and are most sensitive to garbage collection pauses:
1public class MobileMemoryOptimizer : MonoBehaviour
2{
3 [Header("Mobile-Specific Settings")]
4 public bool optimizeForMobile = true;
5 public int mobileTextureQualityLevel = 1; // 0=full, 1=half, 2=quarter resolution
6 public int mobileParticleLimit = 50;
7 public float mobileUpdateInterval = 0.1f; // Reduce update frequency
8
9 void Start()
10 {
11 if (Application.isMobilePlatform && optimizeForMobile)
12 {
13 ApplyMobileOptimizations();
14 MonitorLowMemoryWarnings();
15 }
16 }
17
18 void ApplyMobileOptimizations()
19 {
20 // Reduce texture quality
21 QualitySettings.masterTextureLimit = mobileTextureQualityLevel;
22
23 // Optimize rendering settings
24 QualitySettings.shadowResolution = ShadowResolution.Low;
25 QualitySettings.shadowDistance = 30f; // Shorter shadow distance
26 QualitySettings.anisotropicFiltering = AnisotropicFiltering.Disable;
27
28 // Limit particle systems
29 OptimizeParticleSystems();
30
31 // Reduce physics update rate for non-critical objects
32 Physics.defaultSolverIterations = 4; // Default is 6
33 Physics.defaultSolverVelocityIterations = 1; // Default is 1
34
35 // Enable GPU instancing where possible
36 EnableGPUInstancing();
37 }
38
39 void OptimizeParticleSystems()
40 {
41 ParticleSystem[] allParticleSystems = FindObjectsOfType<ParticleSystem>();
42
43 foreach (var ps in allParticleSystems)
44 {
45 var main = ps.main;
46
47 // Limit particle count
48 if (main.maxParticles > mobileParticleLimit)
49 {
50 main.maxParticles = mobileParticleLimit;
51 }
52
53 // Disable expensive features
54 var collision = ps.collision;
55 if (collision.enabled)
56 {
57 collision.enabled = false; // Particle collision is expensive
58 }
59
60 // Use simpler simulation space
61 if (main.simulationSpace == ParticleSystemSimulationSpace.World)
62 {
63 main.simulationSpace = ParticleSystemSimulationSpace.Local;
64 }
65 }
66 }
67
68 void EnableGPUInstancing()
69 {
70 // Find materials that could benefit from GPU instancing
71 Renderer[] renderers = FindObjectsOfType<Renderer>();
72
73 foreach (var renderer in renderers)
74 {
75 foreach (var material in renderer.materials)
76 {
77 if (material.shader.name.Contains("Standard") ||
78 material.shader.name.Contains("Universal"))
79 {
80 material.enableInstancing = true;
81 }
82 }
83 }
84 }
85
86 void MonitorLowMemoryWarnings()
87 {
88 Application.lowMemory += OnLowMemoryWarning;
89 }
90
91 void OnLowMemoryWarning()
92 {
93 Debug.LogWarning("Low memory warning received! Performing emergency cleanup.");
94
95 // Immediate cleanup actions
96 Resources.UnloadUnusedAssets();
97 System.GC.Collect();
98
99 // Further reduce quality if needed
100 QualitySettings.masterTextureLimit = Mathf.Min(QualitySettings.masterTextureLimit + 1, 3);
101
102 // Disable non-essential effects
103 DisableNonEssentialEffects();
104 }
105
106 void DisableNonEssentialEffects()
107 {
108 // Disable particle effects
109 ParticleSystem[] particles = FindObjectsOfType<ParticleSystem>();
110 foreach (var ps in particles)
111 {
112 if (!ps.CompareTag("Essential"))
113 {
114 ps.gameObject.SetActive(false);
115 }
116 }
117
118 // Disable audio effects (keep music and essential sounds)
119 AudioSource[] audioSources = FindObjectsOfType<AudioSource>();
120 foreach (var audio in audioSources)
121 {
122 if (!audio.CompareTag("Essential"))
123 {
124 audio.Stop();
125 audio.enabled = false;
126 }
127 }
128 }
129}Web Platform Considerations
WebGL builds have unique memory constraints due to browser limitations:
1public class WebGLMemoryOptimizer : MonoBehaviour
2{
3 void Start()
4 {
5 #if UNITY_WEBGL && !UNITY_EDITOR
6 ApplyWebGLOptimizations();
7 #endif
8 }
9
10 void ApplyWebGLOptimizations()
11 {
12 // WebGL has limited memory - be more aggressive with optimization
13 QualitySettings.masterTextureLimit = 2; // Quarter resolution textures
14
15 // Disable features not supported or poorly performing on WebGL
16 QualitySettings.shadows = ShadowQuality.Disable;
17 QualitySettings.softParticles = false;
18
19 // Reduce audio quality to save memory
20 OptimizeAudioForWeb();
21
22 // Limit concurrent operations
23 Application.targetFrameRate = 30; // Cap frame rate for stability
24
25 // Preload critical assets to avoid loading hitches
26 StartCoroutine(PreloadCriticalAssets());
27 }
28
29 void OptimizeAudioForWeb()
30 {
31 AudioSource[] audioSources = FindObjectsOfType<AudioSource>();
32
33 foreach (var source in audioSources)
34 {
35 if (source.clip != null)
36 {
37 // Use compressed audio for all non-critical sounds
38 if (!source.CompareTag("HighQualityAudio"))
39 {
40 // These settings would be applied at import time,
41 // but we can check at runtime
42 if (source.clip.length > 5f) // Long clips should stream
43 {
44 Debug.LogWarning($"Audio clip {source.clip.name} is long and may cause memory issues on WebGL");
45 }
46 }
47 }
48 }
49 }
50
51 System.Collections.IEnumerator PreloadCriticalAssets()
52 {
53 // Preload essential assets to avoid hitches during gameplay
54 string[] criticalAssets = {
55 "UI/MainMenu",
56 "Sounds/ButtonClick",
57 "Effects/LoadingSpinner"
58 };
59
60 foreach (string assetPath in criticalAssets)
61 {
62 var request = Resources.LoadAsync(assetPath);
63 yield return request;
64
65 if (request.asset != null)
66 {
67 // Asset is now loaded and cached
68 Debug.Log($"Preloaded critical asset: {assetPath}");
69 }
70 }
71 }
72}Desktop Optimization
Desktop platforms generally have more memory available, allowing for different optimization strategies:
1public class DesktopMemoryOptimizer : MonoBehaviour
2{
3 [Header("Desktop Settings")]
4 public bool enableHighQualityMode = true;
5 public int targetMemoryUsageMB = 2048; // 2GB target
6
7 void Start()
8 {
9 #if (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_STANDALONE_LINUX) && !UNITY_EDITOR
10 ApplyDesktopOptimizations();
11 #endif
12 }
13
14 void ApplyDesktopOptimizations()
15 {
16 // Desktop can handle higher quality settings
17 if (enableHighQualityMode && SystemInfo.systemMemorySize >= 8192) // 8GB+ RAM
18 {
19 QualitySettings.masterTextureLimit = 0; // Full resolution
20 QualitySettings.shadowResolution = ShadowResolution.VeryHigh;
21 QualitySettings.shadowDistance = 150f;
22 QualitySettings.anisotropicFiltering = AnisotropicFiltering.ForceEnable;
23 }
24
25 // Use more sophisticated memory management
26 StartCoroutine(MonitorMemoryUsage());
27
28 // Enable advanced features
29 EnableAdvancedFeatures();
30 }
31
32 System.Collections.IEnumerator MonitorMemoryUsage()
33 {
34 while (true)
35 {
36 yield return new WaitForSeconds(10f); // Check every 10 seconds
37
38 long currentMemoryMB = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / (1024 * 1024);
39
40 if (currentMemoryMB > targetMemoryUsageMB)
41 {
42 Debug.LogWarning($"Memory usage ({currentMemoryMB}MB) exceeds target ({targetMemoryUsageMB}MB)");
43
44 // Perform cleanup
45 Resources.UnloadUnusedAssets();
46
47 // Optionally reduce quality temporarily
48 if (currentMemoryMB > targetMemoryUsageMB * 1.5f)
49 {
50 TemporaryQualityReduction();
51 }
52 }
53 }
54 }
55
56 void TemporaryQualityReduction()
57 {
58 Debug.Log("Applying temporary quality reduction due to high memory usage");
59
60 QualitySettings.masterTextureLimit = 1; // Half resolution
61
62 // Restore quality after a delay
63 StartCoroutine(RestoreQualityAfterDelay(30f));
64 }
65
66 System.Collections.IEnumerator RestoreQualityAfterDelay(float delay)
67 {
68 yield return new WaitForSeconds(delay);
69
70 // Check if memory situation has improved
71 long currentMemoryMB = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / (1024 * 1024);
72
73 if (currentMemoryMB < targetMemoryUsageMB * 0.8f)
74 {
75 QualitySettings.masterTextureLimit = 0; // Restore full resolution
76 Debug.Log("Quality settings restored");
77 }
78 }
79
80 void EnableAdvancedFeatures()
81 {
82 // Desktop can handle more sophisticated memory pooling
83 // Enable advanced object pooling systems
84 // Use higher-resolution textures for UI elements
85 // Enable advanced particle effects
86 // Use higher-quality audio compression
87 }
88}Production Memory Monitoring
Monitoring memory usage in production helps identify issues before they impact users and provides data for ongoing optimization:
1public class ProductionMemoryMonitor : MonoBehaviour
2{
3 [Header("Monitoring Configuration")]
4 public bool enableMonitoring = true;
5 public float monitoringInterval = 30f; // Check every 30 seconds
6 public int memoryWarningThresholdMB = 500;
7 public int memoryCriticalThresholdMB = 800;
8 public int maxReportedEventsPerSession = 10;
9
10 private int reportedEventsThisSession = 0;
11 private float lastMonitorTime;
12 private Queue<MemorySnapshot> memoryHistory = new Queue<MemorySnapshot>();
13
14 [System.Serializable]
15 public class MemorySnapshot
16 {
17 public float timestamp;
18 public long totalMemoryMB;
19 public long managedMemoryMB;
20 public long nativeMemoryMB;
21 public string currentScene;
22 public int activeGameObjects;
23
24 public MemorySnapshot(float time, long total, long managed, long native, string scene, int objects)
25 {
26 timestamp = time;
27 totalMemoryMB = total;
28 managedMemoryMB = managed;
29 nativeMemoryMB = native;
30 currentScene = scene;
31 activeGameObjects = objects;
32 }
33 }
34
35 void Start()
36 {
37 if (enableMonitoring && !Application.isEditor)
38 {
39 StartMonitoring();
40 }
41 }
42
43 void StartMonitoring()
44 {
45 // Monitor application lifecycle events
46 Application.focusChanged += OnApplicationFocusChanged;
47 Application.lowMemory += OnLowMemoryWarning;
48
49 // Start memory monitoring coroutine
50 StartCoroutine(MonitorMemoryUsage());
51
52 // Monitor scene changes
53 UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
54 }
55
56 System.Collections.IEnumerator MonitorMemoryUsage()
57 {
58 while (enableMonitoring)
59 {
60 yield return new WaitForSeconds(monitoringInterval);
61 RecordMemorySnapshot();
62 }
63 }
64
65 void RecordMemorySnapshot()
66 {
67 long totalMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong();
68 long managedMemory = System.GC.GetTotalMemory(false);
69 long nativeMemory = totalMemory - managedMemory;
70
71 long totalMB = totalMemory / (1024 * 1024);
72 long managedMB = managedMemory / (1024 * 1024);
73 long nativeMB = nativeMemory / (1024 * 1024);
74
75 string currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
76 int activeObjects = FindObjectsOfType<GameObject>().Length;
77
78 MemorySnapshot snapshot = new MemorySnapshot(
79 Time.time, totalMB, managedMB, nativeMB, currentScene, activeObjects
80 );
81
82 // Keep a rolling window of memory snapshots
83 memoryHistory.Enqueue(snapshot);
84 if (memoryHistory.Count > 20) // Keep last 20 snapshots
85 {
86 memoryHistory.Dequeue();
87 }
88
89 // Check for memory warnings
90 CheckMemoryThresholds(snapshot);
91 }
92
93 void CheckMemoryThresholds(MemorySnapshot snapshot)
94 {
95 if (reportedEventsThisSession >= maxReportedEventsPerSession)
96 {
97 return; // Don't spam reports
98 }
99
100 if (snapshot.totalMemoryMB >= memoryCriticalThresholdMB)
101 {
102 ReportMemoryEvent("CRITICAL", snapshot, "Memory usage has reached critical levels");
103 }
104 else if (snapshot.totalMemoryMB >= memoryWarningThresholdMB)
105 {
106 ReportMemoryEvent("WARNING", snapshot, "Memory usage is above warning threshold");
107 }
108
109 // Check for rapid memory growth
110 if (memoryHistory.Count >= 3)
111 {
112 var snapshots = memoryHistory.ToArray();
113 int count = snapshots.Length;
114
115 long growthMB = snapshots[count - 1].totalMemoryMB - snapshots[count - 3].totalMemoryMB;
116
117 if (growthMB > 100) // More than 100MB growth in 2 intervals
118 {
119 ReportMemoryEvent("LEAK_SUSPECTED", snapshot,
120 $"Rapid memory growth detected: +{growthMB}MB");
121 }
122 }
123 }
124
125 void ReportMemoryEvent(string eventType, MemorySnapshot snapshot, string message)
126 {
127 reportedEventsThisSession++;
128
129 var eventData = new Dictionary<string, object>
130 {
131 ["event_type"] = eventType,
132 ["total_memory_mb"] = snapshot.totalMemoryMB,
133 ["managed_memory_mb"] = snapshot.managedMemoryMB,
134 ["native_memory_mb"] = snapshot.nativeMemoryMB,
135 ["scene"] = snapshot.currentScene,
136 ["active_objects"] = snapshot.activeGameObjects,
137 ["device_memory_mb"] = SystemInfo.systemMemorySize,
138 ["platform"] = Application.platform.ToString(),
139 ["unity_version"] = Application.unityVersion,
140 ["app_version"] = Application.version
141 };
142
143 // Send to your analytics service
144 // Examples: Firebase Analytics, Unity Analytics, GameAnalytics, custom backend
145 SendAnalyticsEvent("memory_event", eventData);
146
147 Debug.LogWarning($"[MEMORY] {eventType}: {message} (Total: {snapshot.totalMemoryMB}MB)");
148 }
149
150 void SendAnalyticsEvent(string eventName, Dictionary<string, object> parameters)
151 {
152 // Example implementations:
153
154 // Firebase Analytics
155 // Firebase.Analytics.FirebaseAnalytics.LogEvent(eventName, parameters);
156
157 // Unity Analytics (deprecated, but example)
158 // Unity.Analytics.Analytics.CustomEvent(eventName, parameters);
159
160 // GameAnalytics
161 // GameAnalytics.NewDesignEvent(eventName, parameters);
162
163 // Custom backend
164 // StartCoroutine(SendToCustomBackend(eventName, parameters));
165
166 // For now, just log locally
167 Debug.Log($"Analytics Event: {eventName} with {parameters.Count} parameters");
168 }
169
170 void OnApplicationFocusChanged(bool hasFocus)
171 {
172 if (!hasFocus)
173 {
174 // App is being backgrounded - good time for cleanup
175 Resources.UnloadUnusedAssets();
176 System.GC.Collect();
177 }
178
179 RecordMemorySnapshot(); // Record memory state during focus changes
180 }
181
182 void OnLowMemoryWarning()
183 {
184 var snapshot = new MemorySnapshot(
185 Time.time,
186 UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / (1024 * 1024),
187 System.GC.GetTotalMemory(false) / (1024 * 1024),
188 0, // Calculate if needed
189 UnityEngine.SceneManagement.SceneManager.GetActiveScene().name,
190 FindObjectsOfType<GameObject>().Length
191 );
192
193 ReportMemoryEvent("LOW_MEMORY_WARNING", snapshot,
194 "System reported low memory condition");
195
196 // Perform emergency cleanup
197 Resources.UnloadUnusedAssets();
198 System.GC.Collect();
199 }
200
201 void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
202 {
203 // Record memory usage after scene loads
204 StartCoroutine(DelayedSceneMemoryCheck(scene.name));
205 }
206
207 System.Collections.IEnumerator DelayedSceneMemoryCheck(string sceneName)
208 {
209 yield return new WaitForSeconds(2f); // Wait for scene to fully load
210 RecordMemorySnapshot();
211 }
212
213 void OnDestroy()
214 {
215 // Clean up event subscriptions
216 Application.focusChanged -= OnApplicationFocusChanged;
217 Application.lowMemory -= OnLowMemoryWarning;
218 UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
219 }
220
221 // Debug utility to manually trigger memory report
222 [ContextMenu("Generate Memory Report")]
223 void GenerateMemoryReport()
224 {
225 Debug.Log("=== MEMORY REPORT ===");
226
227 if (memoryHistory.Count > 0)
228 {
229 var snapshots = memoryHistory.ToArray();
230
231 Debug.Log($"Memory snapshots over last {memoryHistory.Count * monitoringInterval / 60f:F1} minutes:");
232
233 foreach (var snapshot in snapshots)
234 {
235 Debug.Log($" {snapshot.timestamp:F0}s: {snapshot.totalMemoryMB}MB " +
236 $"(Managed: {snapshot.managedMemoryMB}MB, Native: {snapshot.nativeMemoryMB}MB) " +
237 $"Scene: {snapshot.currentScene}");
238 }
239
240 // Calculate trend
241 if (snapshots.Length > 1)
242 {
243 long trend = snapshots[snapshots.Length - 1].totalMemoryMB - snapshots[0].totalMemoryMB;
244 Debug.Log($"Memory trend: {(trend >= 0 ? "+" : "")}{trend}MB over {snapshots.Length} samples");
245 }
246 }
247
248 Debug.Log("==================");
249 }
250}Conclusion: Building Memory-Efficient Unity Applications
Memory optimization in Unity requires a multi-layered approach:
- Foundation: Understand Unity's memory architecture and use profiling tools effectively
- Prevention: Identify and fix memory leaks before they become problems
- Optimization: Reduce garbage collection pressure and implement efficient patterns
- Platform Awareness: Adapt strategies for mobile, web, and desktop constraints
- Production Monitoring: Track memory usage in released applications
Key Takeaways
- Profile early and often - Memory issues are easier to fix during development
- Platform matters - Mobile, web, and desktop have different constraints and opportunities
- Prevention beats cure - Building memory-efficient patterns is better than fixing problems later
- Monitor in production - Real-world usage often reveals issues not caught in development
- Iterate and improve - Memory optimization is an ongoing process, not a one-time task
Building Memory-Conscious Development Habits
- Code reviews should always check for potential memory issues
- Automated testing should include memory usage validation
- Performance budgets should be established for different platforms
- Production monitoring should be built into your analytics pipeline
- Team education ensures everyone understands memory optimization principles
Memory optimization might seem daunting, but by following these principles and implementing the techniques covered in this three-part series, you'll be well-equipped to build high-performance Unity applications that run smoothly across all target platforms.
Need help implementing these optimization strategies in your Unity project? Let's discuss your specific performance challenges and requirements.