游戏框架搭建

目录结构

下面的目录结构并非框架完整结构,只是目前阶段所更新到的部分的目录结构,后续可能会发生改变。

项目资源目录

image-20240217123902750

游戏对象目录

image-20240217123756589

前置知识

C#扩展方法的使用

使用扩展方法,能够向现有的类型中添加方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。

扩展方法的结构

  • 扩展方法所在的类必须声明为static
  • 扩展方法必须生命为public和static
  • 扩展方法的第一个参数必须包含关键字this,并且在后面指定需要扩展的类的名称
public static class ExpandMethod
{
    public static int GetNum(this int num)
    {
        return num * 2 * 4 * 3 - 1 / 3;
    }
}

框架中需要一个单独的类型用于存放框架主要的拓展方法

namespace DLFrame.Scripts._1.Base._0.Extension
{
    /// <summary>
    /// DLFrame 框架主要的拓展方法
    /// </summary>
    public static class DLExtension
    {

    }
}

C#自定义特性

C#特性

特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。您可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。

特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。

[attribute(positional_parameters, name_parameter = value, ...)]
element

下面是一个Unity特性的示例,用于将私有字段显示在检视面板中。

[SerializeField]
private int num;

自定义特性

除了现有特性之外,我们还可以自定义一些特性,具体方式如下:

using System;
using UnityEngine;

namespace Test
{
    public class Test:MonoBehaviour
    {
        [SerializeField]
        [Text(false)]
        private TextAsset textAsset;
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, AllowMultiple = true)]
public class TextAttribute : Attribute
{
    public bool CanSwitch;

    public TextAttribute(bool canSwitch)
    {
        CanSwitch = canSwitch;
    }
}

继承Attribute的类即为自定义特性,自定义特性可以添加约束,从上面的例子中我们可以看出,约束也是一种特性,该特性的第一个参数约束它能够修饰的东西,第二个参数约束了他是否可以使用多次。

通过反射可以获取到类所具有的特性,方法具有的特性,以及字段具有的特性,具备不同特性的东西我们对他做不同的处理,实际上C#的特性就和Java的注解差不多

下面在拓展方法类中加入了有关获取特性的拓展方法

using System;
using System.Reflection;

namespace DLFrame.Scripts._1.Base._0.Extension
{
    /// <summary>
    /// DLFrame 框架主要的拓展方法
    /// </summary>
    public static class DLExtension
    {
        #region 通用

        /// <summary>
        /// 获取对象自身的特性
        /// </summary>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        public static T GetAttribute<T>(this object obj) where T : Attribute
        {
            return obj.GetType().GetCustomAttribute<T>();
        }

        /// <summary>
        /// 获取指定类型的特性
        /// </summary>
        /// <param name="type">指定类型</param>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        // ReSharper disable once InvalidXmlDocComment
        public static T GetAttribute<T>(this object obj, Type type) where T : Attribute
        {
            return type.GetCustomAttribute<T>();
        }

        #endregion

    }
}

ScriptableObject

用于创造一个数据容器,一般是用于制造一些配置文件,比如武器的配置信息就可以单独创造一个这样的ScriptableObject,然后就可以创建很多个武器配置对象,只需要调整每个武器配置对象里面的数据,就制作出了不同的武器。

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

[Serializable]
public class GameData
{
    public int gameID;
    public string gamePath;
}

[CreateAssetMenu(fileName = "GameConfig", menuName = "GameConfig/设置")]
public class GameConfig : ScriptableObject
{
    public string path;
    public int id;
    public GameData gameData;
    public List<GameData> gameDatas;
}

Odin插件的使用

Odin插件是一个编辑器拓展插件,用于以更加人性化的方式展示Unity检视面板中的信息,同时也提供了检视面板中的更多组件支持,非常好用。

Odin插件的网盘地址

链接:打开百度网盘链接
提取码:uwbx

SerializedScriptableObject

上文中提到了ScriptableObject,其非常适合制作配置文件,而Odin优化了检视面板中的配置样式并提供了更多配置类组件,因此二者结合将更加好用,Odin也给我们提供了一个比ScriptableObject功能更加强大的基类,名为SerializedScriptableObject,因此导入Odin插件后,我们往往直接使用SerializedScriptableObject。

项目准备

游戏框架的结构

image-20240116101040304

单例基类-C

这里的单例是C#原生的单例,不继承Mono的对象使用这个单例。

namespace DLFrame.Scripts._1.Base._1.Singleton
{
    /// <summary>
    /// 单例模式的基类
    /// </summary>
    /// <typeparam name="T">继承该基类的类型</typeparam>
    public class Singleton<T> where T : Singleton<T>, new()
    {
        private static T _instance;

        public static T Instance => _instance ??= new T();
    }
}

知识点补充

在 C# 中,??= 是一个空合并赋值运算符。用于检查左边的变量(操作数)是否为 null,如果左边的变量不为 null,则保持其原始值,如果左边的变量为 null,则将其设置为右边操作数的值。

使用=>定义的属性是只读的,也就是只实现了get访问器。

上面的例子 public static T Instance => _instance ??= new T(); 就是一个只读属性。这个属性每次被访问时,都会执行 _instance ??= new T() 表达式,并返回结果。如果 _instancenull,它会被初始化为 new T() 的结果,否则保留其现有值。

上方where后面的newnew()是一个泛型约束,它要求类型 T 必须有一个无参数的构造函数(默认构造函数)。这个约束是必要的,因为这允许 Singleton 类在需要的时候创建 T 类型的新实例。如果没有new()这个约束的话,就不能在单例代码里new T();了。

单例基类-Mono

这里是为Unity中继承Mono的游戏物体准备的单例基类,所有场景中只允许存在一个的游戏物体都可以使用它来实现单例模式。

using UnityEngine;

namespace DLFrame.Scripts._1.Base._1.Singleton
{
    /// <summary>
    /// Mono单例模式基类(通过继承该类实现单例最好是在Awake阶段先判断场景中是否存在该单例)
    /// </summary>
    /// <typeparam name="T">继承该基类的类型</typeparam>
    public class SingletonMono<T> : MonoBehaviour where T : SingletonMono<T>
    {
        // ReSharper disable once MemberCanBePrivate.Global
        public static T Instance;

        protected virtual void Awake()
        {
            if (Instance != null)
            {
                Destroy(gameObject);
                return;
            }
            Instance = this as T;
        }
    }
}

知识点补充

where那里是用来对泛型进行约束的,这里的意思是泛型T的类型必须是继承了SingletonMono<T>的类型。上面那个C#单例中也是同理的。

想要制造单例物体时,我们只需要这样做(这里拿GameRoot作为示例):

using DLFrame.Scripts._1.Base._1.Singleton;

namespace DLFrame.Scripts._1.Base
{
    /// <summary>
    /// 框架根组件
    /// </summary>
    public class GameRoot : SingletonMono<GameRoot>
    {

    }
}
  • GameRoot是我们游戏中的根物体组件,根物体永远不会消失,用于管理游戏中各种全局配置,GameRoot组件一般挂载在GameRoot物体上面。

对象池

对象池介绍

为什么需要对象池?

  • 对象的创建以及销毁都有性能开销
  • 我们希望尽可能的不需要创建、销毁,生成部分出来后重复利用

image-20240118103607315

  • 由于Unity开发中,我们有两种类型的实例,一种是GameObject,一种是普通C#类,对象池中也要兼容这两种不同的类型。

注:对象移入对象池之前,记得执行销毁逻辑,从对象池拿出来之后,记得先执行初始化逻辑,方式之前用过的对象残留一些脏数据

管理器基类

游戏框架中会涉及各种各样的管理器,比如即将要实现的对象池管理器,以及资源管理器等,因此我们要创建一个管理器基类,让所有的管理器全部都继承管理器基类,方便我们统一管理。

管理器基类

using UnityEngine;

namespace DLFrame.Scripts._1.Base._1.Singleton
{
    /// <summary>
    /// 管理器基类
    /// </summary>
    public abstract class ManagerBase : MonoBehaviour
    {
        public virtual void Init(){}
    }
    /// <summary>
    /// 管理器基类
    /// </summary>
    /// <typeparam name="T">继承管理器基类的管理器类</typeparam>
    public abstract class ManagerBase<T> : ManagerBase where T : ManagerBase<T>
    {
        public static T Instance;

        /// <summary>
        /// 管理器的初始化
        /// </summary>
        public override void Init()
        {
            Instance = this as T;
        }
    }
}

注意,管理器基类分为了不带泛型的管理器基类和带泛型的管理器基类,带泛型的管理器基类继承了不带泛型的管理器基类,其目的是使所有带泛型的管理器基类拥有一个公共的父类(不带泛型的管理器基类)。

这样处理,我们就可以通过GetComponents<ManagerBase>();直接获取到所有的管理器,方便我们对所有的管理器进行统一的管理,例如在游戏开始时运行所有管理器的Init()方法,以初始化所有的管理器。

GameRoot

GameRoot是框架的根物体组件,根物体永远不会消失,用于管理游戏中各种全局配置,各种管理器一般都会挂载在GameRoot上面,统一管理。

GameRoot

using DLFrame.Scripts._1.Base._1.Singleton;

namespace DLFrame.Scripts._1.Base
{
    /// <summary>
    /// 框架根组件
    /// </summary>
    public class GameRoot : SingletonMono<GameRoot>
    {
        protected override void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(gameObject);

            //初始化所有管理器
            InitManagers();
        }

        /// <summary>
        /// 初始化所有管理器
        /// </summary>
        private void InitManagers()
        {
            ManagerBase[] managers = GetComponents<ManagerBase>();
            foreach (ManagerBase manager in managers)
            {
                manager.Init();
            }
        }
    }
}
  • 由于在开发中,我们可能会涉及到多个场景的开发,对于每个场景我们可能单独进行测试,因此我们每个场景都会有一个GameRoot。GameRoot继承自单例类,防止了出现多个GameRoot的情况发生,这样在切换场景时,多余的GameRoot会在初始化的时候自动被销毁掉。

GameObject对象池

对象池的基本组成

我们需要创建一个对象池容器,并将其放置在GameRoot下方,对象需要使用的时候,如果容器中有该对象,我们就拿出来用,用完再放回去关掉。

image-20240119113343869

PoolManager脚本挂载到GameRoot上面,并将容器赋值给PoolManager。

image-20240119113444874

using DLFrame.Scripts._1.Base._1.Singleton;
using UnityEngine;

namespace DLFrame.Scripts._1.Base._2.Pool
{
    /// <summary>
    /// 对象池管理器
    /// </summary>
    public class PoolManager : ManagerBase<PoolManager>
    {
        /// <summary>
        /// 根节点(SerializeField为了让私有属性可以在检视面板中赋值)
        /// </summary>
        [SerializeField] private GameObject poolRootObj;

        public override void Init()
        {
            base.Init();
            Debug.Log("PoolManager 初始化成功");
        }
    }
}

GameObjectPoolData

因为对象池容器PoolRoot只能算是一个大的容器,里面还要放很多小容器,比如子弹池,背包格子池,花花草草池等等。所以我们需要一个类单独管理这些具体的对象容器,这个类就是GameObjectPoolData

换句话说,每一个GameObjectPoolData对象都管理了对象池中的某一类对象,包括该类对象的父节点,该类对象实例等等,对象池中有多少种类的对象,就有多少个GameObjectPoolData

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

namespace DLFrame.Scripts._1.Base._2.Pool
{
    /// <summary>
    /// GameObject对象池数据
    /// </summary>
    public class GameObjectPoolData
    {
        /// <summary>
        /// 对象池中 父节点
        /// </summary>
        private readonly GameObject _fatherObj;

        /// <summary>
        /// 对象容器
        /// </summary>
        public readonly Queue<GameObject> PoolQueue;

        public GameObjectPoolData(GameObject obj, GameObject poolRootObj)
        {
            // 创建父物体 并设置到对象池根节点下方
            _fatherObj = new GameObject(obj.name);
            _fatherObj.transform.SetParent(poolRootObj.transform);
            PoolQueue = new Queue<GameObject>();
            // 把首次创建时 需要放入的对象 放进容器
            PushObj(obj);
        }

        /// <summary>
        /// 将对象放进对象池
        /// </summary>
        /// <param name="obj">对象</param>
        public void PushObj(GameObject obj)
        {
            // 对象进容器
            PoolQueue.Enqueue(obj);
            // 设置父物体
            obj.transform.SetParent(_fatherObj.transform);
            // 设置隐藏
            obj.SetActive(false);
        }

        /// <summary>
        /// 从对象池中获取对象
        /// </summary>
        /// <returns>对象</returns>
        public GameObject GetObj(Transform parent = null)
        {
            GameObject obj = PoolQueue.Dequeue();

            // 显示对象
            obj.SetActive(true);
            // 先清除父物体(否则无法回归默认场景)
            obj.transform.SetParent(null);
            // 回归默认场景
            SceneManager.MoveGameObjectToScene(obj, SceneManager.GetActiveScene());
            // 设置父物体
            obj.transform.SetParent(parent);
            return obj;
        }
    }
}

具体实现

这里只是PoolManager的部分代码,旨在展示GameObject对象池的基本逻辑,后续会补充更多逻辑以及细节,当前代码并非最终版本。

using System.Collections.Generic;
using DLFrame.Scripts._1.Base._1.Singleton;
using UnityEngine;
using Object = UnityEngine.Object;

namespace DLFrame.Scripts._1.Base._2.Pool
{
    /// <summary>
    /// 对象池管理器
    /// </summary>
    public class PoolManager : ManagerBase<PoolManager>
    {
        /// <summary>
        /// 根节点
        /// </summary>
        [SerializeField] private GameObject poolRootObj;

        /// <summary>
        /// GameObject对象池容器
        /// </summary>
        private readonly Dictionary<string, GameObjectPoolData> _gameObjectPoolDic =
            new Dictionary<string, GameObjectPoolData>();

        public override void Init()
        {
            base.Init();
            Debug.Log("PoolManager 初始化成功");
        }

        #region GameObject对象相关操作

        /// <summary>
        /// 获取GameObject
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <param name="parent">需要绑定的父物体</param>
        /// <typeparam name="T">组件</typeparam>
        /// <returns>待获取游戏对象身上的T组件</returns>
        public T GetGameObject<T>(GameObject prefab, Transform parent = null) where T : Object
        {
            GameObject obj = GetGameObject(prefab, parent);
            return obj.GetComponent<T>();
        }

        /// <summary>
        /// 获取GameObject
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <param name="parent">需要绑定的父物体</param>
        /// <returns>游戏对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject GetGameObject(GameObject prefab, Transform parent = null)
        {
            GameObject obj;
            string prefabName = prefab.name;
            // 检查有没有这一层,如果有这一层,判断该层对象池容器中是否有数据
            if (CheckGameObjectCache(prefab))
            {
                obj = _gameObjectPoolDic[prefabName].GetObj(parent);
            }
            // 没有的话实例化一个
            else
            {
                // 确保实例化后的游戏物体和预制体名称一致(这很重要,因为通过预制体创建的物体名字后面都会加一个Clone,这会导致将物体放回对象池时名称的匹配出现问题)
                obj = Instantiate(prefab, parent);
                obj.name = prefabName;
            }

            return obj;
        }

        /// <summary>
        /// 将GameObject放进对象池
        /// </summary>
        /// <param name="obj">GameObject</param>
        public void PushGameObject(GameObject obj)
        {
            string objName = obj.name;
            // 现在有没有这一层
            if (_gameObjectPoolDic.TryGetValue(objName, out var value))
            {
                value.PushObj(obj);
            }
            else
            {
                _gameObjectPoolDic.Add(objName, new GameObjectPoolData(obj, poolRootObj));
            }
        }

        /// <summary>
        /// 检查有没有某一层对象池数据,如果有的话,判断该层对象池容器中是否有数据
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <returns>是否存在数据</returns>
        private bool CheckGameObjectCache(GameObject prefab)
        {
            string prefabName = prefab.name;
            return _gameObjectPoolDic.ContainsKey(prefabName) && _gameObjectPoolDic[prefabName].PoolQueue.Count > 0;
        }

        #endregion

        #region 删除

        /// <summary>
        /// 清理对象池
        /// </summary>
        // ReSharper disable once MemberCanBePrivate.Global
        public void Clear()
        {
            for (int i = 0; i < poolRootObj.transform.childCount; i++)
            {
                Destroy(poolRootObj.transform.GetChild(0).gameObject);
            }

            _gameObjectPoolDic.Clear();
        }

        #endregion
    }
}

非GameObject对象池

ObjectPoolData

using System.Collections.Generic;

namespace DLFrame.Scripts._1.Base._2.Pool
{
    /// <summary>
    /// 普通类object对象池对象池
    /// </summary>
    public class ObjectPoolData
    {
        /// <summary>
        /// 对象容器
        /// </summary>
        public readonly Queue<object> PoolQueue;

        public ObjectPoolData(object obj)
        {
            PoolQueue = new Queue<object>();
            PushObj(obj);
        }

        /// <summary>
        /// 将对象放进对象池
        /// </summary>
        /// <param name="obj">对象</param>
        public void PushObj(object obj)
        {
            PoolQueue.Enqueue(obj);
        }

        /// <summary>
        /// 从对象池中获取对象
        /// </summary>
        /// <returns>对象</returns>
        public object GetObj()
        {
            return PoolQueue.Dequeue();
        }
    }
}

PoolManager

上面那个PoolManager只实现了对于GameObject对象的管理,但是对非GameObject对象的管理还未实现,因此接下来加入了非GameObject对象管理的相关逻辑,并对PoolManager的各方面细节进行了完善。

using System;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._1.Singleton;
using UnityEngine;
using Object = UnityEngine.Object;

namespace DLFrame.Scripts._1.Base._2.Pool
{
    /// <summary>
    /// 对象池管理器
    /// </summary>
    public class PoolManager : ManagerBase<PoolManager>
    {
        /// <summary>
        /// 根节点
        /// </summary>
        [SerializeField] private GameObject poolRootObj;

        /// <summary>
        /// GameObject对象池容器
        /// </summary>
        private readonly Dictionary<string, GameObjectPoolData> _gameObjectPoolDic =
            new Dictionary<string, GameObjectPoolData>();

        /// <summary>
        /// 普通类对象对象池容器
        /// </summary>
        private readonly Dictionary<string, ObjectPoolData> _objectPoolDic =
            new Dictionary<string, ObjectPoolData>();

        public override void Init()
        {
            base.Init();
            Debug.Log("PoolManager 初始化成功");
        }

        #region GameObject对象相关操作

        /// <summary>
        /// 获取GameObject
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <param name="parent">需要绑定的父物体</param>
        /// <typeparam name="T">组件</typeparam>
        /// <returns>待获取游戏对象身上的T组件</returns>
        public T GetGameObject<T>(GameObject prefab, Transform parent = null) where T : Object
        {
            GameObject obj = GetGameObject(prefab, parent);
            return obj.GetComponent<T>();
        }

        /// <summary>
        /// 获取GameObject
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <param name="parent">需要绑定的父物体</param>
        /// <returns>游戏对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject GetGameObject(GameObject prefab, Transform parent = null)
        {
            GameObject obj;
            string prefabName = prefab.name;
            // 检查有没有这一层,如果有这一层,判断该层对象池容器中是否有数据
            if (CheckGameObjectCache(prefab))
            {
                obj = _gameObjectPoolDic[prefabName].GetObj(parent);
            }
            // 没有的话实例化一个
            else
            {
                // 确保实例化后的游戏物体和预制体名称一致
                obj = Instantiate(prefab, parent);
                obj.name = prefabName;
            }

            return obj;
        }

        /// <summary>
        /// 将GameObject放进对象池
        /// </summary>
        /// <param name="obj">GameObject</param>
        public void PushGameObject(GameObject obj)
        {
            string objName = obj.name;
            // 现在有没有这一层
            if (_gameObjectPoolDic.TryGetValue(objName, out var value))
            {
                value.PushObj(obj);
            }
            else
            {
                _gameObjectPoolDic.Add(objName, new GameObjectPoolData(obj, poolRootObj));
            }
        }

        /// <summary>
        /// 检查有没有某一层对象池数据,如果有的话,判断该层对象池容器中是否有数据
        /// </summary>
        /// <param name="prefab">待获取游戏对象的预制体</param>
        /// <returns>是否存在数据</returns>
        private bool CheckGameObjectCache(GameObject prefab)
        {
            string prefabName = prefab.name;
            return _gameObjectPoolDic.ContainsKey(prefabName) && _gameObjectPoolDic[prefabName].PoolQueue.Count > 0;
        }

        /// <summary>
        /// 检查缓存 如果成功 则加载游戏物体 不成功返回null
        /// </summary>
        /// <returns></returns>
        public GameObject CheckCacheAndLoadGameObject(string path, Transform parent = null)
        {
            // 通过路径获取最终预制体的名称
            string[] pathSplit = path.Split('/');
            string prefabName = pathSplit[pathSplit.Length - 1];
            // 对象池有数据
            if (_gameObjectPoolDic.ContainsKey(prefabName) && _gameObjectPoolDic[prefabName].PoolQueue.Count > 0)
            {
                return _gameObjectPoolDic[prefabName].GetObj(parent);
            }

            return null;
        }

        #endregion

        #region 普通对象相关操作

        /// <summary>
        /// 获取普通对象
        /// </summary>
        /// <typeparam name="T">待对象类型</typeparam>
        /// <returns>待获取对象</returns>
        public T GetObject<T>() where T : class, new()
        {
            T obj;
            if (CheckObjectCache<T>())
            {
                string objFullName = typeof(T).FullName;
                obj = _objectPoolDic[objFullName ?? throw new InvalidOperationException()].GetObj() as T;
            }
            else
            {
                obj = new T();
            }

            return obj;
        }

        /// <summary>
        /// 将普通对象放进对象池
        /// </summary>
        /// <param name="obj">普通对象</param>
        public void PushObject(object obj)
        {
            string objFullName = obj.GetType().FullName;
            if (_objectPoolDic.ContainsKey(objFullName ?? throw new InvalidOperationException()))
            {
                _objectPoolDic[objFullName].PushObj(obj);
            }
            else
            {
                _objectPoolDic.Add(objFullName, new ObjectPoolData(obj));
            }
        }

        /// <summary>
        /// 检查有没有某一层普通对象池数据
        /// </summary>
        /// <returns>是否存在数据</returns>
        private bool CheckObjectCache<T>()
        {
            // 这里使用全名作为key,因为全名是带命名空间的,是唯一的,不使用全名可能会出现重复的key
            string objFullName = typeof(T).FullName;
            return _objectPoolDic.ContainsKey(objFullName ?? throw new InvalidOperationException()) &&
                   _objectPoolDic[objFullName].PoolQueue.Count > 0;
        }

        #endregion

        #region 删除

        /// <summary>
        /// 清理对象池
        /// </summary>
        /// <param name="clearGameObject">是否清理GameObject对象</param>
        /// <param name="clearCObject">是否清理普通对象池</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void Clear(bool clearGameObject = true, bool clearCObject = true)
        {
            if (clearGameObject)
            {
                for (int i = 0; i < poolRootObj.transform.childCount; i++)
                {
                    // 这里不能写i,写0即可,因为当我们删除的时候,子物体的数量、编号其实都发生了变化,所以我们只需要删除N次第一个子物体即可。
                    Destroy(poolRootObj.transform.GetChild(0).gameObject);
                }

                _gameObjectPoolDic.Clear();
            }

            if (clearCObject)
            {
                _objectPoolDic.Clear();
            }
        }

        /// <summary>
        /// 清空GameObject对象池
        /// </summary>
        public void ClearAllGameObject()
        {
            Clear(true, false);
        }

        /// <summary>
        /// 清理GameObject对象池中的某种对象
        /// </summary>
        /// <param name="prefabName">待清除对象名称</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void ClearGameObject(string prefabName)
        {
            if (_gameObjectPoolDic.ContainsKey(prefabName))
            {
                Destroy(poolRootObj.transform.Find(prefabName).gameObject);
                _gameObjectPoolDic.Remove(prefabName);
            }
        }

        /// <summary>
        /// 清理GameObject对象池中的某种对象
        /// </summary>
        /// <param name="prefab">待清除对象预制体</param>
        public void ClearGameObject(GameObject prefab)
        {
            ClearGameObject(prefab.name);
        }

        /// <summary>
        /// 清空普通对象池
        /// </summary>
        public void ClearAllObject()
        {
            Clear(false);
        }

        /// <summary>
        /// 清理普通对象池中某类型对象
        /// </summary>
        /// <typeparam name="T">待清除对象的类型</typeparam>
        public void ClearObject<T>()
        {
            string objFullName = typeof(T).FullName;
            if (objFullName != null && _objectPoolDic.ContainsKey(objFullName)) _objectPoolDic.Remove(objFullName);
        }

        /// <summary>
        /// 清理普通对象池中某类型对象
        /// </summary>
        /// <param name="type">待清除对象的类型</param>
        public void ClearObject(Type type)
        {
            string objFullName = type.FullName;
            if (objFullName != null && _objectPoolDic.ContainsKey(objFullName)) _objectPoolDic.Remove(objFullName);
        }

        #endregion
    }
}

配置系统

  • 系统的配置
  • 装备、角色、NPC的配置

配置文件基类

NPC配置、装备配置等共同基类,所有的配置项都要继承与配置文件基类,便于统一管理所有配置。


using Sirenix.OdinInspector;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 配置基类
    /// 角色配置、武器配置等等
    /// </summary>
    public class ConfigBase:SerializedScriptableObject
    {

    }
}

注:关于ScriptableObject的详细讲解,请前往前置知识中查看

配置设置

配置设置类用于统筹汇总所有的非框架配置项,也就是游戏中的各种配置。

using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 所有游戏中(非框架)配置,游戏运行时只有一个
    /// 包含所有的配置文件
    /// </summary>
    [CreateAssetMenu(fileName = "ConfigSetting",menuName = "DLFrame/ConfigSetting")]
    public class ConfigSetting : ConfigBase
    {
        /// <summary>
        /// 所有配置的容器
        /// (配置类型的名称,(id,具体配置))
        /// </summary>
        [InfoBox("选择字典时请务必选Dictionary<int,ConfigBase>")]
        [DictionaryDrawerSettings(KeyLabel = "类型",ValueLabel = "列表")]
        // ReSharper disable once CollectionNeverUpdated.Global
        // ReSharper disable once UnassignedField.Global
        public Dictionary<string, Dictionary<int, ConfigBase>> ConfigDic;

        /// <summary>
        /// 获取配置
        /// </summary>
        /// <param name="configTypeName">配置类型名称</param>
        /// <param name="id">id</param>
        /// <typeparam name="T">具体的配置类型</typeparam>
        /// <returns></returns>
        public T GetConfig<T>(string configTypeName, int id) where T : ConfigBase
        {
            // 检查类型
            if (!ConfigDic.ContainsKey(configTypeName))
            {
                throw new Exception("DL:配置设置中不包含这个Key:" + configTypeName);
            }
            // 检查ID
            if (!ConfigDic[configTypeName].ContainsKey(id))
            {
                throw new Exception($"DL:配置设置中{configTypeName}不包含这个ID:{id}");
            }
            // 说明一切正常
            return ConfigDic[configTypeName][id] as T;
        }
    }
}

现在我们就可以创建一个用于管理所有游戏中配置项的ScriptableObject了。

image-20240123101313628

上图中的大刀和小刀是游戏开发中具体物品配置的实际应用演示,代码如下:

using DLFrame.Scripts._2.System_DL._0.Config;
using UnityEngine;

namespace Test
{
    [CreateAssetMenu(fileName = "DemoConfig",menuName = "DLFrame/Config/DemoConfig")]
    public class DemoConfig : ConfigBase
    {
        public string weaponName;
    }
}

创建了大刀和小刀的配置(ScriptableObject)

image-20240123101618850

由此可见,我们在应用该框架进行游戏开发的过程中,所有物品的配置都只需要实现其对应类型的脚本,其脚本继承ConfigBase,然后就可以创建很多个这类物体的配置文件,将这类物体的类型及其所有配置文件全部加入到ConfigSetting中即可。

配置管理器

我们通过ConfigSetting统筹汇总了所有的配置,但是ConfigSetting也是一个配置(也是ScriptableObject),因此我们需要一个单独的配置管理类(ConfigManager)来管理所有配置,具体来说,就是将这个ConfigManager挂载到GameRoot物体上,然后将ConfigSetting挂载到ConfigManager上,所有的游戏配置挂载到ConfigManager上。

image-20240124135640925

image-20240124135838934

using DLFrame.Scripts._1.Base._1.Singleton;
using UnityEngine;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 配置管理器
    /// </summary>
    public class ConfigManager : ManagerBase<ConfigManager>
    {
        [SerializeField]
        private ConfigSetting configSetting;

        /// <summary>
        /// 获取配置
        /// </summary>
        /// <param name="configTypeName">配置类型名称</param>
        /// <param name="id">id</param>
        /// <typeparam name="T">具体的配置类型</typeparam>
        /// <returns></returns>
        public T GetConfig<T>(string configTypeName, int id) where T : ConfigBase
        {
            return configSetting.GetConfig<T>(configTypeName, id);
        }

        public override void Init()
        {
            base.Init();
            Debug.Log("ConfigManager 初始化成功");
        }
    }
}

游戏设置

用于对游戏框架进行各种配置,涉及到的都是框架层面的设置,例如对象池设置、UI元素设置,与具体游戏的设置无关。

using UnityEngine;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 框架层面的游戏设置
    /// 对象池缓存设置、UI元素的设置
    /// </summary>
    [CreateAssetMenu(fileName = "GameSetting", menuName = "DLFrame/GameSetting")]
    public class GameSetting : ConfigBase
    {

    }
}

这里目前只是放个架子,还没有内容,后续会主键填充。

完善GameRoot

GameSetting虽然属于配置类,但是我们不要用ConfigManager进行管理,因为GameSetting是整个框架层面的配置,让其挂载到GameRoot上显然更加合理。

这里我们对GameRoot进行了完善,增加了用于挂载GameSetting的属性。

using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._2.System_DL._0.Config;
using UnityEngine;

namespace DLFrame.Scripts._1.Base
{
    /// <summary>
    /// 框架根组件
    /// </summary>
    public class GameRoot : SingletonMono<GameRoot>
    {
        /// <summary>
        /// 框架设置
        /// </summary>
        [SerializeField]
        private GameSetting gameSetting;
        /// <summary>
        /// 框架设置
        /// </summary>
        public GameSetting GameSetting => gameSetting;

        protected override void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(gameObject);

            //初始化所有管理器
            InitManagers();
        }

        /// <summary>
        /// 初始化所有管理器
        /// </summary>
        private void InitManagers()
        {
            ManagerBase[] managers = GetComponents<ManagerBase>();
            foreach (ManagerBase manager in managers)
            {
                manager.Init();
            }
        }
    }
}

注:由于GameSetting非常重要,这是整个框架的配置,我们不希望它能够在代码中被修改,因此我们将其变量设置为私有,并且实现一个只读属性,让外界只能读取GameSetting而不能修改。

Unity编译前运行

编译:我们编写的代码在保存的时候,Unity会有一个Loading这个过程就是编译。

编译前运行:Unity编译之前可以执行我们指定的函数。

框架后续会有一些配置需要在编译前自动计算,所以我们需要实现这个机制为后续做准备。

我们直接将需要编译前运行的部分放入GameSetting中,用UNITY_EDITOR代码块包裹,就可以让其只在unity编辑器中生效。

using DLFrame.Scripts._1.Base;
using Sirenix.OdinInspector;
using UnityEditor;
using UnityEngine;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 框架层面的游戏设置
    /// 对象池缓存设置、UI元素的设置
    /// </summary>
    [CreateAssetMenu(fileName = "GameSetting", menuName = "DLFrame/GameSetting")]
    public class GameSetting : ConfigBase
    {

#if UNITY_EDITOR

        /// <summary>
        /// 初始化游戏配置
        /// </summary>
        [Button(Name = "初始化游戏配置", ButtonHeight = 50)]
        [GUIColor(0, 1, 0)]
        private void Init()
        {
            Debug.Log("GameSetting 初始化");
        }

        /// <summary>
        /// 编译前执行函数
        /// </summary>
        [InitializeOnLoadMethod]
        private static void LoadForEditor()
        {
            if (FindObjectOfType<GameRoot>() != null)
            {
                FindObjectOfType<GameRoot>().GameSetting.Init();
            }
        }
#endif
    }
}

资源服务

资源管理器

在我们后续业务逻辑的开发中,不需要Resource.Load或者Instantiate俩获取资源和实例化物体,同时也基本不需要直接访问对象池提供的方法,因为无论是通过对象池获取对象,还是手动new对象,亦或是实例化预制体,其实都属于业务逻辑中对于资源的管理,我们完全可以实现一个资源管理器,专门负责各种资源的生产与管理,这样我们业务中任何涉及到资源创建相关的工作,只需要用资源管理器(ResManager)即可实现,这是一种更加符合工程化的做法。

为什么要这么做?

  • 我们希望某些自己写的组件所在的游戏物体可以自动应用“对象池”
  • 提供一些易于开发的函数
  • 统一资源的调度

注意:这里的资源管理服务,普通的C#实例、GameObject、AudioClip等都包含。

核心功能

这里实现了资源管理的基础核心功能,只实现了普通C#对象和GameObject对象的同步创建,类似AudioClip这种资源对象的创建还未实现,另外异步加载资源也没有实现,因此并不完整,后文中还会补充完整版本。

using System;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._1.Base._2.Pool;
using UnityEngine;

namespace DLFrame.Scripts._3.Service._0.Res
{
    public class ResManager : ManagerBase<ResManager>
    {
        /// <summary>
        /// 需要缓存的类型
        /// </summary>
        // ReSharper disable once CollectionNeverUpdated.Local
        private Dictionary<Type, bool> _wantCacheDic;

        public override void Init()
        {
            base.Init();
            _wantCacheDic = new Dictionary<Type, bool>();
        }

        /// <summary>
        /// 检查一个类型是否需要缓存
        /// </summary>
        /// <param name="type">待检查类型</param>
        /// <returns>是否需要缓存</returns>
        private bool CheckCacheDic(Type type)
        {
            return _wantCacheDic.ContainsKey(type);
        }

        /// <summary>
        /// 获取实例-普通Class
        /// 如果类型需要缓存,会从对象池中获取,否则直接new
        /// </summary>
        /// <typeparam name="T">待获取的类型</typeparam>
        /// <returns>类型实例</returns>
        public T Load<T>() where T : class, new()
        {
            return CheckCacheDic(typeof(T)) ? PoolManager.Instance.GetObject<T>() : new T();
        }

        /// <summary>
        /// 获取实例-组件
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <typeparam name="T">组件类型</typeparam>
        /// <returns>组件</returns>
        public T Load<T>(string path, Transform parent = null) where T : Component
        {
            return CheckCacheDic(typeof(T))
                ? PoolManager.Instance.GetGameObject<T>(GetPrefab(path), parent)
                : InstantiateForPrefab(path).GetComponent<T>();
        }

        /// <summary>
        /// 获取预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <returns>预制体</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject GetPrefab(string path)
        {
            GameObject prefab = Resources.Load<GameObject>(path);
            if (prefab == null)
            {
                throw new Exception("DL:预制体路径有误,没有找到预制体");
            }

            return prefab;
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(string path, Transform parent = null)
        {
            return InstantiateForPrefab(GetPrefab(path), parent);
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="prefab">预制体</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(GameObject prefab, Transform parent = null)
        {
            GameObject go = Instantiate(prefab, parent);
            go.name = prefab.name;
            return go;
        }
    }
}

异步加载资源

这里完善了资源管理器,增加了对Unity资源对象的管理,以及一些异步加载资源的方法,这里需要注意的是,Unity资源对象无需缓存(对象池),因为他们无需实例化即可直接使用。

注意,当前版本的资源管理器仍然不是完整版,因为这里面还没有进行需缓存类型wantCacheDic的自动装配,后面将在GameSetting中实现装配逻辑并自动载入到wantCacheDic中。

using System;
using System.Collections;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._1.Base._2.Pool;
using UnityEngine;

namespace DLFrame.Scripts._3.Service._0.Res
{
    public class ResManager : ManagerBase<ResManager>
    {
        /// <summary>
        /// 需要缓存的类型
        /// </summary>
        // ReSharper disable once CollectionNeverUpdated.Local
        private Dictionary<Type, bool> _wantCacheDic;

        public override void Init()
        {
            base.Init();
            _wantCacheDic = new Dictionary<Type, bool>();
        }

        /// <summary>
        /// 检查一个类型是否需要缓存
        /// </summary>
        /// <param name="type">待检查类型</param>
        /// <returns>是否需要缓存</returns>
        private bool CheckCacheDic(Type type)
        {
            return _wantCacheDic.ContainsKey(type);
        }

        /// <summary>
        /// 加载Unity资源 如AudioClip Spite
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <typeparam name="T">资源类型</typeparam>
        /// <returns>Unity资源</returns>
        public T LoadAsset<T>(string path) where T : UnityEngine.Object
        {
            return Resources.Load<T>(path);
        }

        /// <summary>
        /// 获取实例-普通Class
        /// 如果类型需要缓存,会从对象池中获取,否则直接new
        /// </summary>
        /// <typeparam name="T">待获取的类型</typeparam>
        /// <returns>类型实例</returns>
        public T Load<T>() where T : class, new()
        {
            return CheckCacheDic(typeof(T)) ? PoolManager.Instance.GetObject<T>() : new T();
        }

        /// <summary>
        /// 获取实例-组件
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <typeparam name="T">组件类型</typeparam>
        /// <returns>组件</returns>
        public T Load<T>(string path, Transform parent = null) where T : Component
        {
            return CheckCacheDic(typeof(T))
                ? PoolManager.Instance.GetGameObject<T>(GetPrefab(path), parent)
                : InstantiateForPrefab(path).GetComponent<T>();
        }

        /// <summary>
        /// 异步加载GameObject,并获取其上的组件
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="callBack">回调函数</param>
        /// <param name="parent">父物体</param>
        /// <typeparam name="T">GameObject上绑定的组件</typeparam>
        public void LoadGameObjectAsync<T>(string path, Action<T> callBack = null, Transform parent = null)
            where T : UnityEngine.Object
        {
            // 考虑对象池的情况
            if (CheckCacheDic(typeof(T)))
            {
                GameObject go = PoolManager.Instance.CheckCacheAndLoadGameObject(path, parent);
                // 对象有
                if (go != null)
                {
                    callBack?.Invoke(go.GetComponent<T>());
                }
                // 对象没有
                else
                {
                    // ReSharper disable once RedundantTypeArgumentsOfMethod
                    StartCoroutine(DoLoadGameObjectAsync<T>(path, callBack, parent));
                }
            }
            // 对象池没有
            else
            {
                // ReSharper disable once RedundantTypeArgumentsOfMethod
                StartCoroutine(DoLoadGameObjectAsync<T>(path, callBack, parent));
            }
        }

        IEnumerator DoLoadGameObjectAsync<T>(string path, Action<T> callBack = null,
            Transform parent = null)
            where T : UnityEngine.Object
        {
            ResourceRequest request = Resources.LoadAsync<GameObject>(path);
            yield return request;
            GameObject go = InstantiateForPrefab(request.asset as GameObject, parent);
            callBack?.Invoke(go.GetComponent<T>());
        }


        /// <summary>
        /// 异步加载Unity资源 AudioClip Sprite GameObject(预制体)
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <param name="callBack">回调函数</param>
        /// <typeparam name="T">资源类型</typeparam>
        public void LoadAssetAsync<T>(string path, Action<T> callBack) where T : UnityEngine.Object
        {
            // ReSharper disable once RedundantTypeArgumentsOfMethod
            StartCoroutine(DoLoadAssetAsync<T>(path, callBack));
        }

        IEnumerator DoLoadAssetAsync<T>(string path, Action<T> callBack) where T : UnityEngine.Object
        {
            ResourceRequest request = Resources.LoadAsync<T>(path);
            yield return request;
            callBack?.Invoke(request.asset as T);
        }

        /// <summary>
        /// 获取预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <returns>预制体</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject GetPrefab(string path)
        {
            GameObject prefab = Resources.Load<GameObject>(path);
            if (prefab == null)
            {
                throw new Exception("DL:预制体路径有误,没有找到预制体");
            }

            return prefab;
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(string path, Transform parent = null)
        {
            return InstantiateForPrefab(GetPrefab(path), parent);
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="prefab">预制体</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(GameObject prefab, Transform parent = null)
        {
            GameObject go = Instantiate(prefab, parent);
            go.name = prefab.name;
            return go;
        }
    }
}

对象池自定义特性

在实际开发中,我们并不希望直接与底层对象池进行交互,因为这样很麻烦,而且对于资源的管理非常冗余。

我们可以创建一个Pool特性,打上特性的class意味着其对象需要从对象池中获取,这样我们只需要扫描所有带有Pool对象的特性,资源管理器就能明白哪些对象要从对象池中获取,哪些对象不需要。

而我们在使用的时候,想要缓存(对象池)的对象,直接在其类上加入Pool特性即可,剩下的一切关于资源的操作全部由资源管理器(ResManager)进行管理。

创建Pool特性

using System;

namespace DLFrame.Scripts._1.Base._0.Extension
{
    /// <summary>
    /// 确定一个类是否需要对象池
    /// </summary>
    [AttributeUsage(AttributeTargets.Class)]
    public class PoolAttribute : Attribute
    {

    }
}

GameSetting补充

我们希望GameSetting的编译前执行初始化游戏配置时,能够自动的扫描出全部带有Pool特性的类,将他们自动装载到缓存字典中,便于ResManager的管理,使我们能够完全无感的使用对象池。

using System;
using System.Collections.Generic;
using System.Reflection;
using DLFrame.Scripts._1.Base;
using DLFrame.Scripts._1.Base._0.Extension;
using Sirenix.OdinInspector;
using UnityEditor;
using UnityEngine;

namespace DLFrame.Scripts._2.System_DL._0.Config
{
    /// <summary>
    /// 框架层面的游戏设置
    /// 对象池缓存设置、UI元素的设置
    /// </summary>
    [CreateAssetMenu(fileName = "GameSetting", menuName = "DLFrame/GameSetting")]
    public class GameSetting : ConfigBase
    {
        /// <summary>
        /// 需要缓存的类型
        /// </summary>
        [LabelText("对象池设置")] [DictionaryDrawerSettings(KeyLabel = "类型", ValueLabel = "皆可缓存")] [ReadOnly]
        public readonly Dictionary<Type, bool> CacheDic = new Dictionary<Type, bool>();


#if UNITY_EDITOR

        /// <summary>
        /// 初始化游戏配置
        /// </summary>
        [Button(Name = "初始化游戏配置", ButtonHeight = 50)]
        [GUIColor(0, 1, 0)]
        private void Init()
        {
            PoolAttributeOnEditor();
            Debug.Log("GameSetting 初始化");
        }

        /// <summary>
        /// 编译前执行函数
        /// </summary>
        [InitializeOnLoadMethod]
        private static void LoadForEditor()
        {
            if (FindObjectOfType<GameRoot>() != null)
            {
                FindObjectOfType<GameRoot>().GameSetting.Init();
            }
        }

        /// <summary>
        /// 将带有Pool特性的类型加入缓存池字典
        /// </summary>
        private void PoolAttributeOnEditor()
        {
            CacheDic.Clear();
            // 获取所有程序集
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            // 遍历程序集
            foreach (Assembly assembly in assemblies)
            {
                // 遍历程序集下的每一个类型
                Type[] types = assembly.GetTypes();
                foreach (Type type in types)
                {
                    // 获取pool特性
                    PoolAttribute pool = type.GetCustomAttribute<PoolAttribute>();
                    if (pool != null)
                    {
                        CacheDic.Add(type, true);

                    }
                }
            }
        }
#endif
    }
}

完善ResManager类

这里我们完成了ResManager的最后一部分内容,就是自动装配缓存,实际上就是从GameSetting中获取到编译前扫描出来的所有带有Pool特性的类型字典,并将其赋值给wantCacheDic字段。

using System;
using System.Collections;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base;
using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._1.Base._2.Pool;
using UnityEngine;

namespace DLFrame.Scripts._3.Service._0.Res
{
    public class ResManager : ManagerBase<ResManager>
    {
        /// <summary>
        /// 需要缓存的类型
        /// </summary>
        // ReSharper disable once CollectionNeverUpdated.Local
        private Dictionary<Type, bool> _wantCacheDic;

        public override void Init()
        {
            base.Init();
            _wantCacheDic = GameRoot.Instance.GameSetting.CacheDic;
        }

        /// <summary>
        /// 检查一个类型是否需要缓存
        /// </summary>
        /// <param name="type">待检查类型</param>
        /// <returns>是否需要缓存</returns>
        private bool CheckCacheDic(Type type)
        {
            return _wantCacheDic.ContainsKey(type);
        }

        /// <summary>
        /// 加载Unity资源 如AudioClip Spite
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <typeparam name="T">资源类型</typeparam>
        /// <returns>Unity资源</returns>
        public T LoadAsset<T>(string path) where T : UnityEngine.Object
        {
            return Resources.Load<T>(path);
        }

        /// <summary>
        /// 获取实例-普通Class
        /// 如果类型需要缓存,会从对象池中获取,否则直接new
        /// </summary>
        /// <typeparam name="T">待获取的类型</typeparam>
        /// <returns>类型实例</returns>
        public T Load<T>() where T : class, new()
        {
            return CheckCacheDic(typeof(T)) ? PoolManager.Instance.GetObject<T>() : new T();
        }

        /// <summary>
        /// 获取实例-组件
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <typeparam name="T">组件类型</typeparam>
        /// <returns>组件</returns>
        public T Load<T>(string path, Transform parent = null) where T : Component
        {
            return CheckCacheDic(typeof(T))
                ? PoolManager.Instance.GetGameObject<T>(GetPrefab(path), parent)
                : InstantiateForPrefab(path).GetComponent<T>();
        }

        /// <summary>
        /// 异步加载GameObject,并获取其上的组件
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="callBack">回调函数</param>
        /// <param name="parent">父物体</param>
        /// <typeparam name="T">GameObject上绑定的组件</typeparam>
        public void LoadGameObjectAsync<T>(string path, Action<T> callBack = null, Transform parent = null)
            where T : UnityEngine.Object
        {
            // 考虑对象池的情况
            if (CheckCacheDic(typeof(T)))
            {
                GameObject go = PoolManager.Instance.CheckCacheAndLoadGameObject(path, parent);
                // 对象有
                if (go != null)
                {
                    callBack?.Invoke(go.GetComponent<T>());
                }
                // 对象没有
                else
                {
                    // ReSharper disable once RedundantTypeArgumentsOfMethod
                    StartCoroutine(DoLoadGameObjectAsync<T>(path, callBack, parent));
                }
            }
            // 对象池没有
            else
            {
                // ReSharper disable once RedundantTypeArgumentsOfMethod
                StartCoroutine(DoLoadGameObjectAsync<T>(path, callBack, parent));
            }
        }

        IEnumerator DoLoadGameObjectAsync<T>(string path, Action<T> callBack = null,
            Transform parent = null)
            where T : UnityEngine.Object
        {
            ResourceRequest request = Resources.LoadAsync<GameObject>(path);
            yield return request;
            GameObject go = InstantiateForPrefab(request.asset as GameObject, parent);
            callBack?.Invoke(go.GetComponent<T>());
        }


        /// <summary>
        /// 异步加载Unity资源 AudioClip Sprite GameObject(预制体)
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <param name="callBack">回调函数</param>
        /// <typeparam name="T">资源类型</typeparam>
        public void LoadAssetAsync<T>(string path, Action<T> callBack) where T : UnityEngine.Object
        {
            // ReSharper disable once RedundantTypeArgumentsOfMethod
            StartCoroutine(DoLoadAssetAsync<T>(path, callBack));
        }

        IEnumerator DoLoadAssetAsync<T>(string path, Action<T> callBack) where T : UnityEngine.Object
        {
            ResourceRequest request = Resources.LoadAsync<T>(path);
            yield return request;
            callBack?.Invoke(request.asset as T);
        }

        /// <summary>
        /// 获取预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <returns>预制体</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject GetPrefab(string path)
        {
            GameObject prefab = Resources.Load<GameObject>(path);
            if (prefab == null)
            {
                throw new Exception("DL:预制体路径有误,没有找到预制体");
            }

            return prefab;
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="path">预制体路径</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(string path, Transform parent = null)
        {
            return InstantiateForPrefab(GetPrefab(path), parent);
        }

        /// <summary>
        /// 实例化预制体
        /// </summary>
        /// <param name="prefab">预制体</param>
        /// <param name="parent">父物体</param>
        /// <returns>预制体实例化后的对象</returns>
        // ReSharper disable once MemberCanBePrivate.Global
        public GameObject InstantiateForPrefab(GameObject prefab, Transform parent = null)
        {
            GameObject go = Instantiate(prefab, parent);
            go.name = prefab.name;
            return go;
        }
    }
}

对象池拓展方法

目前我们已经完全实现了ResManager对资源的封装,一切资源获取都可以通过ResManager进行,同时我们也通过Pool特性确定了哪些物体需要对象池缓存。

针对需要对象池缓存的对象,我们拿它的时候通过ResManager来获取,用完之后还需要放回对象池中,这时我们就需要直接和对象池进行交互,但是我们并不希望直接与对象池产生交互,因此我们完全可以实现一系列拓展方法,使我们可以直接调用任何对象的拓展方法,来将其放入对象池中,这样做大大简化了操作难度,统一了操作流程。

至此,我们无论是从对象池中拿对象,还是将对象放入对象池,都不需要与对象池直接进行交互,解除了与对象池层的耦合。

新增资源管理拓展方法

using System;
using System.Reflection;
using DLFrame.Scripts._1.Base._2.Pool;
using UnityEngine;

namespace DLFrame.Scripts._1.Base._0.Extension
{
    /// <summary>
    /// DLFrame 框架主要的拓展方法
    /// </summary>
    public static class DLExtension
    {
        #region 通用

        /// <summary>
        /// 获取对象自身的特性
        /// </summary>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        public static T GetAttribute<T>(this object obj) where T : Attribute
        {
            return obj.GetType().GetCustomAttribute<T>();
        }

        /// <summary>
        /// 获取指定类型的特性
        /// </summary>
        /// <param name="type">指定类型</param>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        // ReSharper disable once InvalidXmlDocComment
        public static T GetAttribute<T>(this object obj, Type type) where T : Attribute
        {
            return type.GetCustomAttribute<T>();
        }

        #endregion

        #region 资源管理

        /// <summary>
        /// GameObject放入对象池
        /// </summary>
        /// <param name="go">GameObject</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public static void DLGameObjectPushPool(this GameObject go)
        {
            PoolManager.Instance.PushGameObject(go);
        }

        /// <summary>
        /// 通过组件将GameObject放入对象池
        /// </summary>
        /// <param name="component">组件</param>
        public static void DLGameObjectPushPool(this Component component)
        {
            DLGameObjectPushPool(component.gameObject);
        }

        /// <summary>
        /// 将普通对象放入对象池
        /// </summary>
        /// <param name="obj">普通对象</param>
        public static void DLObjectPushPool(this object obj)
        {
            PoolManager.Instance.PushObject(obj);
        }

        #endregion
    }
}

事件系统

事件工具

Unity给我们提供了很多有关事件的解决方案,如按钮,鼠标进入事件之类的,但是他们并不通用,比如按钮只提供按钮点击事件,鼠标进入事件只对有碰撞箱的物体生效,我们希望有一个更加统一的事件处理系统。

Unity实际上给我们提供了很多符合接口隔离原则的事件接口,我们将这些事件接口进行封装,即可得到一个统一的事件处理系统。

关于接口隔离原则的详细解释,可以前往这篇文章查看:设计模式基础

注意

这里需要格外注意的是,由于事件接口是基于射线检测实现的,所以我们要为摄像机(Camera)添加物理射线组件(Physics Raycaster),才能对非UI物体生效。

image-20240206203248743

另外还需要注意,我们的事件系统是基于Unity自带事件系统的二次封装,因此场景中一定要有EventSystem物体。

image-20240206203318526

事件工具两层包装

image-20240129094019606

其实上面这段解释不够直观,通俗讲解就是我们写了一个脚本,把Unity中可能发生的所有事件整合到了一个类当中,形成一个组件。任何想要添加事件的物体,都先添加这个组件,然后通过组件内部统一的事件添加方法来添加事件,当任何事件发生的时候,触发的都是我们自己制作的组件中的统一处理机制。这样做的好处是单一职责原则,将有关事件的处理都统一起来,集中到一起。

数据包装

一个大事件底下有很多子事件,因此我们将最小的事件作为一次事件,而整个大事件其实是n个同种类型一次事件的容器,称之为一类事件。

通俗来讲,碰撞器相关的事件为一类事件,然后发生了这类事件后,就会导致某些一次事件,比如主角碰到了地刺,触发了碰撞器进入事件(一类事件),然后发生了好几个一次事件,如主角扣血、主角贴图变红、主角被击退等。

  • 一次事件
  • 一类事件
using System;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._0.Extension;
using DLFrame.Scripts._1.Base._2.Pool;
using DLFrame.Scripts._3.Service._0.Res;
using UnityEngine;
using UnityEngine.EventSystems;

namespace DLFrame.Scripts._2.System_DL._1.Event
{
    /// <summary>
    /// 事件类型
    /// </summary>
    public enum DLEventType
    {
        OnMouseEnter,
        OnMouseExit,
        OnClick,
        OnClickDown,
        OnClickUp,
        OnDrag,
        OnBeginDrag,
        OnEndDrag,
        OnCollisionEnter,
        OnCollisionStay,
        OnCollisionExit,
        OnCollisionEnter2D,
        OnCollisionStay2D,
        OnCollisionExit2D,
        OnTriggerEnter,
        OnTriggerStay,
        OnTriggerExit,
        OnTriggerEnter2D,
        OnTriggerStay2D,
        OnTriggerExit2D,
    }

    /// <summary>
    /// 鼠标事件接口
    /// </summary>
    public interface IMouseEvent : IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler, IPointerDownHandler,
        IPointerUpHandler, IBeginDragHandler, IEndDragHandler, IDragHandler
    {}

    /// <summary>
    /// 事件工具
    /// 可以添加 鼠标、碰撞、触发等事件
    /// </summary>
    public class DLEventListener : MonoBehaviour,IMouseEvent
    {

        #region 内部类、接口等

        /// <summary>
        /// 某个事件中一个事件的数据包装类
        /// </summary>
        /// <typeparam name="T">事件类型</typeparam>
        private class DLEventListenerInfo<T>
        {
            /// <summary>
            /// T:事件本身的参数(PointerEventData、Collision、Collider)
            /// object[]:事件的参数
            /// </summary>
            public Action<T, object[]> Action;

            public object[] Args;

            public void Init(Action<T,object[]> action,object[] args)
            {
                Action = action;
                Args = args;
            }

            public void Destroy()
            {
                Action = null;
                Args = null;
                this.DLObjectPushPool();
            }

            public void TriggerEvent(T eventData)
            {
                Action?.Invoke(eventData, Args);
            }
        }

        interface IDLEventListenerInfos
        {
            void RemoveAll();
        }

        /// <summary>
        /// 一类事件的数据包装类型,包含多个 DLEventListenerInfo
        /// </summary>
        /// <typeparam name="T">事件类型</typeparam>
        private class DLEventListenerInfos<T>:IDLEventListenerInfos
        {
            /// <summary>
            /// 所有的事件
            /// </summary>
            private readonly List<DLEventListenerInfo<T>> _eventList = new List<DLEventListenerInfo<T>>();

            /// <summary>
            /// 添加事件
            /// </summary>
            /// <param name="action">事件</param>
            /// <param name="args">参数</param>
            public void AddListener(Action<T, object[]> action, params object[] args)
            {
                // 这里不能用对象池特性,而是直接操作对象池,是因为对象池特性获取到的仅仅是其泛型,而不是泛型确定的类型,所以ResManager不会将其当成对象池中的对象来操作。
                DLEventListenerInfo<T> info = PoolManager.Instance.GetObject<DLEventListenerInfo<T>>();
                info.Init(action, args);
                _eventList.Add(info);
            }

            /// <summary>
            /// 移除事件
            /// </summary>
            /// <param name="action">事件</param>
            /// <param name="checkArgs">是否比对参数</param>
            /// <param name="args">参数</param>
            public void RemoveListener(Action<T, object[]> action, bool checkArgs = false, params object[] args)
            {
                for (int i = 0; i < _eventList.Count; i++)
                {
                    // 找到这个事件
                    if (_eventList[i].Action.Equals(action))
                    {
                        // 是否需要比对参数
                        if (checkArgs&&args.Length>0)
                        {
                            // 参数如果相等
                            if (args.ArrayEquals(_eventList[i].Args))
                            {
                                // 移除
                                _eventList[i].Destroy();
                                _eventList.RemoveAt(i);
                                return;
                            }
                        }
                        else
                        {
                            // 移除-移除全部action
                            _eventList[i].Destroy();
                            _eventList.RemoveAt(i);
                            return;
                        }
                    }
                }
            }

            /// <summary>
            /// 移除全部,全部放进对象池
            /// </summary>
            public void RemoveAll()
            {
                foreach (DLEventListenerInfo<T> e in _eventList)
                {
                    e.Destroy();
                }

                _eventList.Clear();
                this.DLObjectPushPool();
            }

            /// <summary>
            /// 事件调用
            /// </summary>
            /// <param name="eventData">事件数据</param>
            public void TriggerEvent(T eventData)
            {
                foreach (DLEventListenerInfo<T> e in _eventList)
                {
                    e.TriggerEvent(eventData);
                }
            }
        }

        #endregion

        private readonly Dictionary<DLEventType, IDLEventListenerInfos> _eventInfoDic =
            new Dictionary<DLEventType, IDLEventListenerInfos>();

        #region 外部的访问

        /// <summary>
        /// 添加事件
        /// </summary>
        /// <param name="eventType">事件类型</param>
        /// <param name="action">事件</param>
        /// <param name="args">参数</param>
        /// <typeparam name="T">事件类型</typeparam>
        public void AddListener<T>(DLEventType eventType, Action<T, object[]> action,params object[] args)
        {
            if (!_eventInfoDic.ContainsKey(eventType))
            {
                DLEventListenerInfos<T> infos = PoolManager.Instance.GetObject<DLEventListenerInfos<T>>();
                _eventInfoDic.Add(eventType,infos);
            }
            (_eventInfoDic[eventType] as DLEventListenerInfos<T>)?.AddListener(action, args);
        }

        /// <summary>
        /// 移除事件
        /// </summary>
        /// <param name="eventType">事件类型</param>
        /// <param name="action">事件</param>
        /// <param name="checkArgs">是否比对参数</param>
        /// <param name="args">参数</param>
        /// <typeparam name="T">事件类型</typeparam>
        public void RemoveListener<T>(DLEventType eventType, Action<T, object[]> action, bool checkArgs = false,
            params object[] args)
        {
            if (_eventInfoDic.TryGetValue(eventType, out var value))
            {
                (value as DLEventListenerInfos<T>)?.RemoveListener(action, checkArgs, args);
            }
        }

        /// <summary>
        /// 移除某一个事件类型下的全部事件
        /// </summary>
        /// <param name="eventType">事件类型</param>
        public void RemoveAllListener(DLEventType eventType)
        {
            if (_eventInfoDic.TryGetValue(eventType, out var value))
            {
                value.RemoveAll();
                _eventInfoDic.Remove(eventType);
            }
        }

        /// <summary>
        /// 移除全部事件
        /// </summary>
        public void RemoveAllListener()
        {
            foreach (IDLEventListenerInfos infos in _eventInfoDic.Values)
            {
                infos.RemoveAll();
            }
            _eventInfoDic.Clear();
        }

        #endregion

        /// <summary>
        /// 触发事件
        /// </summary>
        /// <param name="eventType">事件类型</param>
        /// <param name="eventData">事件数据</param>
        /// <typeparam name="T">事件类型</typeparam>
        private void TriggerAction<T>(DLEventType eventType, T eventData)
        {
            if (_eventInfoDic.TryGetValue(eventType, out var value))
            {
                (value as DLEventListenerInfos<T>)?.TriggerEvent(eventData);
            }
        }

        #region 鼠标事件

        public void OnPointerEnter(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnMouseEnter,eventData);
        }

        public void OnPointerExit(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnMouseExit,eventData);
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnClick,eventData);
        }

        public void OnPointerDown(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnClickDown,eventData);
        }

        public void OnPointerUp(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnClickUp,eventData);
        }

        public void OnBeginDrag(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnBeginDrag,eventData);
        }

        public void OnEndDrag(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnEndDrag,eventData);
        }

        public void OnDrag(PointerEventData eventData)
        {
            TriggerAction(DLEventType.OnDrag,eventData);
        }

        #endregion

        #region 碰撞事件

        private void OnCollisionEnter(Collision other)
        {
            TriggerAction(DLEventType.OnCollisionEnter,other);
        }

        private void OnCollisionExit(Collision other)
        {
            TriggerAction(DLEventType.OnCollisionExit,other);
        }

        private void OnCollisionStay(Collision other)
        {
            TriggerAction(DLEventType.OnCollisionStay,other);
        }

        private void OnCollisionEnter2D(Collision2D other)
        {
            TriggerAction(DLEventType.OnCollisionEnter2D,other);
        }

        private void OnCollisionExit2D(Collision2D other)
        {
            TriggerAction(DLEventType.OnCollisionExit2D,other);
        }

        private void OnCollisionStay2D(Collision2D other)
        {
            TriggerAction(DLEventType.OnCollisionStay2D,other);
        }

        #endregion

        #region 触发事件

        private void OnTriggerEnter(Collider other)
        {
            TriggerAction(DLEventType.OnTriggerEnter,other);
        }

        private void OnTriggerExit(Collider other)
        {
            TriggerAction(DLEventType.OnTriggerExit,other);
        }

        private void OnTriggerStay(Collider other)
        {
            TriggerAction(DLEventType.OnTriggerStay,other);
        }

        private void OnTriggerEnter2D(Collider2D other)
        {
            TriggerAction(DLEventType.OnTriggerEnter2D,other);
        }

        private void OnTriggerExit2D(Collider2D other)
        {
            TriggerAction(DLEventType.OnTriggerExit2D,other);
        }

        private void OnTriggerStay2D(Collider2D other)
        {
            TriggerAction(DLEventType.OnTriggerStay2D,other);
        }

        #endregion

    }
}

上方代码中,我们目前只需要关注事件系统的两层包装即可,我们会发现在一类事件的包装中,应用到了数组相等对比函数,这个函数其实是我们自己实现的拓展方法,下面的代码中完善了之前的拓展方法类,新增了数组相等对比拓展方法。

using System;
using System.Reflection;
using DLFrame.Scripts._1.Base._2.Pool;
using UnityEngine;

namespace DLFrame.Scripts._1.Base._0.Extension
{
    /// <summary>
    /// DLFrame 框架主要的拓展方法
    /// </summary>
    public static class DLExtension
    {
        #region 通用

        /// <summary>
        /// 获取对象自身的特性
        /// </summary>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        public static T GetAttribute<T>(this object obj) where T : Attribute
        {
            return obj.GetType().GetCustomAttribute<T>();
        }

        /// <summary>
        /// 获取指定类型的特性
        /// </summary>
        /// <param name="type">指定类型</param>
        /// <typeparam name="T">待获取特性的类型</typeparam>
        /// <returns>特性</returns>
        // ReSharper disable once InvalidXmlDocComment
        public static T GetAttribute<T>(this object obj, Type type) where T : Attribute
        {
            return type.GetCustomAttribute<T>();
        }

        /// <summary>
        /// 数组相等对比
        /// </summary>
        /// <param name="other">另一个数组</param>
        /// <returns>是否相等</returns>
        // ReSharper disable once InvalidXmlDocComment
        public static bool ArrayEquals(this object[] objs, object[] other)
        {
            if (other == null || objs.GetType()!=other.GetType())
            {
                return false;
            }

            if (objs.Length!=other.Length)
            {
                return false;
            }

            for (int i = 0; i < objs.Length; i++)
            {
                if (!objs[i].Equals(other[i]))
                {
                    return false;
                }
            }

            return true;
        }

        #endregion

        #region 资源管理

        /// <summary>
        /// GameObject放入对象池
        /// </summary>
        /// <param name="go">GameObject</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public static void DLGameObjectPushPool(this GameObject go)
        {
            PoolManager.Instance.PushGameObject(go);
        }

        /// <summary>
        /// 通过组件将GameObject放入对象池
        /// </summary>
        /// <param name="component">组件</param>
        public static void DLGameObjectPushPool(this Component component)
        {
            DLGameObjectPushPool(component.gameObject);
        }

        /// <summary>
        /// 将普通对象放入对象池
        /// </summary>
        /// <param name="obj">普通对象</param>
        public static void DLObjectPushPool(this object obj)
        {
            PoolManager.Instance.PushObject(obj);
        }

        #endregion
    }
}

事件工具核心逻辑

本节代码与“事件工具两层包装”是重复的,因此这里只对原理进行讲解,实际上我们做的事情就是制作了一个统一事件拦截器组件,任何需要触发事件的物体,只需要挂载上这个组件,该物体所有可能发生的事件都会被这个组件所拦截,组件里各种迥乎不同的事件拦截器都调用了相同的事件触发方法private void TriggerAction<T>(DLEventType eventType, T eventData),实际上我们就是把不同事件的触发后要做的事情统一交给TriggerAction进行处理。

那么我们如何区分发生的是哪一种事件呢,DLEventType枚举类帮我们区分了各种事件,换句话说,我们把事件触发的不同逻辑和方式进行了统一,由此我们触发各种不同事件的时候不再需要使用不同的方式,只需要通过枚举类DLEventType来指明发生的是哪种事件即可。

但是这里又衍生出另外一个问题,就是不同的事件发生后,其产生的事件数据类型是不一样的,比如碰撞事件的事件数据类型为Collision,触发事件的事件数据类型为Collider,这里只列举了两种,实际上还有很多,不同的事件数据类型我们肯定要以不同的方式进行处理,所以我们的事件触发方法的第二个参数eventData才被定义成多态的,以便于传递不同类型的事件数据。

至此我们理解了为什么以及如何制作这样一个统一的事件拦截器,其本质就是对迥乎不同的事件触发方式统一到了一个组件中,一个物体只要挂载了这个组件,其所有事件都由该组件统一处理,也就是将一个物体在Unity中可能发生所有的事件全部接入了我们自己实现的事件系统,那么问题又来了,我们自己实现的事件系统是怎样处理的呢?

经过刚才的讲解,我们知道了DLEventType帮我们区分了各种事件,但是一个物体的Unity事件发生后,我们如何通过DLEventType找到我们自己的事件系统的对应事件呢?我们通过一个字典解决了这一问题Dictionary<DLEventType, IDLEventListenerInfos>(),字典的key很好理解,就是不同种类的事件,而字典中的value实际上是我们自己的事件系统中对应的该类事件,也就是说,其实每一种Unity事件对对应了一个IDLEventListenerInfos,因此我们称IDLEventListenerInfos一类事件

而在代码中我们可以看到,一类事件使用了泛型private class DLEventListenerInfos<T>:IDLEventListenerInfos,这是因为他要接收不同品种的事件数据(Collision、Collider)。这点千万要注意一下,这个T是为了指明其要处理的事件数据的类型(如Collision、Collider),而不是事件类型,事件类型是DLEventType,千万不要搞混了。

通过上述内容,我们可以得知,物体挂载该组件后,触发了某种Unity事件,就会被我们的组件所拦截,并通过DLEventType指明事件类型,凭借字典将其映射到我们的事件系统与之对应的某种事件(一类事件),随即将我们的事件系统中的这种事件触发。

我们知道,某种事件发生之后,会触发绑定在这种事件上的很多子事件(比如主角与地刺发生了碰撞器进入事件(某种事件/一类事件),随即触发了主角扣血、主角变红、主角被击退等子事件(一次事件),因此我们的DLEventListenerInfos类中存在一个列表List<DLEventListenerInfo<T>>,专门用来存放这些子事件(一次事件),当我们的某种事件触发后,便会遍历其中的子事件依次触发:

/// <summary>
/// 事件调用
/// </summary>
/// <param name="eventData">事件数据</param>
public void TriggerEvent(T eventData)
{
    foreach (DLEventListenerInfo<T> e in _eventList)
    {
        e.TriggerEvent(eventData);
    }
}

至此,我们便彻底理解了整个事件系统的核心逻辑,本质上就是我们实现一套统一的事件处理系统,这套事件处理系统的事件种类和Unity中的事件种类一一对应,但是比Unity事件系统优秀的地方在于我们用相同的方式就能统一处理所有截然不同的事件,属于一种工程化的思想。下面提供了具体逻辑流程:

物体挂载我们的统一事件拦截器组件 -> 物体触发某种Unity事件 -> 我们的事件拦截器拦截 -> 通过我们自己定义的统一事件类型DLEventType查找字典 -> 找到我们的事件系统中与此种Unity事件所对应的一种事件(一类事件) -> 触发这类事件中存放的所有子事件

通过主角碰到地刺的例子,我们进一步说明事件系统的逻辑:

地刺挂载了我们的统一事件拦截器组件 -> 主角碰到了地刺(地刺的碰撞体进入事件被触发) -> 通过DLEventType.OnCollisionEnter(碰撞体进入事件)查找字典 -> 找到了我们的事件系统中对应的碰撞体进入事件(一类事件) -> 触发这类事件中存放的三个子事件(扣血、贴图变红、击退)

事件工具完整拓展方法

为了方便使用,我们定义了一系列的拓展方法,使所有游戏中想要绑定事件的物体都可以直接通过其不同的拓展方法绑定不同的事件,十分的方便。

using System;
using UnityEngine;
using UnityEngine.EventSystems;

namespace DLFrame.Scripts._2.System_DL._1.Event
{
    /// <summary>
    /// 事件系统拓展方法
    /// </summary>
    public static class DLEventListenerExtend
    {
        #region 工具函数

        /// <summary>
        /// 获取或添加事件监听器
        /// </summary>
        private static DLEventListener GetOrAddDLEventListener(Component component)
        {
            DLEventListener listener = component.GetComponent<DLEventListener>();
            if (listener == null)
            {
                listener = component.gameObject.AddComponent<DLEventListener>();
            }

            return listener;
        }

        /// <summary>
        /// 添加指定事件
        /// </summary>
        // ReSharper disable once MemberCanBePrivate.Global
        public static void AddEventListener<T>(this Component component, DLEventType eventType,
            Action<T, object[]> action, params object[] args)
        {
            DLEventListener listener = GetOrAddDLEventListener(component);
            listener.AddListener(eventType, action, args);
        }

        /// <summary>
        /// 移除指定事件
        /// </summary>
        // ReSharper disable once MemberCanBePrivate.Global
        public static void RemoveEventListener<T>(this Component component, DLEventType eventType,
            Action<T, object[]> action, bool checkArgs = false, params object[] args)
        {
            DLEventListener listener = GetOrAddDLEventListener(component);
            listener.RemoveListener(eventType, action, checkArgs, args);
        }

        /// <summary>
        /// 移除所有某类事件
        /// </summary>
        public static void RemoveAllEventListener(this Component component, DLEventType eventType)
        {
            DLEventListener listener = GetOrAddDLEventListener(component);
            listener.RemoveAllListener(eventType);
        }

        /// <summary>
        /// 移除全部事件
        /// </summary>
        public static void RemoveAllEventListener(this Component component)
        {
            DLEventListener listener = GetOrAddDLEventListener(component);
            listener.RemoveAllListener();
        }

        #endregion

        #region 鼠标相关事件

        public static void OnMouseEnter(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnMouseEnter, action, args);
        }

        public static void OnMouseExit(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnMouseExit, action, args);
        }

        public static void OnClick(this Component com, Action<PointerEventData, object[]> action, params object[] args)
        {
            AddEventListener(com, DLEventType.OnClick, action, args);
        }

        public static void OnClickDown(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnClickDown, action, args);
        }

        public static void OnClickUp(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnClickUp, action, args);
        }

        public static void OnDrag(this Component com, Action<PointerEventData, object[]> action, params object[] args)
        {
            AddEventListener(com, DLEventType.OnDrag, action, args);
        }

        public static void OnBeginDrag(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnBeginDrag, action, args);
        }

        public static void OnEndDrag(this Component com, Action<PointerEventData, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnEndDrag, action, args);
        }

        public static void RemoveClick(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnClick, action, checkArgs, args);
        }

        public static void RemoveClickDown(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnClickDown, action, checkArgs, args);
        }

        public static void RemoveClickUp(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnClickUp, action, checkArgs, args);
        }

        public static void RemoveDrag(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnDrag, action, checkArgs, args);
        }

        public static void RemoveBeginDrag(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnBeginDrag, action, checkArgs, args);
        }

        public static void RemoveEndDrag(this Component com, Action<PointerEventData, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnEndDrag, action, checkArgs, args);
        }

        #endregion

        #region 碰撞相关事件

        public static void OnCollisionEnter(this Component com, Action<Collision, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionEnter, action, args);
        }


        public static void OnCollisionStay(this Component com, Action<Collision, object[]> action, params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionStay, action, args);
        }

        public static void OnCollisionExit(this Component com, Action<Collision, object[]> action, params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionExit, action, args);
        }

        public static void OnCollisionEnter2D(this Component com, Action<Collision, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionEnter2D, action, args);
        }

        public static void OnCollisionStay2D(this Component com, Action<Collision, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionStay2D, action, args);
        }

        public static void OnCollisionExit2D(this Component com, Action<Collision, object[]> action,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnCollisionExit2D, action, args);
        }

        public static void RemoveCollisionEnter(this Component com, Action<Collision, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionEnter, action, checkArgs, args);
        }

        public static void RemoveCollisionStay(this Component com, Action<Collision, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionStay, action, checkArgs, args);
        }

        public static void RemoveCollisionExit(this Component com, Action<Collision, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionExit, action, checkArgs, args);
        }

        public static void RemoveCollisionEnter2D(this Component com, Action<Collision2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionEnter2D, action, checkArgs, args);
        }

        public static void RemoveCollisionStay2D(this Component com, Action<Collision2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionStay2D, action, checkArgs, args);
        }

        public static void RemoveCollisionExit2D(this Component com, Action<Collision2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnCollisionExit2D, action, checkArgs, args);
        }

        #endregion

        #region 触发相关事件

        public static void OnTriggerEnter(this Component com, Action<Collider, object[]> action, bool checkArgs = false,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerEnter, action, checkArgs, args);
        }

        public static void OnTriggerStay(this Component com, Action<Collider, object[]> action, bool checkArgs = false,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerStay, action, checkArgs, args);
        }

        public static void OnTriggerExit(this Component com, Action<Collider, object[]> action, bool checkArgs = false,
            params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerExit, action, checkArgs, args);
        }

        public static void OnTriggerEnter2D(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerEnter2D, action, checkArgs, args);
        }

        public static void OnTriggerStay2D(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerStay2D, action, checkArgs, args);
        }

        public static void OnTriggerExit2D(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            AddEventListener(com, DLEventType.OnTriggerExit2D, action, checkArgs, args);
        }

        public static void RemoveTriggerEnter(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerEnter, action, checkArgs, args);
        }

        public static void RemoveTriggerStay(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerStay, action, checkArgs, args);
        }

        public static void RemoveTriggerExit(this Component com, Action<Collider, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerExit, action, checkArgs, args);
        }

        public static void RemoveTriggerEnter2D(this Component com, Action<Collider2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerEnter2D, action, checkArgs, args);
        }

        public static void RemoveTriggerStay2D(this Component com, Action<Collider2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerStay2D, action, checkArgs, args);
        }

        public static void RemoveTriggerExit2D(this Component com, Action<Collider2D, object[]> action,
            bool checkArgs = false, params object[] args)
        {
            RemoveEventListener(com, DLEventType.OnTriggerExit2D, action, checkArgs, args);
        }

        #endregion
    }
}

具体使用示例

using DLFrame.Scripts._2.System_DL._1.Event;
using DLFrame.Scripts._3.Service._0.Res;
using UnityEngine;
using UnityEngine.EventSystems;

namespace Test
{
    public class Test : MonoBehaviour
    {
        private void Start()
        {
            var cubeController = ResManager.Instance.Load<CubeController>("Cube");
            cubeController.OnClick(Click,1,"hello",0.34f);
            cubeController.OnClick(Click,2,"hello",0.34f);
            cubeController.RemoveClick(Click,true,1,"hello",0.34f);
            cubeController.RemoveAllEventListener();
            cubeController.OnClick(Click);
        }

        void Click(PointerEventData eventData, params object[] args)
        {
            Debug.Log("鼠标点击的位置:"+eventData.position);
            Debug.Log("参数数量:"+args.Length);
            Debug.Log("参数1:"+args[0]);
            Debug.Log("参数2:"+args[1]);
            Debug.Log("参数3:"+args[2]);
        }
    }
}

事件系统介绍

image-20240208111418882

举个例子,就比如按一个按钮打开背包这个功能,用Unity自带的事件或者C#的委托,我们要找到某个按钮,然后给按钮绑定某个方法,也就是说按钮和方法是耦合在一起的,我们该如何解耦呢,可以实现一个事件中心,统一存放所有的事件,比如把打开背包的逻辑封装成一个委托,存放到事件中心,然后当我们实现按钮的时候,只需要去事件中心找到打开背包的委托,将其绑定到点击按钮事件上,功能就实现了。可以发现,这种方式实现该功能,我们写打开背包逻辑的时候不需要关注按钮存不存在,只需要写好逻辑(委托),将其放到事件中心中即可,而我们实现按钮的时候只需要去事件中心找到我们想要处理的委托即可。

其实本质上来说相当于多了一个中介,将n-n的复杂耦合关系解耦为了n-1-n的简单耦合关系,符合迪米特法则

总而言之,我们实现事件系统的好处如下:

  1. 解耦UI和逻辑:UI元素(如按钮)不需要知道具体的业务逻辑,它们只需要知道要通知哪个事件。同样,业务逻辑不需要知道是哪个UI元素触发了它们。
  2. 易于扩展和维护:在此模式下,增加新的事件或更改事件处理逻辑更为简单,因为这些改动通常只需要在事件中心中进行,而不需要触及到多个UI元素。
  3. 符合迪米特法则(最少知识原则):每个部分只与它直接的朋友交谈,而不与陌生人交谈。在这种情况下,UI元素和业务逻辑都只需要与事件中心交流,而不需要直接相互作用。

事件系统思路

image-20240208115206945

事件系统数据包装

image-20240212101059163

using System;
using System.Collections.Generic;

namespace DLFrame.Scripts._2.System_DL._1.Event
{
    /// <summary>
    /// 事件系统管理器
    /// </summary>
    public static class EventManager
    {
        #region 内部接口、内部类

        /// <summary>
        /// 事件信息接口
        /// </summary>
        private interface IEventInfo
        {
        }

        /// <summary>
        /// 无参-事件信息
        /// </summary>
        private class EventInfo : IEventInfo
        {
            public Action Action;

            public void Init(Action action)
            {
                this.Action += action;
            }
        }

        /// <summary>
        /// 1个参数-事件信息
        /// </summary>
        private class EventInfo<T> : IEventInfo
        {
            public Action<T> Action;

            public void Init(Action<T> action)
            {
                this.Action += action;
            }
        }

        /// <summary>
        /// 2个参数-事件信息
        /// </summary>
        private class EventInfo<T, TK> : IEventInfo
        {
            public Action<T, TK> Action;

            public void Init(Action<T, TK> action)
            {
                this.Action += action;
            }
        }

        /// <summary>
        /// 3个参数-事件信息
        /// </summary>
        private class EventInfo<T, TK, TL> : IEventInfo
        {
            public Action<T, TK, TL> Action;

            public void Init(Action<T, TK, TL> action)
            {
                this.Action += action;
            }
        }

        #endregion

        private static Dictionary<string, IEventInfo> _eventInfoDic = new Dictionary<string, IEventInfo>();
    }
}

事件系统管理器核心逻辑

该部分内容主要实现了不同种类事件委托(无参、一个参数、两个参数、三个参数)的添加、触发和删除操作。

using System;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._0.Extension;
using DLFrame.Scripts._1.Base._2.Pool;

namespace DLFrame.Scripts._2.System_DL._1.Event
{
    /// <summary>
    /// 事件系统管理器
    /// </summary>
    public static class EventManager
    {
        #region 内部接口、内部类

        /// <summary>
        /// 事件信息接口
        /// </summary>
        private interface IEventInfo
        {
            void Destroy();
        }

        /// <summary>
        /// 无参-事件信息
        /// </summary>
        private class EventInfo : IEventInfo
        {
            public Action Action;

            public void Init(Action action)
            {
                this.Action = action;
            }

            public void Destroy()
            {
                this.Action = null;
            }
        }

        /// <summary>
        /// 1个参数-事件信息
        /// </summary>
        private class EventInfo<T> : IEventInfo
        {
            public Action<T> Action;

            public void Init(Action<T> action)
            {
                this.Action = action;
            }

            public void Destroy()
            {
                this.Action = null;
            }
        }

        /// <summary>
        /// 2个参数-事件信息
        /// </summary>
        private class EventInfo<T, TK> : IEventInfo
        {
            public Action<T, TK> Action;

            public void Init(Action<T, TK> action)
            {
                this.Action = action;
            }

            public void Destroy()
            {
                this.Action = null;
            }
        }

        /// <summary>
        /// 3个参数-事件信息
        /// </summary>
        private class EventInfo<T, TK, TL> : IEventInfo
        {
            public Action<T, TK, TL> Action;

            public void Init(Action<T, TK, TL> action)
            {
                this.Action = action;
            }

            public void Destroy()
            {
                this.Action = null;
            }
        }

        #endregion

        private static readonly Dictionary<string, IEventInfo> EventInfoDic = new Dictionary<string, IEventInfo>();

        #region 添加事件监听(为事件添加委托)

        /// <summary>
        /// 添加无参事件
        /// </summary>
        /// <param name="eventName">事件名称</param>
        /// <param name="action">委托</param>
        public static void AddEventListener(string eventName, Action action)
        {
            // 有没有对应的事件
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo info) info.Action += action;
            }
            // 没有对应的事件,需要新增
            else
            {
                EventInfo eventInfo = PoolManager.Instance.GetObject<EventInfo>();
                eventInfo.Init(action);
                EventInfoDic.Add(eventName, eventInfo);
            }
        }

        /// <summary>
        /// 添加1个参数事件
        /// </summary>
        /// <param name="eventName">事件名称</param>
        /// <param name="action">委托</param>
        public static void AddEventListener<T>(string eventName, Action<T> action)
        {
            // 有没有对应的事件
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T> info) info.Action += action;
            }
            // 没有对应的事件,需要新增
            else
            {
                EventInfo<T> eventInfo = PoolManager.Instance.GetObject<EventInfo<T>>();
                eventInfo.Init(action);
                EventInfoDic.Add(eventName, eventInfo);
            }
        }

        /// <summary>
        /// 添加2个参数事件
        /// </summary>
        /// <param name="eventName">事件名称</param>
        /// <param name="action">委托</param>
        public static void AddEventListener<T, TK>(string eventName, Action<T, TK> action)
        {
            // 有没有对应的事件
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T, TK> info) info.Action += action;
            }
            // 没有对应的事件,需要新增
            else
            {
                EventInfo<T, TK> eventInfo = PoolManager.Instance.GetObject<EventInfo<T, TK>>();
                eventInfo.Init(action);
                EventInfoDic.Add(eventName, eventInfo);
            }
        }

        /// <summary>
        /// 添加3个参数事件
        /// </summary>
        /// <param name="eventName">事件名称</param>
        /// <param name="action">委托</param>
        public static void AddEventListener<T, TK, TL>(string eventName, Action<T, TK, TL> action)
        {
            // 有没有对应的事件
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T, TK, TL> info) info.Action += action;
            }
            // 没有对应的事件,需要新增
            else
            {
                EventInfo<T, TK, TL> eventInfo = PoolManager.Instance.GetObject<EventInfo<T, TK, TL>>();
                eventInfo.Init(action);
                EventInfoDic.Add(eventName, eventInfo);
            }
        }

        #endregion

        #region 触发事件

        /// <summary>
        /// 触发无参事件
        /// </summary>
        /// <param name="eventName">事件名</param>
        public static void EventTrigger(string eventName)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                (value as EventInfo)?.Action?.Invoke();
            }
        }

        /// <summary>
        /// 触发1个参数事件
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="arg">参数</param>
        public static void EventTrigger<T>(string eventName, T arg)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                (value as EventInfo<T>)?.Action?.Invoke(arg);
            }
        }

        /// <summary>
        /// 触发2个参数事件
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="arg1">参数1</param>
        /// <param name="arg2">参数2</param>
        public static void EventTrigger<T, TK>(string eventName, T arg1, TK arg2)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                (value as EventInfo<T, TK>)?.Action?.Invoke(arg1, arg2);
            }
        }

        /// <summary>
        /// 触发3个参数事件
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="arg1">参数1</param>
        /// <param name="arg2">参数2</param>
        /// <param name="arg3">参数3</param>
        public static void EventTrigger<T, TK, TL>(string eventName, T arg1, TK arg2, TL arg3)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                (value as EventInfo<T, TK, TL>)?.Action?.Invoke(arg1, arg2, arg3);
            }
        }

        #endregion

        #region 移除事件监听(移除事件绑定的委托)

        /// <summary>
        /// 移除无参事件监听
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="action">委托</param>
        public static void RemoveEventListener(string eventName, Action action)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo info) info.Action -= action;
            }
        }

        /// <summary>
        /// 移除1个参数事件监听
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="action">委托</param>
        public static void RemoveEventListener<T>(string eventName, Action<T> action)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T> info) info.Action -= action;
            }
        }

        /// <summary>
        /// 移除2个参数事件监听
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="action">委托</param>
        public static void RemoveEventListener<T, TK>(string eventName, Action<T, TK> action)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T, TK> info) info.Action -= action;
            }
        }

        /// <summary>
        /// 移除3个参数事件监听
        /// </summary>
        /// <param name="eventName">事件名</param>
        /// <param name="action">委托</param>
        public static void RemoveEventListener<T, TK, TL>(string eventName, Action<T, TK, TL> action)
        {
            if (EventInfoDic.TryGetValue(eventName, out var value))
            {
                if (value is EventInfo<T, TK, TL> info) info.Action -= action;
            }
        }

        #endregion

        #region 移除事件

        /// <summary>
        /// 移除一个事件
        /// </summary>
        /// <param name="eventName">事件名</param>
        public static void RemoveEventListener(string eventName)
        {
            if (EventInfoDic.ContainsKey(eventName))
            {
                EventInfoDic[eventName].DLObjectPushPool();
                EventInfoDic.Remove(eventName);
            }
        }

        /// <summary>
        /// 移除所有事件
        /// </summary>
        public static void Clear()
        {
            foreach (var key in EventInfoDic.Keys)
            {
                EventInfoDic[key].Destroy();
                EventInfoDic[key].DLObjectPushPool();
            }
            EventInfoDic.Clear();
        }

        #endregion
    }
}

音效服务

游戏中的音效,主要可以分为背景音乐、特效音乐两大类,围绕这两类来给我们的框架完成一些较为方便的功能。

背景音乐

  • 背景音乐的特点是不会销毁、可能存在跨场景。
  • 背景音乐的音量有时候我们需要可以单独调整。

特效音乐

image-20240216110114719

另外还可能有一些回调也需要支持,比如音乐播放完后执行某件事,音乐播放完几秒后执行某件事等。

音量控制

image-20240217105106247

我们不希望背景音乐因场景切换等原因而消失,所以我们将背景音乐放置在GameRoot中。

image-20240217123730027

这里我们新增了一个音频管理器,并在其中完成了音量控制相关的逻辑。

using System;
using DLFrame.Scripts._1.Base._1.Singleton;
using Sirenix.OdinInspector;
using UnityEngine;

namespace DLFrame.Scripts._3.Service._1.Audio
{
    /// <summary>
    /// 音频管理器
    /// </summary>
    public class AudioManager : ManagerBase<AudioManager>
    {
        [SerializeField] [LabelText("背景音乐 (BgAudioSource)")]
        private AudioSource bgAudioSource;

        #region 音量、播放控制

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateAllAudioPlay))] [LabelText("全局音量 (GlobalVolume)")]
        private float globalVolume;

        public float GlobalVolume
        {
            get => globalVolume;
            set
            {
                if (Math.Abs(globalVolume - value) < 0.001f) return;
                globalVolume = value;
                UpdateAllAudioPlay();
            }
        }

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateBgAudioPlay))] [LabelText("背景音乐 (BgVolume)")]
        private float bgVolume;

        public float BgVolume
        {
            get => bgVolume;
            set
            {
                if (Math.Abs(bgVolume - value) < 0.001f) return;
                bgVolume = value;
                UpdateBgAudioPlay();
            }
        }

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateEffectAudioPlay))] [LabelText("音效音乐 (EffectVolume)")]
        private float effectVolume;

        public float EffectVolume
        {
            get => effectVolume;
            set
            {
                if (Math.Abs(effectVolume - value) < 0.001f) return;
                effectVolume = value;
                UpdateEffectAudioPlay();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateMute))] [LabelText("静音 (IsMute)")]
        private bool isMute;

        public bool IsMute
        {
            get => isMute;
            set
            {
                if (isMute == value) return;
                isMute = value;
                UpdateMute();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateLoop))] [LabelText("背景音乐循环 (IsLoop)")]
        private bool isLoop = true;

        public bool IsLoop
        {
            get => isLoop;
            set
            {
                if (isLoop == value) return;
                isLoop = value;
                UpdateLoop();
            }
        }

        private bool isPause;

        public bool IsPause
        {
            get => isPause;
            set
            { 
                if (isPause == value) return;
                isPause = value;
                if (isPause)
                {
                    bgAudioSource.Pause();
                }
                else
                {
                    bgAudioSource.UnPause();
                }
                UpdateEffectAudioPlay();
            }
        }

        /// <summary>
        /// 更新全部音频播放
        /// </summary>
        private void UpdateAllAudioPlay()
        {
            UpdateBgAudioPlay();
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐
        /// </summary>
        private void UpdateBgAudioPlay()
        {
            bgAudioSource.volume = bgVolume * globalVolume;
        }

        /// <summary>
        /// 更新音效音乐
        /// </summary>
        private void UpdateEffectAudioPlay()
        {
            Debug.Log("更新音效音乐");
        }

        /// <summary>
        /// 更新静音
        /// </summary>
        private void UpdateMute()
        {
            bgAudioSource.mute = isMute;
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐循环
        /// </summary>
        private void UpdateLoop()
        {
            bgAudioSource.loop = isLoop;
        }

        public override void Init()
        {
            base.Init();
            UpdateAllAudioPlay();
            UpdateMute();
            UpdateLoop();
            Debug.Log("音频管理器(AudioManager)初始化成功");
        }

        #endregion
    }
}

背景音乐控制

这里更新了AudioManager,在其中添加了背景音乐控制逻辑。

using System;
using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._3.Service._0.Res;
using Sirenix.OdinInspector;
using UnityEngine;

namespace DLFrame.Scripts._3.Service._1.Audio
{
    /// <summary>
    /// 音频管理器
    /// </summary>
    public class AudioManager : ManagerBase<AudioManager>
    {
        [SerializeField] [LabelText("背景音乐 (BgAudioSource)")]
        private AudioSource bgAudioSource;

        #region 音量、播放控制

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateAllAudioPlay))] [LabelText("全局音量 (GlobalVolume)")]
        private float globalVolume;

        public float GlobalVolume
        {
            get => globalVolume;
            set
            {
                if (Math.Abs(globalVolume - value) < 0.001f) return;
                globalVolume = value;
                UpdateAllAudioPlay();
            }
        }

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateBgAudioPlay))] [LabelText("背景音乐 (BgVolume)")]
        private float bgVolume;

        public float BgVolume
        {
            get => bgVolume;
            set
            {
                if (Math.Abs(bgVolume - value) < 0.001f) return;
                bgVolume = value;
                // 限制音量范围
                if (bgVolume < 0)
                {
                    Debug.LogWarning("背景音乐音量不能小于0,已自动设置为0");
                    bgVolume = 0;
                }

                if (bgVolume > 1)
                {
                    Debug.LogWarning("背景音乐音量不能大于1,已自动设置为1");
                    bgVolume = 1;
                }

                UpdateBgAudioPlay();
            }
        }

        [SerializeField]
        [Range(0, 1)]
        [OnValueChanged(nameof(UpdateEffectAudioPlay))]
        [LabelText("音效音乐 (EffectVolume)")]
        private float effectVolume;

        public float EffectVolume
        {
            get => effectVolume;
            set
            {
                if (Math.Abs(effectVolume - value) < 0.001f) return;
                effectVolume = value;
                UpdateEffectAudioPlay();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateMute))] [LabelText("静音 (IsMute)")]
        private bool isMute;

        public bool IsMute
        {
            get => isMute;
            set
            {
                if (isMute == value) return;
                isMute = value;
                UpdateMute();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateLoop))] [LabelText("背景音乐循环 (IsLoop)")]
        private bool isLoop = true;

        public bool IsLoop
        {
            get => isLoop;
            set
            {
                if (isLoop == value) return;
                isLoop = value;
                UpdateLoop();
            }
        }

        private bool isPause;

        public bool IsPause
        {
            get => isPause;
            set
            {
                if (isPause == value) return;
                isPause = value;
                if (isPause)
                {
                    bgAudioSource.Pause();
                }
                else
                {
                    bgAudioSource.UnPause();
                }

                UpdateEffectAudioPlay();
            }
        }

        /// <summary>
        /// 更新全部音频播放
        /// </summary>
        private void UpdateAllAudioPlay()
        {
            UpdateBgAudioPlay();
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐
        /// </summary>
        private void UpdateBgAudioPlay()
        {
            bgAudioSource.volume = bgVolume * globalVolume;
        }

        /// <summary>
        /// 更新音效音乐
        /// </summary>
        private void UpdateEffectAudioPlay()
        {
            Debug.Log("更新音效音乐");
        }

        /// <summary>
        /// 更新静音
        /// </summary>
        private void UpdateMute()
        {
            bgAudioSource.mute = isMute;
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐循环
        /// </summary>
        private void UpdateLoop()
        {
            bgAudioSource.loop = isLoop;
        }

        public override void Init()
        {
            base.Init();
            UpdateAllAudioPlay();
            UpdateMute();
            UpdateLoop();
            Debug.Log("音频管理器(AudioManager)初始化成功");
        }

        #endregion

        #region 背景音乐控制

        /// <summary>
        /// 播放背景音乐
        /// </summary>
        /// <param name="audioClip">音乐资源</param>
        /// <param name="loop">是否循环播放</param>
        /// <param name="volume">音量大小</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void PlayBgAudio(AudioClip audioClip, bool loop = true, float volume = -1)
        {
            bgAudioSource.clip = audioClip;
            IsLoop = loop;
            if (volume >= 0)
            {
                BgVolume = volume;
            }

            bgAudioSource.Play();
        }

        /// <summary>
        /// 播放背景音乐
        /// </summary>
        /// <param name="clipPath">音乐资源地址</param>
        /// <param name="loop">是否循环播放</param>
        /// <param name="volume">音量大小</param>
        public void PlayBgAudio(string clipPath, bool loop = true, float volume = -1)
        {
            AudioClip clip = ResManager.Instance.LoadAsset<AudioClip>(clipPath);
            PlayBgAudio(clip, loop, volume);
        }

        #endregion
    }
}

效果音效控制

image-20240219100440571

制作并初始化一个AudioPlay预制体

image-20240219114054268

下面的代码完全完成了音频服务的所有内容,本次新增的内容是与效果音效控制相关的逻辑。

using System;
using System.Collections;
using System.Collections.Generic;
using DLFrame.Scripts._1.Base._0.Extension;
using DLFrame.Scripts._1.Base._1.Singleton;
using DLFrame.Scripts._1.Base._2.Pool;
using DLFrame.Scripts._3.Service._0.Res;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Events;

namespace DLFrame.Scripts._3.Service._1.Audio
{
    /// <summary>
    /// 音频管理器
    /// </summary>
    public class AudioManager : ManagerBase<AudioManager>
    {
        [SerializeField] [LabelText("背景音乐 (BgAudioSource)")]
        private AudioSource bgAudioSource;

        [SerializeField] [LabelText("音效音乐 (EffectAudioSource)")]
        private GameObject prefabAudioPlay;

        // 场景中所有生效的音效音乐播放器列表
        private readonly List<AudioSource> audioPlayList = new List<AudioSource>();

        public override void Init()
        {
            base.Init();
            UpdateAllAudioPlay();
            UpdateMute();
            UpdateLoop();
            Debug.Log("音频管理器(AudioManager)初始化成功");
        }

        #region 音量、播放控制

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateAllAudioPlay))] [LabelText("全局音量 (GlobalVolume)")]
        private float globalVolume;

        public float GlobalVolume
        {
            get => globalVolume;
            set
            {
                if (Math.Abs(globalVolume - value) < 0.001f) return;
                globalVolume = value;
                // 限制音量范围
                if (globalVolume < 0)
                {
                    Debug.LogWarning("全局音量不能小于0,已自动设置为0");
                    globalVolume = 0;
                }

                if (globalVolume > 1)
                {
                    Debug.LogWarning("全局音量不能大于1,已自动设置为1");
                    globalVolume = 1;
                }

                UpdateAllAudioPlay();
            }
        }

        [SerializeField] [Range(0, 1)] [OnValueChanged(nameof(UpdateBgAudioPlay))] [LabelText("背景音乐 (BgVolume)")]
        private float bgVolume;

        public float BgVolume
        {
            get => bgVolume;
            set
            {
                if (Math.Abs(bgVolume - value) < 0.001f) return;
                bgVolume = value;
                // 限制音量范围
                if (bgVolume < 0)
                {
                    Debug.LogWarning("背景音乐音量不能小于0,已自动设置为0");
                    bgVolume = 0;
                }

                if (bgVolume > 1)
                {
                    Debug.LogWarning("背景音乐音量不能大于1,已自动设置为1");
                    bgVolume = 1;
                }

                UpdateBgAudioPlay();
            }
        }

        [SerializeField]
        [Range(0, 1)]
        [OnValueChanged(nameof(UpdateEffectAudioPlay))]
        [LabelText("音效音乐 (EffectVolume)")]
        private float effectVolume;

        public float EffectVolume
        {
            get => effectVolume;
            set
            {
                if (Math.Abs(effectVolume - value) < 0.001f) return;
                effectVolume = value;
                // 限制音量范围
                if (effectVolume < 0)
                {
                    Debug.LogWarning("音效音乐音量不能小于0,已自动设置为0");
                    effectVolume = 0;
                }

                if (effectVolume > 1)
                {
                    Debug.LogWarning("音效音乐音量不能大于1,已自动设置为1");
                    effectVolume = 1;
                }

                UpdateEffectAudioPlay();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateMute))] [LabelText("静音 (IsMute)")]
        private bool isMute;

        public bool IsMute
        {
            get => isMute;
            set
            {
                if (isMute == value) return;
                isMute = value;
                UpdateMute();
            }
        }

        [SerializeField] [OnValueChanged(nameof(UpdateLoop))] [LabelText("背景音乐循环 (IsLoop)")]
        private bool isLoop = true;

        public bool IsLoop
        {
            get => isLoop;
            set
            {
                if (isLoop == value) return;
                isLoop = value;
                UpdateLoop();
            }
        }

        private bool isPause;

        public bool IsPause
        {
            get => isPause;
            set
            {
                if (isPause == value) return;
                isPause = value;
                if (isPause)
                {
                    bgAudioSource.Pause();
                }
                else
                {
                    bgAudioSource.UnPause();
                }

                UpdateEffectAudioPlay();
            }
        }

        /// <summary>
        /// 更新全部音频播放
        /// </summary>
        private void UpdateAllAudioPlay()
        {
            UpdateBgAudioPlay();
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐
        /// </summary>
        private void UpdateBgAudioPlay()
        {
            bgAudioSource.volume = bgVolume * globalVolume;
        }

        /// <summary>
        /// 更新音效音乐
        /// </summary>
        private void UpdateEffectAudioPlay()
        {
            // 倒序遍历
            for (int i = audioPlayList.Count - 1; i >= 0; i--)
            {
                if (audioPlayList[i] == null)
                {
                    audioPlayList.RemoveAt(i);
                    continue;
                }

                SetEffectAudioPlay(audioPlayList[i]);
            }
        }


        /// <summary>
        /// 设置音效音乐
        /// </summary>
        /// <param name="audioPlay">音乐播放器</param>
        /// <param name="spatial">空间属性[0,1],0是2D,1是3D,-1为默认</param>
        private void SetEffectAudioPlay(AudioSource audioPlay, float spatial = -1)
        {
            audioPlay.mute = isMute;
            audioPlay.volume = effectVolume * globalVolume;
            if (spatial >= 0)
            {
                audioPlay.spatialBlend = spatial;
            }

            if (isPause)
            {
                audioPlay.Pause();
            }
            else
            {
                audioPlay.UnPause();
            }
        }

        /// <summary>
        /// 更新静音
        /// </summary>
        private void UpdateMute()
        {
            bgAudioSource.mute = isMute;
            UpdateEffectAudioPlay();
        }

        /// <summary>
        /// 更新背景音乐循环
        /// </summary>
        private void UpdateLoop()
        {
            bgAudioSource.loop = isLoop;
        }

        #endregion

        #region 背景音乐控制

        /// <summary>
        /// 播放背景音乐
        /// </summary>
        /// <param name="audioClip">音乐资源</param>
        /// <param name="loop">是否循环播放</param>
        /// <param name="volume">音量大小[0,1],-1为默认</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void PlayBgAudio(AudioClip audioClip, bool loop = true, float volume = -1)
        {
            bgAudioSource.clip = audioClip;
            IsLoop = loop;
            if (volume >= 0)
            {
                BgVolume = volume;
            }

            bgAudioSource.Play();
        }

        /// <summary>
        /// 播放背景音乐
        /// </summary>
        /// <param name="clipPath">音乐资源地址</param>
        /// <param name="loop">是否循环播放</param>
        /// <param name="volume">音量大小[0,1],-1为默认</param>
        public void PlayBgAudio(string clipPath, bool loop = true, float volume = -1)
        {
            AudioClip clip = ResManager.Instance.LoadAsset<AudioClip>(clipPath);
            PlayBgAudio(clip, loop, volume);
        }

        #endregion

        #region 音效音乐控制

        private Transform audioPlayRoot;

        /// <summary>
        /// 获取音效音乐播放器
        /// </summary>
        /// <returns></returns>
        private AudioSource GetAudioPlay(bool is3D = true)
        {
            if (audioPlayRoot == null)
            {
                audioPlayRoot = new GameObject("AudioPlayRoot").transform;
            }

            AudioSource audioPlay = PoolManager.Instance.GetGameObject<AudioSource>(prefabAudioPlay, audioPlayRoot);
            SetEffectAudioPlay(audioPlay, is3D ? 1f : 0f);
            audioPlayList.Add(audioPlay);
            return audioPlay;
        }

        /// <summary>
        /// 回收音效音乐播放器,播放结束后执行回调
        /// </summary>
        private void RecycleAudioPlay(AudioSource audioPlay, AudioClip clip, UnityAction callback, float callBackTime)
        {
            StartCoroutine(DoRecycleAudioPlay(audioPlay, clip, callback, callBackTime));
        }

        private IEnumerator DoRecycleAudioPlay(AudioSource audioPlay, AudioClip clip, UnityAction callback,
            float callBackTime)
        {
            // 延迟Clip的长度
            yield return new WaitForSeconds(clip.length);
            // 放回对象池
            if (audioPlay != null)
            {
                audioPlay.DLGameObjectPushPool();
                // 播放结束callBackTime秒后执行回调
                yield return new WaitForSeconds(callBackTime);
                callback?.Invoke();
            }
        }

        /// <summary>
        /// 播放一次音效音乐
        /// </summary>
        /// <param name="audioClip">音效片段</param>
        /// <param name="component">挂载组件</param>
        /// <param name="volumeScale">音量[0,1]</param>
        /// <param name="is3D">是否3D</param>
        /// <param name="callback">回调函数-在音乐播放完成后执行</param>
        /// <param name="callBackTime">回调函数在音乐播放完成后执行的延迟时间</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void PlayOnShot(AudioClip audioClip, Component component, float volumeScale = 1, bool is3D = true,
            UnityAction callback = null, float callBackTime = 0)
        {
            // 初始化音乐播放器
            AudioSource audioPlay = GetAudioPlay(is3D);
            Transform audioPlayTransform = audioPlay.transform;
            audioPlayTransform.SetParent(component.transform);
            audioPlayTransform.localPosition = Vector3.zero;

            // 播放一次音效音乐
            audioPlay.PlayOneShot(audioClip, volumeScale);

            // 回收音乐播放器以及执行回调
            RecycleAudioPlay(audioPlay, audioClip, callback, callBackTime);
        }

        /// <summary>
        /// 在指定位置播放一次音效音乐
        /// </summary>
        /// <param name="audioClip">音效片段</param>
        /// <param name="position">播放的位置</param>
        /// <param name="volumeScale">音量[0,1]</param>
        /// <param name="is3D">是否3D</param>
        /// <param name="callback">回调函数-在音乐播放完成后执行</param>
        /// <param name="callBackTime">回调函数在音乐播放完成后执行的延迟时间</param>
        // ReSharper disable once MemberCanBePrivate.Global
        public void PlayOnShot(AudioClip audioClip, Vector3 position, float volumeScale = 1, bool is3D = true,
            UnityAction callback = null, float callBackTime = 0)
        {
            // 初始化音乐播放器
            AudioSource audioPlay = GetAudioPlay(is3D);
            audioPlay.transform.position = position;

            // 播放一次音效音乐
            audioPlay.PlayOneShot(audioClip, volumeScale);

            // 回收音乐播放器以及执行回调
            RecycleAudioPlay(audioPlay, audioClip, callback, callBackTime);
        }

        /// <summary>
        /// 播放一次指定路径的音效音乐
        /// </summary>
        /// <param name="clipPath">音效片段资源路径</param>
        /// <param name="component">挂载组件</param>
        /// <param name="volumeScale">音量[0,1]</param>
        /// <param name="is3D">是否3D</param>
        /// <param name="callback">回调函数-在音乐播放完成后执行</param>
        /// <param name="callBackTime">回调函数在音乐播放完成后执行的延迟时间</param>
        public void PlayOnShot(string clipPath, Component component, float volumeScale = 1, bool is3D = true,
            UnityAction callback = null, float callBackTime = 0)
        {
            AudioClip audioClip = ResManager.Instance.LoadAsset<AudioClip>(clipPath);
            if (audioClip != null)
            {
                PlayOnShot(audioClip, component, volumeScale, is3D, callback, callBackTime);
            }
            else
            {
                Debug.LogError($"音效片段资源[{clipPath}]不存在");
            }
        }

        /// <summary>
        /// 在指定位置播放一次指定路径的音效音乐
        /// </summary>
        /// <param name="clipPath">音效片段资源路径</param>
        /// <param name="position">播放的位置</param>
        /// <param name="volumeScale">音量[0,1]</param>
        /// <param name="is3D">是否3D</param>
        /// <param name="callback">回调函数-在音乐播放完成后执行</param>
        /// <param name="callBackTime">回调函数在音乐播放完成后执行的延迟时间</param>
        public void PlayOnShot(string clipPath, Vector3 position, float volumeScale = 1, bool is3D = true,
            UnityAction callback = null, float callBackTime = 0)
        {
            AudioClip audioClip = ResManager.Instance.LoadAsset<AudioClip>(clipPath);
            if (audioClip != null)
            {
                PlayOnShot(audioClip, position, volumeScale, is3D, callback, callBackTime);
            }
            else
            {
                Debug.LogError($"音效片段资源[{clipPath}]不存在");
            }
        }

        #endregion
    }
}
除特殊说明,博客文章均为东篱原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。

评论

  1. 爱你的苗苗
    Android Chrome
    12 月前
    2024-1-15 10:41:17

    ٩(๑^o^๑)۶

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇