Unity3D程序基础框架(上)

前言:

最近在B站看到了唐老师出的课程——学习Unity3D基础框架,我觉得对我会很有用,特写此文章做以记录。

这篇需要你了解一些C#的知识。

 

 

 

 

 

 


单例模式基类模块

我们创建一个新项目,创建标准的目录结构

在Base下面新建脚本——BaseManager.cs

 

单例模式基类模块的作用主要是——减少单例模式重复代码的书写

 

我们都知道单例模式是怎么一种设计模式,那么什么是单例模式基类模块呢?其实就是单例模式的基础模板。

代码奉上

public class BaseManager<T> where T:new()
{
    private static T _instance;
    public static T GetInstance() {
        if (_instance == null) {
            _instance = new T();
        }
        return _instance;
    }
}

之后只要项目中需要用到单例模式,只要继承此类即可。

public class BaseManager<T> where T:new() 这一句后面的部分可能很多朋友不了解,这是一个C#中泛型的知识点——泛型约束。

关于泛型约束的概念可以参考这篇文章

 


缓存池模块

基本原理与实现

缓存池模块主要是为了节约性能,减少CPU和内存消耗。

Unity每一次创建对象,实际都是C#在内存中申请了一块空间,之后在Unity场景中Destroy这个物体对象,实际上只是断开了实例化对象对于内存的那块空间的一个引用,内存中依然存在那个对象对应的空间。

直到内存占用满了,CPU才会回过头来找内存中没有被引用的空间(一次GC),然后释放该空间,然后建立新的对象,如此往复。

这个GC步骤是比较消耗CPU的,所以在比较不好的机器上可能造成游戏的卡顿,所以,我们需要缓存池。

缓存池可以将你创建的对象在你用完后收录起来,等到你再要调用的时候,就可以直接调用缓存池中收录的对象,如此往复,形成闭环。不必再去申请新的内存。

我们在ProjectBase目录下创建一个新目录——Pool,用来存放缓存池脚本。

public class PoolMgr : BaseManager<PoolMgr>
{
    //这里是缓存池模块

    //创建字段存储容器
    public Dictionary<string,List<GameObject>> pool1Dic
        =new Dictionary<string, List<GameObject>>();


    //取得游戏物体
    public GameObject GetObj(string name) {
        GameObject obj = null;
        if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0)
        {
            //取得List中的第一个
            obj = pool1Dic[name][0];
            //移除第零个(这样才能允许同时创建多个物体)
            pool1Dic[name].RemoveAt(0); 
        }
        else {
            //缓存池中没有该物体,我们去目录中加载
            //外面传一个预设体的路径和名字,我内部就去加载它
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //创建对象后,将对象的名字与池中名字相符
            obj.name = name;
        }
        //让物体显示出来
        obj.SetActive(true);
        return obj;
    }

    //外界返还游戏物体
    public void PushObj(string name,GameObject obj) {
        //让物体失活
        obj.SetActive(false);
        //里面有记录这个键
        if (pool1Dic.ContainsKey(name))
        {
            pool1Dic[name].Add(obj);
        }
        //未曾记录这个键
        else {
            pool1Dic.Add(name, new List<GameObject>() { obj});
        }
    }
}

我们来做个试验,给一个场景创建这样的脚本,实现点击鼠标创建物体

public class test : MonoBehaviour
{
    void Update()
    {
        if (Input.GetMouseButtonDown(0)) {
            //向缓存池中拿东西
            PoolMgr.GetInstance().GetObj("Cube");
        }
        if (Input.GetMouseButtonDown(1))
        {
            PoolMgr.GetInstance().GetObj("Sphere");
        }
    }
}

物体身上挂着另一个脚本

public class DelayPush : MonoBehaviour
{
    void  OnEnable()
    {
        Invoke("Push", 1);
    }

    void Push() {
        PoolMgr.GetInstance().PushObj(transform.name, this.gameObject);
    }
}

最终效果

这样创造新物体实际就是唤醒旧物体,虽然好像占用了内存,但是却为CPU有很大好处,减少了GC,增加了游戏体验感。

优化

规范化

为了使资源管理更加工整,我们考虑到应该让都存放在一个空物体下,这样会非常便利我们的管理。

public class PoolMgr : BaseManager<PoolMgr>
{
    //这里是缓存池模块

    //创建字段存储容器
    public Dictionary<string,List<GameObject>> pool1Dic
        =new Dictionary<string, List<GameObject>>();

    private GameObject poolObj;

    //取得游戏物体
    public GameObject GetObj(string name) {
        GameObject obj = null;
        if (pool1Dic.ContainsKey(name) && pool1Dic[name].Count > 0)
        {
            //取得List中的第一个
            obj = pool1Dic[name][0];
            //移除第零个(这样才能允许同时创建多个物体)
            pool1Dic[name].RemoveAt(0); 
        }
        else {
            //缓存池中没有该物体,我们去目录中加载
            //外面传一个预设体的路径和名字,我内部就去加载它
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //创建对象后,将对象的名字与池中名字相符
            obj.name = name;
        }
        //让物体显示出来
        obj.SetActive(true);

        //断开了缓存池物体与poolObj的父子关系
        obj.transform.parent = null;

        return obj;
    }

    //外界返还游戏物体
    public void PushObj(string name,GameObject obj) {
        if (poolObj == null)
        {
            poolObj = new GameObject("Pool");

        }
        //将这个物体设置为空物体
        obj.transform.parent = poolObj.transform;



        //让物体失活
        obj.SetActive(false);
        //里面有记录这个键
        if (pool1Dic.ContainsKey(name))
        {
            pool1Dic[name].Add(obj);
        }
        //未曾记录这个键
        else {
            pool1Dic.Add(name, new List<GameObject>() { obj});
        }
    }
}

这样,就可以实现所有申请的对象都放在Pool这个空物体下面,当某个对象物体被激活才会回到主目录下。

场景跳转问题

还有一个问题,当场景切换时,缓存池的对象物体都会被销毁,但是引用还存在,这会占用内存又没有用处,

我们可以给PoolMgr添加一个清空方法来应对场景转换的情况。

    //清空缓存池的方法
    //主要用在场景切换时
    public void Clear() {
        pool1Dic.Clear();
        poolObj = null;
    }

这样,跳转场景之前调用这个Clear方法就好了。

更细节的规范化

我们现在虽然可以实现生成的缓存对象全部在Pool这个空物体下,但是却没有明确的划分,如果各式各样的物体很多,就会很杂乱。

所以我们可以修改代码对他们进行分类。

思路是这样的:我们创建了一个字典来保存记录缓存对象,键是字符串、值是Gameobject的集合,我们可以将值替换成一个新的类型——PoolData。

下面是最终的PoolMgr.cs

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

//抽屉数据,池子中的一列容器
public class PoolData
{
    //抽屉中,对象挂载的父节点
    public GameObject fatherObj;
    //对象的容器
    public List<GameObject> poolList;

    public PoolData(GameObject obj, GameObject poolObj)
    {
        //根据obj创建一个同名父类空物体,它的父物体为总Pool空物体
        fatherObj = new GameObject(obj.name);
        fatherObj.transform.parent = poolObj.transform;

        poolList =  new List<GameObject>() {  };

        PushObj(obj);
    }

    //像抽屉里面压东西并且设置好父对象
    public void PushObj(GameObject obj)
    {
        //存起来
        poolList.Add(obj);
        //设置父对象
        obj.transform.parent = fatherObj.transform;
        //失活,让其隐藏
        obj.SetActive(false);
    }

    //像抽屉中取东西
    public GameObject GetObj() {
        GameObject obj = null;
        //取出第一个
        obj = poolList[0];
        poolList.RemoveAt(0);
        //激活,让其展示
        obj.SetActive(true);
        //断开父子关系
        obj.transform.parent = null;

        return obj;
    }
}


public class PoolMgr : BaseManager<PoolMgr>
{
    //这里是缓存池模块

    //创建字段存储容器
    public Dictionary<string, PoolData> pool1Dic
        =new Dictionary<string, PoolData>();

    private GameObject poolObj;

    //取得游戏物体
    public GameObject GetObj(string name) {
        GameObject obj = null;
        if (pool1Dic.ContainsKey(name) && pool1Dic[name].poolList.Count > 0)
        {
            //取得List中的第一个
            obj = pool1Dic[name].GetObj();
        }
        else {
            //缓存池中没有该物体,我们去目录中加载
            //外面传一个预设体的路径和名字,我内部就去加载它
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
            //创建对象后,将对象的名字与池中名字相符
            obj.name = name;
        }
        return obj;
    }

    //外界返还游戏物体
    public void PushObj(string name,GameObject obj) {
        if (poolObj == null)
        {
            poolObj = new GameObject("Pool");

        }
        //里面有记录这个键
        if (pool1Dic.ContainsKey(name))
        {
            pool1Dic[name].PushObj(obj);
        }
        //未曾记录这个键
        else {
            pool1Dic.Add(name, new PoolData(obj,poolObj) { });
        }
    }
    
    //清空缓存池的方法
    //主要用在场景切换时
    public void Clear() {
        pool1Dic.Clear();
        poolObj = null;
    }
}

 


事件中心模块(基于观察者设计模式)

这个模块时游戏开发过程中非常重要的模块,他可以减小程序的耦合性(文件关联度小,独立性大),减小复杂性。

事件中心模块主要就是说收集全场的事件,在得到某个事件后(比如怪物X死亡),令关心这个事件的对象(比如玩家)进行相应的处理(比如加金币)。

我们先创建一个脚本——EventCenter.cs,这个就是事件中心

这个观察者模式的事件中心模块中也是通过字典来存储相应对象的。

基本框架奉上

public class EventCenter : BaseManager<EventCenter>
{
    //字典中,key对应着事件的名字,
    //value对应的是监听这个事件对应的委托方法们(重点圈住:们)
    private Dictionary<string, UnityAction> eventDic
        = new Dictionary<string, UnityAction>();

    //添加事件监听
    //第一个参数:事件的名字
    //第二个参数:处理事件的方法
    public void AddEventListener(string name, UnityAction action) {
        //有没有对应的事件监听
        //有的情况
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] += action;
        }
        //没有的情况
        else {
            eventDic.Add(name, action);
        }
    }
    //通过事件名字进行事件触发
    public void EventTrigger(string name) {
        //有没有对应的事件监听
        //有的情况(有人关心这个事件)
        if (eventDic.ContainsKey(name))
        {
            //调用委托(依次执行委托中的方法)
            eventDic[name]();
        }
    }
}
//后面还有两个重要方法,一会你就知道了

其实代码并不多,但是效果非常牛逼。

我们来一段测试

test.cs

public class test : MonoBehaviour
{
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //触发事件
            EventCenter.GetInstance().EventTrigger("LeftMouse");
        }
        else if (Input.GetMouseButtonDown(1)) {
            //触发事件
            EventCenter.GetInstance().EventTrigger("RightMouse");
        }
    }
}

Player.cs

public class Player : MonoBehaviour
{
    private void Start()
    {
        UnityAction LeftAction=null;
        LeftAction += leftDown;
        LeftAction += leftDown2;
        UnityAction RightAction = null;
        RightAction += RightDown;
        RightAction += RightDown2;
        EventCenter.GetInstance().AddEventListener("LeftMouse", LeftAction);
        EventCenter.GetInstance().AddEventListener("RightMouse", RightAction);
    }
    private void leftDown() {
        Debug.Log("左键按下");
    }
    private void leftDown2()
    {
        Debug.Log("白天了");
    }
    private void RightDown()
    {
        Debug.Log("右键按下");
    }
    private void RightDown2()
    {
        Debug.Log("晚上了");
    }
}

相信我不用多解释,你也能看懂,我们按下左键就调用了两个方法,按下右键又是另外两个方法,这只是个小demo,如果是大型项目,这种中心事件处理极大的降低了耦合度,想要触发事件直接触发就好,想要给事件添加方法直接添加就好,一切都是向 中心事件 发送消息,很好的管理了原本错综复杂的事件们。

但是我们还有几个问题,就是如果你销毁了某个物体的话,这个物体身上脚本的AddEventListener添加的监听的绑定并没有消除,他们之间总是建立着引用关系,这样下去慢慢会造成内存泄露。

所以我们在上面的基础上添加一个移除监听的方法

    //移除对应的事件监听
    public void RemoveEventListener(string name, UnityAction action) {
        if (eventDic.ContainsKey(name))
        {
            //移除这个委托
            eventDic[name] -= action;
        }
    }

在建立中心事件监听的对象的OnDestroy方法中,可以调用该方法实现及时的移除。

考虑到在跳转场景中Unity会将所有旧场景物体对象删除,所以我们不如写一个清空方法在每次跳转场景时确保不要有不合适的内存问题。

    //清空所有事件监听(主要用在切换场景时)
    public void Clear() {
        eventDic.Clear();
    }

到此为止,基本上够用了。

另外,考虑到可能会传来不同的参数,我们可以给委托添加一个泛型,从而允许给方法传入参数

public class Player : MonoBehaviour
{
    private void Start()
    {
        UnityAction<object> LeftAction=null;
        LeftAction += leftDown;
        LeftAction += leftDown2;
        UnityAction<object> RightAction = null;
        RightAction += RightDown;
        RightAction += RightDown2;
        EventCenter.GetInstance().AddEventListener("LeftMouse", LeftAction);
        EventCenter.GetInstance().AddEventListener("RightMouse", RightAction);
    }
    private void leftDown(object info) {
        Debug.Log("左键按下");
        //将Object的类型转换成test类,从而调用test属性name
        Debug.Log("test的对象的name是:" + (info as test).name);
    }
    private void leftDown2(object info)
    {
        Debug.Log("白天了");
    }
    private void RightDown(object info)
    {
        Debug.Log("右键按下");
        Debug.Log("test的对象的name是:" + (info as test).name);
    }
    private void RightDown2(object info)
    {
        Debug.Log("晚上了");
    }
}
public class test : MonoBehaviour
{
    public string name="123";
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //触发事件
            EventCenter.GetInstance().EventTrigger("LeftMouse",this);
        }
        else if (Input.GetMouseButtonDown(1)) {
            //触发事件
            EventCenter.GetInstance().EventTrigger("RightMouse",this);
        }
    }
}
public class EventCenter : BaseManager<EventCenter>
{
    //字典中,key对应着事件的名字,
    //value对应的是监听这个事件对应的委托方法们(重点圈住:们)
    private Dictionary<string, UnityAction<object>> eventDic
        = new Dictionary<string, UnityAction<object>>();

    //添加事件监听
    //第一个参数:事件的名字
    //第二个参数:处理事件的方法
    public void AddEventListener(string name, UnityAction<object> action) {
        //有没有对应的事件监听
        //有的情况
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] += action;
        }
        //没有的情况
        else {
            eventDic.Add(name, action);
        }
    }
    //通过事件名字进行事件触发
    public void EventTrigger(string name,objectinfo) {
        //有没有对应的事件监听
        //有的情况(有人关心这个事件)
        if (eventDic.ContainsKey(name))
        {
            //调用委托(依次执行委托中的方法)
            eventDic[name](info);
        }
    }

    //移除对应的事件监听
    public void RemoveEventListener(string name, UnityAction<object> action) {
        if (eventDic.ContainsKey(name))
        {
            //移除这个委托
            eventDic[name] -= action;
        }
    }

    //清空所有事件监听(主要用在切换场景时)
    public void Clear() {
        eventDic.Clear();
    }
}


公共Mono模块

我们都知道,继承了Monobehavior之后,我们就可以使用Unity给我们提供的一些生命周期函数。

但是有的时候,我们会有的类没有继承Monobehavior,但是我们又希望使用到Mono的东西,比如帧更新方法Update、协程等,这该怎么做呢?

公共Mono模块就可以解决这个问题。

帧更新

我们在ProjectBase下面新建一个目录——Mono,里面新建一个脚本MonoController.cs

代码奉上

//Mono的管理者
public class MonoController : MonoBehaviour
{
    private event UnityAction updateEvent;
    private void Start()
    {
        //此对象不可移除
        //从而方便别的对象找到该物体,从而获取脚本,从而添加方法
        DontDestroyOnLoad(this.gameObject);
    }
    private void Update()
    {
        if (updateEvent != null) {
            updateEvent();
        }
    }
    //为外部提供的添加帧更新事件的方法
    public void AddUpdateListener(UnityAction func)
    {
        updateEvent += func;
    }
    //为外部提供的移除帧更新事件的方法
    public void RemoveUpdateListener(UnityAction func) {
        updateEvent -= func;
    } 
}

意思就是说,一个外部的没有继承mono的脚本,它调用了MonoController的AddUpdateLisener方法,把自己的一个方法添加到了updateEvent中,而在MonoController中每一帧都执行了updateEvent,也就每一帧都执行了没有继承mono的脚本的某个方法,也就实现了没有继承mono的脚本具有了Update方法!

但是如果我们每次去调用,都要找到这个物体,找到这个方法,不免有些麻烦,所以,我们可以这样做:

在 MonoController.cs 同目录下新建一个文件 MonoMgr.cs

public class MonoMgr : BaseManager<MonoMgr>
{
    private MonoController controller;
    public MonoMgr() {
        //新建一个物体(等同于Instantiate)
        GameObject obj = new GameObject("MonoController");
        //给物体添加组件
        controller = obj.AddComponent<MonoController>();
    }
    public void AddUpdateListener(UnityAction func)
    {
        controller.AddUpdateListener(func);
    }
    public void RemoveUpdateListener(UnityAction func)
    {
        controller.RemoveUpdateListener(func);
    }
}

还记得单例模式模板的代码吗?

public static T GetInstance() { 
         if (_instance == null) { 
                 _instance = new T();
         }
         return _instance; 
}

我们这个MonoMgr也是一个单例模式,所以外界第一次调用它的时候,就会实例化对象,从而触发MonoMgr的构造方法。

并且也保证了MonoController对象的唯一性!

下面是测试代码test.cs文件中

public class testtest
{
    public void choot() {
        Debug.Log("123");
    }
}
public class test : MonoBehaviour {
    private void Start()
    {
        testtest t = new testtest();
        MonoMgr.GetInstance().AddUpdateListener(t.choot);
    }
}

然后,就会看到控制台不断打印123.

协程

实现协程非常简单,只要在MonoMgr中重写协程方法就好了

public class MonoMgr : BaseManager<MonoMgr>
{
    private MonoController controller;
    public MonoMgr() {
        //新建一个物体
        GameObject obj = new GameObject("MonoController");
        //给物体添加组件
        controller = obj.AddComponent<MonoController>();
    }
    public void AddUpdateListener(UnityAction func)
    {
        controller.AddUpdateListener(func);

    }
    public void RemoveUpdateListener(UnityAction func)
    {
        controller.RemoveUpdateListener(func);
    }
    public Coroutine StartCoroutine(IEnumerator routine) {
        return controller.StartCoroutine(routine);
    }
    public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value) {
        return controller.StartCoroutine(methodName,value);
    }
    public Coroutine StartCoroutine(string methodName) {
        return controller.StartCoroutine(methodName);
    }
}

测试调用

public class testtest
{
    public testtest(){
        MonoMgr.GetInstance().StartCoroutine(Test123());
    }
    IEnumerator Test123() {
        yield return new WaitForSeconds(1f);
        Debug.Log("等了1秒钟");
    }
}
public class test : MonoBehaviour {
    private void Start()
    {
        testtest t = new testtest();
    }
}

隔了一秒之后,会输出“等了1秒钟”


场景切换模块

很多时候,我们加载场景是需要动态创建一些角色的,比如王者荣耀,你不能把出场的英雄各种搭配组合都先摆在场景中吧?

这个时候,我们就需要“场景切换模块”

ProjectBase下面新建一个Scenes目录,下面再创建一个ScenesMgr.cs

代码参考

//场景切换模块
public class SceneMgr : BaseManager<SceneMgr>
{

    //切换场景
    public void LoadScene(string name,UnityAction func) {
        //场景同步加载
        SceneManager.LoadScene(name);
        //加载完成过后才会执行func
        func();
    }
    public void LoadSceneAsyn(string name, UnityAction func) {
        //公共Mono模块
        MonoMgr.GetInstance().StartCoroutine(ReallyLoadSceneAsyn(name,func));
    }
    private IEnumerator ReallyLoadSceneAsyn(string name,UnityAction func) {
        AsyncOperation ao=SceneManager.LoadSceneAsync(name);
        while (!ao.isDone) {
            //像事件中心分发进度情况,外面想用就用
            EventCenter.GetInstance().EventTrigger("Loading",ao.progress);
            //挂起一帧
            yield return ao.progress;
        }
        //加载完成后执行func
        func();
    }
}

异步加载中,我们将加载进度信息分发给了事件中心,如果你想对加载信息做一些处理,都可以在事件中心进行处理。

func方法就是进行动态加载的方法,想写什么都可以写。

 


资源加载模块

ProjectBase中新建一个目录Res,再里面新建一个脚本——ResMgr.cs


//资源加载模块
public class ResMgr : BaseManager<ResMgr>
{
    //同步加载资源
    public T Load<T>(string name) where T:Object{
        T res = Resources.Load<T>(name); ;
        //如果对象是一个GameObject类型的,我把它实例化后,再返回出去直接使用。
        if (res is GameObject) 
            return GameObject.Instantiate(res);
        else //else情况示例:TextAsset、AudioClip
            return res;  
    }
    
    //异步加载资源 
    public void LoadAsync<T>(string name,UnityAction<T> callback) where T:Object
    {
        //开启异步加载的协程
        MonoMgr.GetInstance().StartCoroutine(ReallyLoadAsync<T>(name,callback));
    }
    private IEnumerator ReallyLoadAsync<T>(string name,UnityAction<T> callback) where T:Object{
        ResourceRequest r=Resources.LoadAsync<T>(name);
        yield return r;

        if (r.asset is GameObject)
        {
            //实例化一下再传给方法
            callback(GameObject.Instantiate(r.asset) as T);
        }
        else {
            //直接传给方法
            callback(r.asset as T);
        }        
    }
}

需要注意的是,异步加载中,调用异步加载后,物体并没有被加载,需要等n帧才会被加载,所以我们不能直接返回物体,而是在方法中将物体传出去。

我这里来简单示例一下如何使用异步加载

    private void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            ResMgr.GetInstance().LoadAsync<GameObject>("cube", DoSome);
        }
    }
    private void DoSome(GameObject obj) {
        Debug.Log("调用了回调函数");
    }

为了方便,我们还可以直接使用Lambda表达式来方便书写

    private void Update()
    {
        if (Input.GetMouseButtonDown(1)) {
            ResMgr.GetInstance().LoadAsync<GameObject>("cube", (obj) => {
                Debug.Log("调用了回调函数");
            });
        }
    }

缓存池结合异步加载

既然我的做出了异步加载的模块,那么在我们的缓存池中,完全就可以通过异步加载来得到(创建)物体。

作用:使得加载很大的东西时避免卡顿感。

新的PoolMgr.cs,主要修改了PoolMgr中的GetObj方法。

//抽屉数据,池子中的一列容器
public class PoolData
{
    //抽屉中,对象挂载的父节点
    public GameObject fatherObj;
    //对象的容器
    public List<GameObject> poolList;

    public PoolData(GameObject obj, GameObject poolObj)
    {
        //根据obj创建一个同名父类空物体,它的父物体为总Pool空物体
        fatherObj = new GameObject(obj.name);
        fatherObj.transform.parent = poolObj.transform;

        poolList =  new List<GameObject>() {  };

        PushObj(obj);
    }

    //像抽屉里面压东西并且设置好父对象
    public void PushObj(GameObject obj)
    {
        //存起来
        poolList.Add(obj);
        //设置父对象
        obj.transform.parent = fatherObj.transform;
        //失活,让其隐藏
        obj.SetActive(false);
    }

    //像抽屉中取东西
    public GameObject GetObj() {
        GameObject obj = null;
        //取出第一个
        obj = poolList[0];
        poolList.RemoveAt(0);
        //激活,让其展示
        obj.SetActive(true);
        //断开父子关系
        obj.transform.parent = null;

        return obj;
    }
}


public class PoolMgr : BaseManager<PoolMgr>
{
    //这里是缓存池模块

    //创建字段存储容器
    public Dictionary<string, PoolData> pool1Dic
        =new Dictionary<string, PoolData>();

    private GameObject poolObj;

    //取得游戏物体
    public void GetObj(string name,UnityAction<GameObject> callback) {
        if (pool1Dic.ContainsKey(name) && pool1Dic[name].poolList.Count > 0)
        {
            //拖过委托返回给外部,让外部进行使用
            callback(pool1Dic[name].GetObj());
        }
        else {
            //缓存池中没有该物体,我们去目录中加载
            //外面传一个预设体的路径和名字,我内部就去加载它
            ResMgr.GetInstance().LoadAsync<GameObject>(name,(o)=> {
                o.name = name;
                callback(o);
            });
        }
    }

    //外界返还游戏物体
    public void PushObj(string name,GameObject obj) {
        if (poolObj == null)
        {
            poolObj = new GameObject("Pool");

        }
        //里面有记录这个键
        if (pool1Dic.ContainsKey(name))
        {
            pool1Dic[name].PushObj(obj);
        }
        //未曾记录这个键
        else {
            pool1Dic.Add(name, new PoolData(obj,poolObj) { });
        }
    }
    
    //清空缓存池的方法
    //主要用在场景切换时
    public void Clear() {
        pool1Dic.Clear();
        poolObj = null;
    }
}

外界调用时,第二个参数直接给GetObj传递一个Lambda表达式即可。

 


输入控制模块

我们可以将游戏中所有的输入检测汇集到一个地方,搭配事件中心模块,从而降低耦合性。

老样子,在ProjectBase目录下新建一个目录——Input,然后在下面新建一个文件——InputMgr.cs

最基础版本如下:

public class InputMgr : BaseManager<InputMgr>
{
    private bool isStart = false;

    //构造方法中,添加Update监听
    public InputMgr() {
        MonoMgr.GetInstance().AddUpdateListener(MyUpdate);
    }
    //检测是否需要开启输入检测
    public void StartOrEndCheck(bool isOpen) {
        isStart = isOpen;
    } 
    private void MyUpdate() {
        //没有开启输入检测,就不去检测
        if (!isStart)
            return;
        CheckKeyCode(KeyCode.A);
        CheckKeyCode(KeyCode.D);
        CheckKeyCode(KeyCode.W);
        CheckKeyCode(KeyCode.S);
    }
    private void CheckKeyCode(KeyCode key) {
        if (Input.GetKeyDown(key))
        {
            //事件中心模块,分发按下抬起事件(把哪个按键也发送出去)
            EventCenter.GetInstance().EventTrigger("KeyisDown", key);
        }
        if (Input.GetKeyUp(key))
        {
            EventCenter.GetInstance().EventTrigger("KeyisUp", key);
        }
    }
}

对应测试代码

public class Inputtest : MonoBehaviour
{
    void Start()
    {
        //开启输入检测
        InputMgr.GetInstance().StartOrEndCheck(true);
        //添加事件监听
        EventCenter.GetInstance().AddEventListener("KeyisDown", CheckInputDown);
        EventCenter.GetInstance().AddEventListener("KeyisUp", CheckInputUp);
    }
    private void CheckInputDown(object obj) {
        KeyCode code = (KeyCode)obj;
        switch (code) {
            case KeyCode.W:
                Debug.Log("前进");
                break;
            case KeyCode.A:
                break;
            case KeyCode.S:
                break;
            case KeyCode.D:
                break;
        }
    }
    private void CheckInputUp(object obj) {
        KeyCode code = (KeyCode)obj;
        switch (code)
        {
            case KeyCode.W:
                Debug.Log("不再前进");
                break;
            case KeyCode.A:
                break;
            case KeyCode.S:
                break;
            case KeyCode.D:
                break;
        }
    }
    private void OnDestroy()
    {
        EventCenter.GetInstance().RemoveEventListener("KeyisDown", CheckInputDown);
        EventCenter.GetInstance().RemoveEventListener("KeyisUp", CheckInputUp);
    }
}

 


事件中心优化

在进行事件中心的优化之前,你需要来了解一下什么是C#中的装箱和拆箱——参考这里

我们现在的代码是允许传进来的委托是object类型,这样会有个装箱拆箱的过程,我们现在做优化就是想要避免这个装箱拆箱的过程。

处理方法很骚气,使用泛型来处理不同的类型,但是EventCenter是单例模式,所以不能再字典中把值类型设置成泛型,所以有个骚气操作——使用一个空接口

关于里式转换,更多可以参考这里

EventCenter.cs文件

public interface IEventInfo {
    //这是一个空接口
}
public class EventInfo<T> : IEventInfo {
    public UnityAction<T> actions;

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


public class EventCenter : BaseManager<EventCenter>
{
    //字典中,key对应着事件的名字,
    //value对应的是监听这个事件对应的委托方法们(重点圈住:们)
    private Dictionary<string, IEventInfo> eventDic
        = new Dictionary<string, IEventInfo>();

    //添加事件监听
    //第一个参数:事件的名字
    //第二个参数:处理事件的方法(有参数(类型为T)的委托)
    public void AddEventListener<T>(string name, UnityAction<T> action) {
        //有没有对应的事件监听
        //有的情况
        if (eventDic.ContainsKey(name))
        {
            (eventDic[name] as EventInfo<T>).actions += action;
        }
        //没有的情况
        else {
            eventDic.Add(name, new EventInfo<T>(action));
        }
    }

    //通过事件名字进行事件触发
    public void EventTrigger<T>(string name,T info) {
        //有没有对应的事件监听
        //有的情况(有人关心这个事件)
        if (eventDic.ContainsKey(name))
        {
            //调用委托(依次执行委托中的方法)
            //?是一个C#的简化操作
            (eventDic[name] as EventInfo<T>).actions?.Invoke(info);
        }
    }

    //移除对应的事件监听
    public void RemoveEventListener<T>(string name, UnityAction<T> action) {
        if (eventDic.ContainsKey(name))
        {
            //移除这个委托
            (eventDic[name] as EventInfo<T>).actions -= action;
        }
    }

    //清空所有事件监听(主要用在切换场景时)
    public void Clear() {
        eventDic.Clear();
    }
}

外部使用的话,直接给泛型指定类型就好了。

修改我们之前写的那个输入控制模块的测试代码,

public class Inputtest : MonoBehaviour
{
    void Start()
    {
        //开启输入检测
        InputMgr.GetInstance().StartOrEndCheck(true);
        //添加事件监听
        EventCenter.GetInstance().AddEventListener<KeyCode>("KeyisDown", CheckInputDown);
        EventCenter.GetInstance().AddEventListener<KeyCode> ("KeyisUp", CheckInputUp);
    }
    private void CheckInputDown(KeyCode obj) {
        switch (obj) {
            case KeyCode.W:
                Debug.Log("前进");
                break;
            case KeyCode.A:
                break;
            case KeyCode.S:
                break;
            case KeyCode.D:
                break;
        }
    }
    private void CheckInputUp(KeyCode obj) {
        switch (obj)
        {
            case KeyCode.W:
                Debug.Log("不再前进");
                break;
            case KeyCode.A:
                break;
            case KeyCode.S:
                break;
            case KeyCode.D:
                break;
        }
    }
    private void OnDestroy()
    {
        EventCenter.GetInstance().RemoveEventListener<KeyCode>("KeyisDown", CheckInputDown);
        EventCenter.GetInstance().RemoveEventListener<KeyCode>("KeyisUp", CheckInputUp);
    }
}

到这里,基本的OK了。

还可以完善一下,就是说,如果我不需要传参数呢?重载一个不需要泛型的EventInfo类,然后将EventCenter中的一些方法重载即可。

public interface IEventInfo {
    //这是一个空接口
}
public class EventInfo<T> : IEventInfo {
    public UnityAction<T> actions;

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

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


public class EventCenter : BaseManager<EventCenter>
{
    //字典中,key对应着事件的名字,
    //value对应的是监听这个事件对应的委托方法们(重点圈住:们)
    private Dictionary<string, IEventInfo> eventDic
        = new Dictionary<string, IEventInfo>();

    //添加事件监听
    //第一个参数:事件的名字
    //第二个参数:处理事件的方法
    public void AddEventListener<T>(string name, UnityAction<T> action) {
        //有没有对应的事件监听
        //有的情况
        if (eventDic.ContainsKey(name))
        {
            (eventDic[name] as EventInfo<T>).actions += action;
        }
        //没有的情况
        else {
            eventDic.Add(name, new EventInfo<T>(action));
        }
    }
    //对于不需要参数的情况的重载方法
    public void AddEventListener(string name, UnityAction action)
    {
        if (eventDic.ContainsKey(name))
        {
            (eventDic[name] as EventInfo).actions += action;
        }
        else
        {
            eventDic.Add(name, new EventInfo(action));
        }
    }

    //通过事件名字进行事件触发
    public void EventTrigger<T>(string name,T info) {
        //有没有对应的事件监听
        //有的情况(有人关心这个事件)
        if (eventDic.ContainsKey(name))
        {
            //调用委托(依次执行委托中的方法)
            //?是一个C#的简化操作,存在,则直接调用委托
            (eventDic[name] as EventInfo<T>).actions?.Invoke(info);
        }
    }
    //对于不需要参数的情况的重载方法
    public void EventTrigger(string name)
    {
        if (eventDic.ContainsKey(name))
        {
            (eventDic[name] as EventInfo).actions?.Invoke();
        }
    }
    //移除对应的事件监听
    public void RemoveEventListener<T>(string name, UnityAction<T> action) {
        if (eventDic.ContainsKey(name))
        {
            //移除这个委托
            (eventDic[name] as EventInfo<T>).actions -= action;
        }
    }
    //对于不需要参数的情况的重载方法
    public void RemoveEventListener(string name, UnityAction action)
    {
        if (eventDic.ContainsKey(name))
        {
            (eventDic[name] as EventInfo).actions -= action;
        }
    }

    //清空所有事件监听(主要用在切换场景时)
    public void Clear() {
        eventDic.Clear();
    }
}

骚气操作。

 


这一篇就先记录到这里了

 

 

真的感谢B站的唐老湿,让我学到很多奇技淫巧。

 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

发表评论