一:泛型单例
要点:[DefaultExecutionOrder(-100)] ,让单例类的运行时间提前一些,可以确保一般类的Awake函数之前单例已经生成。
如果要进行跨场景的功能,可以追加DontDestroyOnLoad(gameObject);或者把通用的部分放在一个持久的场景中,并且采用同时具有多个场景的方式。
[DefaultExecutionOrder(-100)]
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
private static T instance;
public static T Instance { get => instance; }
protected virtual void Awake()
{
if (instance == null) instance = (T)this;
else Destroy(gameObject);
//可以选择添加 DontDestroyOnLoad(gameObject);
}
public static bool IsInitialized()
{
return instance != null;
}
}
二:计时器实现:
要点:使用协程来完成计时操作,到点后自动触发回调函数并且回收计时器。
public class Timer : PoolItemBase
{
private bool timeIsDone;
protected override void SettingObjectName()
{
objectName = "Timer";
}
/// <summary>
/// 创建计时器
/// </summary>
/// <param name="timer">计时时间</param>
/// <param name="callBackAction">回调函数</param>
public void CreateTime(float timer, Action callBackAction, bool timeIsDone = false)
{
this.timeIsDone = timeIsDone;
if (timeIsDone) ExecutiveAction(callBackAction);
else StartCoroutine(TimerCoroutine(timer, callBackAction));
}
IEnumerator TimerCoroutine(float timer, Action callBackAction)
{
yield return new WaitForSeconds(timer);
ExecutiveAction(callBackAction);
}
private void ExecutiveAction(Action callBackAction)
{
callBackAction?.Invoke();
RecycleObject();
}
public override void RecycleObject()
{
StopAllCoroutines();
base.RecycleObject();
}
}
三:对象池的实现
1:物品的接口 IPool
public interface IPool
{
void SettingObject();
void SettingObject(Transform user);
void RecycleObject();
}
2:池中物品的抽象基类 PoolItemBase
要点:定义了对象的使用者以及对象的最长激活时间
为物品设置名字(必要),用于回收时能回到正确的对象池队列中。
public abstract class PoolItemBase : MonoBehaviour, IPool
{
//对象的使用者
protected Transform user;
[SerializeField] protected float maxDelayTime; //定义了对象的最长激活时间
//对象在对象池中的键值 静态变量
protected static string objectName;
//设置对象种类在池中的名称
private void Awake()
{
SettingObjectName();
}
protected abstract void SettingObjectName();
#region 接口
//物品的初始化操作(不指定使用者)
public virtual void SettingObject()
{
}
//物品的初始化操作(附带使用者,并且规定了最大的存在时间,可以进行自动管理)
public virtual void SettingObject(Transform user)
{
this.user = user;
GOPoolManager.Instance.TakeGameObject("Timer").GetComponent<Timer>().CreateTime(maxDelayTime,() => RecycleObject(), false);
}
//回收物品
public virtual void RecycleObject()
{
this.user = null;
GOPoolManager.Instance.RecycleGameObject(gameObject, objectName);
}
#endregion
public Transform GetUser() => user;
}
3:对象池管理类 GOPoolManager
要点:
(1)内部类GameObjectAssets,内含string名字,预创建的个数count,以及Prefab列表。(用于随机生成,比如说同种攻击方式可能随机产生的特效预制体)
(2)数据结构部分采用两个字典,nameToPrefab表示名字string对应的预制体prefabs,用于池中对象的个数扩充。pools表示名字string对应的可使用对象队列。
(3)在编辑器窗口中配置assetsList,然后在Awake中初始化:初始化字典,创建对象。
(4)TakeGameObject:根据名字从池中取出指定的对象,提供了三种使用方式:直接返回对象;设置位置和旋转并且进行对象的Set操作;同上一种方式,多设置了使用者transform。在实现中:如果pools中对应的队列中已经没有可以使用的对象,则直接新创建一个对象放入池中,每次将队列头部对象取出供使用(设置对象状态+设为激活等)。
(5)RecycleGameObject:传入要回收的对象,并且还要传入对象所对应的池中名字(通过PoolItemBase里设置的名字),具体逻辑就是重置对象状态+设为未激活+放回池中.
目前只有动态扩容,后续可以补充在池中物体过剩的时候进行自动回收。(已完成)
(6)回收规则:每1分钟触发一次对象池的回收,利用协程(每一帧清理一种对象,防止卡顿),每次扩容时扩充到1.5倍的容量,判断是否需要清理的阈值为 某种对象未使用的个数占据总个数超过了一半,每次清理到2/3的容量。(规则可以自行测试+定义)
为了更快判断阈值,新增 Dictionary<string, PoolUseInfo> poolUseInfos 来记录每个池中对象的使用信息,具体PoolUseInfo中包含了总对象个数,正在使用的对象个数。
该对象池还可能存在两个问题:
(1):取对象时触发扩容,一次可能扩容太多个物体,造成卡顿,可以用协程来优化。
(2):对于随机的处理,一个动作可能会随机触发10种特效中的一种,代码中将这种特效统一管理,每次随机产生一种特效并放入池中,会导致后续不是随机的情况(因为开始的随机生成并不一定均匀)。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
//简单对象池
public class GOPoolManager : Singleton<GOPoolManager>
{
[SerializeField, Header("预制体")] private List<GameObjectAssets> assetsList = new List<GameObjectAssets>();
[SerializeField] private Transform poolObjectParent;
//名字对应对象的预制体 用于对象池的动态扩充
private Dictionary<string, GameObject[]> nameToPrefab = new();
//实际的对象池
private Dictionary<string, Queue<GameObject>> pools = new ();
//对象池的使用信息
private Dictionary<string, PoolUseInfo> poolUseInfos = new();
protected override void Awake()
{
base.Awake();
InitPool();
}
private void Start()
{
//默认每一分钟清理一次对象池 (后续可变更为不同种类场景不同的清理时间)
InvokeRepeating(nameof(ClearExcessObjects), 60f,60f);
}
private void InitPool()
{
if (assetsList.Count == 0) return;
//遍历外面配置的资源 进行初始化
for (int i = 0; i < assetsList.Count; i++)
{
//检查列表元素的内容是否已经在池子里面了,没有的话就创建一个
if (!pools.ContainsKey(assetsList[i].assetsName))
{
pools.Add(assetsList[i].assetsName, new Queue<GameObject>());
nameToPrefab.Add(assetsList[i].assetsName, assetsList[i].prefab);
poolUseInfos.Add(assetsList[i].assetsName, new PoolUseInfo(assetsList[i].count, 0));
}
//处理assetsList列表中包含了重复的情况
else
{
poolUseInfos[assetsList[i].assetsName].totalCount += assetsList[i].count;
}
//创建完毕后,遍历这个对象的总数,比如总算5,那么就创建5个,然后存进字典
for (int j = 0; j < assetsList[i].count; j++)
{
GameObject tempGameObject = Instantiate(assetsList[i].prefab[Random.Range(0, assetsList[i].prefab.Length)], poolObjectParent, true);
tempGameObject.transform.position = Vector3.zero;
tempGameObject.transform.rotation = Quaternion.identity;
pools[assetsList[i].assetsName].Enqueue(tempGameObject);
tempGameObject.SetActive(false);
}
}
}
//只是想获取一个对象,但不需要立即对其进行操作(如设置位置或旋转)时使用
public GameObject TakeGameObject(string objectName)
{
if (!pools.ContainsKey(objectName)) return null;
if (pools[objectName].Count == 0)
ExpansionPoolCapacity(objectName);
GameObject dequeueObject = pools[objectName].Dequeue();
dequeueObject.SetActive(true);
poolUseInfos[objectName].useCount++;
return dequeueObject;
}
//立即设置其位置和旋转,然后调用其SpawnObject方法。因为这个方法已经对获取的对象进行了操作,所以它不需要返回这个对象。
public void TakeGameObject(string objectName, Vector3 position, Quaternion rotation)
{
if (!pools.ContainsKey(objectName)) return;
if (pools[objectName].Count == 0)
ExpansionPoolCapacity(objectName);
GameObject dequeueObject = pools[objectName].Dequeue();
dequeueObject.SetActive(true);
dequeueObject.transform.position = position;
dequeueObject.transform.rotation = rotation;
poolUseInfos[objectName].useCount++;
dequeueObject.GetComponent<IPool>().SettingObject();
}
//立即设置其位置、旋转和用户,然后调用其SpawnObject方法。同样,因为这个方法已经对获取的对象进行了操作.
public void TakeGameObject(string objectName, Vector3 position, Quaternion rotation, Transform user)
{
if (!pools.ContainsKey(objectName)) return;
if (pools[objectName].Count == 0)
ExpansionPoolCapacity(objectName);
GameObject dequeueObject = pools[objectName].Dequeue();
dequeueObject.SetActive(true);
dequeueObject.transform.position = position;
dequeueObject.transform.rotation = rotation;
poolUseInfos[objectName].useCount++;
dequeueObject.GetComponent<IPool>().SettingObject(user);
}
public void RecycleGameObject(GameObject gameObject,string objectName)
{
gameObject.transform.position = Vector3.zero;
gameObject.transform.rotation = Quaternion.identity;
gameObject.SetActive(false);
poolUseInfos[objectName].useCount--;
pools[objectName].Enqueue(gameObject);
}
//在对象池中已经没有该种类的对象的时候, 根据对象名 Instantiate 出一个新的对象并且分配使用 实现动态扩容
private void ExpansionPoolCapacity(string objectName)
{
//每次扩容1.5倍的容量
int expandCount = (int)(poolUseInfos[objectName].totalCount * 1.5f) - poolUseInfos[objectName].totalCount;
poolUseInfos[objectName].totalCount += expandCount;
for (int i = 0; i < expandCount; i++)
{
var newGo = Instantiate(nameToPrefab[objectName][Random.Range(0, nameToPrefab[objectName].Length)], poolObjectParent, true);
newGo.SetActive(false);
pools[objectName].Enqueue(newGo);
}
}
//清理对象池中过多的对象:既可以定时调用,也可以手动调用
public void ClearExcessObjects()
{
StopAllCoroutines();
StartCoroutine(ClearPools());
}
private IEnumerator ClearPools()
{
foreach (var poolInfo in poolUseInfos)
{
//当某种对象未使用的对象超过了总对象数目的一半时,进行清理工作
if (poolInfo.Value.NeedClearObject())
{
//每次减容到原先总容量的 2/3 即去除掉 1/3 数量的对象
int clearCount = (int)(poolInfo.Value.totalCount * 0.33f);
poolInfo.Value.totalCount -= clearCount;
for (int i = 0; i < clearCount; i++)
{
var clearObject = pools[poolInfo.Key].Dequeue();
clearObject.SetActive(false);
Destroy(clearObject);
}
}
//每帧处理一种对象,放置对象池中对象种类过多,一次性处理造成的卡顿
yield return null;
}
}
[System.Serializable]
private class GameObjectAssets
{
public string assetsName;
public int count;
public GameObject[] prefab;
}
private class PoolUseInfo
{
public PoolUseInfo(int totalCount, int useCount)
{
this.totalCount = totalCount;
this.useCount = useCount;
}
public bool NeedClearObject()
{
return (totalCount - useCount) * 2 >= totalCount;
}
public int totalCount;
public int useCount;
}
}
编辑器中对象池界面: