Unity Performance optimization summary

I have been developing Unity for a while, and I have also learned a few tricks of memory and Performance optimization. Although it is not a system, in order to prevent forgetting, it is also a simple summary.

Resource Cache

In some cases, such as a dress-up scene, the general practice is that we load the model of the clothing, extract the SkinnedMeshRender from it, merge multiple SMRs, and then bind them to the characters through bones, and finally destroy the clothing.

If we only have one character, there is no problem, but if we have n nude characters, even wearing the same set of costumes, we need to initialize all the costumes n times and destroy them n times, which is not good for CPU and memory.

At this time, we can use the cache method, since each time we only need to extract the SMR of the model, then all our characters are extracted from the same model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Threading.Tasks;
using UnityEngine;

namespace ResourceManager
{
public interface IResourceCache
{
public Task<GameObject> GetResource(string location);
public void ReleaseResource(string location);

public void Clear();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Character;
using UnityEngine;

namespace ResourceManager
{
public abstract class AbstractResourceCache: IResourceCache
{
private Dictionary<string, GameObject> _resourcesCache = new Dictionary<string, GameObject>();
private static GameObject _parent;
private int _maxCount = 20;

private static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);

public abstract Task<GameObject> InstantiateAsync(string key);
public abstract void Release(GameObject gameObject);

private GameObject GetParent()
{
if (!_parent)
{
_parent = new GameObject("ResourceCache");
}

return _parent;
}
public async Task<GameObject> GetResource(string location)
{
await _semaphoreSlim.WaitAsync();
try
{
if (_resourcesCache.ContainsKey(location))
{
if (_resourcesCache[location])
{
return _resourcesCache[location];
}
else
{
_resourcesCache.Remove(location);
}
}

GameObject resource = await InstantiateAsync(location);
resource.transform.parent = GetParent().transform;
resource.SetActive(false);

if (_resourcesCache.Count >= _maxCount)
{
ReleaseFirst();
}

_resourcesCache.Add(location, resource);
return resource;
}
finally
{
_semaphoreSlim.Release();
}
}

private void ReleaseFirst()
{
if (_resourcesCache.Count > 0)
{
ReleaseResource(_resourcesCache.First().Key);
}
}

public void ReleaseResource(string location)
{
try
{
if (_resourcesCache.ContainsKey(location))
{
if (_resourcesCache[location])
{
Release(_resourcesCache[location]);
}
_resourcesCache.Remove(location);
}

}
catch (Exception exception)
{
Debug.Log(exception);
}
}

public async void Clear()
{
await _semaphoreSlim.WaitAsync();
try
{
foreach (var item in _resourcesCache)
{
ReleaseResource(item.Key);
}

_resourcesCache.Clear();
}
catch (Exception exception)
{
Debug.Log(exception);
}
finally
{
_semaphoreSlim.Release();
}
}
}
}

Resource pool

The resource caching method above is suitable for the case where all objects share the same set of resources, but in some cases, we may need exclusive resources. For example, n game characters need to be controlled by n different players, so we need n instances.

In this case, how can we optimize it?

We assume such a situation, if the room will continue to have players in and out, every time a player comes in, the game process needs to load an object, the player exits, the object needs to be destroyed, if the frequency is too high, in fact, the CPU is also a burden.

At this time, we can use the resource pool method. After instantiation, if the player quits the game, we do not destroy the object, but return it to the resource pool. After another player connects, we directly take one from the pool and give it to the player. Just control it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Threading.Tasks;
using UnityEngine;

namespace ResourceManager
{
public interface IResourcePool
{
public Task<GameObject> GetResource(string location);
public GameObject GetResourceWithoutCreate(string key);

public void ReturnResource(string location, GameObject gameObject);
public void Clear();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

namespace ResourceManager
{
public abstract class AbstractResourcePool: IResourcePool
{
private static SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
private Dictionary<string, List<GameObject>> _pool =
new Dictionary<string, List<GameObject>>();

private GameObject _parent;

private int _maxCount = 16;

public abstract Task<GameObject> InstantiateAsync(string key);
public abstract void Release(GameObject gameObject);

private GameObject GetParent()
{
if (!_parent)
{
_parent = new GameObject("ResourcePool");
}

return _parent;
}

public GameObject GetResourceWithoutCreate(string key)
{
if (_pool.ContainsKey(key))
{
while (_pool[key].Count > 0)
{
GameObject resource = _pool[key].First();
if (resource != null)
{
resource.SetActive(true);
_pool[key].Remove(resource);
return resource;
}
else
{
_pool[key].RemoveAt(0);
}
}
}

return null;
}

public async Task<GameObject> GetResource(string key)
{
await _semaphoreSlim.WaitAsync();
try
{
if (_pool.ContainsKey(key))
{
while (_pool[key].Count > 0)
{
GameObject resource = _pool[key].First();
if (resource != null)
{
resource.SetActive(true);
_pool[key].Remove(resource);
return resource;
}
else
{
_pool[key].RemoveAt(0);
}
}
}

GameObject newResource = await InstantiateAsync(key);
newResource.transform.SetParent(GetParent().transform);
return newResource;
}
finally
{
_semaphoreSlim.Release();
}
}

public async void ReturnResource(string key, GameObject gameObject)
{
await _semaphoreSlim.WaitAsync();
try
{
gameObject.SetActive(false);
gameObject.transform.SetParent(GetParent().transform);
if (_pool.ContainsKey(key))
{
if (_pool[key].Count >= _maxCount)
{
Release(gameObject);
}
else
{
_pool[key].Add(gameObject);
}
}
else
{
_pool.Add(key, new List<GameObject>(){gameObject});
}
}
finally
{
_semaphoreSlim.Release();
}
}

public void Clear()
{
foreach (var item in _pool)
{
foreach (var obj in item.Value)
{
Release(obj);
}
}
}
}
}

Materials to reuse as much as possible

If conditions permit, such as the material of all objects is the same, and can change at the same time, do not create a new material for each object.

Merge Texture

After merging Texture, if you don’t need to modify it later, try to call the Apply method on the new Texture to release the memory used for modification.

Merge batch

https://zhuanlan.zhihu.com/p/196691845

If the texture does not require Self-Adaptation, turn off minmap