一、引言
在游戏开发中,性能优化一直是开发者关注的焦点之一。为了提高游戏运行的流畅性和降低内存使用,我们经常需要采取一些有效的优化手段。对象池(Object Pool)是一种常见且有效的优化技术之一,特别是在处理大量频繁创建和销毁的游戏对象时。本文将介绍Unity中如何使用对象池进行性能优化,提高游戏的运行效率。
二、什么是对象池?
对象池是一种管理和重复使用游戏对象的机制。在游戏中,经常需要实例化和销毁大量的对象,但这样的操作会导致内存分配和垃圾回收,从而影响游戏的性能。对象池通过提前创建一定数量的对象并将其保存起来,而不是每次需要时都创建新的对象,从而避免了频繁的内存分配和销毁操作。
其核心思想是,使用完不直接删除,而是将其放回池子里,需要用的时候再取出来。 对象池模式的出现主要优化两点:
1、防止对象被频繁的创建和删除,从而内存抖动、频繁GC(垃圾回收)
2、对象初始化成本较高
三、对象池的应用
对象池的应用简单来说分为五点:
借用:在游戏中,经常会遇到需要动态生成大量的临时对象的情况,比如子弹、爆炸效果等。使用对象池的“借用”策略,可以避免频繁的实例化和销毁操作。当需要新对象时,从对象池中借用一个对象,而不是通过new
操作符创建新实例。这减少了内存分配的开销,提高了性能。
归还:使用完对象后,通过“归还”策略将对象放回对象池。这样可以重复使用对象,而不是销毁它们,减少了垃圾回收的频率,降低了内存开销。
预热:在游戏启动或者关键时刻,通过“预热”策略可以提前创建一定数量的对象,减少游戏运行时的对象池扩容和性能波动。这样可以在游戏开始时就确保对象池中有足够的对象,避免在游戏运行时动态创建对象,从而提高游戏的启动速度和稳定性。
缩小:当对象池中的对象过多时,可以通过“缩小”策略来释放一部分对象,以降低内存占用。这通常在游戏运行时的某个合适时机触发,例如切换场景或者进入后台时。
重置:有些对象在被归还到对象池后,可能会带有之前的状态,比如位置、速度等。通过“重置”策略,可以在对象被借用前将其状态重置为初始状态,确保对象在被重新使用时是干净的。
四、Unity中的对象池实现
using System.Collections;
using System.Collections.Generic;
using Mr.Le.Utility.Singleton;
using UnityEngine;
namespace Mr.Le.Utility.Manager
{
#region 对象池管理器
public class ObjectPoolManager : MonoSingleton<ObjectPoolManager>
{
#region 字段
//所有对象池的父物体
private GameObject poolsParent;
//所有对象池的父物体的名字
private const string poolsParentName = "ObjectPools";
//对象池列表
public List<ObjectPool> ObjectPoolsList = new List<ObjectPool>();
//对象池对象集合
public Dictionary<GameObject, ObjectPool> ObjectPoolsDic = new Dictionary<GameObject, ObjectPool>();
#endregion
#region 外部方法
/// <summary>
/// 预加载指定数量的对象
/// </summary>
/// <param name="prefab"></param>
/// <param name="count"></param>
public void Preload(GameObject prefab, int count)
{
ObjectPool pool = FindObjectPool(prefab);
pool.Preload(count);
}
/// <summary>
/// 从对象池中取出对象
/// </summary>
/// <param name="prefab"></param>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <param name="parent"></param>
/// <returns></returns>
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (prefab == null) return null;
ObjectPool objectPool = FindObjectPool(prefab);
GameObject go = objectPool.Spawn(position, rotation, parent);
ObjectPoolsDic.Add(go,objectPool);
return go;
}
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go"></param>
/// <param name="delayTime"></param>
public void Recycle(GameObject go, float delayTime = 0f)
{
if (go == null) return;
StartCoroutine(RecycleCoroutine(go, delayTime));
IEnumerator RecycleCoroutine(GameObject go, float delayTime = 0f)
{
if (delayTime > 0)
yield return new WaitForSeconds(delayTime);
//先从当前正在使用的对象池字典去找指定的对象
if (ObjectPoolsDic.TryGetValue(go, out ObjectPool pool))
{
ObjectPoolsDic.Remove(go);
pool.Recycle(go);
}
else //没找到就到对象池列表里面去找
{
pool = FindUsedObjectPool(go);
if (pool != null)
{
pool.Recycle(go);
}
}
}
}
/// <summary>
/// 把所有对象池中的对象全部回收
/// </summary>
public void RecycleAll()
{
for (int i = 0; i < ObjectPoolsList.Count; i++)
{
ObjectPoolsList[i].RecycleAll();
}
ObjectPoolsDic.Clear();
}
/// <summary>
/// 返回指定对象池的容量
/// </summary>
/// <param name="prefab"></param>
/// <returns></returns>
public int GetCapacity(GameObject prefab)
{
ObjectPool pool = FindObjectPool(prefab);
return pool.capacity;
}
/// <summary>
/// 设置指定对象池的容量
/// </summary>
/// <param name="prefab"></param>
/// <param name="capacity"></param>
public void SetCapacity(GameObject prefab, int capacity = -1)
{
ObjectPool pool = FindObjectPool(prefab);
pool.capacity = capacity;
}
/// <summary>
/// 缩减对象池
/// </summary>
/// <param name="prefab"></param>
/// <param name="count"></param>
public void Shrink(GameObject prefab, int count)
{
if (prefab == null || count <= 0) return;
ObjectPool pool = FindObjectPool(prefab);
for (int i = 0; i < ObjectPoolsList.Count; i++)
{
if (ObjectPoolsList[i] == pool)
{
for (int j = 0; j < count; j++)
{
if (pool.unUsedGameObjectList.Count <= 0)
break;
Destroy(pool.unUsedGameObjectList[0]);
pool.unUsedGameObjectList.RemoveAt(0);
}
}
}
}
#endregion
#region 内部方法
/// <summary>
/// 查找当前正在使用的对象池
/// </summary>
/// <param name="go"></param>
/// <returns></returns>
private ObjectPool FindUsedObjectPool(GameObject go)
{
if (go == null) return null;
for (int i = 0; i < ObjectPoolsList.Count; i++)
{
ObjectPool pool = ObjectPoolsList[i];
for (int j = 0; j < pool.usedGameObjectList.Count; j++)
{
if (pool.usedGameObjectList[i] == go)
return pool;
}
}
return null;
}
/// <summary>
/// 查找一个对象池
/// </summary>
/// <param name="prefab">对象池对象预制体</param>
/// <returns></returns>
private ObjectPool FindObjectPool(GameObject prefab)
{
if (prefab == null) return null;
//创建父对象
CreatePoolsParent();
//先遍历大池子 如果有直接拿出来用
for (int i = 0; i < ObjectPoolsList.Count; i++)
{
if (ObjectPoolsList[i].prefab == prefab)
{
return ObjectPoolsList[i];
}
}
//如果没有就创建一个
ObjectPool objectPool = new GameObject($"ObjectPool{prefab.name}").AddComponent<ObjectPool>();
objectPool.prefab = prefab;
objectPool.transform.SetParent(poolsParent.transform);
//将这个对象池放入对象池列表
ObjectPoolsList.Add(objectPool);
return objectPool;
}
/// <summary>
/// 创建对象池父对象(大池子)
/// </summary>
private void CreatePoolsParent()
{
if (poolsParent == null)
{
ObjectPoolsList.Clear();
ObjectPoolsDic.Clear();
poolsParent = new GameObject(poolsParentName);
}
}
#endregion
#region 对象池类
public class ObjectPool : MonoBehaviour
{
#region 字段
//这个对象池存储的游戏对象预制体
public GameObject prefab;
//对象池的容量 默认为-1 表示容量不限制
public int capacity = -1;
//对象池中正在使用的对象
public List<GameObject> usedGameObjectList = new List<GameObject>();
//对象池中空闲的对象
public List<GameObject> unUsedGameObjectList = new List<GameObject>();
#endregion
#region 属性
/// <summary>
/// 对象池中对象的总个数
/// </summary>
public int TotalGameObejctCount
{
get
{
return usedGameObjectList.Count + unUsedGameObjectList.Count;
}
}
#endregion
#region 方法
/// <summary>
/// 对象预加载
/// </summary>
/// <param name="count"></param>
public void Preload(int count = 1)
{
if (prefab == null || count <= 0) return;
for (int i = 0; i < count; i++)
{
GameObject go = Instantiate(prefab, Vector3.zero, Quaternion.identity);
go.SetActive(false);
go.transform.SetParent(transform,false);
unUsedGameObjectList.Add(go);
go.name = prefab.name;
}
}
/// <summary>
/// 获取一个对象池对象
/// </summary>
/// <param name="position"></param>
/// <param name="rotation"></param>
/// <param name="parent"></param>
/// <returns></returns>
public GameObject Spawn(Vector3 position, Quaternion rotation, Transform parent = null)
{
//要实例化的对象
GameObject go = null;
//如果当前有空闲的对象 就从对象池中直接拿出来用
if (unUsedGameObjectList.Count > 0)
{
go = unUsedGameObjectList[0];
unUsedGameObjectList.RemoveAt(0);
usedGameObjectList.Add(go);
go.transform.localPosition = position;
go.transform.localRotation = rotation;
go.transform.SetParent(parent,false);
go.SetActive(true);
}
else //如果对象池中没有,则实例化一个
{
go = Instantiate(prefab, position, rotation, parent);
usedGameObjectList.Add(go);
}
//执行该对象身上脚本的OnSpawn方法 前提是该对象脚本继承了MonoBehaviour
go.SendMessage("OnSpawn",SendMessageOptions.DontRequireReceiver);
return go;
}
/// <summary>
/// 回收对象池对象
/// </summary>
/// <param name="go"></param>
public void Recycle(GameObject go)
{
if (go == null) return;
for (int i = 0; i < usedGameObjectList.Count; i++)
{
if (usedGameObjectList[i] == go)
{
//如果当前对象池的容量有上限且当前容量已满 把0号对象移除
if (capacity >= 0 && usedGameObjectList.Count >= capacity)
{
if (unUsedGameObjectList.Count > 0)
{
Destroy(unUsedGameObjectList[0]);
unUsedGameObjectList.RemoveAt(0);
}
}
unUsedGameObjectList.Add(go);
usedGameObjectList.RemoveAt(i);
//执行该对象身上脚本的OnRecycle方法 前提是该对象脚本继承了MonoBehaviour
go.SendMessage("OnRecycle",SendMessageOptions.DontRequireReceiver);
go.transform.SetParent(transform,false);
go.transform.localPosition = Vector3.zero;
go.transform.localRotation = Quaternion.identity;
go.SetActive(false);
}
}
}
/// <summary>
/// 回收所有对象
/// </summary>
public void RecycleAll()
{
int count = usedGameObjectList.Count;
for (int i = 0; i < count; i++)
{
Recycle(usedGameObjectList[0]);
}
usedGameObjectList.Clear();
}
#endregion
}
#endregion
}
#endregion
}
五、应用演示
接下来就借用游戏中释放子弹的一个简单案例来演示对象池的效果。
1.先创建一个Gun脚本为挂载到枪预制体
using System;
using System.Collections;
using System.Collections.Generic;
using Mr.Le.Utility.Manager;
using UnityEngine;
public class Gun : MonoBehaviour
{
[SerializeField] private float shotForce = 1000f;
private GameObject bulletPrefab;
private Transform firePoint;
private void Awake()
{
bulletPrefab = Resources.Load<GameObject>("Prefab/Bullet");
firePoint = transform.Find("Cube/firePoint");
ObjectPoolManager.Instance.Preload(bulletPrefab,10);
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject bullet = ObjectPoolManager.Instance.Spawn(bulletPrefab, firePoint.position, firePoint.rotation);
bullet.GetComponent<Rigidbody>().AddForce(Vector3.forward * shotForce);
ObjectPoolManager.Instance.Recycle(bullet,1f);
}
}
}
2.再创建一个Bullet的脚本挂载到子弹对象上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
private void OnSpawn()
{
Debug.Log("子弹发射");
}
private void OnRecycle()
{
Debug.Log("子弹回收");
GetComponent<Rigidbody>().velocity = Vector3.zero;
}
}
六、总结
通过使用对象池,我们可以有效地管理游戏对象,避免频繁的内存分配和销毁操作,从而提高游戏的性能和流畅度。在开发过程中,根据实际情况合理使用对象池,可以在不牺牲可读性的情况下改善游戏的性能。
希望本文能够帮助你更好地理解和应用Unity中的对象池技术,提升游戏开发的效率和体验。感谢阅读!