游戏对象池设计

思考并回答以下问题:

对象池顾名思义就是放置对象的池子,当然这个池子不是大家洗澡的池子,而是一个虚拟的用于存储对象的 Dictionary 或者是 List、Stack 等数据存储。为什么要使用对象池?使用对象池优势是啥?

对象池的优势
对象池主要是针对游戏中频繁生成、频繁销毁的对象而设立的,目的是优化内存。试想一下,如果对象频繁的生成,就表示它每次生成都要从内存中申请空间,而每次释放就会导致很多内存碎片,这样再生成大物体对象时,会导致程序运行卡顿,因为没有一个整的内存供加载达物体使用。而使用对象池可以避免这些问题的产生,最大限度的优化内存。对象池可以把这些频繁生成的对象事先放置到 Dictionary,在生成使用时,先在 Dictionary 中查找,如果找到可以将其显示出来,再根据需求设置位置、方向、缩放等。

而如果找不到,则重新生成,然后将其放置到 Dictionary,方便下次使用。当然这些对象也不是永久存放在 Dictionary 中,我们会对其设置一个缓存时间,如果长时间不用就将其清除掉,也就是从 Dictionary 中删除掉。这样就可以避免频繁的操作内存,下面说说对象池的设计思想。

对象池设计思想
enter image description here

上图是本教程对象池模块的实现框架图,先介绍一下传统的对象池设计、传统的设计,一般会使用两个队列,一个用于存储正在使用的对象,另一个存储使用过的或者没被使用的对象。这种设计方式,如果只是做个简单的 Demo 是可以的,但是如果做产品就不能这么简单的设计了。要考虑的问题比较多,比如设置了对象池中对象的缓存时间,还有要生成不同类型的对象池等。

我们的设计思路是首先定义游戏需要使用对象生成的对象池类型,这个可以使用枚举值表示,代码如下:

1
2
3
4
5
6
7
8
    public enum PoolObjectType
{
POT_Effect,
POT_MiniMap,
POT_Entity,
POT_UITip,
POT_XueTiao,
}

该枚举定义了特效、小地图、实体、UI 提示框、还有血条,这些需要使用对象池的游戏类型。接下来要考虑这些游戏对象包含哪些信息,比如可以通过名字或者 ID 查找对象池中的对象,还有每个对象应该有自己的缓存时间,对象是属于哪种对象池等等属性。可以使用结构体或者类来定义,下面是本课程实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PoolGameObjectInfo
{
public string name;

//缓存时间
public float mCacheTime= 0.0f;

//缓存物体类型
public PoolObjectType type;

//可以重用
public bool mCanUse = true;

//重置时间
public float mResetTime = .0f;

以上结构体就是对象池中的对象信息定义,每个对象都对应着自己的对象信息,我们使用的是字典存放,代码如下所示:

1
2
3
4
5
        public class PoolInfo
{
//缓存队列
public Dictionary<GameObject, PoolGameObjectInfo> mQueue = new Dictionary<GameObject, PoolGameObjectInfo>();
}

这个类定义的是池信息,每个对象池对应着自己的信息,另外再定义存储对象池信息字典,通过名字进行区分,我们将其用 Dictionary 字典存放,代码如下所示:

1
2
//缓存GameObject Map
private Dictionary<String, PoolInfo> mPoolDic = new Dictionary<String, PoolInfo>();

我们还增加了对象删除表,在这里用了 List 列表进行存放,将不用的对象删除掉,定义了 List 表,代码如下:

1
2
   //删除队列
private List<GameObject> mDestoryPoolGameObjects = new List<GameObject>();

下面开始接口的实现,在生成对象池对象时,首先需要获取一下,看看是否以前生成过,如果已生成过,直接拿来,否则就要重新加载资源创建,获取代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//取得可用对象
private bool TryGetObject(PoolInfo poolInfo, out KeyValuePair<GameObject, PoolGameObjectInfo> objPair)
{
if (poolInfo.mQueue.Count > 0)
{
foreach (KeyValuePair<GameObject, PoolGameObjectInfo> pair in poolInfo.mQueue)
{
GameObject go = pair.Key;
PoolGameObjectInfo info = pair.Value;

if (info.mCanUse)
{
objPair = pair;
return true;
}
}
}

objPair = new KeyValuePair<GameObject, PoolGameObjectInfo>();
return false;
}

该函数主要是用于获取是否已经存在对象池的物体,同时判断一下该物体是否可以使用,该函数只对内,不对外。下面就函数创建对象池物体函数,代码如下所示:

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
//获取缓存物体
public GameObject GetGO(String res)
{
//有效性检查
if (null == res)
{
return null;
}

//查找对应pool,如果没有缓存
PoolInfo poolInfo = null;
KeyValuePair<GameObject, PoolGameObjectInfo> pair;
if (!mPoolDic.TryGetValue(res, out poolInfo) || !TryGetObject(poolInfo, out pair))
{
//新创建
//Debug.LogError("res = " + res);
ResourceUnit unit = ResourcesManager.Instance.loadImmediate(res, ResourceType.PREFAB);
if (unit.Asset == null)
{
Debug.Log("can not find the resource" + res);
}
return GameObject.Instantiate(unit.Asset) as GameObject;
}

//出队列数据
GameObject go = pair.Key;
PoolGameObjectInfo info = pair.Value;

poolInfo.mQueue.Remove(go);

//使有效
EnablePoolGameObject(go, info);

//返回缓存Gameobjec
return go;
}

该函数首先判断对象是否在 Dictionary 中,如果不在字典中,就直接生成一个实例化对象。如果已经创建则调用函数 EnablePoolGameObject 将物体设置为 true,代码如下所示:

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
 public void EnablePoolGameObject(GameObject go, PoolGameObjectInfo info)
{
//特效Enable
if (info.type == PoolObjectType.POT_Effect)
{
go.SetActive(true);
go.transform.parent = null;
}
else if (info.type == PoolObjectType.POT_MiniMap)
{
go.SetActive(true);
go.transform.parent = null;
}
else if (info.type == PoolObjectType.POT_Entity)
{
go.SetActive(true);
go.transform.parent = null;
}
else if (info.type == PoolObjectType.POT_UITip)
{
go.SetActive(true);
go.transform.parent = null;
}
else if (info.type == PoolObjectType.POT_XueTiao)
{
//do nothing
}

info.mCacheTime = 0.0f;
}

以上函数理解起来比较容易,其实就是将隐藏的对象设置为 true,并将其显示出来,另外,我们还要知道对象存放位置,还有在使用时判断其是否还有用,如果还有用,就将其隐藏掉,否则就删除掉,请看下面的函数实现:

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
public void ReleaseGO(String res, GameObject go, PoolObjectType type)
{
//获取缓存节点,设置为不可见位置
if (objectsPool == null)
{
objectsPool = new GameObject("ObjectPool");
objectsPool.AddComponent<Canvas>();
objectsPool.transform.localPosition = new Vector3(0, -5000, 0);
}

if (null == res || null == go)
{
return;
}

PoolInfo poolInfo = null;
//没有创建
if (!mPoolDic.TryGetValue(res, out poolInfo))
{
poolInfo = new PoolInfo();
mPoolDic.Add(res, poolInfo);
}

PoolGameObjectInfo poolGameObjInfo = new PoolGameObjectInfo();
poolGameObjInfo.type = type;
poolGameObjInfo.name = res;

//无效缓存物体
DisablePoolGameObject(go, poolGameObjInfo);

//保存缓存GameObject,会传入相同的go, 有隐患
poolInfo.mQueue[go] = poolGameObjInfo;
}

上述函数其实可以用于批量生成对象,比如从配置文件中加载的特效或者是物体,先将其隐藏掉,如果使用时调用 GetGo 函数,ReleaseGO 负责将生成的对象隐藏掉,而 GetGo 函数负责将已生成的对象显示出来,如果没有就直接实例化出来。另外因为加了缓存时间,所以要在每一帧中去判断缓存的物体,看看这些物体的缓存时间是否有效,无效就删除掉,这种处理方式很容易让人想到在 Update 函数中判断。代码如下所示:

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
 public void OnUpdate()
{
//每隔0.1更新一次
mTotalTime += Time.deltaTime;
if (mTotalTime <= 0.1f)
{
return;
}
else
{
mTotalTime = 0;
}

float deltaTime = Time.deltaTime;

//遍历数据
foreach(PoolInfo poolInfo in mPoolDic.Values)
{
//死亡列表
mDestoryPoolGameObjects.Clear();

foreach (KeyValuePair<GameObject, PoolGameObjectInfo> pair in poolInfo.mQueue)
{
GameObject obj = pair.Key;
PoolGameObjectInfo info = pair.Value;

info.mCacheTime += deltaTime;

float mAllCachTime = mCachTime;

//POT_UITip,缓存3600秒
if (info.type == PoolObjectType.POT_UITip)
mAllCachTime = 3600;

//缓存时间到
if (info.mCacheTime >= mAllCachTime)
{
mDestoryPoolGameObjects.Add(obj);
}

//拖尾重置计时
if (!info.mCanUse)
{
info.mResetTime += deltaTime;

if (info.mResetTime > 1.0f)
{
info.mResetTime = .0f;
info.mCanUse = true;

obj.SetActive(false);
}
}
}

//移除
for(int k=0; k < mDestoryPoolGameObjects.Count; k++)
{
GameObject obj = mDestoryPoolGameObjects[k];
GameObject.DestroyImmediate(obj);

poolInfo.mQueue.Remove(obj);
}
}

这样整个对象池的设计就完成了,我们已将该对象池模块应用到商业游戏开发中了。运行效果如下所示:

0%