引言 对象池(Object Pool)是一种优化技术,用于减少游戏运行时的性能开销。这种方法可以显著提高游戏性能,特别是在需要频繁生成和销毁对象的游戏场景中。对象池避免了频繁的内存分配和回收,从而减少了垃圾回收(Garbage Collection)的次数,使游戏运行更加流畅。
什么是对象池 对象池是一种管理和重复使用游戏对象的机制。通过预先创建和存储一定数量的对象(如敌人、武器、水晶等),并在需要时重复使用这些对象,而不是每次需要时都创建新对象。
其核心思想是:使用完不直接删除,而是将其放回池子里,需要的时候取出。主要优化两点:
1、防止对象被频繁创建和删除,从而内存抖动、频繁GC(垃圾回收)
2、对象初始化成本较高
对象池的功能 1、借用:在游戏中,经常会遇到需要动态生成大量的临时对象的情况,比如子弹、爆炸效果等。使用对象池的“借用”策略,可以避免频繁的实例化和销毁操作。当需要新对象时,从对象池中借用一个对象,而不是通过new操作符创建新实例。这减少了内存分配的开销,提高了性能。
2、归还:使用完对象后,通过“归还”策略将对象放回对象池。这样可以重复使用对象,而不是销毁它们,减少了垃圾回收的频率,降低了内存开销。
3、预热:在游戏启动或者关键时刻,通过“预热”策略可以提前创建一定数量的对象,减少游戏运行时的对象池扩容和性能波动。这样可以在游戏开始时就确保对象池中有足够的对象,避免在游戏运行时动态创建对象,从而提高游戏的启动速度和稳定性。
4、缩小:当对象池中的对象过多时,可以通过“缩小”策略来释放一部分对象,以降低内存占用。这通常在游戏运行时的某个合适时机触发,例如切换场景或者进入后台时。
5、重置:有些对象在被归还到对象池后,可能会带有之前的状态,比如位置、速度等。通过“重置”策略,可以在对象被借用前将其状态重置为初始状态,确保对象在被重新使用时是干净的。
实现步骤 1、创建一个单例的对象池类,并在Awake函数中初始化对象池字典。 对象池字典使用游戏对象的名称作为键,值为对象池列表。对象池列表中存储着一批游戏对象。 2、在GetObject函数中,首先判断对象池字典中是否包含指定名称的对象池。如果不存在,则返回null。否则,从对象池列表中找到一个未激活状态的游戏对象,如果找不到则实例化一个新的游戏对象并添加到对象池列表中。最后将游戏对象设置为激活状态并返回。 3、在ReturnObject函数中,将传入的游戏对象设置为不激活状态,从而标记为可重用的对象。
实例——吸血鬼幸存者(VampireSurvivours)
创建一个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 ;
Initialize
方法中,使用枚举类型CharacterData.CharacterType
、WeaponData.WeaponType
和CrystalData.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); } for (int j = 0 ; j < initNumDamage; j++){ damageQue.Enqueue(CreateObject("damage" ));} poolingDict.Add("damage" , damageQue); }
使用传入的类型参数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);} }
使用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);} }
ReturnObject泛型方法,将不再需要的游戏对象(例如被击败的敌人)返回到对象池中,以便将来重用。
1 2 public static void ReturnObject <T >(GameObject deadEnemy, T type ) {instance.poolingDict[type.ToString()].Enqueue(deadEnemy);}