UnityUnity3DPerformanceMemory ManagementOptimizationGame DevelopmentMobile Optimization

Unity Memory Optimization Part 3: Advanced Techniques and Production Strategies

December 10, 2022
14 min read
Gonçalo Bastos

Unity Memory Optimization Part 3: Advanced Techniques and Production Strategies

This is the final part of our Unity memory optimization series. In Part 1, we covered Unity's memory architecture and profiling tools. Part 2 focused on identifying and preventing memory leaks. Now we'll explore advanced optimization techniques, platform-specific strategies, and production monitoring approaches.

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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

  1. Foundation: Understand Unity's memory architecture and use profiling tools effectively
  2. Prevention: Identify and fix memory leaks before they become problems
  3. Optimization: Reduce garbage collection pressure and implement efficient patterns
  4. Platform Awareness: Adapt strategies for mobile, web, and desktop constraints
  5. 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

  1. Code reviews should always check for potential memory issues
  2. Automated testing should include memory usage validation
  3. Performance budgets should be established for different platforms
  4. Production monitoring should be built into your analytics pipeline
  5. 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.