UFL3-3——对象上限优化
UFL3-3——对象上限优化
对象上限优化
目前我们制作的缓存池模块,理论上来说,当动态创建的对象长时间不放回抽屉
每次从缓存池中动态获取对象时,会不停的新建对象,那么也就是对象的数量是没有上限的
场景上的某种对象可以存在n个
而对象上限优化指的就是,我们希望控制对象数量有上限,
对于不重要的资源我们没必要让其无限加量,而是将“使用最久”的资源直接抢来用
主要目的:
更加彻底的复用资源,对对象的数量上限加以限制
可以优化内存空间,甚至优化性能(减少数量上限,可以减小渲染压力)
制作思路和具体实现
-
在抽屉里声明一个容器
usedList
用来记录正在使用的资源,
并开放一个属性UsedCount
用于获取正在使用的资源的数量,开放一个方法PushUsedList
用于将资源放入到usedList
内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
31public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象,记录没有使用的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//记录使用中的对象的列表
private List<GameObject> usedList = new List<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;
//获取使用中的对象的数量
public int UsedCount => usedList.Count;
public PoolData(GameObject poolRootObj, string poolName); //..
// 从抽屉中弹出数据对象
public GameObject Pop(); //..
// 将数据压入到抽屉内
public void Push(GameObject obj); // ..
/// <summary>
/// 将对象添加到使用中的列表
/// </summary>
/// <param name="obj">要添加到使用中列表的对象</param>
public void PushUsedList(GameObject obj)
{
usedList.Add(obj);
}
} -
每次获取对象
GetObj
时,传入一个抽屉最大容量值maxNum
(可以给一个默认值)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
26public class PoolManager : BaseManager<PoolManager>
{
//柜子容器当中有抽屉的体现
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
//池子根对象
private GameObject poolObj;
//是否开启Hierarchy窗口自动布局功能
public static bool isOpenLayout = true;
private PoolManager() { }
// 拿东西的方法
public GameObject GetObj(string name, int maxNum = 50)
{
GameObject obj;
// TODO.. 根据maxNum,PoolData的是否存在,以及所有中的对象的数量来决定如何取对象
return obj;
}
// 往缓存池中放入对象
public void PushObj(GameObject obj); //..
// 清除整个柜子当中的数据
public void ClearPool(); //..
} -
从缓存池中获取对象
GetObj
时就需要创建抽屉PoolData
,用于记录当前使用着的对象由于总是是先获取
GetObj
后压入PushObj
,因此PushObj
内不再需要检查抽屉是否存在,只需要向抽屉压入对象即可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
35public class PoolManager : BaseManager<PoolManager>
{
//柜子容器当中有抽屉的体现
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
//池子根对象
private GameObject poolObj;
//是否开启Hierarchy窗口自动布局功能
public static bool isOpenLayout = true;
private PoolManager() { }
// 拿东西的方法
public GameObject GetObj(string name, int maxNum = 50)
{
GameObject obj;
// TODO.. 根据maxNum,PoolData的是否存在,以及所有中的对象的数量来决定如何取对象
return obj;
}
// 往缓存池中放入对象
public void PushObj(GameObject obj)
{
//如果根物体为空且自动排布开启,就创建
if (poolObj == null && isOpenLayout)
poolObj = new GameObject("Pool");
poolDic[obj.name].Push(obj);
}
// 清除整个柜子当中的数据
public void ClearPool(); //..
} -
每次取对象
GetObj
时应该分情况考虑-
情况1:没有抽屉时
需要创建一个抽屉
PoolData
和一个对象,抽屉添加到poolDic
内,
这个对象同时需要压入到抽屉的使用中对象的列表usedList
内当创建一个抽屉
PoolData
时,往往还实例化了一个新的对象,因此在实例化PoolData
的时候,应该同时将对象压入到usedList
内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
41public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象,记录没有使用的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//记录使用中的对象的列表
private List<GameObject> usedList = new List<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;
public int UsedCount => usedList.Count;
public PoolData(GameObject poolRootObj, string poolName, GameObject usedObj)
{
//开启功能时,才会动态创建父子关系
if (PoolManager.isOpenLayout)
{
//创建抽屉父对象,和柜子父对象建立父子关系
poolObj = new GameObject(poolName);
poolObj.transform.SetParent(poolRootObj.transform);
}
//创建抽屉时,外部肯定是会动态创建一个对象的,我们应该将其记录到使用中的对象容器中
PushUsedList(usedObj);
}
// 从抽屉中弹出数据对象
public GameObject Pop(); //..
// 将数据压入到抽屉内
public void Push(GameObject obj); //..
/// <summary>
/// 将对象添加到使用中的列表
/// </summary>
/// <param name="obj">要添加到使用中列表的对象</param>
public void PushUsedList(GameObject obj)
{
usedList.Add(obj);
}
} -
情况2:有抽屉,并且 抽屉里有没用的对象 或者 使用中对象超过上限时
调用
PoolData
的Pop
方法取出对象,
其中,当使用中对象超过上限时,Pop
方法执行的是从usedList
内取出最早放入的对象,并重新放入到usedList
内的逻辑
当抽屉里有没用的对象时,Pop
方法直接从dataStack
内取出一个对象激活即可因此
PoolData
的Pop
方法需要添加一个当抽屉内没有没用的对象时需要执行的分支: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
57public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象,记录没有使用的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//记录使用中的对象的列表
private List<GameObject> usedList = new List<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;
public int UsedCount => usedList.Count;
public PoolData(GameObject poolRootObj, string poolName, GameObject usedObj); //..
/// <summary>
/// 从抽屉中弹出数据对象
/// </summary>
/// <returns>弹出的数据对象</returns>
public GameObject Pop()
{
//从抽屉内取出并激活对象
GameObject obj;
//如果有未使用的对象,就取出这个对象
if (Count > 0)
{
obj = dataStack.Pop();
usedList.Add(obj); //将取出的对象压入到使用中的列表内
}
//如果没有未使用的对象,就需要从正在使用的对象列表内抢来一个最早使用的
else
{
obj = usedList[0]; //取0索引的对象,它就是使用时间最长的对象
usedList.RemoveAt(0); //并将其从使用中的列表中移除出去
usedList.Add(obj); //由于该对象还需要使用,所以将其移到列表的最尾部使用
}
obj.SetActive(true);
//开启布局功能时,才需要断开父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(null);
return obj;
}
// 将数据压入到抽屉内
public void Push(GameObject obj); //..
/// <summary>
/// 将对象添加到使用中的列表
/// </summary>
/// <param name="obj">要添加到使用中列表的对象</param>
public void PushUsedList(GameObject obj)
{
usedList.Add(obj);
}
} -
情况3:有抽屉,但是抽屉里没有对象,使用中对象也没有超过上限时
实例化一个对象出来,并将这个对象添加到抽屉
PoolData
的usedList
内
最终
PoolManager
的GetObj
修改为: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// 缓存池模块 管理器
public class PoolManager : BaseManager<PoolManager>
{
//柜子容器当中有抽屉的体现
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
//池子根对象
private GameObject poolObj;
//是否开启Hierarchy窗口自动布局功能
public static bool isOpenLayout = true;
private PoolManager() { }
/// <summary>
/// 拿东西的方法
/// </summary>
/// <param name="name">抽屉容器的名字</param>
/// <returns>从缓存池中取出的对象</returns>
public GameObject GetObj(string name, int maxNum = 50)
{
GameObject obj;
//无抽屉,或者抽屉无对象且使用中的对象也未超上限,需要新实例化一个对象
if (!poolDic.ContainsKey(name) ||
(poolDic[name].Count == 0 && poolDic[name].UsedCount < maxNum))
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name;
//无抽屉时,创建一个抽屉并将新对象传入到构造函数内
if (!poolDic.ContainsKey(name))
poolDic.Add(name, new PoolData(poolObj, name, obj));
//有抽屉时,将新创建的对象传入到使用中的列表内
else
poolDic[name].PushUsedList(obj);
}
//当抽屉有对象或者使用中的对象超上限了,就直接从抽屉内取出来使用
else // if (poolDic[name].Count > 0 || poolDic[name].UsedCount >= maxNum)
{
obj = poolDic[name].Pop();
}
return obj;
}
// 往缓存池中放入对象
public void PushObj(GameObject obj); //..
// 清除整个柜子当中的数据
public void ClearPool(); //..
} -
-
每次放回对象时
由于记录了正在使用的资源,因此每次放入抽屉时还需要从记录容器中移除对象
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
37public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象,记录没有使用的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//记录使用中的对象的列表
private List<GameObject> usedList = new List<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;
public int UsedCount => usedList.Count;
public PoolData(GameObject poolRootObj, string poolName, GameObject usedObj); //..
// 从抽屉中弹出数据对象
public GameObject Pop();
/// <summary>
/// 将数据压入到抽屉内
/// </summary>
/// <param name="obj">压入到抽屉内</param>
public void Push(GameObject obj)
{
//失活后压入到抽屉内
obj.SetActive(false);
//开启了布局功能,才需要将要压入的对象设置父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(poolObj.transform);
dataStack.Push(obj);
//这个对象不再使用了,因此需要从记录容器中移除出去
usedList.Remove(obj);
}
// 将对象添加到使用中的列表
public void PushUsedList(GameObject obj); //..
}
值得一提的是,按照上述思路实现了代码后,在Hierarchy窗口布局优化功能内会存在一个Bug需要我们去修复
由于我们优化中加入了上限判断,并且在GetObj
时,就会去new PoolData
,
当开启布局优化功能时,由于此时Pool
根对象没有创建,会报空,因为原本Pool
根对象的创建逻辑是在PushObj
中实现的
因此我们需要将原本在PushObj
中创建Pool
根对象的判断,移动到GetObj
中
1 | //取东西的方法 |
-
至此,缓存池相关代码如下
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PoolData
{
//抽屉根对象,用来就行布局管理的对象
private GameObject poolObj;
//用来存储抽屉中的对象,记录没有使用的对象
private Stack<GameObject> dataStack = new Stack<GameObject>();
//记录使用中的对象的列表
private List<GameObject> usedList = new List<GameObject>();
//获取容器中是否有对象
public int Count => dataStack.Count;
public int UsedCount => usedList.Count;
public PoolData(GameObject poolRootObj, string poolName, GameObject usedObj)
{
//开启功能时,才会动态创建父子关系
if (PoolManager.isOpenLayout)
{
//创建抽屉父对象,和柜子父对象建立父子关系
poolObj = new GameObject(poolName);
poolObj.transform.SetParent(poolRootObj.transform);
}
//创建抽屉时,外部肯定是会动态创建一个对象的,我们应该将其记录到使用中的对象容器中
PushUsedList(usedObj);
}
/// <summary>
/// 从抽屉中弹出数据对象
/// </summary>
/// <returns>弹出的数据对象</returns>
public GameObject Pop()
{
//从抽屉内取出并激活对象
GameObject obj;
if (Count > 0)
{
obj = dataStack.Pop();
usedList.Add(obj); //将取出的对象压入到使用中的列表内
}
else
{
obj = usedList[0]; //取0索引的对象,它就是使用时间最长的对象
usedList.RemoveAt(0); //并将其从使用中的列表中移除出去
usedList.Add(obj); //由于该对象还需要使用,所以将其移到列表的最尾部使用
}
obj.SetActive(true);
//开启布局功能时,才需要断开父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(null);
return obj;
}
/// <summary>
/// 将数据压入到抽屉内
/// </summary>
/// <param name="obj">压入到抽屉内</param>
public void Push(GameObject obj)
{
//失活后压入到抽屉内
obj.SetActive(false);
//开启了布局功能,才需要将要压入的对象设置父子关系
if (PoolManager.isOpenLayout)
obj.transform.SetParent(poolObj.transform);
dataStack.Push(obj);
//这个对象不再使用了,因此需要从记录容器中移除出去
usedList.Remove(obj);
}
/// <summary>
/// 将对象添加到使用中的列表
/// </summary>
/// <param name="obj">要添加到使用中列表的对象</param>
public void PushUsedList(GameObject obj)
{
usedList.Add(obj);
}
}
/// <summary>
/// 缓存池模块 管理器
/// </summary>
public class PoolManager : BaseManager<PoolManager>
{
//柜子容器当中有抽屉的体现
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
//池子根对象
private GameObject poolObj;
//是否开启Hierarchy窗口自动布局功能
public static bool isOpenLayout = false;
private PoolManager() { }
/// <summary>
/// 拿东西的方法
/// </summary>
/// <param name="name">抽屉容器的名字</param>
/// <returns>从缓存池中取出的对象</returns>
public GameObject GetObj(string name, int maxNum = 50)
{
//如果根物体为空且自动排布开启,就创建
if (poolObj == null && isOpenLayout)
poolObj = new GameObject("Pool");
GameObject obj;
//无抽屉,或者抽屉无对象且使用中的对象也未超上限,需要新实例化一个对象
if (!poolDic.ContainsKey(name) ||
(poolDic[name].Count == 0 && poolDic[name].UsedCount < maxNum))
{
obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
obj.name = name;
//无抽屉时,创建一个抽屉并将新对象传入到构造函数内
if (!poolDic.ContainsKey(name))
poolDic.Add(name, new PoolData(poolObj, name, obj));
//有抽屉时,将新创建的对象传入到使用中的列表内
else
poolDic[name].PushUsedList(obj);
}
//当抽屉有对象或者使用中的对象超上限了,就直接从抽屉内取出来使用
else // if (poolDic[name].Count > 0 || poolDic[name].UsedCount >= maxNum)
{
obj = poolDic[name].Pop();
}
return obj;
}
/// <summary>
/// 往缓存池中放入对象
/// </summary>
/// <param name="name">抽屉(对象池)的名字</param>
/// <param name="obj">要放入的对象</param>
public void PushObj(GameObject obj)
{
////如果根物体为空且自动排布开启,就创建
//if (poolObj == null && isOpenLayout)
// poolObj = new GameObject("Pool");
poolDic[obj.name].Push(obj);
}
/// <summary>
/// 清除整个柜子当中的数据
/// </summary>
public void ClearPool()
{
poolDic.Clear();
poolObj = null;
}
}
对象上限测试
设置从缓存池获取的对象上限分别为3和4,其他内容不变
1 | using System; |
按照一定频率反复按下鼠标左键和右键,场景上创建的两种对象数量会限制在3和4
将创建出来的对象会执行的逻辑改为向前无限移动而不是延迟删除
1 | public class TestMove : MonoBehaviour |
由于从缓存池拿出来的对象的状态(位置,旋转,缩放等等)是不确定的,因此我们必须在拿出来的那一刻就对这个对象进行初始化
1 | using System; |
按照一定频率反复按下鼠标左键和右键,创建上向前移动的对象数量会恒定在在3和4,当达到上限时在从对象池取出,对象会自动回到起点
上限消息由参数传入改为对象属性
现在我们确定最大容量是通过在获取时传入参数maxNum
,若传入参数出错可能会导致“超上限”
能否优化下,以其它思路去制作,让我们可以更加方便的处理上限逻辑
- 让使用者不用每次设置上限值
- 初始化抽屉时,第一次就直接定好上限为多少,之后直接在内部判断即可
让缓存池对象挂载一个用于配置上限值的脚本,只需要在制作预设体时,将脚本挂好,设置好上限即可
1 | using UnityEngine; |
然后在PoolData
内声明一个上限值,在构造函数内通过读取预设体上的脚本设置的上限值属性
在声明一个NeedCreate
属性,当使用中的对象数量小于最大容量时,代表可以创建对象
1 | public class PoolData |
同时修改PoolManager
的GetObj
方法,去除maxNum
参数,使用NeedCreate
属性
1 | public GameObject GetObj(string name) |