杂项功能——单例,计时器,对象池

一:泛型单例

        要点:[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;
    }

}

编辑器中对象池界面:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值