UNITY常用基础程序小框架

单例模式基类

实现不继承MonoBehaviour的单例模式基类

/// <summary>
/// 单例模式基类 主要目的是避免代码的冗余 方便我们实现单例模式的类
/// </summary>
/// <typeparam name="T"></typeparam>
public class BaseManager<T> where T:class,new()
{
    private static T instance;

    //属性的方式
    public static T Instance
    {
        get
        {
            if (instance == null)
                instance = new T();
            return instance;
        }
    }

    //方法的方式
    //public static T GetInstance()
    //{
    //    if (instance == null)
    //        instance = new T();
    //    return instance;
    //}
}

潜在的安全问题

        //1.构造函数问题:构造函数可在外部调用 可能会破坏唯一性
        //2.多线程问题:当多个线程同时访问管理器时,可能会出现共享资源的安全访问问题

继承MonoBehaviour的单例模式基类

        //继承MonoBehaviour的脚本不能new!

        //继承MonoBehaviour的脚本一定得依附在GameObject上

实现挂载式的单例模式基类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//这种方式不建议使用
//因为很容易被破坏单例模式的唯一性
//1.挂载多个脚本
//2.切换场景回来时,由于场景放置了挂载脚本的对象,回到该场景时 又会有一个该单例模式对象
//3.还可以通过代码动态的添加多个该脚本 也会破坏唯一性
/// <summary>
/// 挂载式 继承Mono的单例模式基类
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonMono<T>: MonoBehaviour where T:MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            return instance;
        }
    }

    protected virtual void Awake()
    {
        instance = this as T;
    }
}

实现自动挂载式的单例模式基类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// <summary>
/// 自动挂载式的 继承Mono的单例模式基类
/// 推荐使用 
/// 无需手动挂载 无需动态添加 无需关心切场景带来的问题
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonAutoMono<T> : MonoBehaviour where T:MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if(instance == null)
            {
                //动态创建 动态挂载
                //在场景上创建空物体
                GameObject obj = new GameObject();
                //得到T脚本的类名 为对象改名 这样再编辑器中可以明确的看到该
                //单例模式脚本对象依附的GameObject
                obj.name = typeof(T).ToString();
                //动态挂载对应的 单例模式脚本
                instance = obj.AddComponent<T>();
                //过场景时不移除对象 保证它在整个游戏生命周期中都存在
                DontDestroyOnLoad(obj);
            }
            return instance;
        }
    }

}

潜在的安全问题

        //1.构造函数问题:
        //  继承MonoBehaviour的函数,不能new,所以不用担心公共构造函数
        //2.多线程问题:
        //  Unity主线程中相关内容,不允许其他线程直接调用,很少有这样的需求,所以也不用太担心
        //3.重复挂载问题:
        //  1.手动重复挂载
        //  2.代码重复添加
        //  需要人为干涉,定规则,或者通过代码逻辑强制处理

构造函数的唯一性问题

构造函数带来的唯一性问题指什么

        //1.对于不继承MonoBehaviour的单例模式基类
        //  我们要避免在外部 new 单例模式类对象

        //2.对于继承MonoBehaviour的单例模式基类
        //  由于继承MonoBehaviour的脚本不能通过new创建,因此不用过多考虑

解决构造函数带来的安全问题

        //1.父类变为抽象类

        //2.规定继承单例模式基类的类必须显示实现私有无参构造函数

        //3.在基类中通过反射来调用私有构造函数实例化对象
        //  主要知识点:
        //  利用Type中的 GetConstructor(约束条件, 绑定对象, 参数类型, 参数修饰符)方法
        //  来获取私有无参构造函数
        //  ConstructorInfo constructor = typeof(T).GetConstructor(
        //  BindingFlags.Instance | BindingFlags.NonPublic, //表示成员私有方法
        //    null,                                         //表示没有绑定对象
        //    Type.EmptyTypes,                              //表示没有参数
        //    null);                                        //表示没有参数修饰符
        //  

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// 单例模式基类 主要目的是避免代码的冗余 方便我们实现单例模式的类
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class BaseManager<T> where T : class//,new()
{
    private static T instance;

    //属性的方式
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                //instance = new T();
                //利用反射得到无参私有的构造函数 来用于对象的实例化
                Type type = typeof(T);
                ConstructorInfo info = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic,
                                                            null,
                                                            Type.EmptyTypes,
                                                            null);
                if (info != null)
                    instance = info.Invoke(null) as T;
                else
                    Debug.LogError("没有得到对应的无参构造函数");
            }

            return instance;
        }
    }
}

重复挂载带来的唯一性问题

        //对于继承MonoBehaviour的挂载式的单例模式基类
        //1.手动挂载多个相同单例模式脚本
        //2.代码动态添加多个相同单例模式脚本

解决重复挂载带来的安全问题

        //对于挂载式的单例模式脚本
        //1.同个对象的重复挂载
        //  为脚本添加特性[DisallowMultipleComponent]

        //能够防止一个对象上挂载多个相同的脚本,却无法防止多个对象挂载多个相同的单类模式脚本

        //2.修改代码逻辑
        //  判断如果存在对象,移除脚本

        //对于自动挂载式的单例模式脚本
        //  制定使用规则,不允许手动挂载或代码添加

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 挂载式 继承Mono的单例模式基类
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonMono<T>: MonoBehaviour where T:MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            return instance;
        }
    }

    protected virtual void Awake()
    {
        //已经存在一个对应的单例模式对象了 不需要在有一个了
        if(instance != null)
        {
            Destroy(this);
            return;
        }
        instance = this as T;
        //我们挂载继承该单例模式基类的脚本后 依附的对象过场景时就不会被移除了
        //就可以保证在游戏的整个生命周期中都存在 
        DontDestroyOnLoad(this.gameObject);
    }
}

        //为了避免重复挂载我们一般采用以下几种方案:

        //1.对于挂载式的单例模式基类,相同对象上重复挂载问题,通过添加特性解决
        //2.对于挂载式的单例模式基类,不同对象上的重复挂载,通过逻辑判断,代码移除多余的脚本

        //3.最好的避免重复挂载的方式,就是使用自动挂载式的单例模式基类,并且制定使用规则(不允许手动挂载和代码添加)

线程安全——安全锁

        //如果程序当中存在多线程
        //我们需要考虑当多个线程同时访问同一个内存空间时出现的问题
        //如果不加以控制,可能会导致数据出错
        //我们一般称这种问题为多线程并发问题,指多线程对共享数据的并发访问和操作。

        //而一般解决该问题的方式,就是通过C#中的lock关键字进行加锁
        //我们需要考虑我们的单例模式对象们是否需要加锁(lock)

        //lock 的原理保证了在任何时刻只有一个线程能够执行被锁保护的代码块
        //从而防止多个线程同时访问或修改共享资源,确保线程安全

解决多线程并发来带的问题

        //1.不继承MonoBehaviour的单例模式
        //  建议加锁,避免以后使用多线程时出现并发问题
        //  比如在处理网络通讯模块、复杂算法模块时,经常会进行多线程并发处理

        //2.继承MonoBehaviour的单例模式
        //  可加可不加,但是建议不加。
        //  因为Unity中的机制是,Unity主线程中处理的一些对象(如GameObject、Transform等等)
        //  是不允许被其他多线程修改访问的,会直接报错
        //  因此我们一般不会通过多线程去访问继承MonoBehaviour的相关对象
        //  既然如何,就不会发生多线程并发问题

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

/// <summary>
/// 单例模式基类 主要目的是避免代码的冗余 方便我们实现单例模式的类
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class BaseManager<T> where T:class//,new()
{
    private static T instance;

    //用于加锁的对象
    protected static readonly object lockObj = new object();

    //属性的方式
    public static T Instance
    {
        get
        {
            if(instance == null)
            {
                lock (lockObj)
                {
                    if (instance == null)
                    {
                        //instance = new T();
                        //利用反射得到无参私有的构造函数 来用于对象的实例化
                        Type type = typeof(T);
                        ConstructorInfo info = type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic,
                                                                    null,
                                                                    Type.EmptyTypes,
                                                                    null);
                        if (info != null)
                            instance = info.Invoke(null) as T;
                        else
                            Debug.LogError("没有得到对应的无参构造函数");
                    }
                }
            }
            return instance;
        }
    }

公共Mono模块

        //公共Mono模块的主要作用
        //让不继承MonoBehaviour的脚本也能
        //1.利用帧更新或定时更新处理逻辑
        //2.利用协同程序处理逻辑
        //3.可以统一执行管理帧更新或定时更新相关逻辑(不管你是否继承MonoBehaviour)

        //公共Mono模块的基本原理
        //1.通过事件或委托 管理 相关更新函数
        //2.提供协同程序开启或关闭的方法

实现公共Mono模块

        //1.创建MonoMgr继承 自动挂载式的继承MonoBehaviour的单例模式基类
        //2.实现Update、FixedUpdate、LateUpdate生命周期函数
        //3.声明对应事件或委托用于存储外部函数,并提供添加移除方法,从而达到让不继承MonoBehaviour的脚本可以执行帧更新或定时更新的目的
        //4.声明协同程序开启关闭函数,从而达到让不继承MonoBehaviour的脚本可以执行协同程序的目的

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 公共Mono模块管理器
/// </summary>
public class MonoMgr : SingletonAutoMono<MonoMgr>
{
    private event UnityAction updateEvent;
    private event UnityAction fixedUpdateEvent;
    private event UnityAction lateUpdateEvent;

    /// <summary>
    /// 添加Update帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddUpdateListener(UnityAction updateFun)
    {
        updateEvent += updateFun;
    }

    /// <summary>
    /// 移除Update帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveUpdateListener(UnityAction updateFun)
    {
        updateEvent -= updateFun;
    }

    /// <summary>
    /// 添加FixedUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddFixedUpdateListener(UnityAction updateFun)
    {
        fixedUpdateEvent += updateFun;
    }
    /// <summary>
    /// 移除FixedUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveFixedUpdateListener(UnityAction updateFun)
    {
        fixedUpdateEvent -= updateFun;
    }

    /// <summary>
    /// 添加LateUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddLateUpdateListener(UnityAction updateFun)
    {
        lateUpdateEvent += updateFun;
    }

    /// <summary>
    /// 移除LateUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveLateUpdateListener(UnityAction updateFun)
    {
        lateUpdateEvent -= updateFun;
    }


    private void Update()
    {
        updateEvent?.Invoke();
    }

    private void FixedUpdate()
    {
        fixedUpdateEvent?.Invoke();
    }

    private void LateUpdate()
    {
        lateUpdateEvent?.Invoke();
    }
}

测试类:继承了不继承Monobehaviour的BaseManager单类模式基类

使用MonoMgr的方法进行封装

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test6Mgr : BaseManager<Test6Mgr>
{
    private Coroutine testFun;
    //申明私有无参构造函数
    private Test6Mgr()
    {

    }

    public void ICanUpdateAndCoroutine()
    {
        MonoMgr.Instance.AddUpdateListener(MyUpdate);

        testFun = MonoMgr.Instance.StartCoroutine(Test());
    }

    public void ICanStopUpdateAndCoroutine()
    {
        MonoMgr.Instance.RemoveUpdateListener(MyUpdate);

        MonoMgr.Instance.StopCoroutine(testFun);
    }


    private IEnumerator Test()
    {
        yield return new WaitForSeconds(3f);
        Debug.Log("TestTestTest");
    }


    private void MyUpdate()
    {
        Debug.Log("Test6Mgr");
    }
}

测试:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Main : MonoBehaviour
{
 

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            Test6Mgr.Instance.ICanUpdateAndCoroutine();

        if (Input.GetKeyUp(KeyCode.Space))
            Test6Mgr.Instance.ICanStopUpdateAndCoroutine();
    }
}

缓存池

        缓存池(对象池)的主要作用
        通过重复利用已经创建的对象,避免频繁的创建和销毁
        从而减少系统的内存分配和垃圾回收带来的开销

        缓存池(对象池)的基本原理
        用一个“柜子”中的“各种抽屉”来装“东西”
        用时去拿(没有就创造,存在就获取)
        不用就还(将“东西”分门别类的放入“抽屉”中)

实现缓存池(对象池)模块

        1.创建PoolMgr继承 不继承MonoBehaviour的单例模式基类
        2.声明柜子(Dictionary)和抽屉(List、Stack、Queue等)容器
        3.拿东西方法
          3 - 1:有抽屉并且抽屉里有东西 直接获取
          3 - 2: 没有抽屉或者抽屉里没东西 创造
        4.放东西方法
          4 - 1:有抽屉,直接放
          4 - 2:没抽屉,创建抽屉,再放
        5.清空柜子方法
          我们在切场景时,对象都会被移除,这时应该清空柜子
          否则会出现内存泄漏,并且下次取东西会出问题

代码:

不继承MonoBehaviour的单例模式基类在上文

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 缓存池(对象池)模块 管理器
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
{
    //柜子容器当中有抽屉的体现
    private Dictionary<string, Stack<GameObject>> poolDic = new Dictionary<string, Stack<GameObject>>();

    private PoolMgr() { }

    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public GameObject GetObj(string name)
    {
        GameObject obj;
        //有抽屉 并且 抽屉里 有对象 才去直接拿
        if(poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        {
            //弹出栈中的对象 直接返回给外部使用
            obj = poolDic[name].Pop();
            //激活对象 再返回
            obj.SetActive(true);
        }
        //否则,就应该去创造
        else
        {
            //没有的时候 通过资源加载 去实例化出一个GameObject
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //避免实例化出来的对象 默认会在名字后面加一个(Clone)
            //我们重命名过后 方便往里面放
            obj.name = name;
        }

        return obj;
    }


    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象</param>
    public void PushObj(GameObject obj)
    {
        //总之,目的就是要把对象隐藏起来
        //并不是直接移除对象 而是将对象失活 一会儿再用 用的时候再激活它
        //除了这种方式,还可以把对象放倒屏幕外看不见的地方
        obj.SetActive(false);

        //没有抽屉 创建抽屉
        if(!poolDic.ContainsKey(obj.name))
            poolDic.Add(obj.name, new Stack<GameObject>());

        //往抽屉当中放对象
        poolDic[obj.name].Push(obj);

        如果存在对应的抽屉容器 直接放
        //if(poolDic.ContainsKey(name))
        //{
        //    //往栈(抽屉)中放入对象
        //    poolDic[name].Push(obj);
        //}
        否则 需要先创建抽屉 再放
        //else
        //{
        //    //先创建抽屉
        //    poolDic.Add(name, new Stack<GameObject>());
        //    //再往抽屉里面放
        //    poolDic[name].Push(obj);
        //}
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据 
    /// 使用场景 主要是 切场景时
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
    }
}

窗口布局优化

        现在直接失活对象,当之后项目做大了,抽屉多了,对象多了
        游戏中成百上千个对象,在开发测试时不方便从Hierarchy窗口中查看对象获取信息
        因此我们希望能优化一下Hierarchy窗口中的布局
        将对象和抽屉的关系可视化

制作思路和具体实现

        制作思路:
        1.柜子管理自己的柜子根物体
        2.抽屉管理自己的抽屉根物体
        3.失活时建立父子关系,激活活时断开父子关系

        具体实现:
        1.先实现将所有对象放入柜子根物体中
        2.再实现将对象放入对应的抽屉根物体中
          用面向对象的思想将抽屉相关数据行为封装起来

        3.父子关系的设置成为一个可选项,避免频繁设置父子关系,消耗内存

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 抽屉(池子中的数据)对象
/// </summary>
public class PoolData
{
    //用来存储抽屉中的对象
    private Stack<GameObject> dataStack = new Stack<GameObject>();
    //抽屉根对象 用来进行布局管理的对象
    private GameObject rootObj;

    //获取容器中是否有对象
    public int Count => dataStack.Count;

    /// <summary>
    /// 初始化构造函数
    /// </summary>
    /// <param name="root">柜子(缓存池)父对象</param>
    /// <param name="name">抽屉父对象的名字</param>
    public PoolData(GameObject root, string name)
    {
        //开启功能时 才会动态创建 建立父子关系
        if(PoolMgr.isOpenLayout)
        {
            //创建抽屉父对象
            rootObj = new GameObject(name);
            //和柜子父对象建立父子关系
            rootObj.transform.SetParent(root.transform);
        }
        
    }

    /// <summary>
    /// 从抽屉中弹出数据对象
    /// </summary>
    /// <returns>想要的对象数据</returns>
    public GameObject Pop()
    {
        //取出对象
        GameObject obj = dataStack.Pop();
        //激活对象
        obj.SetActive(true);
        //断开父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(null);

        return obj;
    }

    /// <summary>
    /// 将物体放入到抽屉对象中
    /// </summary>
    /// <param name="obj"></param>
    public void Push(GameObject obj)
    {
        //失活放入抽屉的对象
        obj.SetActive(false);
        //放入对应抽屉的根物体中 建立父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(rootObj.transform);
        //通过栈记录对应的对象数据
        dataStack.Push(obj);
    }

}

/// <summary>
/// 缓存池(对象池)模块 管理器
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
{
    //柜子容器当中有抽屉的体现
    //值 其实代表的就是一个 抽屉对象
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

    //池子根对象
    private GameObject poolObj;

    //是否开启布局功能
    public static bool isOpenLayout = false;

    private PoolMgr() { }

    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public GameObject GetObj(string name)
    {
        GameObject obj;
        //有抽屉 并且 抽屉里 有对象 才去直接拿
        if(poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        {
            //弹出栈中的对象 直接返回给外部使用
            obj = poolDic[name].Pop();
        }
        //否则,就应该去创造
        else
        {
            //没有的时候 通过资源加载 去实例化出一个GameObject
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //避免实例化出来的对象 默认会在名字后面加一个(Clone)
            //我们重命名过后 方便往里面放
            obj.name = name;
        }

        return obj;
    }


    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象</param>
    public void PushObj(GameObject obj)
    {
        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");

        #region 因为失活 父子关系都放入了 抽屉对象中处理 所以不需要再处理这些内容了
        总之,目的就是要把对象隐藏起来
        并不是直接移除对象 而是将对象失活 一会儿再用 用的时候再激活它
        除了这种方式,还可以把对象放倒屏幕外看不见的地方
        //obj.SetActive(false);

        把失活的对象(要放入抽屉中的对象) 父对象先设置为 柜子(缓存池)根对象
        //obj.transform.SetParent(poolObj.transform);
        #endregion

        //没有抽屉 创建抽屉
        if (!poolDic.ContainsKey(obj.name))
            poolDic.Add(obj.name, new PoolData(poolObj, obj.name));

        //往抽屉当中放对象
        poolDic[obj.name].Push(obj);

        如果存在对应的抽屉容器 直接放
        //if(poolDic.ContainsKey(name))
        //{
        //    //往栈(抽屉)中放入对象
        //    poolDic[name].Push(obj);
        //}
        否则 需要先创建抽屉 再放
        //else
        //{
        //    //先创建抽屉
        //    poolDic.Add(name, new Stack<GameObject>());
        //    //再往抽屉里面放
        //    poolDic[name].Push(obj);
        //}
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据 
    /// 使用场景 主要是 切场景时
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
        poolObj = null;
    }
}

对象上限优化

        目前我们制作的缓存池模块
        理论上来说,当动态创建的对象长时间不放回抽屉
        每次从缓存池中动态获取对象时,会不停的新建对象
        那么也就是对象的数量是没有上限的
        场景上的某种对象可以存在n个

        而对象上限优化指的就是
        我们希望控制对象数量有上限
        对于不重要的资源我们没必要让其无限加量
        而是将“使用最久”的资源直接抢来用

        主要目的:
        更加彻底的复用资源
        对对象的数量上限加以限制
        可以优化内存空间,甚至优化性能(减少数量上限,可以减小渲染压力)

制作思路和具体实现

        制作思路:
        1.在抽屉里声明一个容器用来记录正在使用的资源
        2.每次获取对象时,传入一个抽屉最大容量值(可以给一个默认值)
        3.从缓存池中获取对象时就需要创建抽屉,用于记录当前使用着的对象
        4.每次取对象时应该分情况考虑
          情况1:没有抽屉时
          情况2:有抽屉,并且抽屉里有没用的对象或者使用中对象超过上限时
          情况3:有抽屉,但是抽屉里没有对象,使用中对象也没有超过上限时
        4.每次放回对象时
          由于记录了正在使用的资源,因此每次放入抽屉时还需要从记录容器中移除对象

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 抽屉(池子中的数据)对象
/// </summary>
public class PoolData
{
    //用来存储抽屉中的对象 记录的是没有使用的对象
    private Stack<GameObject> dataStack = new Stack<GameObject>();

    //用来记录使用中的对象的 
    private List<GameObject> usedList = new List<GameObject>();


    //抽屉根对象 用来进行布局管理的对象
    private GameObject rootObj;

    //获取容器中是否有对象
    public int Count => dataStack.Count;

    public int UsedCount => usedList.Count;

    /// <summary>
    /// 初始化构造函数
    /// </summary>
    /// <param name="root">柜子(缓存池)父对象</param>
    /// <param name="name">抽屉父对象的名字</param>
    public PoolData(GameObject root, string name, GameObject usedObj)
    {
        //开启功能时 才会动态创建 建立父子关系
        if(PoolMgr.isOpenLayout)
        {
            //创建抽屉父对象
            rootObj = new GameObject(name);
            //和柜子父对象建立父子关系
            rootObj.transform.SetParent(root.transform);
        }

        //创建抽屉时 外部肯定是会动态创建一个对象的
        //我们应该将其记录到 使用中的对象容器中
        PushUsedList(usedObj);
    }

    /// <summary>
    /// 从抽屉中弹出数据对象
    /// </summary>
    /// <returns>想要的对象数据</returns>
    public GameObject Pop()
    {
        //取出对象
        GameObject obj;

        if (Count > 0)
        {
            //从没有的容器当中取出使用
            obj = dataStack.Pop();
            //现在要使用了 应该要用使用中的容器记录它
            usedList.Add(obj);
        }
        else
        {
            //取0索引的对象 代表的就是使用时间最长的对象
            obj = usedList[0];
            //并且把它从使用着的对象中移除
            usedList.RemoveAt(0);
            //由于它还要拿出去用,所以我们应该把它又记录到 使用中的容器中去 
            //并且添加到尾部 表示 比较新的开始
            usedList.Add(obj);
        }

        //激活对象
        obj.SetActive(true);
        //断开父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(null);

        return obj;
    }

    /// <summary>
    /// 将物体放入到抽屉对象中
    /// </summary>
    /// <param name="obj"></param>
    public void Push(GameObject obj)
    {
        //失活放入抽屉的对象
        obj.SetActive(false);
        //放入对应抽屉的根物体中 建立父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(rootObj.transform);
        //通过栈记录对应的对象数据
        dataStack.Push(obj);
        //这个对象已经不再使用了 应该把它从记录容器中移除
        usedList.Remove(obj);
    }


    /// <summary>
    /// 将对象压入到使用中的容器中记录
    /// </summary>
    /// <param name="obj"></param>
    public void PushUsedList(GameObject obj)
    {
        usedList.Add(obj);
    }
}

/// <summary>
/// 缓存池(对象池)模块 管理器
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
{
    //柜子容器当中有抽屉的体现
    //值 其实代表的就是一个 抽屉对象
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

    //池子根对象
    private GameObject poolObj;

    //是否开启布局功能
    public static bool isOpenLayout = false;

    private PoolMgr() { }

    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public GameObject GetObj(string name, int maxNum = 50)
    {
        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");
        
        GameObject obj;

        #region 加入了数量上限后的逻辑判断
        if(!poolDic.ContainsKey(name) ||
            (poolDic[name].Count == 0 && poolDic[name].UsedCount < maxNum))
        {
            //动态创建对象
            //没有的时候 通过资源加载 去实例化出一个GameObject
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //避免实例化出来的对象 默认会在名字后面加一个(Clone)
            //我们重命名过后 方便往里面放
            obj.name = name;

            //创建抽屉
            if(!poolDic.ContainsKey(name))
                poolDic.Add(name, new PoolData(poolObj, name, obj));
            else//实例化出来的对象 需要记录到使用中的对象容器中
                poolDic[name].PushUsedList(obj);
        }
        //当抽屉中有对象 或者 使用中的对象超上限了 直接去取出来用
        else
        {
            obj = poolDic[name].Pop();
        }

        #endregion


        #region 没有加入 上限时的逻辑
        有抽屉 并且 抽屉里 有对象 才去直接拿
        //if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        //{
        //    //弹出栈中的对象 直接返回给外部使用
        //    obj = poolDic[name].Pop();
        //}
        否则,就应该去创造
        //else
        //{
        //    //没有的时候 通过资源加载 去实例化出一个GameObject
        //    obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
        //    //避免实例化出来的对象 默认会在名字后面加一个(Clone)
        //    //我们重命名过后 方便往里面放
        //    obj.name = name;
        //}
        #endregion
        return obj;
    }


    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象</param>
    public void PushObj(GameObject obj)
    {

        #region 因为失活 父子关系都放入了 抽屉对象中处理 所以不需要再处理这些内容了
        总之,目的就是要把对象隐藏起来
        并不是直接移除对象 而是将对象失活 一会儿再用 用的时候再激活它
        除了这种方式,还可以把对象放倒屏幕外看不见的地方
        //obj.SetActive(false);

        把失活的对象(要放入抽屉中的对象) 父对象先设置为 柜子(缓存池)根对象
        //obj.transform.SetParent(poolObj.transform);
        #endregion

        //没有抽屉 创建抽屉
        //if (!poolDic.ContainsKey(obj.name))
        //    poolDic.Add(obj.name, new PoolData(poolObj, obj.name));

        //往抽屉当中放对象
        poolDic[obj.name].Push(obj);

        如果存在对应的抽屉容器 直接放
        //if(poolDic.ContainsKey(name))
        //{
        //    //往栈(抽屉)中放入对象
        //    poolDic[name].Push(obj);
        //}
        否则 需要先创建抽屉 再放
        //else
        //{
        //    //先创建抽屉
        //    poolDic.Add(name, new Stack<GameObject>());
        //    //再往抽屉里面放
        //    poolDic[name].Push(obj);
        //}
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据 
    /// 使用场景 主要是 切场景时
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
        poolObj = null;
    }
}

补充

进一步封装,通过单独挂载脚本控制缓冲池对象数量

单独挂载的脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 该脚本主要用于挂载到需要使用缓存池功能的预设体对象上
/// </summary>
public class PoolObj : MonoBehaviour
{
    public int maxNum;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 抽屉(池子中的数据)对象
/// </summary>
public class PoolData
{
    //用来存储抽屉中的对象 记录的是没有使用的对象
    private Stack<GameObject> dataStack = new Stack<GameObject>();

    //用来记录使用中的对象的 
    private List<GameObject> usedList = new List<GameObject>();

    //抽屉上限 场景上同时存在的对象的上限个数
    private int maxNum;

    //抽屉根对象 用来进行布局管理的对象
    private GameObject rootObj;

    //获取容器中是否有对象
    public int Count => dataStack.Count;

    public int UsedCount => usedList.Count;

    /// <summary>
    /// 进行使用中对象数量和最大容量进行比较 小于返回true 需要实例化
    /// </summary>
    public bool NeedCreate => usedList.Count < maxNum;

    /// <summary>
    /// 初始化构造函数
    /// </summary>
    /// <param name="root">柜子(缓存池)父对象</param>
    /// <param name="name">抽屉父对象的名字</param>
    public PoolData(GameObject root, string name, GameObject usedObj)
    {
        //开启功能时 才会动态创建 建立父子关系
        if(PoolMgr.isOpenLayout)
        {
            //创建抽屉父对象
            rootObj = new GameObject(name);
            //和柜子父对象建立父子关系
            rootObj.transform.SetParent(root.transform);
        }

        //创建抽屉时 外部肯定是会动态创建一个对象的
        //我们应该将其记录到 使用中的对象容器中
        PushUsedList(usedObj);

        PoolObj poolObj = usedObj.GetComponent<PoolObj>();
        if (poolObj == null)
        {
            Debug.LogError("请为使用缓存池功能的预设体对象挂载PoolObj脚本 用于设置数量上限");
            return;
        }
        //记录上限数量值
        maxNum = poolObj.maxNum;
    }

    /// <summary>
    /// 从抽屉中弹出数据对象
    /// </summary>
    /// <returns>想要的对象数据</returns>
    public GameObject Pop()
    {
        //取出对象
        GameObject obj;

        if (Count > 0)
        {
            //从没有的容器当中取出使用
            obj = dataStack.Pop();
            //现在要使用了 应该要用使用中的容器记录它
            usedList.Add(obj);
        }
        else
        {
            //取0索引的对象 代表的就是使用时间最长的对象
            obj = usedList[0];
            //并且把它从使用着的对象中移除
            usedList.RemoveAt(0);
            //由于它还要拿出去用,所以我们应该把它又记录到 使用中的容器中去 
            //并且添加到尾部 表示 比较新的开始
            usedList.Add(obj);
        }

        //激活对象
        obj.SetActive(true);
        //断开父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(null);

        return obj;
    }

    /// <summary>
    /// 将物体放入到抽屉对象中
    /// </summary>
    /// <param name="obj"></param>
    public void Push(GameObject obj)
    {
        //失活放入抽屉的对象
        obj.SetActive(false);
        //放入对应抽屉的根物体中 建立父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(rootObj.transform);
        //通过栈记录对应的对象数据
        dataStack.Push(obj);
        //这个对象已经不再使用了 应该把它从记录容器中移除
        usedList.Remove(obj);
    }


    /// <summary>
    /// 将对象压入到使用中的容器中记录
    /// </summary>
    /// <param name="obj"></param>
    public void PushUsedList(GameObject obj)
    {
        usedList.Add(obj);
    }
}

/// <summary>
/// 方便在字典当中用里式替换原则 存储子类对象
/// </summary>
public abstract class PoolObjectBase { }

/// <summary>
/// 用于存储 数据结构类 和 逻辑类 (不继承mono的)容器类
/// </summary>
/// <typeparam name="T"></typeparam>
public class PoolObject<T> : PoolObjectBase where T:class
{
    public Queue<T> poolObjs = new Queue<T>();
}

/// <summary>
/// 想要被复用的 数据结构类、逻辑类 都必须要继承该接口
/// </summary>
public interface IPoolObject
{
    /// <summary>
    /// 重置数据的方法
    /// </summary>
    void ResetInfo();
}

/// <summary>
/// 缓存池(对象池)模块 管理器
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
{
    //柜子容器当中有抽屉的体现
    //值 其实代表的就是一个 抽屉对象
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

    /// <summary>
    /// 用于存储数据结构类、逻辑类对象的 池子的字典容器
    /// </summary>
    private Dictionary<string, PoolObjectBase> poolObjectDic = new Dictionary<string, PoolObjectBase>();

    //池子根对象
    private GameObject poolObj;

    //是否开启布局功能
    public static bool isOpenLayout = true;

    private PoolMgr() {

        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");

    }

    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public GameObject GetObj(string name)
    {
        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");

        GameObject obj;

        #region 加入了数量上限后的逻辑判断
        if(!poolDic.ContainsKey(name) ||
            (poolDic[name].Count == 0 && poolDic[name].NeedCreate))
        {
            //动态创建对象
            //没有的时候 通过资源加载 去实例化出一个GameObject
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //避免实例化出来的对象 默认会在名字后面加一个(Clone)
            //我们重命名过后 方便往里面放
            obj.name = name;

            //创建抽屉
            if(!poolDic.ContainsKey(name))
                poolDic.Add(name, new PoolData(poolObj, name, obj));
            else//实例化出来的对象 需要记录到使用中的对象容器中
                poolDic[name].PushUsedList(obj);
        }
        //当抽屉中有对象 或者 使用中的对象超上限了 直接去取出来用
        else
        {
            obj = poolDic[name].Pop();
        }

        #endregion


        #region 没有加入 上限时的逻辑
        有抽屉 并且 抽屉里 有对象 才去直接拿
        //if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        //{
        //    //弹出栈中的对象 直接返回给外部使用
        //    obj = poolDic[name].Pop();
        //}
        否则,就应该去创造
        //else
        //{
        //    //没有的时候 通过资源加载 去实例化出一个GameObject
        //    obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
        //    //避免实例化出来的对象 默认会在名字后面加一个(Clone)
        //    //我们重命名过后 方便往里面放
        //    obj.name = name;
        //}
        #endregion
        return obj;
    }

    /// <summary>
    /// 获取自定义的数据结构类和逻辑类对象 (不继承Mono的)
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <returns></returns>
    public T GetObj<T>(string nameSpace = "") where T:class,IPoolObject,new()
    {
        //池子的名字 是根据类的类型来决定的 就是它的类名
        string poolName = nameSpace + "_" + typeof(T).Name;
        //有池子
        if(poolObjectDic.ContainsKey(poolName))
        {
            PoolObject<T> pool = poolObjectDic[poolName] as PoolObject<T>;
            //池子当中是否有可以复用的内容
            if(pool.poolObjs.Count > 0)
            {
                //从队列中取出对象 进行复用
                T obj = pool.poolObjs.Dequeue() as T;
                return obj;
            }
            //池子当中是空的
            else
            {
                //必须保证存在无参构造函数
                T obj = new T();
                return obj;
            }
        }
        else//没有池子
        {
            T obj = new T();
            return obj;
        }
        
    }

    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象</param>
    public void PushObj(GameObject obj)
    {
        #region 因为失活 父子关系都放入了 抽屉对象中处理 所以不需要再处理这些内容了
        总之,目的就是要把对象隐藏起来
        并不是直接移除对象 而是将对象失活 一会儿再用 用的时候再激活它
        除了这种方式,还可以把对象放倒屏幕外看不见的地方
        //obj.SetActive(false);

        把失活的对象(要放入抽屉中的对象) 父对象先设置为 柜子(缓存池)根对象
        //obj.transform.SetParent(poolObj.transform);
        #endregion

        //没有抽屉 创建抽屉
        //if (!poolDic.ContainsKey(obj.name))
        //    poolDic.Add(obj.name, new PoolData(poolObj, obj.name));

        //往抽屉当中放对象
        poolDic[obj.name].Push(obj);

        如果存在对应的抽屉容器 直接放
        //if(poolDic.ContainsKey(name))
        //{
        //    //往栈(抽屉)中放入对象
        //    poolDic[name].Push(obj);
        //}
        否则 需要先创建抽屉 再放
        //else
        //{
        //    //先创建抽屉
        //    poolDic.Add(name, new Stack<GameObject>());
        //    //再往抽屉里面放
        //    poolDic[name].Push(obj);
        //}
    }

    /// <summary>
    /// 将自定义数据结构类和逻辑类 放入池子中
    /// </summary>
    /// <typeparam name="T">对应类型</typeparam>
    public void PushObj<T>(T obj, string nameSpace = "") where T:class,IPoolObject
    {
        //如果想要压入null对象 是不被允许的
        if (obj == null)
            return;
        //池子的名字 是根据类的类型来决定的 就是它的类名
        string poolName = nameSpace + "_" + typeof(T).Name;
        //有池子
        PoolObject<T> pool;
        if (poolObjectDic.ContainsKey(poolName))
            //取出池子 压入对象
            pool = poolObjectDic[poolName] as PoolObject<T>;
        else//没有池子
        {
            pool = new PoolObject<T>();
            poolObjectDic.Add(poolName, pool);
        }
        //在放入池子中之前 先重置对象的数据
        obj.ResetInfo();
        pool.poolObjs.Enqueue(obj);
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据 
    /// 使用场景 主要是 切场景时
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
        poolObj = null;
        poolObjectDic.Clear();
    }
}

缓存池模块(数据结构类、逻辑类)优化 

主要目的

        我们目前实现的缓存池
        主要是针对场景上的GameObject对象的
        我们只能对场景上的对象进行缓存池功能处理

        但是在游戏开发中,还会存在大量的不需要挂载到场景上的实例化对象
        比如一些数据结构类、逻辑类,它们并不依附于场景上的对象,而仅仅是被引用
        举例:
        一个自定义数据结构类
        TestData t = new TestData();
        当我们不使用它时,往往会将其置空
        t = null;
        下次又要使用时,再new
        t = new TestData();
        那么对于一些频繁使用的数据结构类或逻辑类
        这样做也会产生大量的垃圾
        因此我们完全可以修改缓存池模块,让其也支持对不挂载的类对象也进行复用

        说人话
        缓存池模块(数据结构类、逻辑类)优化 主要目的是
        让缓存池支持回收复用不继承MonoBehaviour(不挂载GameObject)的类对象

具体实现

        主要思路:
        1.修改缓存池管理器PoolMgr
        2.添加一个新池子容器,专门用来记录不继承MonoBehaviour(不挂载GameObject)的类对象
        3.提供专门的方法供外部使用
          3 - 1.从池子中获取对象的方法
          3 - 2.压入池子中的方法
          3 - 3.清空池子容器的方法

        注意:由于这些自定义数据或逻辑类中可能有对其他内容的引用
             因此需要让其有一个重置数据的方法,在压入池子时重置数据

        关键点:
        1.池子容器父类 里式替换 父类装子类
        2.池子容器子类 泛型类 确定对象类型
        3.对象父接口,用于实现重置数据方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 抽屉(池子中的数据)对象
/// </summary>
public class PoolData
{
    //用来存储抽屉中的对象 记录的是没有使用的对象
    private Stack<GameObject> dataStack = new Stack<GameObject>();

    //用来记录使用中的对象的 
    private List<GameObject> usedList = new List<GameObject>();

    //抽屉上限 场景上同时存在的对象的上限个数
    private int maxNum;

    //抽屉根对象 用来进行布局管理的对象
    private GameObject rootObj;

    //获取容器中是否有对象
    public int Count => dataStack.Count;

    public int UsedCount => usedList.Count;

    /// <summary>
    /// 进行使用中对象数量和最大容量进行比较 小于返回true 需要实例化
    /// </summary>
    public bool NeedCreate => usedList.Count < maxNum;

    /// <summary>
    /// 初始化构造函数
    /// </summary>
    /// <param name="root">柜子(缓存池)父对象</param>
    /// <param name="name">抽屉父对象的名字</param>
    public PoolData(GameObject root, string name, GameObject usedObj)
    {
        //开启功能时 才会动态创建 建立父子关系
        if(PoolMgr.isOpenLayout)
        {
            //创建抽屉父对象
            rootObj = new GameObject(name);
            //和柜子父对象建立父子关系
            rootObj.transform.SetParent(root.transform);
        }

        //创建抽屉时 外部肯定是会动态创建一个对象的
        //我们应该将其记录到 使用中的对象容器中
        PushUsedList(usedObj);

        PoolObj poolObj = usedObj.GetComponent<PoolObj>();
        if (poolObj == null)
        {
            Debug.LogError("请为使用缓存池功能的预设体对象挂载PoolObj脚本 用于设置数量上限");
            return;
        }
        //记录上限数量值
        maxNum = poolObj.maxNum;
    }

    /// <summary>
    /// 从抽屉中弹出数据对象
    /// </summary>
    /// <returns>想要的对象数据</returns>
    public GameObject Pop()
    {
        //取出对象
        GameObject obj;

        if (Count > 0)
        {
            //从没有的容器当中取出使用
            obj = dataStack.Pop();
            //现在要使用了 应该要用使用中的容器记录它
            usedList.Add(obj);
        }
        else
        {
            //取0索引的对象 代表的就是使用时间最长的对象
            obj = usedList[0];
            //并且把它从使用着的对象中移除
            usedList.RemoveAt(0);
            //由于它还要拿出去用,所以我们应该把它又记录到 使用中的容器中去 
            //并且添加到尾部 表示 比较新的开始
            usedList.Add(obj);
        }

        //激活对象
        obj.SetActive(true);
        //断开父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(null);

        return obj;
    }

    /// <summary>
    /// 将物体放入到抽屉对象中
    /// </summary>
    /// <param name="obj"></param>
    public void Push(GameObject obj)
    {
        //失活放入抽屉的对象
        obj.SetActive(false);
        //放入对应抽屉的根物体中 建立父子关系
        if (PoolMgr.isOpenLayout)
            obj.transform.SetParent(rootObj.transform);
        //通过栈记录对应的对象数据
        dataStack.Push(obj);
        //这个对象已经不再使用了 应该把它从记录容器中移除
        usedList.Remove(obj);
    }


    /// <summary>
    /// 将对象压入到使用中的容器中记录
    /// </summary>
    /// <param name="obj"></param>
    public void PushUsedList(GameObject obj)
    {
        usedList.Add(obj);
    }
}

/// <summary>
/// 方便在字典当中用里式替换原则 存储子类对象
/// </summary>
public abstract class PoolObjectBase { }

/// <summary>
/// 用于存储 数据结构类 和 逻辑类 (不继承mono的)容器类
/// </summary>
/// <typeparam name="T"></typeparam>
public class PoolObject<T> : PoolObjectBase where T:class
{
    public Queue<T> poolObjs = new Queue<T>();
}

/// <summary>
/// 想要被复用的 数据结构类、逻辑类 都必须要继承该接口
/// </summary>
public interface IPoolObject
{
    /// <summary>
    /// 重置数据的方法
    /// </summary>
    void ResetInfo();
}

/// <summary>
/// 缓存池(对象池)模块 管理器
/// </summary>
public class PoolMgr : BaseManager<PoolMgr>
{
    //柜子容器当中有抽屉的体现
    //值 其实代表的就是一个 抽屉对象
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();

    /// <summary>
    /// 用于存储数据结构类、逻辑类对象的 池子的字典容器
    /// </summary>
    private Dictionary<string, PoolObjectBase> poolObjectDic = new Dictionary<string, PoolObjectBase>();

    //池子根对象
    private GameObject poolObj;

    //是否开启布局功能
    public static bool isOpenLayout = true;

    private PoolMgr() {

        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");

    }

    /// <summary>
    /// 拿东西的方法
    /// </summary>
    /// <param name="name">抽屉容器的名字</param>
    /// <returns>从缓存池中取出的对象</returns>
    public GameObject GetObj(string name)
    {
        //如果根物体为空 就创建
        if (poolObj == null && isOpenLayout)
            poolObj = new GameObject("Pool");

        GameObject obj;

        #region 加入了数量上限后的逻辑判断
        if(!poolDic.ContainsKey(name) ||
            (poolDic[name].Count == 0 && poolDic[name].NeedCreate))
        {
            //动态创建对象
            //没有的时候 通过资源加载 去实例化出一个GameObject
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //避免实例化出来的对象 默认会在名字后面加一个(Clone)
            //我们重命名过后 方便往里面放
            obj.name = name;

            //创建抽屉
            if(!poolDic.ContainsKey(name))
                poolDic.Add(name, new PoolData(poolObj, name, obj));
            else//实例化出来的对象 需要记录到使用中的对象容器中
                poolDic[name].PushUsedList(obj);
        }
        //当抽屉中有对象 或者 使用中的对象超上限了 直接去取出来用
        else
        {
            obj = poolDic[name].Pop();
        }

        #endregion


        #region 没有加入 上限时的逻辑
        有抽屉 并且 抽屉里 有对象 才去直接拿
        //if (poolDic.ContainsKey(name) && poolDic[name].Count > 0)
        //{
        //    //弹出栈中的对象 直接返回给外部使用
        //    obj = poolDic[name].Pop();
        //}
        否则,就应该去创造
        //else
        //{
        //    //没有的时候 通过资源加载 去实例化出一个GameObject
        //    obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
        //    //避免实例化出来的对象 默认会在名字后面加一个(Clone)
        //    //我们重命名过后 方便往里面放
        //    obj.name = name;
        //}
        #endregion
        return obj;
    }

    /// <summary>
    /// 获取自定义的数据结构类和逻辑类对象 (不继承Mono的)
    /// </summary>
    /// <typeparam name="T">数据类型</typeparam>
    /// <returns></returns>
    public T GetObj<T>(string nameSpace = "") where T:class,IPoolObject,new()
    {
        //池子的名字 是根据类的类型来决定的 就是它的类名
        string poolName = nameSpace + "_" + typeof(T).Name;
        //有池子
        if(poolObjectDic.ContainsKey(poolName))
        {
            PoolObject<T> pool = poolObjectDic[poolName] as PoolObject<T>;
            //池子当中是否有可以复用的内容
            if(pool.poolObjs.Count > 0)
            {
                //从队列中取出对象 进行复用
                T obj = pool.poolObjs.Dequeue() as T;
                return obj;
            }
            //池子当中是空的
            else
            {
                //必须保证存在无参构造函数
                T obj = new T();
                return obj;
            }
        }
        else//没有池子
        {
            T obj = new T();
            return obj;
        }
        
    }

    /// <summary>
    /// 往缓存池中放入对象
    /// </summary>
    /// <param name="name">抽屉(对象)的名字</param>
    /// <param name="obj">希望放入的对象</param>
    public void PushObj(GameObject obj)
    {
        #region 因为失活 父子关系都放入了 抽屉对象中处理 所以不需要再处理这些内容了
        总之,目的就是要把对象隐藏起来
        并不是直接移除对象 而是将对象失活 一会儿再用 用的时候再激活它
        除了这种方式,还可以把对象放倒屏幕外看不见的地方
        //obj.SetActive(false);

        把失活的对象(要放入抽屉中的对象) 父对象先设置为 柜子(缓存池)根对象
        //obj.transform.SetParent(poolObj.transform);
        #endregion

        //没有抽屉 创建抽屉
        //if (!poolDic.ContainsKey(obj.name))
        //    poolDic.Add(obj.name, new PoolData(poolObj, obj.name));

        //往抽屉当中放对象
        poolDic[obj.name].Push(obj);

        如果存在对应的抽屉容器 直接放
        //if(poolDic.ContainsKey(name))
        //{
        //    //往栈(抽屉)中放入对象
        //    poolDic[name].Push(obj);
        //}
        否则 需要先创建抽屉 再放
        //else
        //{
        //    //先创建抽屉
        //    poolDic.Add(name, new Stack<GameObject>());
        //    //再往抽屉里面放
        //    poolDic[name].Push(obj);
        //}
    }

    /// <summary>
    /// 将自定义数据结构类和逻辑类 放入池子中
    /// </summary>
    /// <typeparam name="T">对应类型</typeparam>
    public void PushObj<T>(T obj, string nameSpace = "") where T:class,IPoolObject
    {
        //如果想要压入null对象 是不被允许的
        if (obj == null)
            return;
        //池子的名字 是根据类的类型来决定的 就是它的类名
        string poolName = nameSpace + "_" + typeof(T).Name;
        //有池子
        PoolObject<T> pool;
        if (poolObjectDic.ContainsKey(poolName))
            //取出池子 压入对象
            pool = poolObjectDic[poolName] as PoolObject<T>;
        else//没有池子
        {
            pool = new PoolObject<T>();
            poolObjectDic.Add(poolName, pool);
        }
        //在放入池子中之前 先重置对象的数据
        obj.ResetInfo();
        pool.poolObjs.Enqueue(obj);
    }

    /// <summary>
    /// 用于清除整个柜子当中的数据 
    /// 使用场景 主要是 切场景时
    /// </summary>
    public void ClearPool()
    {
        poolDic.Clear();
        poolObj = null;
        poolObjectDic.Clear();
    }
}

事件中心模块

        事件中心的主要作用
        解耦程序模块,降低程序耦合度
        它可以降低游戏中不同模块的耦合度
        不需要直接引用或依赖于彼此的具体实现

        事件中心的基本原理
        利用字典和委托相关的知识点
        再结合观察者设计模式的基本原理
        实现一个中心化的机制
        使得多个系统、模块、对象之间可以进行松耦合的通信

实现事件中心模块

        1.创建EventCenter继承 不继承MonoBehaviour的单例模式基类
        2.声明管理事件用容器
        3.实现关键方法
          触发(分发)事件 方法
          添加事件监听者 方法
          移除事件监听者 方法
          清除所有事件监听者 方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 事件中心模块 
/// </summary>
public class EventCenter : BaseManager<EventCenter>
{
    //用于记录对应事件 关联的 对应的逻辑
    private Dictionary<string, UnityAction> eventDic = new Dictionary<string, UnityAction>();

    private EventCenter() { }

    /// <summary>
    /// 触发事件 
    /// </summary>
    /// <param name="eventName">事件名字</param>
    public void EventTrigger(string eventName)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if(eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            eventDic[eventName]?.Invoke();
        }
    }

    /// <summary>
    /// 添加事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void AddEventListener(string eventName, UnityAction func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
            eventDic[eventName] += func;
        else
        {
            eventDic.Add(eventName, null);
            eventDic[eventName] += func;
        }
            
    }

    /// <summary>
    /// 移除事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void RemoveEventListener(string eventName, UnityAction func)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic[eventName] -= func;
    }

    /// <summary>
    /// 清空所有事件的监听
    /// </summary>
    public void Clear()
    {
        eventDic.Clear();
    }

    /// <summary>
    /// 清除指定某一个事件的所有监听
    /// </summary>
    /// <param name="eventName"></param>
    public void Claer(string eventName)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic.Remove(eventName);
    }
}

事件中心中传递参数:object

        希望在触发事件时传递数据
        比如
        怪物死亡时,将怪物信息传递出去
        获取奖励时,将奖励信息传递出去
        等等

        传递数据的主要目的是
        我们可以在各系统、模块、对象中获取到我们希望获取的有用信息

制作思路和具体实现

        我们目前触发事件,是通过执行委托中存储的函数来执行各系统对应的逻辑
        那么需要传递参数,我们很自然的联想到应该从委托入手

        但是参数的类型可能有多种多样,我们可以采用万物之父 object 利用里式替换原则
        父类容器装载子类对象的方式来传递参数

缺点:使用object进行参数传递,当传递值类型数据时,会存在装箱拆箱,增加性能开销

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 事件中心模块 
/// </summary>
public class EventCenter : BaseManager<EventCenter>
{
    //用于记录对应事件 关联的 对应的逻辑
    private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>();

    private EventCenter() { }

    /// <summary>
    /// 触发事件 
    /// </summary>
    /// <param name="eventName">事件名字</param>
    public void EventTrigger(string eventName, object info = null)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if(eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            eventDic[eventName]?.Invoke(info);
        }
    }

    /// <summary>
    /// 添加事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void AddEventListener(string eventName, UnityAction<object> func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
            eventDic[eventName] += func;
        else
        {
            eventDic.Add(eventName, null);
            eventDic[eventName] += func;
        }
            
    }

    /// <summary>
    /// 移除事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void RemoveEventListener(string eventName, UnityAction<object> func)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic[eventName] -= func;
    }

    /// <summary>
    /// 清空所有事件的监听
    /// </summary>
    public void Clear()
    {
        eventDic.Clear();
    }

    /// <summary>
    /// 清除指定某一个事件的所有监听
    /// </summary>
    /// <param name="eventName"></param>
    public void Claer(string eventName)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic.Remove(eventName);
    }
}

事件中心中传递参数:泛型

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 用于 里式替换原则 装载 子类的父类
/// </summary>
public abstract class EventInfoBase{ }

/// <summary>
/// 用来包裹 对应观察者 函数委托的 类
/// </summary>
/// <typeparam name="T"></typeparam>
public class EventInfo<T>:EventInfoBase
{
    //真正观察者 对应的 函数信息 记录在其中
    public UnityAction<T> actions;

    public EventInfo(UnityAction<T> action)
    {
        actions += action;
    }
}

/// <summary>
/// 主要用来记录无参无返回值委托
/// </summary>
public class EventInfo: EventInfoBase
{
    public UnityAction actions;
     
    public EventInfo(UnityAction action)
    {
        actions += action;
    }
}


/// <summary>
/// 事件中心模块 
/// </summary>
public class EventCenter: BaseManager<EventCenter>
{
    //用于记录对应事件 关联的 对应的逻辑
    private Dictionary<string, EventInfoBase> eventDic = new Dictionary<string, EventInfoBase>();

    private EventCenter() { }

    /// <summary>
    /// 触发事件 
    /// </summary>
    /// <param name="eventName">事件名字</param>
    public void EventTrigger<T>(string eventName, T info)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if(eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            (eventDic[eventName] as EventInfo<T>).actions?.Invoke(info);
        }
    }

    /// <summary>
    /// 触发事件 无参数
    /// </summary>
    /// <param name="eventName"></param>
    public void EventTrigger(string eventName)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if (eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            (eventDic[eventName] as EventInfo).actions?.Invoke();
        }
    }


    /// <summary>
    /// 添加事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void AddEventListener<T>(string eventName, UnityAction<T> func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
        {
            (eventDic[eventName] as EventInfo<T>).actions += func;
        }
        else
        {
            eventDic.Add(eventName, new EventInfo<T>(func));
        }
    }

    public void AddEventListener(string eventName, UnityAction func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
        {
            (eventDic[eventName] as EventInfo).actions += func;
        }
        else
        {
            eventDic.Add(eventName, new EventInfo(func));
        }
    }

    /// <summary>
    /// 移除事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void RemoveEventListener<T>(string eventName, UnityAction<T> func)
    {
        if (eventDic.ContainsKey(eventName))
            (eventDic[eventName] as EventInfo<T>).actions -= func;
    }

    public void RemoveEventListener(string eventName, UnityAction func)
    {
        if (eventDic.ContainsKey(eventName))
            (eventDic[eventName] as EventInfo).actions -= func;
    }

    /// <summary>
    /// 清空所有事件的监听
    /// </summary>
    public void Clear()
    {
        eventDic.Clear();
    }

    /// <summary>
    /// 清除指定某一个事件的所有监听
    /// </summary>
    /// <param name="eventName"></param>
    public void Claer(string eventName)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic.Remove(eventName);
    }
}

事件名优化

        目前我们通过 字符串 作为事件名来区分各事件
        明显的缺点:
        若触发或监听时,事件名 字符串 拼写错误会导致
        无法正确监听或触发事件

将事件名使用枚举进行统一管理

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 事件类型 枚举
/// </summary>
public enum E_EventType 
{
    /// <summary>
    /// 怪物死亡事件 —— 参数:Monster
    /// </summary>
    E_Monster_Dead,
    /// <summary>
    /// 玩家获取奖励 —— 参数:int
    /// </summary>
    E_Player_GetReward,
    /// <summary>
    /// 测试用事件 —— 参数:无
    /// </summary>
    E_Test,
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 用于 里式替换原则 装载 子类的父类
/// </summary>
public abstract class EventInfoBase{ }

/// <summary>
/// 用来包裹 对应观察者 函数委托的 类
/// </summary>
/// <typeparam name="T"></typeparam>
public class EventInfo<T>:EventInfoBase
{
    //真正观察者 对应的 函数信息 记录在其中
    public UnityAction<T> actions;

    public EventInfo(UnityAction<T> action)
    {
        actions += action;
    }
}

/// <summary>
/// 主要用来记录无参无返回值委托
/// </summary>
public class EventInfo: EventInfoBase
{
    public UnityAction actions;
     
    public EventInfo(UnityAction action)
    {
        actions += action;
    }
}


/// <summary>
/// 事件中心模块 
/// </summary>
public class EventCenter: BaseManager<EventCenter>
{
    //用于记录对应事件 关联的 对应的逻辑
    private Dictionary<E_EventType, EventInfoBase> eventDic = new Dictionary<E_EventType, EventInfoBase>();

    private EventCenter() { }

    /// <summary>
    /// 触发事件 
    /// </summary>
    /// <param name="eventName">事件名字</param>
    public void EventTrigger<T>(E_EventType eventName, T info)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if(eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            (eventDic[eventName] as EventInfo<T>).actions?.Invoke(info);
        }
    }

    /// <summary>
    /// 触发事件 无参数
    /// </summary>
    /// <param name="eventName"></param>
    public void EventTrigger(E_EventType eventName)
    {
        //存在关心我的人 才通知别人去处理逻辑
        if (eventDic.ContainsKey(eventName))
        {
            //去执行对应的逻辑
            (eventDic[eventName] as EventInfo).actions?.Invoke();
        }
    }


    /// <summary>
    /// 添加事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void AddEventListener<T>(E_EventType eventName, UnityAction<T> func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
        {
            (eventDic[eventName] as EventInfo<T>).actions += func;
        }
        else
        {
            eventDic.Add(eventName, new EventInfo<T>(func));
        }
    }

    public void AddEventListener(E_EventType eventName, UnityAction func)
    {
        //如果已经存在关心事件的委托记录 直接添加即可
        if (eventDic.ContainsKey(eventName))
        {
            (eventDic[eventName] as EventInfo).actions += func;
        }
        else
        {
            eventDic.Add(eventName, new EventInfo(func));
        }
    }

    /// <summary>
    /// 移除事件监听者
    /// </summary>
    /// <param name="eventName"></param>
    /// <param name="func"></param>
    public void RemoveEventListener<T>(E_EventType eventName, UnityAction<T> func)
    {
        if (eventDic.ContainsKey(eventName))
            (eventDic[eventName] as EventInfo<T>).actions -= func;
    }

    public void RemoveEventListener(E_EventType eventName, UnityAction func)
    {
        if (eventDic.ContainsKey(eventName))
            (eventDic[eventName] as EventInfo).actions -= func;
    }

    /// <summary>
    /// 清空所有事件的监听
    /// </summary>
    public void Clear()
    {
        eventDic.Clear();
    }

    /// <summary>
    /// 清除指定某一个事件的所有监听
    /// </summary>
    /// <param name="eventName"></param>
    public void Claer(E_EventType eventName)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic.Remove(eventName);
    }
}

 测试用代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Other : MonoBehaviour
{
    private void Awake()
    {
        EventCenter.Instance.AddEventListener<Monster>(E_EventType.E_Monster_Dead, OtherWaitMonsterDeadDo);
    }

    public void OtherWaitMonsterDeadDo(Monster info)
    {
        Debug.Log("其他相关处理" + info.monsterID);
    }

    private void OnDestroy()
    {
        EventCenter.Instance.RemoveEventListener<Monster>(E_EventType.E_Monster_Dead, OtherWaitMonsterDeadDo);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Monster : MonoBehaviour
{
    public string monsterName = "123123";
    public int monsterID = 1;

    // Start is called before the first frame update
    void Start()
    {
        //Dead();
    }

    public void Dead()
    {
        Debug.Log("怪物死亡了");
        //其他对象在怪物死亡时想做的事情
        EventCenter.Instance.EventTrigger<Monster>(E_EventType.E_Monster_Dead, this);
        比如
        1.任务更新
        //GameObject.Find("Task").GetComponent<Task>().TaskWaitMonsterDeadDo();
        2.玩家得奖励
        //GameObject.Find("Player").GetComponent<Player>().PlayerWaitMonsterDeadDo();
        3.其他相关系统
        //GameObject.Find("Other").GetComponent<Other>().OtherWaitMonsterDeadDo();
        n个其他相关处理
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    private void Awake()
    {
        EventCenter.Instance.AddEventListener<Monster>(E_EventType.E_Monster_Dead, PlayerWaitMonsterDeadDo);

        EventCenter.Instance.AddEventListener(E_EventType.E_Test, Test);
    }

    public void Test()
    {
        print("无参事件监听者");
    }

    public void PlayerWaitMonsterDeadDo(Monster info)
    {
        Debug.Log("玩家得奖励" + info.monsterName);
    }

    private void OnDestroy()
    {
        EventCenter.Instance.RemoveEventListener<Monster>(E_EventType.E_Monster_Dead, PlayerWaitMonsterDeadDo);

        EventCenter.Instance.RemoveEventListener(E_EventType.E_Test, Test);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Task : MonoBehaviour
{
    private void Awake()
    {
        EventCenter.Instance.AddEventListener<Monster>(E_EventType.E_Monster_Dead, TaskWaitMonsterDeadDo);
    }

    public void TaskWaitMonsterDeadDo(Monster info)
    {
        Debug.Log("任务记录" + info.monsterName);
    }

    private void OnDestroy()
    {
        EventCenter.Instance.RemoveEventListener<Monster>(E_EventType.E_Monster_Dead, TaskWaitMonsterDeadDo);
    }
}

Resources资源加载模块

        1.创建ResourcesMgr 继承 不继承MonoBehaviour的单例模式基类
        2.为它封装Resources异步加载资源的相关方法(主要目的 避免异步加载的代码冗余)
        3.为它封装Resources同步加载资源的相关方法(顺便封装)
        4.为它封装资源卸载相关方法(顺便封装)

代码中用到了公告mono模块实现异步加载

前置代码:自动挂载式的 继承Mono的单例模式基类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/// <summary>
/// 自动挂载式的 继承Mono的单例模式基类
/// 推荐使用 
/// 无需手动挂载 无需动态添加 无需关心切场景带来的问题
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonAutoMono<T> : MonoBehaviour where T:MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if(instance == null)
            {
                //动态创建 动态挂载
                //在场景上创建空物体
                GameObject obj = new GameObject();
                //得到T脚本的类名 为对象改名 这样再编辑器中可以明确的看到该
                //单例模式脚本对象依附的GameObject
                obj.name = typeof(T).ToString();
                //动态挂载对应的 单例模式脚本
                instance = obj.AddComponent<T>();
                //过场景时不移除对象 保证它在整个游戏生命周期中都存在
                DontDestroyOnLoad(obj);
            }
            return instance;
        }
    }

}

 公共Mono模块管理器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 公共Mono模块管理器
/// </summary>
public class MonoMgr : SingletonAutoMono<MonoMgr>
{
    private event UnityAction updateEvent;
    private event UnityAction fixedUpdateEvent;
    private event UnityAction lateUpdateEvent;

    /// <summary>
    /// 添加Update帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddUpdateListener(UnityAction updateFun)
    {
        updateEvent += updateFun;
    }

    /// <summary>
    /// 移除Update帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveUpdateListener(UnityAction updateFun)
    {
        updateEvent -= updateFun;
    }

    /// <summary>
    /// 添加FixedUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddFixedUpdateListener(UnityAction updateFun)
    {
        fixedUpdateEvent += updateFun;
    }
    /// <summary>
    /// 移除FixedUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveFixedUpdateListener(UnityAction updateFun)
    {
        fixedUpdateEvent -= updateFun;
    }

    /// <summary>
    /// 添加LateUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void AddLateUpdateListener(UnityAction updateFun)
    {
        lateUpdateEvent += updateFun;
    }

    /// <summary>
    /// 移除LateUpdate帧更新监听函数
    /// </summary>
    /// <param name="updateFun"></param>
    public void RemoveLateUpdateListener(UnityAction updateFun)
    {
        lateUpdateEvent -= updateFun;
    }


    private void Update()
    {
        updateEvent?.Invoke();
    }

    private void FixedUpdate()
    {
        fixedUpdateEvent?.Invoke();
    }

    private void LateUpdate()
    {
        lateUpdateEvent?.Invoke();
    }
}

资源加载模块 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// Resources 资源加载模块管理器
/// </summary>
public class ResMgr : BaseManager<ResMgr>
{
    private ResMgr() { }

    /// <summary>
    /// 同步加载Resources下资源的方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="path"></param>
    /// <returns></returns>
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        return Resources.Load<T>(path);
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T: UnityEngine.Object
    {
        //要通过协同程序去异步加载资源
        MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path, callBack));
    }

    private IEnumerator ReallyLoadAsync<T>(string path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        callBack(rq.asset as T);
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    public void LoadAsync(string path, Type type, UnityAction<UnityEngine.Object> callBack) 
    {
        //要通过协同程序去异步加载资源
        MonoMgr.Instance.StartCoroutine(ReallyLoadAsync(path, type, callBack));
    }

    private IEnumerator ReallyLoadAsync(string path, Type type, UnityAction<UnityEngine.Object> callBack)
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync(path, type);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        callBack(rq.asset);
    }

    /// <summary>
    /// 指定卸载一个资源
    /// </summary>
    /// <param name="assetToUnload"></param>
    public void UnloadAsset(UnityEngine.Object assetToUnload)
    {
        Resources.UnloadAsset(assetToUnload);
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// </summary>
    /// <param name="callBack">回调函数</param>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        //卸载完毕后 通知外部
        callBack();
    }

}

Resources资源加载 异步加载优化

        Resources加载一次资源过后
        该资源就一直存放在内存中作为缓存
        第二次加载时发现缓存中存在该资源
        会直接取出来进行使用
        所以 多次重复加载不会浪费内存
        但是 会浪费性能(每次加载都会去查找取出,始终伴随一些性能消耗)

        Resources.UnloadAsset 卸载指定资源 但不能卸载GameObject对象
        它只能用于一些 不需要实例化的内容 比如 图片 和 音效 文本等等

        Resources.UnloadUnusedAssets 卸载未使用资源 一般过场景时配合GC使用

        每次进行异步加载时,都会开启一个协同程序
        虽然Resources资源会在内部进行缓存,加载已加载过的资源,性能消耗不会太大
        但是每次开启协程的过程也会浪费性能
        因此我们希望对ResMgr进行优化
        不依赖Resources内部的缓存机制
        而是自己来管理已经加载过的资源
        从而解决异步加载时协同程序的频繁开启造成的性能浪费


        我们想要达到的目的是
        通过一个字典记录已经加载过的资源
        每次在进行资源加载时,如果发现是已经加载过的资源
        我们直接使用即可

思路:

        1.字典容器结构设计
          主要考虑点
          key - 资源名(路径 + 类型 拼接而成)
          value - 自定义数据结构类:资源、委托、协程对象等

        2.修改异步加载相关逻辑
          字典中不存在资源记录时
              开启协同程序进行加载,并且此时就要记录进字典中(这样可以避免重复异步加载)
          字典中存在资源记录时
              1 - 资源还没加载完 —— 记录委托
              2 - 资源已经加载完 —— 直接使用

        3.修改同步加载相关逻辑
          字典中不存在资源记录时
              直接同步加载资源记录即可
          字典中存在资源记录时
              1 - 资源还没加载完 —— 停止协程
              2 - 资源已经加载完 —— 直接使用

        4.修改卸载资源相关逻辑
          字典中存在资源记录时
              1 - 资源还没加载完 —— 记录删除标识,待加载完后真正移除 或者 停止协程,并且移除
              2 - 资源已经加载完 —— 直接卸载,并且移除字典中资源记录

缺点:       

        1.在卸载资源时,我们并不知道是否还有地方使用着该资源
        2.UnloadUnusedAssets是卸载没有使用的资源,我们无法判断是否使用

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源信息基类 主要用于里式替换原则 父类容器装子类对象
/// </summary>
public abstract class ResInfoBase { }

/// <summary>
/// 资源信息对象 主要用于存储资源信息 异步加载委托信息 异步加载 协程信息
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
public class ResInfo<T> : ResInfoBase
{
    //资源
    public T asset;
    //主要用于异步加载结束后 传递资源到外部的委托
    public UnityAction<T> callBack;
    //用于存储异步加载时 开启的协同程序
    public Coroutine coroutine;
    //是否需要移除
    public bool isDel;
}


/// <summary>
/// Resources 资源加载模块管理器
/// </summary>
public class ResMgr : BaseManager<ResMgr>
{
    //用于存储加载过的资源或者加载中的资源的容器
    private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();

    private ResMgr() { }

    /// <summary>
    /// 同步加载Resources下资源的方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="path"></param>
    /// <returns></returns>
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;
        //字典中不存在资源时
        if (!resDic.ContainsKey(resName))
        {
            //直接同步加载 并且记录资源信息 到字典中 方便下次直接取出来用
            T res = Resources.Load<T>(path);
            info = new ResInfo<T>();
            info.asset = res;
            resDic.Add(resName, info);
            return res;
        }
        else
        {
            //取出字典中的记录
            info = resDic[resName] as ResInfo<T>;
            //存在异步加载 还在加载中
            if(info.asset == null)
            {
                //停止异步加载 
                MonoMgr.Instance.StopCoroutine(info.coroutine);
                //直接采用同步的方式加载成功
                T res = Resources.Load<T>(path);
                //记录 
                info.asset = res;
                //还应该把那些等待着异步加载结束的委托去执行了
                info.callBack?.Invoke(res);
                //回调结束 异步加载也停了 所以清除无用的引用
                info.callBack = null;
                info.coroutine = null;
                // 并使用
                return res;
            }
            else
            {
                //如果已经加载结束 直接用
                return info.asset;
            }
        }
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T: UnityEngine.Object
    {
        //资源的唯一ID,是通过 路径名_资源类型 拼接而成的
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;
        if (!resDic.ContainsKey(resName))
        {
            //声明一个 资源信息对象
            info = new ResInfo<T>();
            //将资源记录添加到字典中(资源还没有加载成功)
            resDic.Add(resName, info);
            //记录传入的委托函数 一会儿加载完成了 再使用
            info.callBack += callBack;
            //开启协程去进行 异步加载 并且记录协同程序 (用于之后可能的 停止)
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
        }
        else
        {
            //从字典中取出资源信息
            info = resDic[resName] as ResInfo<T>;
            //如果资源还没有加载完 
            //意味着 还在进行异步加载
            if (info.asset == null)
                info.callBack += callBack;
            else
                callBack?.Invoke(info.asset);
        }

        //要通过协同程序去异步加载资源
        //MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path, callBack));
    }

    private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;

        string resName = path + "_" + typeof(T).Name;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        if (resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            //取出资源信息 并且记录加载完成的资源
            resInfo.asset = rq.asset as T;

            //如果发现需要删除 再去移除资源
            if (resInfo.isDel)
                UnloadAsset<T>(path);
            else
            {
                //将加载完成的资源传递出去
                resInfo.callBack?.Invoke(resInfo.asset);
                //加载完毕后 这些引用就可以清空 避免引用的占用 可能带来的潜在的内存泄漏问题
                resInfo.callBack = null;
                resInfo.coroutine = null;
            }
        }
        
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    [Obsolete("注意:建议使用泛型加载方式,如果实在要用Type加载,一定不能和泛型加载混用去加载同类型同名资源")]
    public void LoadAsync(string path, Type type, UnityAction<UnityEngine.Object> callBack) 
    {
        //资源的唯一ID,是通过 路径名_资源类型 拼接而成的
        string resName = path + "_" + type.Name;
        ResInfo<UnityEngine.Object> info;
        if (!resDic.ContainsKey(resName))
        {
            //声明一个 资源信息对象
            info = new ResInfo<UnityEngine.Object>();
            //将资源记录添加到字典中(资源还没有加载成功)
            resDic.Add(resName, info);
            //记录传入的委托函数 一会儿加载完成了 再使用
            info.callBack += callBack;
            //开启协程去进行 异步加载 并且记录协同程序 (用于之后可能的 停止)
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync(path, type));
        }
        else
        {
            //从字典中取出资源信息
            info = resDic[resName] as ResInfo<UnityEngine.Object>;
            //如果资源还没有加载完 
            //意味着 还在进行异步加载
            if (info.asset == null)
                info.callBack += callBack;
            else
                callBack?.Invoke(info.asset);
        }
    }

    private IEnumerator ReallyLoadAsync(string path, Type type)
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync(path, type);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;

        string resName = path + "_" + type.Name;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        if (resDic.ContainsKey(resName))
        {
            ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
            //取出资源信息 并且记录加载完成的资源
            resInfo.asset = rq.asset;
            //如果发现需要删除 再去移除资源
            if (resInfo.isDel)
                UnloadAsset(path, type);
            else
            {
                //将加载完成的资源传递出去
                resInfo.callBack?.Invoke(resInfo.asset);
                //加载完毕后 这些引用就可以清空 避免引用的占用 可能带来的潜在的内存泄漏问题
                resInfo.callBack = null;
                resInfo.coroutine = null;
            }
        }
    }

    /// <summary>
    /// 指定卸载一个资源
    /// </summary>
    /// <param name="assetToUnload"></param>
    public void UnloadAsset<T>(string path)
    {
        string resName = path + "_" + typeof(T).Name;
        //判断是否存在对应资源
        if(resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            //资源已经加载结束 
            if(resInfo.asset != null)
            {
                //从字典移除
                resDic.Remove(resName);
                //通过api 卸载资源
                Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
            }
            else//资源正在异步加载中
            {
                //MonoMgr.Instance.StopCoroutine(resInfo.coroutine);
                //resDic.Remove(resName);
                //为了保险起见 一定要让资源移除了
                //改变表示 待删除
                resInfo.isDel = true;
            }
        }
    }

    public void UnloadAsset(string path, Type type)
    {
        string resName = path + "_" + type.Name;
        //判断是否存在对应资源
        if (resDic.ContainsKey(resName))
        {
            ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
            //资源已经加载结束 
            if (resInfo.asset != null)
            {
                //从字典移除
                resDic.Remove(resName);
                //通过api 卸载资源
                Resources.UnloadAsset(resInfo.asset);
            }
            else//资源正在异步加载中
            {
                //MonoMgr.Instance.StopCoroutine(resInfo.coroutine);
                //resDic.Remove(resName);
                //为了保险起见 一定要让资源移除了
                //改变表示 待删除
                resInfo.isDel = true;
            }
        }
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// </summary>
    /// <param name="callBack">回调函数</param>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        //卸载完毕后 通知外部
        callBack();
    }

}

Resources资源加载 优化:引用计数

        什么是引用计数?
        引用计数是一种内存管理技术,用于跟踪资源被引用的次数
        我们通过一个整形变量来记录资源的使用次数
        当有对象引用该资源时,计数器会增加;当对象不再引用该资源时,计数器会减少

向ResMgr中加入引用计数功能

        1.为ResInfo类加入引用计数成员变量和方法
        2.使用资源时加
        3.不使用资源时减
        4.处理异步回调问题,某一个异步不使用资源了应该移除回调函数的记录
        5.修改移除资源函数逻辑,引用计数为0时才真正移除资源
        6.考虑资源频繁移除问题,加入马上移除bool标签
        7.修改移除不使用资源函数逻辑,释放时清楚引用计数为0的记录

注意事项

        1.加入引用计数的ResMgr
          我们在使用资源时就需要有用就有删
          当使用某个资源的对象移除时,一定要记得调用移除方法

        2.如果觉得卸载资源的功能麻烦,也完全可以不使用卸载的相关方法
          加载相关逻辑不会有任何影响,和以前直接使用Resources的用法几乎一样
          只需要再添加一个主动清空字典的方法即可

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 资源信息基类 主要用于里式替换原则 父类容器装子类对象
/// </summary>
public abstract class ResInfoBase {
    //引用计数
    public int refCount;
}

/// <summary>
/// 资源信息对象 主要用于存储资源信息 异步加载委托信息 异步加载 协程信息
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
public class ResInfo<T> : ResInfoBase
{
    //资源
    public T asset;
    //主要用于异步加载结束后 传递资源到外部的委托
    public UnityAction<T> callBack;
    //用于存储异步加载时 开启的协同程序
    public Coroutine coroutine;
    //决定引用计数为0时 是否真正需要移除
    public bool isDel;
    

    public void AddRefCount()
    {
        ++refCount;
    }

    public void SubRefCount()
    {
        --refCount;
        if (refCount < 0)
            Debug.LogError("引用计数小于0了,请检查使用和卸载是否配对执行");
    }
}


/// <summary>
/// Resources 资源加载模块管理器
/// </summary>
public class ResMgr : BaseManager<ResMgr>
{
    //用于存储加载过的资源或者加载中的资源的容器
    private Dictionary<string, ResInfoBase> resDic = new Dictionary<string, ResInfoBase>();

    private ResMgr() { }

    /// <summary>
    /// 同步加载Resources下资源的方法
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="path"></param>
    /// <returns></returns>
    public T Load<T>(string path) where T : UnityEngine.Object
    {
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;
        //字典中不存在资源时
        if (!resDic.ContainsKey(resName))
        {
            //直接同步加载 并且记录资源信息 到字典中 方便下次直接取出来用
            T res = Resources.Load<T>(path);
            info = new ResInfo<T>();
            info.asset = res;
            //引用计数增加
            info.AddRefCount();
            resDic.Add(resName, info);
            return res;
        }
        else
        {
            //取出字典中的记录
            info = resDic[resName] as ResInfo<T>;
            //引用计数增加
            info.AddRefCount();
            //存在异步加载 还在加载中
            if (info.asset == null)
            {
                //停止异步加载 
                MonoMgr.Instance.StopCoroutine(info.coroutine);
                //直接采用同步的方式加载成功
                T res = Resources.Load<T>(path);
                //记录 
                info.asset = res;
                //还应该把那些等待着异步加载结束的委托去执行了
                info.callBack?.Invoke(res);
                //回调结束 异步加载也停了 所以清除无用的引用
                info.callBack = null;
                info.coroutine = null;
                // 并使用
                return res;
            }
            else
            {
                //如果已经加载结束 直接用
                return info.asset;
            }
        }
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    public void LoadAsync<T>(string path, UnityAction<T> callBack) where T: UnityEngine.Object
    {
        //资源的唯一ID,是通过 路径名_资源类型 拼接而成的
        string resName = path + "_" + typeof(T).Name;
        ResInfo<T> info;
        if (!resDic.ContainsKey(resName))
        {
            //声明一个 资源信息对象
            info = new ResInfo<T>();
            //引用计数增加
            info.AddRefCount();
            //将资源记录添加到字典中(资源还没有加载成功)
            resDic.Add(resName, info);
            //记录传入的委托函数 一会儿加载完成了 再使用
            info.callBack += callBack;
            //开启协程去进行 异步加载 并且记录协同程序 (用于之后可能的 停止)
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path));
        }
        else
        {
            //从字典中取出资源信息
            info = resDic[resName] as ResInfo<T>;
            //引用计数增加
            info.AddRefCount();
            //如果资源还没有加载完 
            //意味着 还在进行异步加载
            if (info.asset == null)
                info.callBack += callBack;
            else
                callBack?.Invoke(info.asset);
        }

        //要通过协同程序去异步加载资源
        //MonoMgr.Instance.StartCoroutine(ReallyLoadAsync<T>(path, callBack));
    }

    private IEnumerator ReallyLoadAsync<T>(string path) where T : UnityEngine.Object
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync<T>(path);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;

        string resName = path + "_" + typeof(T).Name;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        if (resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            //取出资源信息 并且记录加载完成的资源
            resInfo.asset = rq.asset as T;

            //如果发现需要删除 再去移除资源
            //引用计数为0 才真正去移除
            if (resInfo.refCount == 0)
                UnloadAsset<T>(path, resInfo.isDel, null, false);
            else
            {
                //将加载完成的资源传递出去
                resInfo.callBack?.Invoke(resInfo.asset);
                //加载完毕后 这些引用就可以清空 避免引用的占用 可能带来的潜在的内存泄漏问题
                resInfo.callBack = null;
                resInfo.coroutine = null;
            }
        }
        
    }

    /// <summary>
    /// 异步加载资源的方法
    /// </summary>
    /// <param name="path">资源路径(Resources下的)</param>
    /// <param name="callBack">加载结束后的回调函数 当异步加载资源结束后才会调用</param>
    [Obsolete("注意:建议使用泛型加载方式,如果实在要用Type加载,一定不能和泛型加载混用去加载同类型同名资源")]
    public void LoadAsync(string path, Type type, UnityAction<UnityEngine.Object> callBack) 
    {
        //资源的唯一ID,是通过 路径名_资源类型 拼接而成的
        string resName = path + "_" + type.Name;
        ResInfo<UnityEngine.Object> info;
        if (!resDic.ContainsKey(resName))
        {
            //声明一个 资源信息对象
            info = new ResInfo<UnityEngine.Object>();
            //引用计数增加
            info.AddRefCount();
            //将资源记录添加到字典中(资源还没有加载成功)
            resDic.Add(resName, info);
            //记录传入的委托函数 一会儿加载完成了 再使用
            info.callBack += callBack;
            //开启协程去进行 异步加载 并且记录协同程序 (用于之后可能的 停止)
            info.coroutine = MonoMgr.Instance.StartCoroutine(ReallyLoadAsync(path, type));
        }
        else
        {
            //从字典中取出资源信息
            info = resDic[resName] as ResInfo<UnityEngine.Object>;
            //引用计数增加
            info.AddRefCount();
            //如果资源还没有加载完 
            //意味着 还在进行异步加载
            if (info.asset == null)
                info.callBack += callBack;
            else
                callBack?.Invoke(info.asset);
        }
    }

    private IEnumerator ReallyLoadAsync(string path, Type type)
    {
        //异步加载资源
        ResourceRequest rq = Resources.LoadAsync(path, type);
        //等待资源加载结束后 才会继续执行yield return后面的代码
        yield return rq;

        string resName = path + "_" + type.Name;
        //资源加载结束 将资源传到外部的委托函数去进行使用
        if (resDic.ContainsKey(resName))
        {
            ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
            //取出资源信息 并且记录加载完成的资源
            resInfo.asset = rq.asset;
            //如果发现需要删除 再去移除资源
            //引用计数为0 才真正去移除
            if (resInfo.refCount == 0)
                UnloadAsset(path, type, resInfo.isDel, null, false);
            else
            {
                //将加载完成的资源传递出去
                resInfo.callBack?.Invoke(resInfo.asset);
                //加载完毕后 这些引用就可以清空 避免引用的占用 可能带来的潜在的内存泄漏问题
                resInfo.callBack = null;
                resInfo.coroutine = null;
            }
        }
    }

    /// <summary>
    /// 指定卸载一个资源
    /// </summary>
    /// <param name="assetToUnload"></param>
    public void UnloadAsset<T>(string path, bool isDel = false, UnityAction<T> callBack = null, bool isSub = true)
    {
        string resName = path + "_" + typeof(T).Name;
        //判断是否存在对应资源
        if(resDic.ContainsKey(resName))
        {
            ResInfo<T> resInfo = resDic[resName] as ResInfo<T>;
            //引用计数-1
            if(isSub)
                resInfo.SubRefCount();
            //记录 引用计数为0时  是否马上移除标签
            resInfo.isDel = isDel;
            //资源已经加载结束 
            if(resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel)
            {
                //从字典移除
                resDic.Remove(resName);
                //通过api 卸载资源
                Resources.UnloadAsset(resInfo.asset as UnityEngine.Object);
            }
            else if(resInfo.asset == null)//资源正在异步加载中
            {
                //MonoMgr.Instance.StopCoroutine(resInfo.coroutine);
                //resDic.Remove(resName);
                //为了保险起见 一定要让资源移除了
                //改变表示 待删除
                //resInfo.isDel = true;
                //当异步加载不想使用时 我们应该移除它的回调记录 而不是直接去卸载资源
                if (callBack != null)
                    resInfo.callBack -= callBack;

            }
        }
    }

    public void UnloadAsset(string path, Type type, bool isDel = false, UnityAction<UnityEngine.Object> callBack = null, bool isSub = true)
    {
        string resName = path + "_" + type.Name;
        //判断是否存在对应资源
        if (resDic.ContainsKey(resName))
        {
            ResInfo<UnityEngine.Object> resInfo = resDic[resName] as ResInfo<UnityEngine.Object>;
            //引用计数-1
            if(isSub)
                resInfo.SubRefCount();
            //记录 引用计数为0时  是否马上移除标签
            resInfo.isDel = isDel;
            //资源已经加载结束 
            if (resInfo.asset != null && resInfo.refCount == 0 && resInfo.isDel)
            {
                //从字典移除
                resDic.Remove(resName);
                //通过api 卸载资源
                Resources.UnloadAsset(resInfo.asset);
            }
            else if (resInfo.asset == null)//资源正在异步加载中
            {
                //MonoMgr.Instance.StopCoroutine(resInfo.coroutine);
                //resDic.Remove(resName);
                //为了保险起见 一定要让资源移除了
                //改变表示 待删除
                //resInfo.isDel = true;
                //当异步加载不想使用时 我们应该移除它的回调记录 而不是直接去卸载资源
                if (callBack != null)
                    resInfo.callBack -= callBack;
            }
        }
    }

    /// <summary>
    /// 异步卸载对应没有使用的Resources相关的资源
    /// </summary>
    /// <param name="callBack">回调函数</param>
    public void UnloadUnusedAssets(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyUnloadUnusedAssets(callBack));
    }

    private IEnumerator ReallyUnloadUnusedAssets(UnityAction callBack)
    {
        //就是在真正移除不使用的资源之前 应该把我们自己记录的那些引用计数为0 并且没有被移除记录的资源
        //移除掉
        List<string> list = new List<string>();
        foreach (string path in resDic.Keys)
        {
            if (resDic[path].refCount == 0)
                list.Add(path);
        }
        foreach (string path in list)
        {
            resDic.Remove(path);
        }

        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        //卸载完毕后 通知外部
        callBack();
    }

    /// <summary>
    /// 获取当前某个资源的引用计数
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="path"></param>
    /// <returns></returns>
    public int GetRefCount<T>(string path)
    {
        string resName = path + "_" + typeof(T).Name;
        if(resDic.ContainsKey(resName))
        {
            return (resDic[resName] as ResInfo<T>).refCount;
        }
        return 0;
    }


    /// <summary>
    /// 清空字典
    /// </summary>
    /// <param name="callBack"></param>
    public void ClearDic(UnityAction callBack)
    {
        MonoMgr.Instance.StartCoroutine(ReallyClearDic(callBack));
    }

    private IEnumerator ReallyClearDic(UnityAction callBack)
    {
        resDic.Clear();
        AsyncOperation ao = Resources.UnloadUnusedAssets();
        yield return ao;
        //卸载完毕后 通知外部
        callBack();
    }
}

Editor资源加载模块

主要使用的API

        1.assetdatabase.loadassetatpath                       用于加载单个资源
        2.assetdatabase.loadallassetrepresentationsatpath     用于加载图集资源中的内容(该api用于加载所有子资源

具体实现

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;


/// <summary>
/// 编辑器资源管理器
/// 注意:只有在开发时能使用该管理器加载资源 用于开发功能
/// 发布后 是无法使用该管理器的 因为它需要用到编辑器相关功能
/// </summary>
public class EditorResMgr : BaseManager<EditorResMgr>
{
    //用于放置需要打包进AB包中的资源路径 
    private string rootPath = "Assets/Editor/ArtRes/";

    private EditorResMgr() { }

    //1.加载单个资源的
    public T LoadEditorRes<T>(string path) where T:Object
    {
        string suffixName = "";
        //预设体、纹理(图片)、材质球、音效等等
        if (typeof(T) == typeof(GameObject))
            suffixName = ".prefab";
        else if (typeof(T) == typeof(Material))
            suffixName = ".mat";
        else if (typeof(T) == typeof(Texture))
            suffixName = ".png";
        else if (typeof(T) == typeof(AudioClip))
            suffixName = ".mp3";
        T res = AssetDatabase.LoadAssetAtPath<T>(rootPath + path + suffixName);
        return res;
    }

    //2.加载图集相关资源的
    public Sprite LoadSprite(string path, string spriteName)
    {
        //加载图集中的所有子资源 
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        //遍历所有子资源 得到同名图片返回
        foreach (var item in sprites)
        {
            if (spriteName == item.name)
                return item as Sprite;
        }
        return null;
    }

    //加载图集文件中的所有子图片并返回给外部
    public Dictionary<string, Sprite> LoadSprites(string path)
    {
        Dictionary<string, Sprite> spriteDic = new Dictionary<string, Sprite>();
        Object[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath(rootPath + path);
        foreach (var item in sprites)
        {
            spriteDic.Add(item.name, item as Sprite);
        }
        return spriteDic;
    }
}

Assetbundle资源加载模块

具体实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;


public class ABMgr : SingletonAutoMono<ABMgr>
{
    //主包
    private AssetBundle mainAB = null;
    //主包依赖获取配置文件
    private AssetBundleManifest manifest = null;

    //选择存储 AB包的容器
    //AB包不能够重复加载 否则会报错
    //字典知识 用来存储 AB包对象
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl
    {
        get
        {
            return Application.streamingAssetsPath + "/";
        }
    }

    /// <summary>
    /// 主包名 根据平台不同 报名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    /// <summary>
    /// 加载主包 和 配置文件
    /// 因为加载所有包是 都得判断 通过它才能得到依赖信息
    /// 所以写一个方法
    /// </summary>
    private void LoadMainAB()
    {
        if( mainAB == null )
        {
            mainAB = AssetBundle.LoadFromFile( PathUrl + MainName);
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 加载指定包的依赖包
    /// </summary>
    /// <param name="abName"></param>
    private void LoadDependencies(string abName)
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                abDic.Add(strs[i], ab);
            }
        }
    }

    /// <summary>
    /// 泛型资源同步加载
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <returns></returns>
    public T LoadRes<T>(string abName, string resName) where T:Object
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if ( !abDic.ContainsKey(abName) )
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        T obj = abDic[abName].LoadAsset<T>(resName);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// Type同步加载指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="type"></param>
    /// <returns></returns>
    public Object LoadRes(string abName, string resName, System.Type type) 
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        Object obj = abDic[abName].LoadAsset(resName, type);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// 名字 同步加载指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <returns></returns>
    public Object LoadRes(string abName, string resName)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }

        //得到加载出来的资源
        Object obj = abDic[abName].LoadAsset(resName);
        //如果是GameObject 因为GameObject 100%都是需要实例化的
        //所以我们直接实例化
        if (obj is GameObject)
            return Instantiate(obj);
        else
            return obj;
    }

    /// <summary>
    /// 泛型异步加载资源
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T:Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack));
    }
    //正儿八经的 协程函数
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset) as T);
        else
            callBack(abq.asset as T);
    }

    /// <summary>
    /// Type异步加载资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="type"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName, type);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset));
        else
            callBack(abq.asset);
    }

    /// <summary>
    /// 名字 异步加载 指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, callBack));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callBack)
    {
        //加载依赖包
        LoadDependencies(abName);
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
            abDic.Add(abName, ab);
        }
        //异步加载包中资源
        AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName);
        yield return abq;

        if (abq.asset is GameObject)
            callBack(Instantiate(abq.asset));
        else
            callBack(abq.asset);
    }

    //卸载AB包的方法
    public void UnLoadAB(string name)
    {
        if( abDic.ContainsKey(name) )
        {
            abDic[name].Unload(false);
            abDic.Remove(name);
        }
    }

    //清空AB包的方法
    public void ClearAB()
    {
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        //卸载主包
        mainAB = null;
    }
}

异步/同步Assetbundle资源加载模块

问题:

        如果我们想要将ABMgr中的异步加载方法改为真正意义上的异步
        所谓真正意义上的异步是指:不仅从AB包中加载资源是异步的,还需要在加载AB包时也采用异步

        那么我们就需要考虑一个问题
        如果当我们正在异步加载AB包时,又进行了一次同步加载AB包
        会报错

同理:        在进行异步加载时再重复加载相同AB包是会报错的
        即使是同步加载,我们也必须等待异步加载结束,再进行下一步

解决思路:

异步

        1.某个AB包当正在异步加载时又进行重复加载
          遇到这种情况时,我们需要避免重复加载报错
          因此我们不应再次加载,而是等待之前的异步加载结束后直接使用

        2.正在加载某个AB包时
          卸载AB包:如果正在加载中,不允许卸载
          清空AB包:停止所有协同程序,在清理AB包

同步:

       1. 在异步加载的基础上进行修改

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;


public class ABMgr : SingletonAutoMono<ABMgr>
{
    //主包
    private AssetBundle mainAB = null;
    //主包依赖获取配置文件
    private AssetBundleManifest manifest = null;

    //选择存储 AB包的容器
    //AB包不能够重复加载 否则会报错
    //字典知识 用来存储 AB包对象
    private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();

    /// <summary>
    /// 获取AB包加载路径
    /// </summary>
    private string PathUrl
    {
        get
        {
            return Application.streamingAssetsPath + "/";
        }
    }

    /// <summary>
    /// 主包名 根据平台不同 报名不同
    /// </summary>
    private string MainName
    {
        get
        {
#if UNITY_IOS
            return "IOS";
#elif UNITY_ANDROID
            return "Android";
#else
            return "PC";
#endif
        }
    }

    /// <summary>
    /// 加载主包 和 配置文件
    /// 因为加载所有包是 都得判断 通过它才能得到依赖信息
    /// 所以写一个方法
    /// </summary>
    private void LoadMainAB()
    {
        if( mainAB == null )
        {
            mainAB = AssetBundle.LoadFromFile( PathUrl + MainName);
            manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        }
    }

    /// <summary>
    /// 加载指定包的依赖包
    /// </summary>
    /// <param name="abName"></param>
    private void LoadDependencies(string abName)
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            if (!abDic.ContainsKey(strs[i]))
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                abDic.Add(strs[i], ab);
            }
        }
    }

    / <summary>
    / 泛型资源同步加载
    / </summary>
    / <typeparam name="T"></typeparam>
    / <param name="abName"></param>
    / <param name="resName"></param>
    / <returns></returns>
    //public T LoadRes<T>(string abName, string resName) where T:Object
    //{
    //    //加载依赖包
    //    LoadDependencies(abName);
    //    //加载目标包
    //    if ( !abDic.ContainsKey(abName) )
    //    {
    //        AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
    //        abDic.Add(abName, ab);
    //    }

    //    //得到加载出来的资源
    //    T obj = abDic[abName].LoadAsset<T>(resName);
    //    //如果是GameObject 因为GameObject 100%都是需要实例化的
    //    //所以我们直接实例化
    //    if (obj is GameObject)
    //        return Instantiate(obj);
    //    else
    //        return obj;
    //}

    / <summary>
    / Type同步加载指定资源
    / </summary>
    / <param name="abName"></param>
    / <param name="resName"></param>
    / <param name="type"></param>
    / <returns></returns>
    //public Object LoadRes(string abName, string resName, System.Type type) 
    //{
    //    //加载依赖包
    //    LoadDependencies(abName);
    //    //加载目标包
    //    if (!abDic.ContainsKey(abName))
    //    {
    //        AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
    //        abDic.Add(abName, ab);
    //    }

    //    //得到加载出来的资源
    //    Object obj = abDic[abName].LoadAsset(resName, type);
    //    //如果是GameObject 因为GameObject 100%都是需要实例化的
    //    //所以我们直接实例化
    //    if (obj is GameObject)
    //        return Instantiate(obj);
    //    else
    //        return obj;
    //}

    / <summary>
    / 名字 同步加载指定资源
    / </summary>
    / <param name="abName"></param>
    / <param name="resName"></param>
    / <returns></returns>
    //public Object LoadRes(string abName, string resName)
    //{
    //    //加载依赖包
    //    LoadDependencies(abName);
    //    //加载目标包
    //    if (!abDic.ContainsKey(abName))
    //    {
    //        AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
    //        abDic.Add(abName, ab);
    //    }

    //    //得到加载出来的资源
    //    Object obj = abDic[abName].LoadAsset(resName);
    //    //如果是GameObject 因为GameObject 100%都是需要实例化的
    //    //所以我们直接实例化
    //    if (obj is GameObject)
    //        return Instantiate(obj);
    //    else
    //        return obj;
    //}

    /// <summary>
    /// 泛型异步加载资源
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync = false) where T:Object
    {
        StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack, isSync));
    }
    //正儿八经的 协程函数
    private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack, bool isSync) where T : Object
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            //还没有加载过该AB包
            if (!abDic.ContainsKey(strs[i]))
            {
                //同步加载
                if(isSync)
                {
                    AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                    abDic.Add(strs[i], ab);
                }
                //异步加载
                else
                {
                    //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                    abDic.Add(strs[i], null);
                    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
                    yield return req;
                    //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                    abDic[strs[i]] = req.assetBundle;
                }
            }
            //就证明 字典中已经记录了一个AB包相关信息了
            else
            {
                //如果字典中记录的信息是null 那就证明正在加载中
                //我们只需要等待它加载结束 就可以继续执行后面的代码了
                while (abDic[strs[i]] == null)
                {
                    //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                    yield return 0;
                }
            }
        }
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            //同步加载
            if (isSync)
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
                abDic.Add(abName, ab);
            }
            else
            {
                //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                abDic.Add(abName, null);
                AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
                yield return req;
                //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                abDic[abName] = req.assetBundle;
            }
        }
        else
        {
            //如果字典中记录的信息是null 那就证明正在加载中
            //我们只需要等待它加载结束 就可以继续执行后面的代码了
            while (abDic[abName] == null)
            {
                //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                yield return 0;
            }
        }

        //同步加载AB包中的资源
        if(isSync)
        {
            //即使是同步加载 也需要使用回调函数传给外部进行使用
            T res = abDic[abName].LoadAsset<T>(resName);
            callBack(res);
        }
        //异步加载包中资源
        else
        {
            AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
            yield return abq;

            callBack(abq.asset as T);
        }
    }

    /// <summary>
    /// Type异步加载资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="type"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack, bool isSync = false)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack, isSync));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack, bool isSync)
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            //还没有加载过该AB包
            if (!abDic.ContainsKey(strs[i]))
            {
                //同步加载
                if (isSync)
                {
                    AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                    abDic.Add(strs[i], ab);
                }
                //异步加载
                else
                {
                    //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                    abDic.Add(strs[i], null);
                    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
                    yield return req;
                    //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                    abDic[strs[i]] = req.assetBundle;
                }
            }
            //就证明 字典中已经记录了一个AB包相关信息了
            else
            {
                //如果字典中记录的信息是null 那就证明正在加载中
                //我们只需要等待它加载结束 就可以继续执行后面的代码了
                while (abDic[strs[i]] == null)
                {
                    //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                    yield return 0;
                }
            }
        }
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            //同步加载
            if (isSync)
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
                abDic.Add(abName, ab);
            }
            else
            {
                //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                abDic.Add(abName, null);
                AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
                yield return req;
                //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                abDic[abName] = req.assetBundle;
            }
        }
        else
        {
            //如果字典中记录的信息是null 那就证明正在加载中
            //我们只需要等待它加载结束 就可以继续执行后面的代码了
            while (abDic[abName] == null)
            {
                //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                yield return 0;
            }
        }

        if(isSync)
        {
            Object res = abDic[abName].LoadAsset(resName, type);
            callBack(res);
        }
        else
        {
            //异步加载包中资源
            AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName, type);
            yield return abq;

            callBack(abq.asset);
        }
        
    }

    /// <summary>
    /// 名字 异步加载 指定资源
    /// </summary>
    /// <param name="abName"></param>
    /// <param name="resName"></param>
    /// <param name="callBack"></param>
    public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack, bool isSync = false)
    {
        StartCoroutine(ReallyLoadResAsync(abName, resName, callBack, isSync));
    }

    private IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callBack, bool isSync)
    {
        //加载主包
        LoadMainAB();
        //获取依赖包
        string[] strs = manifest.GetAllDependencies(abName);
        for (int i = 0; i < strs.Length; i++)
        {
            //还没有加载过该AB包
            if (!abDic.ContainsKey(strs[i]))
            {
                //同步加载
                if (isSync)
                {
                    AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
                    abDic.Add(strs[i], ab);
                }
                //异步加载
                else
                {
                    //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                    abDic.Add(strs[i], null);
                    AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + strs[i]);
                    yield return req;
                    //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                    abDic[strs[i]] = req.assetBundle;
                }
            }
            //就证明 字典中已经记录了一个AB包相关信息了
            else
            {
                //如果字典中记录的信息是null 那就证明正在加载中
                //我们只需要等待它加载结束 就可以继续执行后面的代码了
                while (abDic[strs[i]] == null)
                {
                    //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                    yield return 0;
                }
            }
        }
        //加载目标包
        if (!abDic.ContainsKey(abName))
        {
            //同步加载
            if (isSync)
            {
                AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
                abDic.Add(abName, ab);
            }
            else
            {
                //一开始异步加载 就记录 如果此时的记录中的值 是null 那证明这个ab包正在被异步加载
                abDic.Add(abName, null);
                AssetBundleCreateRequest req = AssetBundle.LoadFromFileAsync(PathUrl + abName);
                yield return req;
                //异步加载结束后 再替换之前的null  这时 不为null 就证明加载结束了
                abDic[abName] = req.assetBundle;
            }
        }
        else
        {
            //如果字典中记录的信息是null 那就证明正在加载中
            //我们只需要等待它加载结束 就可以继续执行后面的代码了
            while (abDic[abName] == null)
            {
                //只要发现正在加载中 就不停的等待一帧 下一帧再进行判断
                yield return 0;
            }
        }

        if(isSync)
        {
            Object obj = abDic[abName].LoadAsset(resName);
            callBack(obj);
        }
        else
        {

            //异步加载包中资源
            AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName);
            yield return abq;

            callBack(abq.asset);
        }

    }

    //卸载AB包的方法
    public void UnLoadAB(string name, UnityAction<bool> callBackResult)
    {
        if( abDic.ContainsKey(name) )
        {
            if (abDic[name] == null)
            {
                //代表正在异步加载 没有卸载成功
                callBackResult(false);
                return;
            }
            abDic[name].Unload(false);
            abDic.Remove(name);
            //卸载成功
            callBackResult(true);
        }
    }

    //清空AB包的方法
    public void ClearAB()
    {
        //由于AB包都是异步加载了 因此在清理之前 停止协同程序
        StopAllCoroutines();
        AssetBundle.UnloadAllAssetBundles(false);
        abDic.Clear();
        //卸载主包
        mainAB = null;
    }
}

UnityWebRequest资源加载器

        我们主要封装UnityWebRequest当中的
        1.获取文本或二进制数据 方法
        2.获取纹理数据 方法
        3.获取AB包数据 方法
        制作UWQResMgr,让外部使用UnityWebRequest加载资源时更加方便

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

public class UWQResMgr : SingletonAutoMono<UWQResMgr>
{
    /// <summary>
    /// 利用UnityWebRequest去加载资源
    /// </summary>
    /// <typeparam name="T">类型只能是string、byte[]、Texture、AssetBundle 不能是其他类型 目前不支持</typeparam>
    /// <param name="path">资源路径、要自己加上协议 http、ftp、file</param>
    /// <param name="callBack">加载成功的回调函数</param>
    /// <param name="failCallBack">加载失败的回调函数</param>
    public void LoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T : class
    {
        StartCoroutine(ReallyLoadRes<T>(path, callBack, failCallBack));
    }

    private IEnumerator ReallyLoadRes<T>(string path, UnityAction<T> callBack, UnityAction failCallBack) where T:class
    {
        //string
        //byte[]
        //Texture
        //AssetBundle
        Type type = typeof(T);
        //用于加载的对象
        UnityWebRequest req = null;
        if (type == typeof(string) ||
            type == typeof(byte[]))
            req = UnityWebRequest.Get(path);
        else if (type == typeof(Texture))
            req = UnityWebRequestTexture.GetTexture(path);
        else if (type == typeof(AssetBundle))
            req = UnityWebRequestAssetBundle.GetAssetBundle(path);
        else
        {
            failCallBack?.Invoke();
            yield break;
        }

        yield return req.SendWebRequest();
        //如果加载成功 
        if (req.result == UnityWebRequest.Result.Success)
        {
            if (type == typeof(string))
                callBack?.Invoke(req.downloadHandler.text as T);
            else if (type == typeof(byte[]))
                callBack?.Invoke(req.downloadHandler.data as T);
            else if (type == typeof(Texture))
                callBack?.Invoke(DownloadHandlerTexture.GetContent(req) as T);
            else if (type == typeof(AssetBundle))
                callBack?.Invoke(DownloadHandlerAssetBundle.GetContent(req) as T);
        }
        else
            failCallBack?.Invoke();
        //释放UWQ对象
        req.Dispose();
    }
}

 音效管理模块

实现音效管理模块 音乐 相关内容

        主要实现内容
        1.单例模式管理器
        2.播放背景音乐
        3.停止背景音乐
        4.暂停背景音乐
        5.设置背景音乐大小

实现音效管理模块 音效 相关内容

        主要实现内容
        1.播放音效
        2.自动移除播放完成的音效
        3.停止播放指定音效
        4.设置音效声音大小
        5.暂停或继续播放所有音效

        注意:
          1.音效和背景音乐不同
            音效存在多个,并且音效需要管理是否结束
            因此需要用容器记录音效组件
          2.音效分为循环和非循环
            非循环的需要我们检测它播放结束
            循环的需要让外部进行管理

具体实现

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 音乐音效管理器
/// </summary>
public class MusicMgr : BaseManager<MusicMgr>
{
    //背景音乐播放组件
    private AudioSource bkMusic = null;

    //背景音乐大小
    private float bkMusicValue = 0.1f;

    //用于音效组件依附的对象
    private GameObject soundObj = null;
    //管理正在播放的音效
    private List<AudioSource> soundList = new List<AudioSource>();
    //音效音量大小
    private float soundValue = 0.1f;
    //音效是否在播放
    private bool soundIsPlay = true;


    private MusicMgr() 
    {
        MonoMgr.Instance.AddFixedUpdateListener(Update);
    }


    private void Update()
    {
        if (!soundIsPlay)
            return;

        //不停的遍历容器 检测有没有音效播放完毕 播放完了 就移除销毁它
        //为了避免边遍历边移除出问题 我们采用逆向遍历
        for (int i = soundList.Count - 1; i >= 0; --i)
        {
            if(!soundList[i].isPlaying)
            {
                GameObject.Destroy(soundList[i]);
                soundList.RemoveAt(i);
            }
        }
    }


    //播放背景音乐
    public void PlayBKMusic(string name)
    {
        //动态创建播放背景音乐的组件 并且 不会过场景移除 
        //保证背景音乐在过场景时也能播放
        if(bkMusic == null)
        {
            GameObject obj = new GameObject();
            obj.name = "BKMusic";
            GameObject.DontDestroyOnLoad(obj);
            bkMusic = obj.AddComponent<AudioSource>();
        }

        //根据传入的背景音乐名字 来播放背景音乐
        ABResMgr.Instance.LoadResAsync<AudioClip>("music", name, (clip) =>
        {
            bkMusic.clip = clip;
            bkMusic.loop = true;
            bkMusic.volume = bkMusicValue;
            bkMusic.Play();
        });
    }

    //停止背景音乐
    public void StopBKMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Stop();
    }

    //暂停背景音乐
    public void PauseBKMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Pause();
    }

    //设置背景音乐大小
    public void ChangeBKMusicValue(float v)
    {
        bkMusicValue = v;
        if (bkMusic == null)
            return;
        bkMusic.volume = bkMusicValue;
    }

    /// <summary>
    /// 播放音效
    /// </summary>
    /// <param name="name">音效名字</param>
    /// <param name="isLoop">是否循环</param>
    /// <param name="isSync">是否同步加载</param>
    /// <param name="callBack">加载结束后的回调</param>
    public void PlaySound(string name, bool isLoop = false, bool isSync = false, UnityAction<AudioSource> callBack = null)
    {
        if (soundObj == null)
        {
            //音效依附的对象 一般过场景音效都需要停止 所以我们可以不处理它过场景不移除
            soundObj = new GameObject("soundObj");
        }
        //加载音效资源 进行播放
        ABResMgr.Instance.LoadResAsync<AudioClip>("sound", name, (clip) =>
        {
            AudioSource source = soundObj.AddComponent<AudioSource>();
            source.clip = clip;
            source.loop = isLoop;
            source.volume = soundValue;
            source.Play();
            //存储容器 用于记录 方便之后判断是否停止
            soundList.Add(source);
            //传递给外部使用
            callBack?.Invoke(source);
        }, isSync);
    }

    /// <summary>
    /// 停止播放音效
    /// </summary>
    /// <param name="source">音效组件对象</param>
    public void StopSound(AudioSource source)
    {
        if(soundList.Contains(source))
        {
            //停止播放
            source.Stop();
            //从容器中移除
            soundList.Remove(source);
            //从依附对象上移除
            GameObject.Destroy(source);
        }
    }

    /// <summary>
    /// 改变音效大小
    /// </summary>
    /// <param name="v"></param>
    public void ChangeSoundValue(float v)
    {
        soundValue = v;
        for (int i = 0; i < soundList.Count; i++)
        {
            soundList[i].volume = v;
        }
    }

    /// <summary>
    /// 继续播放或者暂停所有音效
    /// </summary>
    /// <param name="isPlay">是否是继续播放 true为播放 false为暂停</param>
    public void PlayOrPauseSound(bool isPlay)
    {
        if(isPlay)
        {
            soundIsPlay = true;
            for (int i = 0; i < soundList.Count; i++)
                soundList[i].Play();
        }
        else
        {
            soundIsPlay = false;
            for (int i = 0; i < soundList.Count; i++)
                soundList[i].Pause();
        }
    }
}

优化(内存/3D)

优化频繁创建删除音效组件

        目前我们音效管理器中的音效组件会频繁的创建和删除
        这样会产生大量的内存垃圾,并且频繁创建对象也会带来性能消耗
        因此我们可以利用缓存池对其进行优化

优化3D音效问题

        由于我们使用了缓存池,因此AudioSource依附的将会是一个个独立的游戏对象
        而3D音效主要考虑的问题是:
        1.音效依附在对象上跟随对象移动
        2.音效中3D相关参数的设置

        如果你想要使用3D音效,那么只需要获取到音效组件和它依附的对象就可以迎刃而解了
        1.获取音效组件依附的对象,改变它的父对象,设置它的位置,音效便可以跟随对象移动
        2.获取音效组件改变其中的3D音效相关参数即可完成相关设置

提供清除所有音效的方法

        目前过场景时,音效相关对象会被自动的删除
        但是音效管理器中我们的容器还占着引用,我们应该提供方法清空容器

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 音乐音效管理器
/// </summary>
public class MusicMgr : BaseManager<MusicMgr>
{
    //背景音乐播放组件
    private AudioSource bkMusic = null;

    //背景音乐大小
    private float bkMusicValue = 0.1f;

    //管理正在播放的音效
    private List<AudioSource> soundList = new List<AudioSource>();
    //音效音量大小
    private float soundValue = 0.1f;
    //音效是否在播放
    private bool soundIsPlay = true;


    private MusicMgr() 
    {
        MonoMgr.Instance.AddFixedUpdateListener(Update);
    }


    private void Update()
    {
        if (!soundIsPlay)
            return;

        //不停的遍历容器 检测有没有音效播放完毕 播放完了 就移除销毁它
        //为了避免边遍历边移除出问题 我们采用逆向遍历
        for (int i = soundList.Count - 1; i >= 0; --i)
        {
            if(!soundList[i].isPlaying)
            {
                //音效播放完毕了 不再使用了 我们将这个音效切片置空
                soundList[i].clip = null;
                PoolMgr.Instance.PushObj(soundList[i].gameObject);
                soundList.RemoveAt(i);
            }
        }
    }


    //播放背景音乐
    public void PlayBKMusic(string name)
    {
        //动态创建播放背景音乐的组件 并且 不会过场景移除 
        //保证背景音乐在过场景时也能播放
        if(bkMusic == null)
        {
            GameObject obj = new GameObject();
            obj.name = "BKMusic";
            GameObject.DontDestroyOnLoad(obj);
            bkMusic = obj.AddComponent<AudioSource>();
        }

        //根据传入的背景音乐名字 来播放背景音乐
        ABResMgr.Instance.LoadResAsync<AudioClip>("music", name, (clip) =>
        {
            bkMusic.clip = clip;
            bkMusic.loop = true;
            bkMusic.volume = bkMusicValue;
            bkMusic.Play();
        });
    }

    //停止背景音乐
    public void StopBKMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Stop();
    }

    //暂停背景音乐
    public void PauseBKMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Pause();
    }

    //设置背景音乐大小
    public void ChangeBKMusicValue(float v)
    {
        bkMusicValue = v;
        if (bkMusic == null)
            return;
        bkMusic.volume = bkMusicValue;
    }

    /// <summary>
    /// 播放音效
    /// </summary>
    /// <param name="name">音效名字</param>
    /// <param name="isLoop">是否循环</param>
    /// <param name="isSync">是否同步加载</param>
    /// <param name="callBack">加载结束后的回调</param>
    public void PlaySound(string name, bool isLoop = false, bool isSync = false, UnityAction<AudioSource> callBack = null)
    {
        //加载音效资源 进行播放
        ABResMgr.Instance.LoadResAsync<AudioClip>("sound", name, (clip) =>
        {
            //从缓存池中取出音效对象得到对应组件
            AudioSource source = PoolMgr.Instance.GetObj("Sound/soundObj").GetComponent<AudioSource>();
            //如果取出来的音效是之前正在使用的 我们先停止它
            source.Stop();

            source.clip = clip;
            source.loop = isLoop;
            source.volume = soundValue;
            source.Play();
            //存储容器 用于记录 方便之后判断是否停止
            //由于从缓存池中取出对象 有可能取出一个之前正在使用的(超上限时)
            //所以我们需要判断 容器中没有记录再去记录 不要重复去添加即可
            if(!soundList.Contains(source))
                soundList.Add(source);
            //传递给外部使用
            callBack?.Invoke(source);
        }, isSync);
    }

    /// <summary>
    /// 停止播放音效
    /// </summary>
    /// <param name="source">音效组件对象</param>
    public void StopSound(AudioSource source)
    {
        if(soundList.Contains(source))
        {
            //停止播放
            source.Stop();
            //从容器中移除
            soundList.Remove(source);
            //不用了 清空切片 避免占用
            source.clip = null;
            //放入缓存池
            PoolMgr.Instance.PushObj(source.gameObject);
        }
    }

    /// <summary>
    /// 改变音效大小
    /// </summary>
    /// <param name="v"></param>
    public void ChangeSoundValue(float v)
    {
        soundValue = v;
        for (int i = 0; i < soundList.Count; i++)
        {
            soundList[i].volume = v;
        }
    }

    /// <summary>
    /// 继续播放或者暂停所有音效
    /// </summary>
    /// <param name="isPlay">是否是继续播放 true为播放 false为暂停</param>
    public void PlayOrPauseSound(bool isPlay)
    {
        if(isPlay)
        {
            soundIsPlay = true;
            for (int i = 0; i < soundList.Count; i++)
                soundList[i].Play();
        }
        else
        {
            soundIsPlay = false;
            for (int i = 0; i < soundList.Count; i++)
                soundList[i].Pause();
        }
    }

    /// <summary>
    /// 清空音效相关记录 过场景时在清空缓存池之前去调用它
    /// 重要的事情说三遍!!!
    /// 过场景时在清空缓存池之前去调用它
    /// 过场景时在清空缓存池之前去调用它
    /// 过场景时在清空缓存池之前去调用它
    /// </summary>
    public void ClearSound()
    {
        for (int i = 0; i < soundList.Count; i++)
        {
            soundList[i].Stop();
            soundList[i].clip = null;
            PoolMgr.Instance.PushObj(soundList[i].gameObject);
        }
    }
}

UI管理模块

制作UI面板的传统流程

        1.拼面板(必须做)
        2.声明组件(重复工作)
        3.查找组件(重复工作)
        4.监听事件(重复工作)
        5.处理逻辑(必须做)

UI管理模块的基本原理

        1.制作UI面板基类,帮助我们自动化的查找组件,监听事件,无需每次写大量冗余代码
          将2、3、4步做成自动化的,无需重复去做
          解决方案一般有两种:
          1 - 1.自动化工具生成代码(Unity编辑器开发之编辑器拓展中)
          1 - 2.基类中规范冗余代码(我们将讲解的)

        2.制作UI管理器,管理所有UI面板,UI面板的显示隐藏都通过UI管理器来进行管理
          提供公共API供外部使用
          比如:
          2 - 1:显示面板
          2 - 2:隐藏面板
          2 - 3:获取面板
          2 - 4:添加自定义事件
          等等

        按照这个思路制作UI管理模块后
        我们之后在制作UI功能时,只需要把重点放在拼面板,和面板逻辑处理上了

UI面板基类

        主要实现思路:
        在基类中完成声明组件、查找组件、监听组件相关功能
        让子类可以直接处理事件逻辑,获取指定控件

        主要实现内容:
        1.通用的查找组件功能
        2.通用的添加事件功能
        3.显示面板、隐藏面板时的逻辑执行虚函数
        4.获取指定组件的功能
        等等

        关键点:
        制定控件命名规则
        1.要使用的组件需要改名
        2.不使用只用于显示的组件可以使用默认名

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public abstract class BasePanel : MonoBehaviour
{
    /// <summary>
    /// 用于存储所有要用到的UI控件,用历史替换原则 父类装子类
    /// </summary>
    protected Dictionary<string, UIBehaviour> controlDic = new Dictionary<string, UIBehaviour>();

    /// <summary>
    /// 控件默认名字 如果得到的控件名字存在于这个容器 意味着我们不会通过代码去使用它 它只会是起到显示作用的控件
    /// </summary>
    private static List<string> defaultNameList = new List<string>() { "Image",
                                                                   "Text (TMP)",
                                                                   "RawImage",
                                                                   "Background",
                                                                   "Checkmark",
                                                                   "Label",
                                                                   "Text (Legacy)",
                                                                   "Arrow",
                                                                   "Placeholder",
                                                                   "Fill",
                                                                   "Handle",
                                                                   "Viewport",
                                                                   "Scrollbar Horizontal",
                                                                   "Scrollbar Vertical"};


    protected virtual void Awake()
    {
        //为了避免 某一个对象上存在两种控件的情况
        //我们应该优先查找重要的组件
        FindChildrenControl<Button>();
        FindChildrenControl<Toggle>();
        FindChildrenControl<Slider>();
        FindChildrenControl<InputField>();
        FindChildrenControl<ScrollRect>();
        FindChildrenControl<Dropdown>();
        //即使对象上挂在了多个组件 只要优先找到了重要组件
        //之后也可以通过重要组件得到身上其他挂载的内容
        FindChildrenControl<Text>();
        FindChildrenControl<TextMeshPro>();
        FindChildrenControl<Image>();
    }

    /// <summary>
    /// 面板显示时会调用的逻辑
    /// </summary>
    public abstract void ShowMe();

    /// <summary>
    /// 面板隐藏时会调用的逻辑
    /// </summary>
    public abstract void HideMe();

    /// <summary>
    /// 获取指定名字以及指定类型的组件
    /// </summary>
    /// <typeparam name="T">组件类型</typeparam>
    /// <param name="name">组件名字</param>
    /// <returns></returns>
    public T GetControl<T>(string name) where T:UIBehaviour
    {
        if(controlDic.ContainsKey(name))
        {
            T control = controlDic[name] as T;
            if (control == null)
                Debug.LogError($"不存在对应名字{name}类型为{typeof(T)}的组件");
            return control;
        }
        else
        {
            Debug.LogError($"不存在对应名字{name}的组件");
            return null;
        }
    }

    protected virtual void ClickBtn(string btnName)
    {

    }

    protected virtual void SliderValueChange(string sliderName, float value)
    {

    }

    protected virtual void ToggleValueChange(string sliderName, bool value)
    {

    }

    private void FindChildrenControl<T>() where T:UIBehaviour
    {
        T[] controls = this.GetComponentsInChildren<T>(true);
        for (int i = 0; i < controls.Length; i++)
        {
            //获取当前控件的名字
            string controlName = controls[i].gameObject.name;
            //通过这种方式 将对应组件记录到字典中
            if (!controlDic.ContainsKey(controlName))
            {
                if(!defaultNameList.Contains(controlName))
                {
                    controlDic.Add(controlName, controls[i]);
                    //判断控件的类型 决定是否加事件监听
                    if(controls[i] is Button)
                    {
                        (controls[i] as Button).onClick.AddListener(() =>
                        {
                            ClickBtn(controlName);
                        });
                    }
                    else if(controls[i] is Slider)
                    {
                        (controls[i] as Slider).onValueChanged.AddListener((value) =>
                        {
                            SliderValueChange(controlName, value);
                        });
                    }
                    else if(controls[i] is Toggle)
                    {
                        (controls[i] as Toggle).onValueChanged.AddListener((value) =>
                        {
                            ToggleValueChange(controlName, value);
                        });
                    }
                }
                    
            }
        }
    }
}

制作UI管理器

层级规划

        主要思路:
        1.ui面板在任何场景都会显示,因此canvas和eventsystem对象应该过场景不移除,并且保证唯一性和动态创建
          1 - 1.ui管理器为不继承monobehaviour的单例模式
          1 - 2.在构造函数中动态创建设置好的canvas和eventsystem预设体(如果使用了ui摄像机,也需要单独处理摄像机预设体)

        2.ui面板的显示可以存在层级(前后)关系,我们可以预先创建好层级对象,提供获取层级对象的方法
          2 - 1.在canvas下创建好管理层级的子对象,之后面板作为对应层级对象的子对象达到分层作用
          2 - 2.提供获取层级对象的方法

具体实现

        1.存储面板的容器
        2.显示面板
        3.隐藏面板
        4.获取面板

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

/// <summary>
/// 层级枚举
/// </summary>
public enum E_UILayer
{
    /// <summary>
    /// 最底层
    /// </summary>
    Bottom,
    /// <summary>
    /// 中层
    /// </summary>
    Middle,
    /// <summary>
    /// 高层
    /// </summary>
    Top,
    /// <summary>
    /// 系统层 最高层
    /// </summary>
    System,
}

/// <summary>
/// 管理所有UI面板的管理器
/// 注意:面板预设体名要和面板类名一致!!!!!
/// </summary>
public class UIMgr : BaseManager<UIMgr>
{
    private Camera uiCamera;
    private Canvas uiCanvas;
    private EventSystem uiEventSystem;

    //层级父对象
    private Transform bottomLayer;
    private Transform middleLayer;
    private Transform topLayer;
    private Transform systemLayer;

    /// <summary>
    /// 用于存储所有的面板对象
    /// </summary>
    private Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();

    private UIMgr()
    {
        //动态创建唯一的Canvas和EventSystem(摄像机)
        uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
        //ui摄像机过场景不移除 专门用来渲染UI面板
        GameObject.DontDestroyOnLoad(uiCamera.gameObject);

        //动态创建Canvas
        uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
        //设置使用的UI摄像机
        uiCanvas.worldCamera = uiCamera;
        //过场景不移除
        GameObject.DontDestroyOnLoad(uiCanvas.gameObject);

        //找到层级父对象
        bottomLayer = uiCanvas.transform.Find("Bottom");
        middleLayer = uiCanvas.transform.Find("Middle");
        topLayer = uiCanvas.transform.Find("Top");
        systemLayer = uiCanvas.transform.Find("System");

        //动态创建EventSystem
        uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
        GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
    }

    /// <summary>
    /// 获取对应层级的父对象
    /// </summary>
    /// <param name="layer">层级枚举值</param>
    /// <returns></returns>
    public Transform GetLayerFather(E_UILayer layer)
    {
        switch (layer)
        {
            case E_UILayer.Bottom:
                return bottomLayer;
            case E_UILayer.Middle:
                return middleLayer;
            case E_UILayer.Top:
                return topLayer;
            case E_UILayer.System:
                return systemLayer;
            default:
                return null;
        }
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    /// <param name="layer">面板显示的层级</param>
    /// <param name="callBack">由于可能是异步加载 因此通过委托回调的形式 将加载完成的面板传递出去进行使用</param>
    /// <param name="isSync">是否采用同步加载 默认为false</param>
    public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T:BasePanel
    {
        //获取面板名 预设体名必须和面板类名一致 
        string panelName = typeof(T).Name;
        //存在面板
        if(panelDic.ContainsKey(panelName))
        {
            //如果要显示面板 会执行一次面板的默认显示逻辑
            panelDic[panelName].ShowMe();
            //如果存在回调 直接返回出去即可
            callBack?.Invoke(panelDic[panelName] as T);
            return;
        }

        //不存在面板 加载面板
        ABResMgr.Instance.LoadResAsync<GameObject>("UI", panelName, (res) =>
        {
            //层级的处理
            Transform father = GetLayerFather(layer);
            //避免没有按指定规则传递层级参数 避免为空
            if (father == null)
                father = middleLayer;
            //将面板预设体创建到对应父对象下 并且保持原本的缩放大小
            GameObject panelObj = GameObject.Instantiate(res, father, false);

            //获取对应UI组件返回出去
            T panel = panelObj.GetComponent<T>();
            //显示面板时执行的默认方法
            panel.ShowMe();
            //传出去使用
            callBack?.Invoke(panel);
            //存储panel
            panelDic.Add(panelName, panel);

        }, isSync);
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <typeparam name="T">面板类型</typeparam>
    public void HidePanel<T>() where T : BasePanel
    {
        string panelName = typeof(T).Name;
        if(panelDic.ContainsKey(panelName))
        {
            //执行默认的隐藏面板想要做的事情
            panelDic[panelName].HideMe();
            //销毁面板
            GameObject.Destroy(panelDic[panelName].gameObject);
            //从容器中移除
            panelDic.Remove(panelName);
        }
    }

    /// <summary>
    /// 获取面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    public T GetPanel<T>() where T:BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
            return panelDic[panelName] as T;
        return null;
    }
}

UI管理模块异步加载优化

为什么要进行异步加载优化

        我们之前制作UI管理器时,加载资源时是在测试模式下
        始终使用的是编辑器同步加载模式
        若真正使用异步加载时,可能会存在报错风险

        举例重现问题:
        1.构建AB包
        2.采用异步加载方式加载AB包中的UI面板资源
        3.同一帧显示两次UI面板
        4.同一帧显示又隐藏UI面板

优化异步加载问题

        主要制作思路:
        1.造成问题的关键点
          由于异步加载 字典容器中没有及时存储将要显示的面板对象
          我们需要在显示面板时 一开始就存储面板的相关信息
          这样不管是二次显示还是隐藏,都能够知道是否已经在加载面板了

        2.分情况考虑问题(异步加载中 和 异步加载结束)
          显示相关
          1.若加载中想要显示,应该记录回调,加载结束后统一调用
          2.若加载结束后想显示,直接显示
          隐藏相关
          1.若加载中想要隐藏,应该改变标识
          2.若加载结束想要隐藏,直接隐藏
          3.若压根没有,不用处理
          获取相关
          1.若加载中想要获取,应该等待加载结束后再处理获取逻辑
          2.若加载结束想要获取,直接获取
          3.若压根没有,不用处理

        主要解决的问题:
        1.同一帧连续显示同一面板,避免重复进行异步加载后回调重复往字典中添加面板数据
        2.同一帧 显示——> 隐藏——> 显示 同一面板问题,面板能够正常显示
        3.获取面板时如果正在加载中,等加载结束后再获取处理逻辑

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

/// <summary>
/// 层级枚举
/// </summary>
public enum E_UILayer
{
    /// <summary>
    /// 最底层
    /// </summary>
    Bottom,
    /// <summary>
    /// 中层
    /// </summary>
    Middle,
    /// <summary>
    /// 高层
    /// </summary>
    Top,
    /// <summary>
    /// 系统层 最高层
    /// </summary>
    System,
}

/// <summary>
/// 管理所有UI面板的管理器
/// 注意:面板预设体名要和面板类名一致!!!!!
/// </summary>
public class UIMgr : BaseManager<UIMgr>
{
    /// <summary>
    /// 主要用于里式替换原则 在字典中 用父类容器装载子类对象
    /// </summary>
    private abstract class BasePanelInfo { }

    /// <summary>
    /// 用于存储面板信息 和加载完成的回调函数的
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    private class PanelInfo<T> : BasePanelInfo where T:BasePanel
    {
        public T panel;
        public UnityAction<T> callBack;
        public bool isHide;

        public PanelInfo(UnityAction<T> callBack)
        {
            this.callBack += callBack;
        }
    }


    private Camera uiCamera;
    private Canvas uiCanvas;
    private EventSystem uiEventSystem;

    //层级父对象
    private Transform bottomLayer;
    private Transform middleLayer;
    private Transform topLayer;
    private Transform systemLayer;

    /// <summary>
    /// 用于存储所有的面板对象
    /// </summary>
    private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();

    private UIMgr()
    {
        //动态创建唯一的Canvas和EventSystem(摄像机)
        uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
        //ui摄像机过场景不移除 专门用来渲染UI面板
        GameObject.DontDestroyOnLoad(uiCamera.gameObject);

        //动态创建Canvas
        uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
        //设置使用的UI摄像机
        uiCanvas.worldCamera = uiCamera;
        //过场景不移除
        GameObject.DontDestroyOnLoad(uiCanvas.gameObject);

        //找到层级父对象
        bottomLayer = uiCanvas.transform.Find("Bottom");
        middleLayer = uiCanvas.transform.Find("Middle");
        topLayer = uiCanvas.transform.Find("Top");
        systemLayer = uiCanvas.transform.Find("System");

        //动态创建EventSystem
        uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
        GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
    }

    /// <summary>
    /// 获取对应层级的父对象
    /// </summary>
    /// <param name="layer">层级枚举值</param>
    /// <returns></returns>
    public Transform GetLayerFather(E_UILayer layer)
    {
        switch (layer)
        {
            case E_UILayer.Bottom:
                return bottomLayer;
            case E_UILayer.Middle:
                return middleLayer;
            case E_UILayer.Top:
                return topLayer;
            case E_UILayer.System:
                return systemLayer;
            default:
                return null;
        }
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    /// <param name="layer">面板显示的层级</param>
    /// <param name="callBack">由于可能是异步加载 因此通过委托回调的形式 将加载完成的面板传递出去进行使用</param>
    /// <param name="isSync">是否采用同步加载 默认为false</param>
    public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T:BasePanel
    {
        //获取面板名 预设体名必须和面板类名一致 
        string panelName = typeof(T).Name;
        //存在面板
        if(panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在异步加载中
            if(panelInfo.panel == null)
            {
                //如果之前显示了又隐藏 现在又想显示 那么直接设为false
                panelInfo.isHide = false;

                //如果正在异步加载 应该等待它加载完毕 只需要记录回调函数 加载完后去调用即可
                if (callBack != null)
                    panelInfo.callBack += callBack;
            }
            else//已经加载结束
            {
                //如果要显示面板 会执行一次面板的默认显示逻辑
                panelInfo.panel.ShowMe();
                //如果存在回调 直接返回出去即可
                callBack?.Invoke(panelInfo.panel);
            }
            return;
        }

        //不存在面板 先存入字典当中 占个位置 之后如果又显示 我才能得到字典中的信息进行判断
        panelDic.Add(panelName, new PanelInfo<T>(callBack));

        //不存在面板 加载面板
        ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //表示异步加载结束前 就想要隐藏该面板了 
            if(panelInfo.isHide)
            {
                panelDic.Remove(panelName);
                return;
            }

            //层级的处理
            Transform father = GetLayerFather(layer);
            //避免没有按指定规则传递层级参数 避免为空
            if (father == null)
                father = middleLayer;
            //将面板预设体创建到对应父对象下 并且保持原本的缩放大小
            GameObject panelObj = GameObject.Instantiate(res, father, false);

            //获取对应UI组件返回出去
            T panel = panelObj.GetComponent<T>();
            //显示面板时执行的默认方法
            panel.ShowMe();
            //传出去使用
            panelInfo.callBack?.Invoke(panel);
            //回调执行完 将其清空 避免内存泄漏
            panelInfo.callBack = null;
            //存储panel
            panelInfo.panel = panel;

        }, isSync);
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <typeparam name="T">面板类型</typeparam>
    public void HidePanel<T>() where T : BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //但是正在加载中
            if(panelInfo.panel == null)
            {
                //修改隐藏表示 表示 这个面板即将要隐藏
                panelInfo.isHide = true;
                //既然要隐藏了 回调函数都不会调用了 直接置空
                panelInfo.callBack = null;
            }
            else//已经加载结束
            {
                //执行默认的隐藏面板想要做的事情
                panelInfo.panel.HideMe();
                //销毁面板
                GameObject.Destroy(panelInfo.panel.gameObject);
                //从容器中移除
                panelDic.Remove(panelName);
            }
        }
    }

    /// <summary>
    /// 获取面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    public void GetPanel<T>( UnityAction<T> callBack ) where T:BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在加载中
            if(panelInfo.panel == null)
            {
                //加载中 应该等待加载结束 再通过回调传递给外部去使用
                panelInfo.callBack += callBack;
            }
            else if(!panelInfo.isHide)//加载结束 并且没有隐藏
            {
                callBack?.Invoke(panelInfo.panel);
            }
        }
    }
}

隐藏面板可选销毁 优化

为什么要进行 隐藏面板可选销毁 优化

        我们目前隐藏面板时,会直接将面板销毁
        下次创建时再重新创建
        优点:
        当存在内存压力时
        直接销毁面板后,当内存不足时会触发gc
        不会因为存在没有使用的面板引用而造成内存崩溃

        缺点:
        会产生内存垃圾加快gc的触发
        频繁的销毁创建会增加性能消耗

        也就是说我们不能直接将面板隐藏改成不销毁
        而应该改为可以让我们自己控制最好
        我们可以根据项目的实际情况 选择性的使用失活或销毁

隐藏面板可选销毁实现

        主要制作思路:
          无需使用缓存池,因为缓存池主要是提供给非唯一对象使用的
          UI面板大部分情况下是唯一的,因此我们直接在UI管理器中修改逻辑即可
          主要实现内容:
          1.隐藏面板时,可以选择销毁还是失活
          2.显示面板时,如果存在直接激活,如果不存在再重新创建

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

/// <summary>
/// 层级枚举
/// </summary>
public enum E_UILayer
{
    /// <summary>
    /// 最底层
    /// </summary>
    Bottom,
    /// <summary>
    /// 中层
    /// </summary>
    Middle,
    /// <summary>
    /// 高层
    /// </summary>
    Top,
    /// <summary>
    /// 系统层 最高层
    /// </summary>
    System,
}

/// <summary>
/// 管理所有UI面板的管理器
/// 注意:面板预设体名要和面板类名一致!!!!!
/// </summary>
public class UIMgr : BaseManager<UIMgr>
{
    /// <summary>
    /// 主要用于里式替换原则 在字典中 用父类容器装载子类对象
    /// </summary>
    private abstract class BasePanelInfo { }

    /// <summary>
    /// 用于存储面板信息 和加载完成的回调函数的
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    private class PanelInfo<T> : BasePanelInfo where T:BasePanel
    {
        public T panel;
        public UnityAction<T> callBack;
        public bool isHide;

        public PanelInfo(UnityAction<T> callBack)
        {
            this.callBack += callBack;
        }
    }


    private Camera uiCamera;
    private Canvas uiCanvas;
    private EventSystem uiEventSystem;

    //层级父对象
    private Transform bottomLayer;
    private Transform middleLayer;
    private Transform topLayer;
    private Transform systemLayer;

    /// <summary>
    /// 用于存储所有的面板对象
    /// </summary>
    private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();

    private UIMgr()
    {
        //动态创建唯一的Canvas和EventSystem(摄像机)
        uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
        //ui摄像机过场景不移除 专门用来渲染UI面板
        GameObject.DontDestroyOnLoad(uiCamera.gameObject);

        //动态创建Canvas
        uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
        //设置使用的UI摄像机
        uiCanvas.worldCamera = uiCamera;
        //过场景不移除
        GameObject.DontDestroyOnLoad(uiCanvas.gameObject);

        //找到层级父对象
        bottomLayer = uiCanvas.transform.Find("Bottom");
        middleLayer = uiCanvas.transform.Find("Middle");
        topLayer = uiCanvas.transform.Find("Top");
        systemLayer = uiCanvas.transform.Find("System");

        //动态创建EventSystem
        uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
        GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
    }

    /// <summary>
    /// 获取对应层级的父对象
    /// </summary>
    /// <param name="layer">层级枚举值</param>
    /// <returns></returns>
    public Transform GetLayerFather(E_UILayer layer)
    {
        switch (layer)
        {
            case E_UILayer.Bottom:
                return bottomLayer;
            case E_UILayer.Middle:
                return middleLayer;
            case E_UILayer.Top:
                return topLayer;
            case E_UILayer.System:
                return systemLayer;
            default:
                return null;
        }
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    /// <param name="layer">面板显示的层级</param>
    /// <param name="callBack">由于可能是异步加载 因此通过委托回调的形式 将加载完成的面板传递出去进行使用</param>
    /// <param name="isSync">是否采用同步加载 默认为false</param>
    public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T:BasePanel
    {
        //获取面板名 预设体名必须和面板类名一致 
        string panelName = typeof(T).Name;
        //存在面板
        if(panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在异步加载中
            if(panelInfo.panel == null)
            {
                //如果之前显示了又隐藏 现在又想显示 那么直接设为false
                panelInfo.isHide = false;

                //如果正在异步加载 应该等待它加载完毕 只需要记录回调函数 加载完后去调用即可
                if (callBack != null)
                    panelInfo.callBack += callBack;
            }
            else//已经加载结束
            {
                //如果是失活状态 直接激活面板 就可以显示了
                if (!panelInfo.panel.gameObject.activeSelf)
                    panelInfo.panel.gameObject.SetActive(true);

                //如果要显示面板 会执行一次面板的默认显示逻辑
                panelInfo.panel.ShowMe();
                //如果存在回调 直接返回出去即可
                callBack?.Invoke(panelInfo.panel);
            }
            return;
        }

        //不存在面板 先存入字典当中 占个位置 之后如果又显示 我才能得到字典中的信息进行判断
        panelDic.Add(panelName, new PanelInfo<T>(callBack));

        //不存在面板 加载面板
        ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //表示异步加载结束前 就想要隐藏该面板了 
            if(panelInfo.isHide)
            {
                panelDic.Remove(panelName);
                return;
            }

            //层级的处理
            Transform father = GetLayerFather(layer);
            //避免没有按指定规则传递层级参数 避免为空
            if (father == null)
                father = middleLayer;
            //将面板预设体创建到对应父对象下 并且保持原本的缩放大小
            GameObject panelObj = GameObject.Instantiate(res, father, false);

            //获取对应UI组件返回出去
            T panel = panelObj.GetComponent<T>();
            //显示面板时执行的默认方法
            panel.ShowMe();
            //传出去使用
            panelInfo.callBack?.Invoke(panel);
            //回调执行完 将其清空 避免内存泄漏
            panelInfo.callBack = null;
            //存储panel
            panelInfo.panel = panel;

        }, isSync);
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <typeparam name="T">面板类型</typeparam>
    public void HidePanel<T>(bool isDestory = false) where T : BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //但是正在加载中
            if(panelInfo.panel == null)
            {
                //修改隐藏表示 表示 这个面板即将要隐藏
                panelInfo.isHide = true;
                //既然要隐藏了 回调函数都不会调用了 直接置空
                panelInfo.callBack = null;
            }
            else//已经加载结束
            {
                //执行默认的隐藏面板想要做的事情
                panelInfo.panel.HideMe();
                //如果要销毁  就直接将面板销毁从字典中移除记录
                if (isDestory)
                {
                    //销毁面板
                    GameObject.Destroy(panelInfo.panel.gameObject);
                    //从容器中移除
                    panelDic.Remove(panelName);
                }
                //如果不销毁 那么就只是失活 下次再显示的时候 直接复用即可
                else
                    panelInfo.panel.gameObject.SetActive(false);
            }
        }
    }

    /// <summary>
    /// 获取面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    public void GetPanel<T>( UnityAction<T> callBack ) where T:BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在加载中
            if(panelInfo.panel == null)
            {
                //加载中 应该等待加载结束 再通过回调传递给外部去使用
                panelInfo.callBack += callBack;
            }
            else if(!panelInfo.isHide)//加载结束 并且没有隐藏
            {
                callBack?.Invoke(panelInfo.panel);
            }
        }
    }
}

自定义事件添加函数 优化

为什么要进行 自定义事件添加函数 优化

        我们在制作UI功能时
        经常会有这样的需求:
        1.为一些不带默认事件的控件添加自定义事件,比如Image、Text这些基础组件,想为他们添加点击、单击、拖拽等事件监听
        2.为一些带默认事件的控件添加自定义事件,比如为 Button 按钮添加鼠标进入、鼠标移除等事件监听
        等等

自定义事件添加函数 实现

        主要实现思路:
        1.为想要添加自定义事件的控件添加EventTrigger组件
        2.通过EventTrigger组件添加对应自定义事件的监听

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

/// <summary>
/// 层级枚举
/// </summary>
public enum E_UILayer
{
    /// <summary>
    /// 最底层
    /// </summary>
    Bottom,
    /// <summary>
    /// 中层
    /// </summary>
    Middle,
    /// <summary>
    /// 高层
    /// </summary>
    Top,
    /// <summary>
    /// 系统层 最高层
    /// </summary>
    System,
}

/// <summary>
/// 管理所有UI面板的管理器
/// 注意:面板预设体名要和面板类名一致!!!!!
/// </summary>
public class UIMgr : BaseManager<UIMgr>
{
    /// <summary>
    /// 主要用于里式替换原则 在字典中 用父类容器装载子类对象
    /// </summary>
    private abstract class BasePanelInfo { }

    /// <summary>
    /// 用于存储面板信息 和加载完成的回调函数的
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    private class PanelInfo<T> : BasePanelInfo where T:BasePanel
    {
        public T panel;
        public UnityAction<T> callBack;
        public bool isHide;

        public PanelInfo(UnityAction<T> callBack)
        {
            this.callBack += callBack;
        }
    }


    private Camera uiCamera;
    private Canvas uiCanvas;
    private EventSystem uiEventSystem;

    //层级父对象
    private Transform bottomLayer;
    private Transform middleLayer;
    private Transform topLayer;
    private Transform systemLayer;

    /// <summary>
    /// 用于存储所有的面板对象
    /// </summary>
    private Dictionary<string, BasePanelInfo> panelDic = new Dictionary<string, BasePanelInfo>();

    private UIMgr()
    {
        //动态创建唯一的Canvas和EventSystem(摄像机)
        uiCamera = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/UICamera")).GetComponent<Camera>();
        //ui摄像机过场景不移除 专门用来渲染UI面板
        GameObject.DontDestroyOnLoad(uiCamera.gameObject);

        //动态创建Canvas
        uiCanvas = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/Canvas")).GetComponent<Canvas>();
        //设置使用的UI摄像机
        uiCanvas.worldCamera = uiCamera;
        //过场景不移除
        GameObject.DontDestroyOnLoad(uiCanvas.gameObject);

        //找到层级父对象
        bottomLayer = uiCanvas.transform.Find("Bottom");
        middleLayer = uiCanvas.transform.Find("Middle");
        topLayer = uiCanvas.transform.Find("Top");
        systemLayer = uiCanvas.transform.Find("System");

        //动态创建EventSystem
        uiEventSystem = GameObject.Instantiate(ResMgr.Instance.Load<GameObject>("UI/EventSystem")).GetComponent<EventSystem>();
        GameObject.DontDestroyOnLoad(uiEventSystem.gameObject);
    }

    /// <summary>
    /// 获取对应层级的父对象
    /// </summary>
    /// <param name="layer">层级枚举值</param>
    /// <returns></returns>
    public Transform GetLayerFather(E_UILayer layer)
    {
        switch (layer)
        {
            case E_UILayer.Bottom:
                return bottomLayer;
            case E_UILayer.Middle:
                return middleLayer;
            case E_UILayer.Top:
                return topLayer;
            case E_UILayer.System:
                return systemLayer;
            default:
                return null;
        }
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    /// <param name="layer">面板显示的层级</param>
    /// <param name="callBack">由于可能是异步加载 因此通过委托回调的形式 将加载完成的面板传递出去进行使用</param>
    /// <param name="isSync">是否采用同步加载 默认为false</param>
    public void ShowPanel<T>(E_UILayer layer = E_UILayer.Middle, UnityAction<T> callBack = null, bool isSync = false) where T:BasePanel
    {
        //获取面板名 预设体名必须和面板类名一致 
        string panelName = typeof(T).Name;
        //存在面板
        if(panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在异步加载中
            if(panelInfo.panel == null)
            {
                //如果之前显示了又隐藏 现在又想显示 那么直接设为false
                panelInfo.isHide = false;

                //如果正在异步加载 应该等待它加载完毕 只需要记录回调函数 加载完后去调用即可
                if (callBack != null)
                    panelInfo.callBack += callBack;
            }
            else//已经加载结束
            {
                //如果是失活状态 直接激活面板 就可以显示了
                if (!panelInfo.panel.gameObject.activeSelf)
                    panelInfo.panel.gameObject.SetActive(true);

                //如果要显示面板 会执行一次面板的默认显示逻辑
                panelInfo.panel.ShowMe();
                //如果存在回调 直接返回出去即可
                callBack?.Invoke(panelInfo.panel);
            }
            return;
        }

        //不存在面板 先存入字典当中 占个位置 之后如果又显示 我才能得到字典中的信息进行判断
        panelDic.Add(panelName, new PanelInfo<T>(callBack));

        //不存在面板 加载面板
        ABResMgr.Instance.LoadResAsync<GameObject>("ui", panelName, (res) =>
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //表示异步加载结束前 就想要隐藏该面板了 
            if(panelInfo.isHide)
            {
                panelDic.Remove(panelName);
                return;
            }

            //层级的处理
            Transform father = GetLayerFather(layer);
            //避免没有按指定规则传递层级参数 避免为空
            if (father == null)
                father = middleLayer;
            //将面板预设体创建到对应父对象下 并且保持原本的缩放大小
            GameObject panelObj = GameObject.Instantiate(res, father, false);

            //获取对应UI组件返回出去
            T panel = panelObj.GetComponent<T>();
            //显示面板时执行的默认方法
            panel.ShowMe();
            //传出去使用
            panelInfo.callBack?.Invoke(panel);
            //回调执行完 将其清空 避免内存泄漏
            panelInfo.callBack = null;
            //存储panel
            panelInfo.panel = panel;

        }, isSync);
    }

    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <typeparam name="T">面板类型</typeparam>
    public void HidePanel<T>(bool isDestory = false) where T : BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //但是正在加载中
            if(panelInfo.panel == null)
            {
                //修改隐藏表示 表示 这个面板即将要隐藏
                panelInfo.isHide = true;
                //既然要隐藏了 回调函数都不会调用了 直接置空
                panelInfo.callBack = null;
            }
            else//已经加载结束
            {
                //执行默认的隐藏面板想要做的事情
                panelInfo.panel.HideMe();
                //如果要销毁  就直接将面板销毁从字典中移除记录
                if (isDestory)
                {
                    //销毁面板
                    GameObject.Destroy(panelInfo.panel.gameObject);
                    //从容器中移除
                    panelDic.Remove(panelName);
                }
                //如果不销毁 那么就只是失活 下次再显示的时候 直接复用即可
                else
                    panelInfo.panel.gameObject.SetActive(false);
            }
        }
    }

    /// <summary>
    /// 获取面板
    /// </summary>
    /// <typeparam name="T">面板的类型</typeparam>
    public void GetPanel<T>( UnityAction<T> callBack ) where T:BasePanel
    {
        string panelName = typeof(T).Name;
        if (panelDic.ContainsKey(panelName))
        {
            //取出字典中已经占好位置的数据
            PanelInfo<T> panelInfo = panelDic[panelName] as PanelInfo<T>;
            //正在加载中
            if(panelInfo.panel == null)
            {
                //加载中 应该等待加载结束 再通过回调传递给外部去使用
                panelInfo.callBack += callBack;
            }
            else if(!panelInfo.isHide)//加载结束 并且没有隐藏
            {
                callBack?.Invoke(panelInfo.panel);
            }
        }
    }


    /// <summary>
    /// 为控件添加自定义事件
    /// </summary>
    /// <param name="control">对应的控件</param>
    /// <param name="type">事件的类型</param>
    /// <param name="callBack">响应的函数</param>
    public static void AddCustomEventListener(UIBehaviour control, EventTriggerType type, UnityAction<BaseEventData> callBack)
    {
        //这种逻辑主要是用于保证 控件上只会挂载一个EventTrigger
        EventTrigger trigger = control.GetComponent<EventTrigger>();
        if (trigger == null)
            trigger = control.gameObject.AddComponent<EventTrigger>();

        EventTrigger.Entry entry = new EventTrigger.Entry();
        entry.eventID = type;
        entry.callback.AddListener(callBack);

        trigger.triggers.Add(entry);
    }
}

场景切换模块

实现场景切换模块的主要思路

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;

/// <summary>
/// 场景切换管理器 主要用于切换场景
/// </summary>
public class SceneMgr : BaseManager<SceneMgr>
{
    private SceneMgr() { }

    //同步切换场景的方法
    public void LoadScene(string name, UnityAction callBack = null)
    {
        //切换场景
        SceneManager.LoadScene(name);
        //调用回调
        callBack?.Invoke();
        callBack = null;
    }

    //异步切换场景的方法
    public void LoadSceneAsyn(string name, UnityAction callBack = null)
    {
        MonoMgr.Instance.StartCoroutine(ReallyLoadSceneAsyn(name, callBack));
    }

    private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction callBack)
    {
        AsyncOperation ao = SceneManager.LoadSceneAsync(name);
        //不停的在协同程序中每帧检测是否加载结束 如果加载结束就不会进这个循环每帧执行了
        while (!ao.isDone)
        {
            //可以在这里利用事件中心 每一帧将进度发送给想要得到的地方
            EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, ao.progress);
            yield return 0;
        }
        //避免最后一帧直接结束了 没有同步1出去
        EventCenter.Instance.EventTrigger<float>(E_EventType.E_SceneLoadChange, 1);

        callBack?.Invoke();
        callBack = null;
    }
}

输入控制模块

实现输入控制模块

        主要要实现的就是监听
        键盘、鼠标、热键的输入
        并分发事件

        1.制作InputMgr单例模式管理器
        2.在输入管理器中进行按键检测
        3.利用事件中心分发事件
        4.在希望处理输入逻辑的位置监听事件
        5.提供输入系统检测开关

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InputMgr : BaseManager<InputMgr>
{
    //是否开启了输入系统检测
    private bool isStart;

    private InputMgr()
    {
        MonoMgr.Instance.AddUpdateListener(InputUpdate);
    }

    /// <summary>
    /// 开启或者关闭我们的输入管理模块的检测
    /// </summary>
    /// <param name="isStart"></param>
    public void StartOrCloseInputMgr(bool isStart)
    {
        this.isStart = isStart;
    }

    private void InputUpdate()
    {
        //如果外部没有开启检测功能 就不要检测
        if (!isStart)
            return;

        CheckKeyCode(KeyCode.W);
        CheckKeyCode(KeyCode.A);
        CheckKeyCode(KeyCode.S);
        CheckKeyCode(KeyCode.D);

        CheckKeyCode(KeyCode.H);
        CheckKeyCode(KeyCode.J);
        CheckKeyCode(KeyCode.K);
        CheckKeyCode(KeyCode.L);

        CheckMouse(0);
        CheckMouse(1);

        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Horizontal, Input.GetAxis("Horizontal"));

        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Vertical, Input.GetAxis("Vertical"));
    }

    private void CheckKeyCode(KeyCode key)
    {
        if (Input.GetKeyDown(key))
            EventCenter.Instance.EventTrigger(E_EventType.E_Keyboard_Down, key);

        if (Input.GetKeyUp(key))
            EventCenter.Instance.EventTrigger(E_EventType.E_Keyboard_Up, key);

        if (Input.GetKey(key))
            EventCenter.Instance.EventTrigger(E_EventType.E_Keyboard, key);
    }

    private void CheckMouse(int mouseID)
    {
        if (Input.GetMouseButtonDown(mouseID))
            EventCenter.Instance.EventTrigger(E_EventType.E_Mouse_Down, mouseID);
        if (Input.GetMouseButtonUp(mouseID))
            EventCenter.Instance.EventTrigger(E_EventType.E_Mouse_Up, mouseID);
        if (Input.GetMouseButton(mouseID))
            EventCenter.Instance.EventTrigger(E_EventType.E_Mouse, mouseID);
    }

}

改建功能需求

        输入管理器主要做的事情
        根据输入的信息,触发对应的事件
        其中输入信息可变,触发的事件也可变

        1.改建功能应该是针对某一个行为的
          触发的事件类型应该是针对行为的,因为行为一般在游戏中是固定的

        2.具体键位的触发不应该写死
          输入管理器中应该声明字典容器
          键:触发事件的类型
          值:具体的输入信息(键盘还是鼠标、按下还是抬起还是长按、那个键)

        3.键位的修改可以是键盘输入,也可以是鼠标输入,输入类型也可以是任意的
          输入管理器应该提供初始化行为对应的键位方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 输入信息
/// </summary>
public class InputInfo
{
    public enum E_KeyOrMouse
    {
        /// <summary>
        /// 键盘输入
        /// </summary>
        Key,
        /// <summary>
        /// 鼠标输入
        /// </summary>
        Mouse,
    }

    public enum E_InputType
    {
        /// <summary>
        /// 按下
        /// </summary>
        Down,
        /// <summary>
        /// 抬起
        /// </summary>
        Up,
        /// <summary>
        /// 长按
        /// </summary>
        Always,
    }

    //具体输入的类型——键盘还是鼠标
    public E_KeyOrMouse keyOrMouse;
    //输入的类型——抬起、按下、长按
    public E_InputType inputType;
    //KeyCode
    public KeyCode key;
    //mouseID
    public int mouseID;

    /// <summary>
    /// 主要给键盘输入初始化
    /// </summary>
    /// <param name="inputType"></param>
    /// <param name="key"></param>
    public InputInfo(E_InputType inputType, KeyCode key)
    {
        this.keyOrMouse = E_KeyOrMouse.Key;
        this.inputType = inputType;
        this.key = key;
    }

    /// <summary>
    /// 主要给鼠标输入初始化
    /// </summary>
    /// <param name="inputType"></param>
    /// <param name="mouseID"></param>
    public InputInfo(E_InputType inputType, int mouseID)
    {
        this.keyOrMouse = E_KeyOrMouse.Mouse;
        this.inputType = inputType;
        this.mouseID = mouseID;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InputMgr : BaseManager<InputMgr>
{
    private Dictionary<E_EventType, InputInfo> inputDic = new Dictionary<E_EventType, InputInfo>();

    //当前遍历时取出的输入信息
    private InputInfo nowInputInfo;

    //是否开启了输入系统检测
    private bool isStart;

    private InputMgr()
    {
        MonoMgr.Instance.AddUpdateListener(InputUpdate);
    }

    /// <summary>
    /// 开启或者关闭我们的输入管理模块的检测
    /// </summary>
    /// <param name="isStart"></param>
    public void StartOrCloseInputMgr(bool isStart)
    {
        this.isStart = isStart;
    }

    /// <summary>
    /// 提供给外部改建或初始化的方法(键盘)
    /// </summary>
    /// <param name="key"></param>
    /// <param name="inputType"></param>
    public void ChangeKeyboardInfo(E_EventType eventType, KeyCode key, InputInfo.E_InputType inputType)
    {
        //初始化
        if(!inputDic.ContainsKey(eventType))
        {
            inputDic.Add(eventType, new InputInfo(inputType, key));
        }
        else//改建
        {
            //如果之前是鼠标 我们必须要修改它的按键类型
            inputDic[eventType].keyOrMouse = InputInfo.E_KeyOrMouse.Key;
            inputDic[eventType].key = key;
            inputDic[eventType].inputType = inputType;
        }
    }

    /// <summary>
    /// 提供给外部改建或初始化的方法(鼠标)
    /// </summary>
    /// <param name="eventType"></param>
    /// <param name="mouseID"></param>
    /// <param name="inputType"></param>
    public void ChangeMouseInfo(E_EventType eventType, int mouseID, InputInfo.E_InputType inputType)
    {
        //初始化
        if (!inputDic.ContainsKey(eventType))
        {
            inputDic.Add(eventType, new InputInfo(inputType, mouseID));
        }
        else//改建
        {
            //如果之前是鼠标 我们必须要修改它的按键类型
            inputDic[eventType].keyOrMouse = InputInfo.E_KeyOrMouse.Mouse;
            inputDic[eventType].mouseID = mouseID;
            inputDic[eventType].inputType = inputType;
        }
    }

    /// <summary>
    /// 移除指定行为的输入监听
    /// </summary>
    /// <param name="eventType"></param>
    public void RemoveInputInfo(E_EventType eventType)
    {
        if (inputDic.ContainsKey(eventType))
            inputDic.Remove(eventType);
    }

    private void InputUpdate()
    {
        //如果外部没有开启检测功能 就不要检测
        if (!isStart)
            return;

        foreach (E_EventType eventType in inputDic.Keys)
        {
            nowInputInfo = inputDic[eventType];
            //如果是键盘输入
            if(nowInputInfo.keyOrMouse == InputInfo.E_KeyOrMouse.Key)
            {
                //是抬起还是按下还是长按
                switch (nowInputInfo.inputType)
                {
                    case InputInfo.E_InputType.Down:
                        if (Input.GetKeyDown(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Up:
                        if (Input.GetKeyUp(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Always:
                        if (Input.GetKey(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    default:
                        break;
                }
            }
            //如果是鼠标输入
            else
            {
                switch (nowInputInfo.inputType)
                {
                    case InputInfo.E_InputType.Down:
                        if (Input.GetMouseButtonDown(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Up:
                        if (Input.GetMouseButtonUp(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Always:
                        if (Input.GetMouseButton(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    default:
                        break;
                }
            }
        }

        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Horizontal, Input.GetAxis("Horizontal"));
        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Vertical, Input.GetAxis("Vertical"));
    }

}

获取输入信息逻辑

        在InputMgr的更新函数中
        获取当前输入内容
        用委托返回给外部

        主要思路:
        当存在按下输入时
        遍历监听KeyCode枚举中所有键位,检测是哪个键位输入了
        监听鼠标左中右键键位,检测是哪个键位输入了

        由于开启检测输入时可能伴随着键盘或者鼠标输入
        因此我们利用协同程序延迟一帧再进行检测

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class InputMgr : BaseManager<InputMgr>
{
    private Dictionary<E_EventType, InputInfo> inputDic = new Dictionary<E_EventType, InputInfo>();

    //当前遍历时取出的输入信息
    private InputInfo nowInputInfo;

    //是否开启了输入系统检测
    private bool isStart;
    //用于在改建时获取输入信息的委托 只有当update中获取到信息的时候 再通过委托传递给外部
    private UnityAction<InputInfo> getInputInfoCallBack;
    //是否开始检测输入信息
    private bool isBeginCheckInput = false;

    private InputMgr()
    {
        MonoMgr.Instance.AddUpdateListener(InputUpdate);
    }

    /// <summary>
    /// 开启或者关闭我们的输入管理模块的检测
    /// </summary>
    /// <param name="isStart"></param>
    public void StartOrCloseInputMgr(bool isStart)
    {
        this.isStart = isStart;
    }

    /// <summary>
    /// 提供给外部改建或初始化的方法(键盘)
    /// </summary>
    /// <param name="key"></param>
    /// <param name="inputType"></param>
    public void ChangeKeyboardInfo(E_EventType eventType, KeyCode key, InputInfo.E_InputType inputType)
    {
        //初始化
        if(!inputDic.ContainsKey(eventType))
        {
            inputDic.Add(eventType, new InputInfo(inputType, key));
        }
        else//改建
        {
            //如果之前是鼠标 我们必须要修改它的按键类型
            inputDic[eventType].keyOrMouse = InputInfo.E_KeyOrMouse.Key;
            inputDic[eventType].key = key;
            inputDic[eventType].inputType = inputType;
        }
    }

    /// <summary>
    /// 提供给外部改建或初始化的方法(鼠标)
    /// </summary>
    /// <param name="eventType"></param>
    /// <param name="mouseID"></param>
    /// <param name="inputType"></param>
    public void ChangeMouseInfo(E_EventType eventType, int mouseID, InputInfo.E_InputType inputType)
    {
        //初始化
        if (!inputDic.ContainsKey(eventType))
        {
            inputDic.Add(eventType, new InputInfo(inputType, mouseID));
        }
        else//改建
        {
            //如果之前是鼠标 我们必须要修改它的按键类型
            inputDic[eventType].keyOrMouse = InputInfo.E_KeyOrMouse.Mouse;
            inputDic[eventType].mouseID = mouseID;
            inputDic[eventType].inputType = inputType;
        }
    }

    /// <summary>
    /// 移除指定行为的输入监听
    /// </summary>
    /// <param name="eventType"></param>
    public void RemoveInputInfo(E_EventType eventType)
    {
        if (inputDic.ContainsKey(eventType))
            inputDic.Remove(eventType);
    }
    
    /// <summary>
    /// 获取下一次的输入信息
    /// </summary>
    /// <param name="callBack"></param>
    public void GetInputInfo(UnityAction<InputInfo> callBack)
    {
        getInputInfoCallBack = callBack;
        MonoMgr.Instance.StartCoroutine(BeginCheckInput());
    }

    private IEnumerator BeginCheckInput()
    {
        //等一帧
        yield return 0;
        //一帧后才会被置成true
        isBeginCheckInput = true;
    }

    private void InputUpdate()
    {
        //当委托不为空时 证明想要获取到输入的信息 传递给外部
        if(isBeginCheckInput)
        {
            //当一个键按下时 然后遍历所有按键信息 得到是谁被按下了
            if (Input.anyKeyDown)
            {
                InputInfo inputInfo = null;
                //我们需要去遍历监听所有键位的按下 来得到对应输入的信息
                //键盘
                Array keyCodes = Enum.GetValues(typeof(KeyCode));
                foreach (KeyCode inputKey in keyCodes)
                {
                    //判断到底是谁被按下了 那么就可以得到对应的输入的键盘信息
                    if (Input.GetKeyDown(inputKey))
                    {
                        inputInfo = new InputInfo(InputInfo.E_InputType.Down, inputKey);
                        break;
                    }
                }
                //鼠标
                for (int i = 0; i < 3; i++)
                {
                    if (Input.GetMouseButtonDown(i))
                    {
                        inputInfo = new InputInfo(InputInfo.E_InputType.Down, i);
                        break;
                    }
                }
                //把获取到的信息传递给外部
                getInputInfoCallBack.Invoke(inputInfo);
                getInputInfoCallBack = null;
                //检测一次后就停止检测了
                isBeginCheckInput = false;
            }
        }
       


        //如果外部没有开启检测功能 就不要检测
        if (!isStart)
            return;

        foreach (E_EventType eventType in inputDic.Keys)
        {
            nowInputInfo = inputDic[eventType];
            //如果是键盘输入
            if(nowInputInfo.keyOrMouse == InputInfo.E_KeyOrMouse.Key)
            {
                //是抬起还是按下还是长按
                switch (nowInputInfo.inputType)
                {
                    case InputInfo.E_InputType.Down:
                        if (Input.GetKeyDown(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Up:
                        if (Input.GetKeyUp(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Always:
                        if (Input.GetKey(nowInputInfo.key))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    default:
                        break;
                }
            }
            //如果是鼠标输入
            else
            {
                switch (nowInputInfo.inputType)
                {
                    case InputInfo.E_InputType.Down:
                        if (Input.GetMouseButtonDown(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Up:
                        if (Input.GetMouseButtonUp(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    case InputInfo.E_InputType.Always:
                        if (Input.GetMouseButton(nowInputInfo.mouseID))
                            EventCenter.Instance.EventTrigger(eventType);
                        break;
                    default:
                        break;
                }
            }
        }

        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Horizontal, Input.GetAxis("Horizontal"));
        EventCenter.Instance.EventTrigger(E_EventType.E_Input_Vertical, Input.GetAxis("Vertical"));
    }

}

计时器模块

为什么要自己制作计时器模块

        主要原因:
        1.使用Unity自带的计时功能,不能为所有类服务(只能继承了MonoBehaviour才能使用)
        2.利用协同程序和Update制作计时功能,如果需要都写,会产生代码冗余(写一堆类似重复的代码)

        因此
        我们将计时功能统一的进行管理,让所有系统都能够通过计时器模块进行计时

计时器模块的基本原理

        想要达到的效果:
        1.能够延时响应逻辑(比如:10s后执行某一逻辑)
        2.在延时过程中,可以固定时间间隔响应逻辑(比如:10s后执行某一逻辑,但是10s内每隔1s又能响应其它逻辑)

        基本原理:
        1.定义一个计时器对象,计时器对象记录延时执行时间、间隔执行时间、执行结束回调委托、间隔执行回调委托等等
        2.计时器模块中统一管理所有计时器对象,每个计时器对象有一个唯一ID,用来区分获取计时器对象
        3.在计时器模块中利用一个协同程序固定间隔时间进行计时器对象的时间检测
        4.计时器模块中提供创建计时器、移除计时器、停止计时器、开始计时器、重置计时器的相关方法

        主要要实现的类:
        计时器模块管理器
        TimerMgr
          需要声明的关键成员
          1.计时器字典容器
          2.待移除计时器列表容器
          等等
          需要实现的关键方法
          1.开启计时器管理器
          2.关闭计时器管理器
          3.创建单个计时器
          4.移除单个计时器
          5.重置单个计时器
          6.开启单个计时器
          7.停止单个计时器
          等等

        计时器对象类
        TimerItem
          需要声明的关键成员
          1.唯一ID
          2.延时执行回调委托
          3.间隔执行回到委托
          4.总时间
          5.间隔时间
          6.是否开启
          等等
          需要实现的关键方法
          1.初始化数据
          2.重置时间
          等等

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 计时器对象 里面存储了计时器的相关数据
/// </summary>
public class TimerItem : IPoolObject
{
    /// <summary>
    /// 唯一ID
    /// </summary>
    public int keyID;
    /// <summary>
    /// 计时结束后的委托回调
    /// </summary>
    public UnityAction overCallBack;
    /// <summary>
    /// 间隔一定时间去执行的委托回调
    /// </summary>
    public UnityAction callBack;

    /// <summary>
    /// 表示计时器总的计时时间 毫秒:1s = 1000ms
    /// </summary>
    public int allTime;
    /// <summary>
    /// 记录一开始计时时的总时间 用于时间重置
    /// </summary>
    public int maxAllTime;

    /// <summary>
    /// 间隔执行回调的时间 毫秒 毫秒:1s = 1000ms
    /// </summary>
    public int intervalTime;
    /// <summary>
    /// 记录一开始的间隔时间
    /// </summary>
    public int maxIntervalTime;

    /// <summary>
    /// 是否在进行计时
    /// </summary>
    public bool isRuning;

    /// <summary>
    /// 初始化计时器数据
    /// </summary>
    /// <param name="keyID">唯一ID</param>
    /// <param name="allTime">总的时间</param>
    /// <param name="overCallBack">总时间计时结束后的回调</param>
    /// <param name="intervalTime">间隔执行的时间</param>
    /// <param name="callBack">间隔执行时间结束后的回调</param>
    public void InitInfo(int keyID, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
    {
        this.keyID = keyID;
        this.maxAllTime = this.allTime = allTime;
        this.overCallBack = overCallBack;
        this.maxIntervalTime = this.intervalTime = intervalTime;
        this.callBack = callBack;
        this.isRuning = true;
    }

    /// <summary>
    /// 重置计时器
    /// </summary>
    public void ResetTimer()
    {
        this.allTime = this.maxAllTime;
        this.intervalTime = this.maxIntervalTime;
        this.isRuning = true;
    }

    /// <summary>
    /// 缓存池回收时  清除相关引用数据
    /// </summary>
    public void ResetInfo()
    {
        overCallBack = null;
        callBack = null;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 计时器管理器 主要用于开启、停止、重置等等操作来管理计时器
/// </summary>
public class TimerMgr : BaseManager<TimerMgr>
{
    /// <summary>
    /// 用于记录当前将要创建的唯一ID的
    /// </summary>
    private int TIMER_KEY = 0;
    /// <summary>
    /// 用于存储管理所有计时器的字典容器
    /// </summary>
    private Dictionary<int, TimerItem> timerDic = new Dictionary<int, TimerItem>();
    /// <summary>
    /// 待移除列表
    /// </summary>
    private List<TimerItem> delList = new List<TimerItem>();

    private Coroutine timer;

    /// <summary>
    /// 计时器管理器中的唯一计时用的协同程序 的间隔时间
    /// </summary>
    private const float intervalTime = 0.1f;

    private TimerMgr() 
    {
        //默认计时器就是开启的
        Start();
    }

    //开启计时器管理器的方法
    public void Start()
    {
         Coroutine timer = MonoMgr.Instance.StartCoroutine(StartTiming());
    }

    //关闭计时器管理器的方法
    public void Stop()
    {
        MonoMgr.Instance.StopCoroutine(timer);
    }

    IEnumerator StartTiming()
    {
        while (true)
        {
            //100毫秒进行一次计时
            yield return new WaitForSeconds(intervalTime);
            //遍历所有的计时器 进行数据更新
            foreach (TimerItem item in timerDic.Values)
            {
                if (!item.isRuning)
                    continue;
                //判断计时器是否有间隔时间执行的需求
                if(item.callBack != null)
                {
                    //减去100毫秒
                    item.intervalTime -= (int)(intervalTime*1000);
                    //满足一次间隔时间执行
                    if(item.intervalTime <= 0)
                    {
                        //间隔一定时间 执行一次回调
                        item.callBack.Invoke();
                        //重置间隔时间
                        item.intervalTime = item.maxIntervalTime;
                    }
                }
                //总的时间更新
                item.allTime -= (int)(intervalTime * 1000);
                //计时时间到 需要执行完成回调函数
                if(item.allTime <= 0)
                {
                    item.overCallBack.Invoke();
                    delList.Add(item);
                }
            }

            //移除待移除列表中的数据
            for (int i = 0; i < delList.Count; i++)
            {
                //从字典中移除
                timerDic.Remove(delList[i].keyID);
                //放入缓存池中
                PoolMgr.Instance.PushObj(delList[i]);
            }
            //移除结束后 清空列表
            delList.Clear();
        }
    }

    /// <summary>
    /// 创建单个计时器
    /// </summary>
    /// <param name="allTime">总的时间 毫秒 1s=1000ms</param>
    /// <param name="overCallBack">总时间结束回调</param>
    /// <param name="intervalTime">间隔计时时间 毫秒 1s=1000ms</param>
    /// <param name="callBack">间隔计时时间结束 回调</param>
    /// <returns>返回唯一ID 用于外部控制对应计时器</returns>
    public int CreateTimer(int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
    {
        //构建唯一ID
        int keyID = ++TIMER_KEY;
        //从缓存池取出对应的计时器
        TimerItem timerItem = PoolMgr.Instance.GetObj<TimerItem>();
        //初始化数据
        timerItem.InitInfo(keyID, allTime, overCallBack, intervalTime, callBack);
        //记录到字典中 进行数据更新
        timerDic.Add(keyID, timerItem);
        return keyID;
    }

    //移除单个计时器
    public void RemoveTimer(int keyID)
    {
        if(timerDic.ContainsKey(keyID))
        {
            //移除对应id计时器 放入缓存池
            PoolMgr.Instance.PushObj(timerDic[keyID]);
            //从字典中移除
            timerDic.Remove(keyID);
        }
    }

    /// <summary>
    /// 重置单个计时器
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void ResetTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].ResetTimer();
        }
    }

    /// <summary>
    /// 开启当个计时器 主要用于暂停后重新开始
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void StartTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].isRuning = true;
        }
    }

    /// <summary>
    /// 停止单个计时器 主要用于暂停
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void StopTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].isRuning = false;
        }
    }

}

计时器模块 进阶优化

        主要制作思路:
        在计时器模块中
        保留之前的受Time.timeScale影响的计时器
        并添加一种不受其影响的计时器
        让开发者可以根据需求选择使用

        主要修改处:
        1.添加一个字典容器专门记录不受其影响的计时器
        2.多开一个协同程序专门用于处理不受其影响的计时器
        3.修改相关方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 计时器管理器 主要用于开启、停止、重置等等操作来管理计时器
/// </summary>
public class TimerMgr : BaseManager<TimerMgr>
{
    /// <summary>
    /// 用于记录当前将要创建的唯一ID的
    /// </summary>
    private int TIMER_KEY = 0;
    /// <summary>
    /// 用于存储管理所有计时器的字典容器
    /// </summary>
    private Dictionary<int, TimerItem> timerDic = new Dictionary<int, TimerItem>();
    /// <summary>
    /// 用于存储管理所有计时器的字典容器(不受Time.timeScale影响的计时器)
    /// </summary>
    private Dictionary<int, TimerItem> realTimerDic = new Dictionary<int, TimerItem>();
    /// <summary>
    /// 待移除列表
    /// </summary>
    private List<TimerItem> delList = new List<TimerItem>();

    //为了避免内存的浪费 每次while都会生成 
    //我们直接将其声明为成员变量
    private WaitForSecondsRealtime waitForSecondsRealtime = new WaitForSecondsRealtime(intervalTime);
    private WaitForSeconds waitForSeconds = new WaitForSeconds(intervalTime);

    private Coroutine timer;
    private Coroutine realTimer;

    /// <summary>
    /// 计时器管理器中的唯一计时用的协同程序 的间隔时间
    /// </summary>
    private const float intervalTime = 0.1f;

    private TimerMgr() 
    {
        //默认计时器就是开启的
        Start();
    }

    //开启计时器管理器的方法
    public void Start()
    {
        timer = MonoMgr.Instance.StartCoroutine(StartTiming(false, timerDic));
        realTimer = MonoMgr.Instance.StartCoroutine(StartTiming(true, realTimerDic));
    }

    //关闭计时器管理器的方法
    public void Stop()
    {
        MonoMgr.Instance.StopCoroutine(timer);
        MonoMgr.Instance.StopCoroutine(realTimer);
    }


    IEnumerator StartTiming(bool isRealTime, Dictionary<int, TimerItem> timerDic)
    {
        while (true)
        {
            //100毫秒进行一次计时
            if (isRealTime)
                yield return waitForSecondsRealtime;
            else
                yield return waitForSeconds;
            //遍历所有的计时器 进行数据更新
            foreach (TimerItem item in timerDic.Values)
            {
                if (!item.isRuning)
                    continue;
                //判断计时器是否有间隔时间执行的需求
                if(item.callBack != null)
                {
                    //减去100毫秒
                    item.intervalTime -= (int)(intervalTime*1000);
                    //满足一次间隔时间执行
                    if(item.intervalTime <= 0)
                    {
                        //间隔一定时间 执行一次回调
                        item.callBack.Invoke();
                        //重置间隔时间
                        item.intervalTime = item.maxIntervalTime;
                    }
                }
                //总的时间更新
                item.allTime -= (int)(intervalTime * 1000);
                //计时时间到 需要执行完成回调函数
                if(item.allTime <= 0)
                {
                    item.overCallBack.Invoke();
                    delList.Add(item);
                }
            }

            //移除待移除列表中的数据
            for (int i = 0; i < delList.Count; i++)
            {
                //从字典中移除
                timerDic.Remove(delList[i].keyID);
                //放入缓存池中
                PoolMgr.Instance.PushObj(delList[i]);
            }
            //移除结束后 清空列表
            delList.Clear();
        }
    }

    /// <summary>
    /// 创建单个计时器
    /// </summary>
    /// <param name="isRealTime">如果是true不受Time.timeScale影响</param>
    /// <param name="allTime">总的时间 毫秒 1s=1000ms</param>
    /// <param name="overCallBack">总时间结束回调</param>
    /// <param name="intervalTime">间隔计时时间 毫秒 1s=1000ms</param>
    /// <param name="callBack">间隔计时时间结束 回调</param>
    /// <returns>返回唯一ID 用于外部控制对应计时器</returns>
    public int CreateTimer(bool isRealTime, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
    {
        //构建唯一ID
        int keyID = ++TIMER_KEY;
        //从缓存池取出对应的计时器
        TimerItem timerItem = PoolMgr.Instance.GetObj<TimerItem>();
        //初始化数据
        timerItem.InitInfo(keyID, allTime, overCallBack, intervalTime, callBack);
        //记录到字典中 进行数据更新
        if (isRealTime)
            realTimerDic.Add(keyID, timerItem);
        else
            timerDic.Add(keyID, timerItem);
        return keyID;
    }

    //移除单个计时器
    public void RemoveTimer(int keyID)
    {
        if(timerDic.ContainsKey(keyID))
        {
            //移除对应id计时器 放入缓存池
            PoolMgr.Instance.PushObj(timerDic[keyID]);
            //从字典中移除
            timerDic.Remove(keyID);
        }
        else if (realTimerDic.ContainsKey(keyID))
        {
            //移除对应id计时器 放入缓存池
            PoolMgr.Instance.PushObj(realTimerDic[keyID]);
            //从字典中移除
            realTimerDic.Remove(keyID);
        }
    }

    /// <summary>
    /// 重置单个计时器
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void ResetTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].ResetTimer();
        }
        else if (realTimerDic.ContainsKey(keyID))
        {
            realTimerDic[keyID].ResetTimer();
        }
    }

    /// <summary>
    /// 开启当个计时器 主要用于暂停后重新开始
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void StartTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].isRuning = true;
        }
        else if (realTimerDic.ContainsKey(keyID))
        {
            realTimerDic[keyID].isRuning = true;
        }
    }

    /// <summary>
    /// 停止单个计时器 主要用于暂停
    /// </summary>
    /// <param name="keyID">计时器唯一ID</param>
    public void StopTimer(int keyID)
    {
        if (timerDic.ContainsKey(keyID))
        {
            timerDic[keyID].isRuning = false;
        }
        else if (realTimerDic.ContainsKey(keyID))
        {
            realTimerDic[keyID].isRuning = false;
        }
    }
}

文本工作模块

主要作用

        在游戏开发中
        经常会对字符串进行一些处理
        而这些处理往往会在多处使用
        为了减少代码冗余
        我们往往会把常用的文本操作逻辑
        封装到一个工具类中提供给外部使用

        因此文本工具模块
        主要就是提取通用的文本处理逻辑封装为方法
        供外部使用

基本原理

        我们将新建一个文本工具类
        主要提供以下方法:
        1.字符串拆封公共方法
          字符串拆分为字符串数组
          字符串拆分为int数组
        2.数字前补0转字符串
        3.秒转时分秒
        4.秒转00:00:00
        5.大数据数值转换
        等等

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 用于处理字符串的一些公共功能的
/// </summary>
public class TextUtil
{
    private static StringBuilder resultStr = new StringBuilder("");

    #region 字符串拆分相关
    /// <summary>
    /// 拆分字符串 返回字符串数组
    /// </summary>
    /// <param name="str">想要被拆分的字符串</param>
    /// <param name="type">拆分字符类型: 1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <returns></returns>
    public static string[] SplitStr(string str, int type = 1)
    {
        if (str == "")
            return new string[0];
        string newStr = str;
        if (type == 1)
        {
            //为了避免英文符号填成了中文符号 我们先进行一个替换
            while (newStr.IndexOf(";") != -1)
                newStr = newStr.Replace(";", ";");
            return newStr.Split(';');
        }
        else if (type == 2)
        {
            //为了避免英文符号填成了中文符号 我们先进行一个替换
            while (newStr.IndexOf(",") != -1)
                newStr = newStr.Replace(",", ",");
            return newStr.Split(',');
        }
        else if (type == 3)
        {
            return newStr.Split('%');
        }
        else if (type == 4)
        {
            //为了避免英文符号填成了中文符号 我们先进行一个替换
            while (newStr.IndexOf(":") != -1)
                newStr = newStr.Replace(":", ":");
            return newStr.Split(':');
        }
        else if (type == 5)
        {
            return newStr.Split(' ');
        }
        else if (type == 6)
        {
            return newStr.Split('|');
        }
        else if (type == 7)
        {
            return newStr.Split('_');
        }

        return new string[0];
    }

    /// <summary>
    /// 拆分字符串 返回整形数组
    /// </summary>
    /// <param name="str">想要被拆分的字符串</param>
    /// <param name="type">拆分字符类型: 1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <returns></returns>
    public static int[] SplitStrToIntArr(string str, int type = 1)
    {
        //得到拆分后的字符串数组
        string[] strs = SplitStr(str, type);
        if (strs.Length == 0)
            return new int[0];
        //把字符串数组 转换成 int数组 
        return Array.ConvertAll<string, int>(strs, (str) =>
        {
            return int.Parse(str);
        });
    }

    /// <summary>
    /// 专门用来拆分多组键值对形式的数据的 以int返回
    /// </summary>
    /// <param name="str">待拆分的字符串</param>
    /// <param name="typeOne">组间分隔符  1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <param name="typeTwo">键值对分隔符 1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <param name="callBack">回调函数</param>
    public static void SplitStrToIntArrTwice(string str, int typeOne, int typeTwo, UnityAction<int, int> callBack)
    {
        string[] strs = SplitStr(str, typeOne);
        if (strs.Length == 0)
            return;
        int[] ints;
        for (int i = 0; i < strs.Length; i++)
        {
            //拆分单个道具的ID和数量信息
            ints = SplitStrToIntArr(strs[i], typeTwo);
            if (ints.Length == 0)
                continue;
            callBack.Invoke(ints[0], ints[1]);
        }
    }

    /// <summary>
    /// 专门用来拆分多组键值对形式的数据的 以string返回
    /// </summary>
    /// <param name="str">待拆分的字符串</param>
    /// <param name="typeOne">组间分隔符 1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <param name="typeTwo">键值对分隔符  1-; 2-, 3-% 4-: 5-空格 6-| 7-_ </param>
    /// <param name="callBack">回调函数</param>
    public static void SplitStrTwice(string str, int typeOne, int typeTwo, UnityAction<string, string> callBack)
    {
        string[] strs = SplitStr(str, typeOne);
        if (strs.Length == 0)
            return;
        string[] strs2;
        for (int i = 0; i < strs.Length; i++)
        {
            //拆分单个道具的ID和数量信息
            strs2 = SplitStr(strs[i], typeTwo);
            if (strs2.Length == 0)
                continue;
            callBack.Invoke(strs2[0], strs2[1]);
        }
    }


    #endregion

    #region 数字转字符串相关
    /// <summary>
    /// 得到指定长度的数字转字符串内容,如果长度不够会在前面补0,如果长度超出,会保留原始数值
    /// </summary>
    /// <param name="value">数值</param>
    /// <param name="len">长度</param>
    /// <returns></returns>
    public static string GetNumStr(int value, int len)
    {
        //tostring中传入一个 Dn 的字符串
        //代表想要将数字转换为长度位n的字符串
        //如果长度不够 会在前面补0
        return value.ToString($"D{len}");
    }
    /// <summary>
    /// 让指定浮点数保留小数点后n位
    /// </summary>
    /// <param name="value">具体的浮点数</param>
    /// <param name="len">保留小数点后n位</param>
    /// <returns></returns>
    public static string GetDecimalStr(float value, int len)
    {
        //tostring中传入一个 Fn 的字符串
        //代表想要保留小数点后几位小数
        return value.ToString($"F{len}");
    }

    /// <summary>
    /// 将较大较长的数 转换为字符串
    /// </summary>
    /// <param name="num">具体数值</param>
    /// <returns>n亿n千万 或 n万n千 或 1000 3434 234</returns>
    public static string GetBigDataToString(int num)
    {
        //如果大于1亿 那么就显示 n亿n千万
        if (num >= 100000000)
        {
            return BigDataChange(num, 100000000, "亿", "千万");
        }
        //如果大于1万 那么就显示 n万n千
        else if (num >= 10000)
        {
            return BigDataChange(num, 10000, "万", "千");
        }
        //都不满足 就直接显示数值本身
        else
            return num.ToString();
    }

    /// <summary>
    /// 把大数据转换成对应的字符串拼接
    /// </summary>
    /// <param name="num">数值</param>
    /// <param name="company">分割单位 可以填 100000000、10000</param>
    /// <param name="bigCompany">大单位 亿、万</param>
    /// <param name="littltCompany">小单位 万、千</param>
    /// <returns></returns>
    private static string BigDataChange(int num, int company, string bigCompany, string littltCompany)
    {
        resultStr.Clear();
        //有几亿、几万
        resultStr.Append(num / company);
        resultStr.Append(bigCompany);
        //有几千万、几千
        int tmpNum = num % company;
        //看有几千万、几千
        tmpNum /= (company / 10);
        //算出来不为0
        if(tmpNum != 0)
        {
            resultStr.Append(tmpNum);
            resultStr.Append(littltCompany);
        }
        return resultStr.ToString();
    }

    #endregion

    #region 时间转换相关
    /// <summary>
    /// 秒转时分秒格式 其中时分秒可以自己传
    /// </summary>
    /// <param name="s">秒数</param>
    /// <param name="egZero">是否忽略0</param>
    /// <param name="isKeepLen">是否保留至少2位</param>
    /// <param name="hourStr">小时的拼接字符</param>
    /// <param name="minuteStr">分钟的拼接字符</param>
    /// <param name="secondStr">秒的拼接字符</param>
    /// <returns></returns>
    public static string SecondToHMS(int s, bool egZero = false, bool isKeepLen = false, string hourStr = "时", string minuteStr = "分", string secondStr = "秒")
    {
        //时间不会有负数 所以我们如果发现是负数直接归0
        if (s < 0)
            s = 0;
        //计算小时
        int hour = s / 3600;
        //计算分钟
        //除去小时后的剩余秒
        int second = s % 3600;
        //剩余秒转为分钟数
        int minute = second / 60;
        //计算秒
        second = s % 60;
        //拼接
        resultStr.Clear();
        //如果小时不为0 或者 不忽略0 
        if (hour != 0 || !egZero)
        {
            resultStr.Append(isKeepLen?GetNumStr(hour, 2):hour);//具体几个小时
            resultStr.Append(hourStr);
        }
        //如果分钟不为0 或者 不忽略0 或者 小时不为0
        if(minute != 0 || !egZero || hour != 0)
        {
            resultStr.Append(isKeepLen?GetNumStr(minute,2): minute);//具体几分钟
            resultStr.Append(minuteStr);
        }
        //如果秒不为0 或者 不忽略0 或者 小时和分钟不为0
        if(second != 0 || !egZero || hour != 0 || minute != 0)
        {
            resultStr.Append(isKeepLen?GetNumStr(second,2): second);//具体多少秒
            resultStr.Append(secondStr);
        }

        //如果传入的参数是0秒时
        if(resultStr.Length == 0)
        {
            resultStr.Append(0);
            resultStr.Append(secondStr);
        }

        return resultStr.ToString();
    }
    
    /// <summary>
    /// 秒转00:00:00格式
    /// </summary>
    /// <param name="s"></param>
    /// <param name="egZero"></param>
    /// <returns></returns>
    public static string SecondToHMS2(int s, bool egZero = false)
    {
        return SecondToHMS(s, egZero, true, ":", ":", "");
    }
    #endregion

}

数学计算工具模块

主要作用

        在游戏开发中
        经常会进行一些通用的数学计算
        为了减少代码冗余
        我们往往会把常用的数学计算逻辑
        封装到一个工具类中提供给外部使用

        因此数学计算工具模块
        主要就是将通用的数学计算逻辑封装为方法
        供外部使用

基本原理

        我们将新建一个数学计算工具类
        主要提供以下方法:
        1.角度和弧度的转换
        2.得到两个对象在xz平面上的距离
        3.判断对象是否在屏幕范围外
        4.判断对象位置是否在xz平面扇形范围内
        5.射线检测
        6.范围检测
        等等

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class MathUtil
{
    #region 角度和弧度
    /// <summary>
    /// 角度转弧度的方法
    /// </summary>
    /// <param name="deg">角度值</param>
    /// <returns>弧度值</returns>
    public static float Deg2Rad(float deg)
    {
        return deg * Mathf.Deg2Rad;
    }

    /// <summary>
    /// 弧度转角度的方法
    /// </summary>
    /// <param name="rad">弧度值</param>
    /// <returns>角度值</returns>
    public static float Rad2Deg(float rad)
    {
        return rad * Mathf.Rad2Deg;
    }
    #endregion

    #region 距离计算相关的
    /// <summary>
    /// 获取XZ平面上 两点的距离
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <returns></returns>
    public static float GetObjDistanceXZ(Vector3 srcPos, Vector3 targetPos)
    {
        srcPos.y = 0;
        targetPos.y = 0;
        return Vector3.Distance(srcPos, targetPos);
    }

    /// <summary>
    /// 判断两点之间距离 是否小于等于目标距离 XZ平面
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <param name="dis">距离</param>
    /// <returns></returns>
    public static bool CheckObjDistanceXZ(Vector3 srcPos, Vector3 targetPos, float dis)
    {
        return GetObjDistanceXZ(srcPos, targetPos) <= dis;
    }

    /// <summary>
    /// 获取XY平面上 两点的距离
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <returns></returns>
    public static float GetObjDistanceXY(Vector3 srcPos, Vector3 targetPos)
    {
        srcPos.z = 0;
        targetPos.z = 0;
        return Vector3.Distance(srcPos, targetPos);
    }

    /// <summary>
    /// 判断两点之间距离 是否小于等于目标距离 XY平面
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <param name="dis">距离</param>
    /// <returns></returns>
    public static bool CheckObjDistanceXY(Vector3 srcPos, Vector3 targetPos, float dis)
    {
        return GetObjDistanceXY(srcPos, targetPos) <= dis;
    }

    #endregion

    #region 位置判断相关
    /// <summary>
    /// 判断世界坐标系下的某一个点 是否在屏幕可见范围外
    /// </summary>
    /// <param name="pos">世界坐标系下的一个点的位置</param>
    /// <returns>如果在可见范围外返回true,否则返回false</returns>
    public static bool IsWorldPosOutScreen(Vector3 pos)
    {
        //将世界坐标转为屏幕坐标
        Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
        //判断是否在屏幕范围内
        if (screenPos.x >= 0 && screenPos.x <= Screen.width &&
            screenPos.y >= 0 && screenPos.y <= Screen.height)
            return false;
        return true;
    }

    /// <summary>
    /// 判断某一个位置 是否在指定扇形范围内(注意:传入的坐标向量都必须是基于同一个坐标系下的)
    /// </summary>
    /// <param name="pos">扇形中心点位置</param>
    /// <param name="forward">自己的面朝向</param>
    /// <param name="targetPos">目标对象</param>
    /// <param name="radius">半径</param>
    /// <param name="angle">扇形的角度</param>
    /// <returns></returns>
    public static bool IsInSectorRangeXZ(Vector3 pos, Vector3 forward, Vector3 targetPos, float radius, float angle)
    {
        pos.y = 0;
        forward.y = 0;
        targetPos.y = 0;
        //距离 + 角度
        return Vector3.Distance(pos, targetPos) <= radius && Vector3.Angle(forward, targetPos - pos) <= angle / 2f;
    }
    #endregion

    #region 射线检测相关

    /// <summary>
    /// 射线检测 获取一个对象 指定距离 指定层级的
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if(Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
            callBack.Invoke(hitInfo);
    }

    /// <summary>
    /// 射线检测 获取一个对象 指定距离 指定层级的
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
            callBack.Invoke(hitInfo.collider.gameObject);
    }

    /// <summary>
    /// 射线检测 获取一个对象 指定距离 指定层级的
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的对象信息上挂在的指定脚本传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
            callBack.Invoke(hitInfo.collider.gameObject.GetComponent<T>());
    }

    /// <summary>
    /// 射线检测 获取到多个对象 指定距离 指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去) 每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
            callBack.Invoke(hitInfos[i]);
    }

    /// <summary>
    /// 射线检测 获取到多个对象 指定距离 指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去) 每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
            callBack.Invoke(hitInfos[i].collider.gameObject);
    }

    /// <summary>
    /// 射线检测 获取到多个对象 指定距离 指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的对象信息上依附的脚本传递出去) 每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
            callBack.Invoke(hitInfos[i].collider.gameObject.GetComponent<T>());
    }
    #endregion

    #region 范围检测相关
    /// <summary>
    /// 进行盒装范围检测
    /// </summary>
    /// <typeparam name="T">想要获取的信息类型 可以填写 Collider GameObject 以及对象上依附的组件类型</typeparam>
    /// <param name="center">盒装中心点</param>
    /// <param name="rotation">盒子的角度</param>
    /// <param name="halfExtents">长宽高的一半</param>
    /// <param name="layerMask">层级筛选</param>
    /// <param name="callBack">回调函数 </param>
    public static void OverlapBox<T>(Vector3 center, Quaternion rotation, Vector3 halfExtents, int layerMask, UnityAction<T> callBack) where T : class
    {
        Type type = typeof(T);
        Collider[] colliders = Physics.OverlapBox(center, halfExtents, rotation, layerMask, QueryTriggerInteraction.Collide);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (type == typeof(Collider))
                callBack.Invoke(colliders[i] as T);
            else if (type == typeof(GameObject))
                callBack.Invoke(colliders[i].gameObject as T);
            else
                callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
        }
    }

    /// <summary>
    /// 进行球体范围检测
    /// </summary>
    /// <typeparam name="T">想要获取的信息类型 可以填写 Collider GameObject 以及对象上依附的组件类型</typeparam>
    /// <param name="center">球体的中心点</param>
    /// <param name="radius">球体的半径</param>
    /// <param name="layerMask">层级筛选</param>
    /// <param name="callBack">回调函数</param>
    public static void OverlapSphere<T>(Vector3 center, float radius, int layerMask, UnityAction<T> callBack) where T:class
    {
        Type type = typeof(T);
        Collider[] colliders = Physics.OverlapSphere(center, radius, layerMask, QueryTriggerInteraction.Collide);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (type == typeof(Collider))
                callBack.Invoke(colliders[i] as T);
            else if (type == typeof(GameObject))
                callBack.Invoke(colliders[i].gameObject as T);
            else
                callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
        }
    }
    #endregion
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值