Unity框架之对象池GameObjectPool
文章目录
对象池的核心思想
将需要频繁创建销毁的游戏对象缓存起来,将创建销毁行为替换成显示和隐藏,大大提高游戏运行效率。
典型的以空间换时间的思想
对象池的使用流程
对象池的设计
根据上图对象池的使用流程分析对象池的特点
- 每个类型的对象应该有属于自己的独立的池子(子弹复用子弹对象,不能获取子弹却返回一个敌人对象)
- 考虑用字典的键值对 (名称 , 池)–(key,value)的形式缓存每个独立的池子
- 每个独立的池子中保存着若干个游戏对象供使用
- 池:
List<GameObject>
用列表存储个数不固定的游戏对象
- 池:
- 需要提供一个获取游戏对象的方法和回收游戏对象的方法
- 全局唯一且经常使用,考虑使用单例模式
通用的对象池框架
对象池的数据结构
根据设计时的分析,应该用(key,List)结构的字典
private Dictionary<string, List<GameObject>> objCache; //创建对象缓存字典
//单例模式提供的Init初始化方法
protected override void Init()
{
base.Init();
//初始化字典
objCache = new Dictionary<string, List<GameObject>>();
}
关键方法:获取游戏对象
/// <summary>
/// 创建对象(从对象池创建/读取对象)
/// </summary>
/// <param name="key">类别---自行定义</param>
/// <param name="prefab">需要创建实例的预制件</param>
/// <param name="pos">创建位置</param>
/// <param name="rotate">创建角度</param>
/// <returns></returns>
public GameObject CreateObject(string key,GameObject prefab,Vector3 pos , Quaternion rotate)
{
GameObject go;
go = FindUsableObject(key); //查找是否有可用的对象 若无则返回null
//若没有查找到--没有键/没有空闲对象
if(go == null)
{
//添加对象
go = AddObject(key, prefab);
}
UseObject(go,pos,rotate); //使用对象 设置基本的位置和旋转
return go;
}
/// <summary>
/// 查找是否有可用的对象
/// </summary>
private GameObject FindUsableObject(string key)
{
//List的Find也是委托,类似于ArrayHelper自己定义的,可同样使用
if (objCache.ContainsKey(key))
{
//返回有被禁用的物体,若无则返回null
return objCache[key].Find(go => !go.activeInHierarchy);
}
else return null;
}
/// <summary>
/// 向对象池中添加对象
/// </summary>
/// <param name="key">类别</param>
/// <param name="prefab">预制件</param>
/// <returns>返回实例对象</returns>
private GameObject AddObject(string key,GameObject prefab)
{
//创建预制件的实例对象
GameObject go = Instantiate(prefab);
//如果缺少键则创建键
if (!objCache.ContainsKey(key)) objCache.Add(key, new List<GameObject>());
//向对象池中添加对象
objCache[key].Add(go);
return go;
}
/// <summary>
/// 使用对象(配置对象的一些位置和旋转)
/// </summary>
/// <param name="go">实例对象</param>
/// <param name="pos">位置</param>
/// <param name="rotate">旋转</param>
private void UseObject(GameObject go,Vector3 pos,Quaternion rotate)
{
go.transform.position = pos;
go.transform.rotation = rotate;
go.SetActive(true);
//遍历执行所有需要被重置的逻辑(实现了IResetable接口的脚本)后面会进行解释
foreach (var item in go.GetComponents<IResetable>())
item.onReset();
}
关键方法:回收游戏物体
这里提供三种回收游戏物体的方法
- 回收单个游戏对象(支持延迟回收)
- 回收一类游戏对象
- 回收全部的游戏对象
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">实例对象</param>
/// <param name="delay">延迟时间(默认参数)</param>
public void CollectObject(GameObject go,float delay=0)
{
//延迟调用
StartCoroutine(CollectObjectDelay(go,delay));
}
private IEnumerator CollectObjectDelay(GameObject go,float delay)
{
yield return new WaitForSeconds(delay);
go.SetActive(false);
}
/// <summary>
/// 清楚指定类别的对象
/// </summary>
/// <param name="key">类别</param>
public void Clear(string key)
{
//Destroy
if(objCache.ContainsKey(key))
{
foreach (GameObject obj in objCache[key])
Destroy(obj);
objCache.Remove(key);
}
}
/// <summary>
/// 清除对象池中所有内容
/// </summary>
public void ClearAll()
{
//foreach只允许读元素,不允许修改删除等
//可以通过new List<string>(objCahce.Keys)解决
foreach(var item in new List<string>(objCache.Keys))
{
//原理,遍历的List元素,删除的字典 遍历A删B 不允许遍历B删B
Clear(item);
}
}
对象池的拓展
使用对象池最容易的易错点也是难点就是 游戏对象的复用
如果对象每次使用都仅需要修改位置和旋转,则当前版本的对象池已经可以完美做到。
但如果一些对象每次生成后都要注册一些事件,动态生成一些属性等等,每次复用都要注销上一次的这些引用,替换成当前所需要的新事件,新引用。
- 一种解决方案是在此对象的每个脚本的OnEnable和OnDisable中添加需要复用重置的属性逻辑,但这样做会造成紧耦合,并且不利于解决一些较为复杂的属性重置复用。
- 此通用对象池框架提供一个IResetable接口,每次获取对象时,都会查询对象身上是否有实现此接口的脚本,然后调用此接口的onReset()方法,这样当一个较为复杂的对象复用时,就可以在相应脚本实现此接口,在接口中的OnReset方法中完成属性的重置。
public interface IResetable
{
//用以重置对象池中的对象
void onReset();
}
对象池的完整源码
本对象池用到了Mono脚本的单例模式,有关单例模式较为简单,读者可以自行查阅了解,笔者在此也贴上源码供参考使用
MonoSingleton.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
///<summary>
///脚本单例类,负责为唯一脚本创建实例
///<summary>
public class MonoSingleton<T> : MonoBehaviour where T:MonoSingleton<T> //注意此约束为T必须为其本身或子类
{
/*
相较于直接在需要唯一创建的脚本中创建实例,Awake初始化的过程需要解决的问题
1.代码重复
2.在Awake里面初始化,其它脚本在Awake中调用其可能会为Null的异常情况
*/
//解决1:使用泛型创建实例 解决2:使用按需加载(即有其它脚本调用时在get中加载)
private static T instance; //创建私有对象记录取值,可只赋值一次避免多次赋值
public static T Instance
{
//实现按需加载
get
{
//当已经赋值,则直接返回即可
if (instance != null) return instance;
instance = FindObjectOfType<T>();
//为了防止脚本还未挂到物体上,找不到的异常情况,可以自行创建空物体挂上去
if (instance == null)
{
//如果创建对象,则会在创建时调用其身上脚本的Awake即调用T的Awake(T的Awake实际上是继承的父类的)
//所以此时无需为instance赋值,其会在Awake中赋值,自然也会初始化所以无需init()
/*instance = */
new GameObject("Singleton of "+typeof(T)).AddComponent<T>();
}
else instance.Init(); //保证Init只执行一次
return instance;
}
}
private void Awake()
{
//若无其它脚本在Awake中调用此实例,则可在Awake中自行初始化instance
instance = this as T;
//初始化
Init();
}
//子类对成员进行初始化如果放在Awake里仍会出现Null问题所以自行制作一个init函数解决(可用可不用)
protected virtual void Init()
{
}
}
}
GameObjectPool.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Common
{
public interface IResetable
{
//用以重置对象池中的对象
void onReset();
}
///<summary>
///对象池--唯一
///<summary>
public class GameObjectPool : MonoSingleton<GameObjectPool>
{
private Dictionary<string, List<GameObject>> objCache; //创建对象缓存字典
protected override void Init()
{
base.Init();
//初始化字典
objCache = new Dictionary<string, List<GameObject>>();
}
/// <summary>
/// 创建对象(从对象池创建/读取对象)
/// </summary>
/// <param name="key">类别---自行定义</param>
/// <param name="prefab">需要创建实例的预制件</param>
/// <param name="pos">创建位置</param>
/// <param name="rotate">创建角度</param>
/// <returns></returns>
public GameObject CreateObject(string key,GameObject prefab,Vector3 pos , Quaternion rotate)
{
GameObject go;
go = FindUsableObject(key); //查找是否有可用的对象 若无则返回null
//若没有查找到--没有键/没有空闲对象
if(go == null)
{
//添加对象
go = AddObject(key, prefab);
}
UseObject(go,pos,rotate); //使用对象 设置位置旋转和启用
return go;
}
/// <summary>
/// 查找是否有可用的对象
/// </summary>
private GameObject FindUsableObject(string key)
{
//List的Find也是委托,类似于ArrayHelper自己定义的,可同样使用
if (objCache.ContainsKey(key))
{
//返回有被禁用的物体,若无则返回null
return objCache[key].Find(go => !go.activeInHierarchy);
}
else return null;
}
/// <summary>
/// 向对象池中添加对象
/// </summary>
/// <param name="key">类别</param>
/// <param name="prefab">预制件</param>
/// <returns>返回实例对象</returns>
private GameObject AddObject(string key,GameObject prefab)
{
//创建预制件的实例对象
GameObject go = Instantiate(prefab);
//如果缺少键则创建键
if (!objCache.ContainsKey(key)) objCache.Add(key, new List<GameObject>());
//向对象池中添加对象
objCache[key].Add(go);
return go;
}
/// <summary>
/// 使用对象(配置对象的一些位置和旋转)
/// </summary>
/// <param name="go">实例对象</param>
/// <param name="pos">位置</param>
/// <param name="rotate">旋转</param>
private void UseObject(GameObject go,Vector3 pos,Quaternion rotate)
{
go.transform.position = pos;
go.transform.rotation = rotate;
go.SetActive(true);
//遍历执行所有需要被重置的逻辑(实现了IResetable接口的脚本)
foreach (var item in go.GetComponents<IResetable>())
item.onReset();
}
/// <summary>
/// 回收对象
/// </summary>
/// <param name="go">实例对象</param>
/// <param name="delay">延迟时间(默认参数)</param>
public void CollectObject(GameObject go,float delay=0)
{
//延迟调用
StartCoroutine(CollectObjectDelay(go,delay));
}
private IEnumerator CollectObjectDelay(GameObject go,float delay)
{
yield return new WaitForSeconds(delay);
go.SetActive(false);
}
/// <summary>
/// 清楚指定类别的对象
/// </summary>
/// <param name="key">类别</param>
public void Clear(string key)
{
//Destroy
if(objCache.ContainsKey(key))
{
foreach (GameObject obj in objCache[key])
Destroy(obj);
objCache.Remove(key);
}
}
/// <summary>
/// 清除对象池中所有内容
/// </summary>
public void ClearAll()
{
/*
foreach(var item in objCache)
{
foreach (var obj in item.Value)
Destroy(obj);
}
objCache.Clear();
*/
//foreach只允许读元素,不允许修改删除等
//可以通过new List<string>(objCahce.Keys)解决
foreach(var item in new List<string>(objCache.Keys))
{
//原理,遍历的List元素,删除的字典 遍历A删B 不允许遍历B删B
Clear(item);
}
}
}
}