引言

对象池(Object Pool)是一种优化技术,用于减少游戏运行时的性能开销。这种方法可以显著提高游戏性能,特别是在需要频繁生成和销毁对象的游戏场景中。对象池避免了频繁的内存分配和回收,从而减少了垃圾回收(Garbage Collection)的次数,使游戏运行更加流畅。

什么是对象池

对象池是一种管理和重复使用游戏对象的机制。通过预先创建和存储一定数量的对象(如敌人、武器、水晶等),并在需要时重复使用这些对象,而不是每次需要时都创建新对象。

其核心思想是:使用完不直接删除,而是将其放回池子里,需要的时候取出。主要优化两点:

1、防止对象被频繁创建和删除,从而内存抖动、频繁GC(垃圾回收)

2、对象初始化成本较高

对象池的功能

1、借用:在游戏中,经常会遇到需要动态生成大量的临时对象的情况,比如子弹、爆炸效果等。使用对象池的“借用”策略,可以避免频繁的实例化和销毁操作。当需要新对象时,从对象池中借用一个对象,而不是通过new操作符创建新实例。这减少了内存分配的开销,提高了性能。

2、归还:使用完对象后,通过“归还”策略将对象放回对象池。这样可以重复使用对象,而不是销毁它们,减少了垃圾回收的频率,降低了内存开销。

3、预热:在游戏启动或者关键时刻,通过“预热”策略可以提前创建一定数量的对象,减少游戏运行时的对象池扩容和性能波动。这样可以在游戏开始时就确保对象池中有足够的对象,避免在游戏运行时动态创建对象,从而提高游戏的启动速度和稳定性。

4、缩小:当对象池中的对象过多时,可以通过“缩小”策略来释放一部分对象,以降低内存占用。这通常在游戏运行时的某个合适时机触发,例如切换场景或者进入后台时。

5、重置:有些对象在被归还到对象池后,可能会带有之前的状态,比如位置、速度等。通过“重置”策略,可以在对象被借用前将其状态重置为初始状态,确保对象在被重新使用时是干净的。

实现步骤

1、创建一个单例的对象池类,并在Awake函数中初始化对象池字典。
对象池字典使用游戏对象的名称作为键,值为对象池列表。对象池列表中存储着一批游戏对象。
2、在GetObject函数中,首先判断对象池字典中是否包含指定名称的对象池。如果不存在,则返回null。否则,从对象池列表中找到一个未激活状态的游戏对象,如果找不到则实例化一个新的游戏对象并添加到对象池列表中。最后将游戏对象设置为激活状态并返回。
3、在ReturnObject函数中,将传入的游戏对象设置为不激活状态,从而标记为可重用的对象。

实例——吸血鬼幸存者(VampireSurvivours)

  1. 创建一个Dictionary,用于存储对象队列,键是对象的类型,值是对象的队列。游戏玩法是主角每发出一个攻击(weapon),造成一定伤害值(damage),击败一个敌人(enemy)后,掉落一定的经验(crystal)。
1
2
3
4
5
6
7
static ObjectPooling instance;    
Dictionary<string, Queue<GameObject>> poolingDict = new Dictionary<string, Queue<GameObject>>();

const int initNumEnemy = 500;
const int initNumWeapon = 500;
const int initNumCrystal = 500;
const int initNumDamage = 500;
  1. Initialize方法中,使用枚举类型CharacterData.CharacterTypeWeaponData.WeaponTypeCrystalData.CrystalType来创建不同类型的对象队列,并将它们添加到字典中。
1
2
3
4
5
6
7
8
9
10
11
12
13
void Initialize()
{
foreach(CharacterData.CharacterType characterType in Enum.GetValues(typeof(CharacterData.CharacterType)))
{
if (IsPlayer(characterType)) continue;Queue<GameObject> newQue = new Queue<GameObject>();

for (int j = 0; j < initNumEnemy; j++){newQue.Enqueue(CreateObject(characterType));}
poolingDict.Add(characterType.ToString(), newQue);
}
//...weaponType、crystalType同CharacterData一样
for (int j = 0; j < initNumDamage; j++){ damageQue.Enqueue(CreateObject("damage"));}
poolingDict.Add("damage", damageQue);
}
  1. 使用传入的类型参数type,将其转换为字符串,并在poolingDict字典中查找对应的对象队列。如果找到的队列中有可用的对象(即队列的Count大于0),则从队列中移除(Dequeue)并返回这个对象。如果队列为空,说明没有可用的对象,那么就调用CreateObject方法来创建一个新的对象,并返回它。
1
2
3
4
5
public static GameObject GetObject<T>(T type)
{
if(instance.poolingDict[type.ToString()].Count > 0){return instance.poolingDict[type.ToString()].Dequeue();}
else{return CreateObject(type);}
}
  1. 使用switch语句根据type的值来实例化不同的预制体(Prefab)。如果创建的对象是某些特定类型的武器,并且这些武器应该是玩家的子对象,那么isParentPlayer会被设置为true。接着,根据isParentPlayer的值,使用SetParent方法将新对象的父对象设置为相应的游戏对象,并且保持其局部变换。最后,新对象被设置为非激活状态,并返回这个新创建的对象。
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
static GameObject CreateObject<T>(T type)
{
GameObject newObject;
bool isParentPlayer = false;

switch (type)
{
default:
case CharacterData.CharacterType.FlyingEye:
newObject = Instantiate(instance.flyingEyePrefab);
break;
case CharacterData.CharacterType.Goblin:
newObject = Instantiate(instance.goblinPrefab);
break;
case CharacterData.CharacterType.Mushroom:
newObject = Instantiate(instance.mushroomPrefab);
break;
case CharacterData.CharacterType.Skeleton:
newObject = Instantiate(instance.skeletonPrefab);
break;

case WeaponData.WeaponType.Whip:
newObject = Instantiate(instance.whipPrefab);
if(ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.Whip).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;
case WeaponData.WeaponType.Bible:
newObject = Instantiate(instance.biblePrefab);
if (ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.Bible).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;
case WeaponData.WeaponType.Axe:
newObject = Instantiate(instance.axePrefab);
if (ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.Axe).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;
case WeaponData.WeaponType.FireWand:
newObject = Instantiate(instance.pigeonPrefab);
if (ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.FireWand).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;
case WeaponData.WeaponType.Lightning:
newObject = Instantiate(instance.lightningPrefab);
if (ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.Lightning).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;
case WeaponData.WeaponType.MagicWand:
newObject = Instantiate(instance.magicWandPrefab);
if (ItemAssets.GetInstance().GetWeaponData(WeaponData.WeaponType.MagicWand).GetParent().Equals(WeaponData.Parent.Player))
isParentPlayer = true;
break;

case CrystalData.CrystalType.blue:
newObject = Instantiate(instance.blueCrystalPrefab);
break;
case CrystalData.CrystalType.green:
newObject = Instantiate(instance.greenCrystalPrefab);
break;
case CrystalData.CrystalType.red:
newObject = Instantiate(instance.redCrystalPrefab);
break;

case "damage":
newObject = Instantiate(instance.DamageText);
break;
}

if (isParentPlayer)
newObject.transform.SetParent (GameObject.FindWithTag("Weapon").transform,false);
else
newObject.transform.SetParent (instance.transform,false);

newObject.SetActive(false);

return newObject;
}

public static GameObject GetObject<T>(T type)
{
if (instance.poolingDict[type.ToString()].Count > 0)
{return instance.poolingDict[type.ToString()].Dequeue();}
else
{return CreateObject(type);}
}
  1. ReturnObject泛型方法,将不再需要的游戏对象(例如被击败的敌人)返回到对象池中,以便将来重用。
1
2
public static void ReturnObject<T>(GameObject deadEnemy, T type)
{instance.poolingDict[type.ToString()].Enqueue(deadEnemy);}