Unity程序基础框架学习笔记

Unity的程序基础框架,视频地址:https://www.bilibili.com/video/BV1C441117wU
一、建立目录
在Assets下建立标准目录,如下图:
Unity程序基础框架学习笔记_第1张图片
Unity程序基础框架学习笔记_第2张图片
二、功能模块
Unity程序基础框架学习笔记_第3张图片

三、脚本
3.1、单例模式基类
知识点:单例模式是设计模式中很常用的一种模式,它的目的是让一个类在程序运行期间有且只有一个实例。因为只有一个实例,减少了内存消耗和系统性能开销。单例模式有三个特点:
(1)单例类只能有一个实例。
(2)单例类必须自己创建自己的唯一实例。
(3)单例类必须给所有其他对象提供这一实例。
脚本:BaseManager.cs

//定义单例模式基类,采用泛型构造,使用无参构造函数约束泛型T的类型。
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 GameManager : BaseManager<GameManager>
{

}
public class ItemManager : BaseManager<ItemManager>
{

}
//其他单例模式类的使用
public class test
{
    void main()
    {
        GameManager.GetInstance();
        ItemManager.GetInstance();
    }
}

3.2、缓存池模块
在GameObject大量生成->销毁的情况下(如子弹射击),销毁的物体只是删除对内存空间的引用,但并未释放实例化物体占用的内存空间。当内存空间占满后进行GC(Garbage Collection),将造成游戏运行卡顿。为了避免这种情况,通过缓存池对销毁物体的内存空间进行暂存,当需要创建新物体时直接从缓存池取出,新物体不再占用新的内存空间(除非缓存池中存放的物体数量不够),从而减少内存的消耗。
知识点:命名空间System.Collections.Generic中有两个非常重要,而且常用的泛型集合类,它们分别是Dictionary字典和List列表。Dictionary字典通常用于保存键/值对的数据,而List列表通中用于保存可通过索引访问的对象的强类型列表。
脚本:PoolMgr.cs

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

public class PoolMgr : BaseManager<PoolMgr>
{
    //定义构造类,每类放入缓存池的物体应当有一个标识其类别的父对象,及存储物体的容器
    public class PoolData
    {
        public GameObject PoolFather;
        public List<GameObject> PoolList;
        //FObj物体用于在缓存池内表明物体具体类别,PoolObj物体用于代表缓存池,多个FObj的父对象都设置为PoolObj以体现层级关系
        public PoolData(GameObject Obj,GameObject PoolObj)
        {
            PoolFather = new GameObject(Obj.name);
            PoolFather.transform.parent = PoolObj.transform;
            PoolList = new List<GameObject>() { };
            PutObj(Obj);
        }
        public void PutObj(GameObject Obj)
        {
            Obj.SetActive(false);
            PoolList.Add(Obj);
            Obj.transform.parent = PoolFather.transform;
        }
        public GameObject GetObj()
        {
            GameObject Obj = null;
            Obj = PoolList[0];
            PoolList.RemoveAt(0);
            //取出物体时使其激活
            Obj.SetActive(true);
            //取出缓存池的物体断开和缓存池物体的父子联系
            Obj.transform.parent = null;
            return Obj;
        }
    }
    //创建缓存池,定义字典参数PoolDict实现多个对象的存储,其中第一个string参数说明缓存物体的类别,用自定义的PoolData类型作为第二个参数存储对象类别(PoolFather)和对象List列表(PoolList)。
    public Dictionary<string, PoolData> PoolDict = new Dictionary<string, PoolData>();
    //定义一个物体,放入缓存池中的物体挂接到该物体下面,实现界面的简介易懂
    private GameObject PoolObj;
    //把物体放入缓存池,第一个参数存储物体名字,第二个参数存储物体实例      
    public void PutObj(string name,GameObject Obj)
    {
        //如果缓存池物体为空,则创建一个
        if (PoolObj == null)
            PoolObj = new GameObject("Pool");
        //如果缓存池中有此类物体的List,则添加进该类物体的List中;如缓存池中无此类物体,则新建一个此类物体的List,并将该物体存入新建的List。
        if (PoolDict.ContainsKey(name))
        {
            //在对应name类的List中存入物体
            PoolDict[name].PutObj(Obj);
        }else 
        {
            //新建一个name类的PoolData,并把Obj物体添加进PoolData的List里
            PoolDict.Add(name, new PoolData(Obj,PoolObj));
        }
    }   
    //采用异步加载方式从缓存池中取出指定物体,通过物体名字进行索引
    public void GetObj(string name,UnityAction<GameObject> CallBack)
    {
        //如果缓存池中存在指定的物体名,且该类物体数量大于0则取出,否则重新实例化一个物体
        if (PoolDict.ContainsKey(name) && PoolDict[name].PoolList.Count > 0)
        {
            CallBack(PoolDict[name].GetObj());
            //调用PoolData中的GetObj()方法,从缓存池中取出物体
            //Obj = PoolDict[name].GetObj();
        }
        else
        {
            ResourcesMgr.GetInstance().LoadAsync<GameObject>(name,(PObj)=>
            {
                PObj.name = name;
                CallBack(PObj);
            });
            //重新实例化要取出的物体,从Resources目录中调用预制体
            //Obj = GameObject.Instantiate(Resources.Load(name));
            //把实例化的物体名字改为传入的调用名字,否则物体实例化后会在名字后面加上(clone),不利于后续处理
            //Obj.name = name;
        }
    }
    //切场景的时候应当清除缓存池并把缓存池物体置空,否则调用会报错
    public void PoolClear()
    {
        PoolDict.Clear();
        PoolObj = null;
    }
}

// PoolGetTest .cs----------------------------------------------------------------------
//从缓存池中取出物体脚本。当按下鼠标左键时,从缓存池取出指定名称为"Test/ Monster "的物体,当缓//存池中没有物体时,将从取出Resources/Test/目录下的Cube预制体进行实例化。按下鼠标右键时,则对"Test/ Player "物体进行相同操作。

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

public class PoolGetTest : MonoBehaviour
{
    void Update()
    {
        if(Input.GetMouseButtonDown(0))
        {
            PoolMgr.GetInstance().GetObj("Test/Monster",(Obj)=> { });
        }
        if (Input.GetMouseButtonDown(1))
        {
            PoolMgr.GetInstance().GetObj("Test/Player", (Obj) => { });
        }
    }
}

// PoolPutTest .cs----------------------------------------------------------------------
//将物体放入缓存池脚本。将此脚本挂在Resources/Test/目录下的预制体上,当该物体被实例化调用后,延迟1秒后放入缓存池。

public class PoolPutTest : MonoBehaviour
{
    //当对象激活时,会进入的生命周期函数
    private void OnEnable()
    {
        Invoke("Put",1);
    }
    void Put()
    {
        PoolMgr.GetInstance().PutObj(this.gameObject.name,this.gameObject);
    }
}

3.3、事件中心模块
通过Dictionary存储事件及对应的处理方法,string存储事件名,UnityAction使用委托来存储多个事件处理方法。
脚本:EventMgr.cs

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

public class EventMgr : BaseManager<EventMgr>
{
    //Dictionary的Key对应字符串类型的事件名(如怪物死亡,玩家死亡等),Value对应的是监听这个事件的委托函数们,此处使用UnityEngine.Events命名空间下自带的事件标识UnityAction。
    private Dictionary<string, UnityAction<object>> EventDict = new Dictionary<string, UnityAction<object>>();
    
    //添加事件监听,添加指定事件名字的监听器,并说明准备处理该事件的方法
    public void AddEventListener(string name,UnityAction<object> action)
    {
        //如果事件管理模块中有name名字的事件,则在该事件中加入一个传入的处理方法
        if(EventDict.ContainsKey(name))
        {
            EventDict[name] += action;
        }
        //如果事件管理模块中没有name名字的事件,则添加这个事件以及对应的处理方法
        else 
        {
            EventDict.Add(name, action);
        }       
    }
    //移除事件监听,当物体销毁时应当在事件管理器中移除该物体监听事件执行的方法
    public void RemoveEventListener(string name,UnityAction<object> action)
    {
        if (EventDict.ContainsKey(name)) 
            EventDict[name] -= action;
    }
    //事件触发器,指定名字的事件将被触发,并传递参数说明触发事件的具体对象
    public void EventTrigger(string name,object info)
    {
        if (EventDict.ContainsKey(name))
        {
            EventDict[name].Invoke(info);
        }
    }
    //当切换场景时,清空事件管理器中存储的事件及对应的方法
    public void Clear()
    {
        EventDict.Clear();
    }
}

//EventTestPlayer.cs-------------------------------------------------------------------
//挂在Player物体上,用来测试在事件触发器中添加当发生某事件时,需要触发的方法。

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

public class EventTestPlayer : MonoBehaviour
{
    //在事件触发器中添加当发生"MonsterDead"事件时,要触发的方法
    private void Start()
    {
        EventMgr.GetInstance().AddEventListener("MonsterDead", MonsterDeadDo);
    }
    //怪物死亡事件触发后,要执行的方法,如获得奖励
    public void MonsterDeadDo(object info)
    {
        Debug.Log((info as EventTestMonster).name+"死亡得奖励");
    }
    //当物体销毁时,要删除事件触发器中已添加的方法
    private void OnDestroy()
    {
        EventMgr.GetInstance().RemoveEventListener("MonsterDead", MonsterDeadDo);
    }
}

//EventTestMonster.cs-------------------------------------------------------------------------------------------------------
//挂在Monster物体上,用来测试当发生某事件时如何触发,并传递参数。

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

public class EventTestMonster : MonoBehaviour
{
    void Start()
    {
        Dead();
    }
    //如果怪物死亡则触发"MonsterDead"事件,并把本身作为参数传递,以便判断是具体哪个怪物触发的事件。触发事件意味着执行该事件对应的一切方法。
    void Dead()
    {
        EventMgr.GetInstance().EventTrigger("MonsterDead",this);
    }
}

3.4、公共MONO模块
所有脚本都执行Update会形成一定系统资源浪费,通过公共Mono模块制作一个唯一的Update脚本,可在一定程度上降低程序开销。同时也可用公共Mono模块制作一个唯一的开启协程的脚本。
脚本:MonoController.cs

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

//Mono管理者
public class MonoController : MonoBehaviour
{
    //定义UpdateEvent作为帧更新事件
    private event UnityAction UpdateEvent;
    void Start()
    {
        DontDestroyOnLoad(this.gameObject);
    }

    // 在自身Update的时候同时执行UpdateEvent帧更新事件
    void Update()
    {
        if(UpdateEvent!=null)
        {
            UpdateEvent();         
        }
    }
    //提供给外部的添加帧更新事件
    public void AddUpdateListener(UnityAction action)
    {
        UpdateEvent += action;
    }
    //提供给外部的移除帧更新事件
    public void RemoveUpdateListener(UnityAction action)
    {
        UpdateEvent -= action;
    }
}
脚本:MonoMgr.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Internal;

//为MonoController.cs提供外部接口
public class MonoMgr : BaseManager<MonoMgr>
{
    //定义一个MonoController类型的变量
    private MonoController Controller;
    public MonoMgr()
    {
        //当MonoMgr被调用时,创建一个名为"MonoController"的游戏物体,并在该游戏物体上挂载MonoController脚本组件。
        //将上面定义的Controller变量赋值为新创建游戏物体上的脚本组件
        GameObject Obj = new GameObject("MonoController");
        Controller = Obj.AddComponent<MonoController>();
    }
    //提供接口,以便通过MonoMgr从外部执行添加帧更新事件
    public void AddUpdateListener(UnityAction action)
    {
        Controller.AddUpdateListener(action);
    }
    //提供接口,以便通过MonoMgr从外部执行移除帧更新事件
    public void RemoveUpdateListener(UnityAction action)
    {
        Controller.RemoveUpdateListener(action);
    }
    //在继承MonoBehaviour的脚本中输入StartCoroutine(),点击按F12可打开MonoBehaviour中的协程相关函数,把相关功能封装到MonoMgr中,从外部即可通过MonoMgr调用协程
    public Coroutine StartCoroutine(string methodName)
    {
        return Controller.StartCoroutine(methodName);
    }
    public Coroutine StartCoroutine(IEnumerator routine)
    {
        return Controller.StartCoroutine(routine);
    }
    public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value)
    {
        return Controller.StartCoroutine(methodName,value);
    }
    //可依次将以下协程函数进行封装,以便通过MonoMgr从外部调用协程函数
    //public Coroutine StartCoroutine_Auto(IEnumerator routine);
    //public void StopAllCoroutines();
    //public void StopCoroutine(IEnumerator routine);
    //public void StopCoroutine(Coroutine routine);
    //public void StopCoroutine(string methodName);
}

MonoTest.cs----------------------------------------------------------------------------

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

public class MonoTest:MonoBehaviour
{
    private void Start()
    {
        //定义并实例化变量TT为未继承MonoBehaviour类的对象
        TestTest TT=new TestTest();
        //通过MonoMgr添加帧更新方法
        MonoMgr.GetInstance().AddUpdateListener(TT.UpdateTest);        
    }
}
public class TestTest
{
    //测试通过MonoMgr调用协程
    public TestTest()
    {
        MonoMgr.GetInstance().StartCoroutine(CoroutineTest());
    }
    //测试帧更新执行情况
    public void UpdateTest()
    {
        Debug.Log("TestTestUpdate");
    }
    //测试协程执行情况
    IEnumerator CoroutineTest()
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("CoroutineTest");
    }
}

3.5、场景切换模块
管理场景切换过程中及切换后需要做的事。尤其是在新场景中需要动态的改变一些事情,如通过配置文件加载新场景中的物体,或根据玩家数据动态创建玩家。另,如果需要使用场景切换功能,必须在Unity3D的Build Setting中把需要加载的场景添加进Scenes in Build中。
脚本:ScenesMgr.cs

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

public class ScenesMgr : BaseManager<ScenesMgr>
{
    // 同步加载场景
    public void LoadScene(string SceneName,UnityAction Action) 
    {
        SceneManager.LoadScene(SceneName);
        //加载SceneName场景后,执行action事件
        Action();
    }
    //提供给外部的异步加载场景接口
    public void LoadSceneAsync(string SceneName, UnityAction Action)
    {
        MonoMgr.GetInstance().StartCoroutine(ReallyLoadSceneAsync(SceneName, Action));
    }
    //通过协程实现异步加载场景
    private IEnumerator ReallyLoadSceneAsync(string SceneName,UnityAction Action)
    {
        AsyncOperation Ao = SceneManager.LoadSceneAsync(SceneName);
        //如果场景没有加载完,则一直返回Ao.progress(0-1之间)表示的加载进度
        while (!Ao.isDone)
        {
            //可使用事件管理模块触发进度条更新事件,触发事件的对象为Ao.progress,外部可在需要的时候添加对应的执行方法
            EventMgr.GetInstance().EventTrigger("进度条更新", Ao.progress);
            //通过获取加载进度,为更新加载进度条提供支持
            yield return Ao.progress;
        }
        Action();
    }
}

3.6、资源加载模块
脚本:ResourcesMgr.cs

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

public class ResourcesMgr : BaseManager<ResourcesMgr>
{
    //同步加载资源,当前资源加载完毕后再执行后续脚本  
    public T Load<T>(string ResPathName)where T:Object
    {
        //加载路径为name的资源
        T res = Resources.Load<T>(ResPathName);
        //如果加载的资源为游戏物体,则直接实例化并返回,否则直接返回
        if (res is GameObject)
            return GameObject.Instantiate(res);
        else
            return res;        
    }
    //异步加载资源,当前资源加载的同时直接执行后续脚本,避免因加载较大资源时的卡顿感
    //因为异步加载只有资源加载完后,才可对资源进行操作,所以定义一个加载完成事件,命名为CallBack,用于执行资源加载完成后需要的操作
    public void LoadAsync<T>(string ResPathName,UnityAction<T> CallBack) where T:Object
    {
        MonoMgr.GetInstance().StartCoroutine(ReallyLoadAsync(ResPathName, CallBack));
    }
    private IEnumerator ReallyLoadAsync<T>(string ResPathName, UnityAction<T> CallBack) where T:Object
    {
        ResourceRequest Res = Resources.LoadAsync<T>(ResPathName);
        yield return Res;
        if (Res.asset is GameObject)
            CallBack(GameObject.Instantiate(Res.asset) as T);
        else
            CallBack(Res.asset as T);
    }
}

ResMgrTest.cs--------------------------------------------------------------------------

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

public class ResMgrTest : MonoBehaviour
{
    void Update()
    {
        //点鼠标左键调用同步加载
        if(Input.GetMouseButtonDown(0))
           ResourcesMgr.GetInstance().Load<GameObject>("Test/Monster");
        //点鼠标右键调用异步加载
        if(Input.GetMouseButtonDown(1))
           ResourcesMgr.GetInstance().LoadAsync<GameObject>("Test/Player",DoSomething);
    }
    private void DoSomething(GameObject Obj)
    {
        //异步加载完毕后,需要执行的操作
    }
}

3.7、输入控制模块
通常需要在角色的Update里面添加输入控制脚本,如需要操作多个角色或物体,则需要在每个角色的Update里重复书写输入控制脚本。这里把输入控制功能独立出来,通过事件管理模块分发至每一个角色中,以达到简化代码,降低程序耦合性的目的。
脚本:InputMgr.cs

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

public class InputMgr :BaseManager<InputMgr>
{
    //定义一个布尔值,用于检测是否允许输入生效
    private bool InputCheck=false;    
    //在InputMgr构造函数中,添加Update监听事件
    public InputMgr()
    {
        MonoMgr.GetInstance().AddUpdateListener(InputUpdate);
    }
    public void InputCheckAllow(bool Check)
    {
        InputCheck = Check;
    }
    private void InputUpdate()
    {
        CheckKeyCode(KeyCode.W);
        CheckKeyCode(KeyCode.S);
        CheckKeyCode(KeyCode.A);
        CheckKeyCode(KeyCode.D);
    }
    private void CheckKeyCode(KeyCode Key)
    {
        //如果InputCheck为false,意为不允许输入,则直接返回,不再分发按键事件
        if (!InputCheck)
            return;
        if(Input.GetKeyDown(Key))
            //事件管理模块,分发按键按下抬起事件
            EventMgr.GetInstance().EventTrigger("按下某键",Key);        
        if (Input.GetKeyUp(Key))
            //事件管理模块,分发按键按下抬起事件
            EventMgr.GetInstance().EventTrigger("抬起某键",Key);
    }
}

InputTest.cs--------------------------------------------------------------------------

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

public class InputTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //将输入检测设置为开启
        InputMgr.GetInstance().InputCheckAllow(true);
        EventMgr.GetInstance().AddEventListener("按下某键",CheckInputDown);
        EventMgr.GetInstance().AddEventListener("抬起某键",CheckInputUp);
    }
    private void CheckInputDown(object Key)
    {
        KeyCode Code = (KeyCode)Key;
        switch (Code)
        {
            case KeyCode.W:
                Debug.Log("前进");
                break;
            case KeyCode.S:
                break;
            case KeyCode.A:
                break;
            case KeyCode.D:
                break;
        }
    }
    private void CheckInputUp(object Key)
    {
        KeyCode Code = (KeyCode)Key;
        switch(Code)
        {
            case KeyCode.W:
                Debug.Log("停止前进");
                break;
            case KeyCode.S:
                break;
            case KeyCode.A:
                break;
            case KeyCode.D:
                break;
        }
    }
    // Update is called once per frame
    void Update()
    {
        
    }
}

3.8、音效管理模块
统一管理音乐音效的调用和管理
脚本:MusicMgr.cs

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

public class MusicMgr : BaseManager<MusicMgr>
{
    private AudioSource BGMusic=null, ASource = null;
    private float MusicVolume=0.1f,SoundVolume = 0.1f;

    private GameObject SoundObj = null;
    private List<AudioSource> SoundList = new List<AudioSource>();
    
    public MusicMgr()
    {
        MonoMgr.GetInstance().AddUpdateListener(Update);
    }
    private void Update()
    {
        //遍历音效List,如果该音效没有在播放,则删除该音效,并从List中删除
        for(int i=SoundList.Count-1;i>=0;i--)
        {
            if(!SoundList[i].isPlaying)
            {
                GameObject.Destroy(SoundList[i]);
                SoundList.RemoveAt(i);
            }
        }
    }
    public void PlayBGM(string BGMName)
    {
        if(BGMusic == null)
        {
            GameObject Obj = new GameObject();
            Obj.name = "BgMusic";
            BGMusic = Obj.AddComponent<AudioSource>();
        }
        ResourcesMgr.GetInstance().LoadAsync<AudioClip>("Music/BGMusic/" + BGMName, (Clip) =>
           {
               BGMusic.clip = Clip;
               BGMusic.volume = MusicVolume;
               BGMusic.loop = true;
               BGMusic.Play();
           });
    }
    public void SetBGMVolume(float MVolume)
    {
        MusicVolume = MVolume;
        if (BGMusic == null)
            return;
        BGMusic.volume = MusicVolume;
    }
    public void PauseBGM()
    {
        if (BGMusic == null)
            return;
        BGMusic.Pause();
    }
    public void StopBGM()
    {
        if (BGMusic == null)
            return;
        BGMusic.Stop();
    }
    public void PlaySound(string SoundName,bool IsLoop,UnityAction<AudioSource> CallBack=null)
    {
        if(SoundObj == null)
        {
            SoundObj = new GameObject();
            SoundObj.name = "Sounds";
            
        }
        
        ResourcesMgr.GetInstance().LoadAsync<AudioClip>("Music/Sounds/" + SoundName, (Clip) =>
        {
            ASource = SoundObj.AddComponent<AudioSource>();
            ASource.clip = Clip;
            ASource.volume = SoundVolume;
            ASource.loop = IsLoop;
            ASource.Play();
            SoundList.Add(ASource);
            if (CallBack != null)
                CallBack(ASource);
        });
    }
    public void SetSoundVolume(float SVolume)
    {
        SoundVolume = SVolume;
        //将音效列表中所有音效的音量都改变
        for(int i=0; i<SoundList.Count; i++)
        {
            SoundList[i].volume = SoundVolume;
        }
    }
    public void StopSound(AudioSource ASource)
    {
        if(SoundList.Contains(ASource))
        {
            SoundList.Remove(ASource);
            ASource.Stop();
            GameObject.Destroy(ASource);
        }
    }
}

MusicTest.cs---------------------------------------------------------------------------

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

public class MusicTest : MonoBehaviour
{
    float Mvolume = 0;
    AudioSource AudioClip;
    private void OnGUI()
    {
        if (GUI.Button(new Rect(100, 50, 100, 50), "播放背景音乐"))
        {
            MusicMgr.GetInstance().SetBGMVolume(0);
            MusicMgr.GetInstance().PlayBGM("AfterBattle");
        }
        if (GUI.Button(new Rect(100, 150, 100, 50), "暂停背景音乐"))
            MusicMgr.GetInstance().PauseBGM();
        if (GUI.Button(new Rect(100, 250, 100, 50), "停止背景音乐"))
            MusicMgr.GetInstance().StopBGM();

        //设置音量值Mvolume随时间推移增加,每帧调用MusicMgr中的SetBGMVolume改变音量
        //实现背景音乐音量从小到大渐变效果
        Mvolume += Time.deltaTime/100;
        MusicMgr.GetInstance().SetBGMVolume(Mvolume);

        if (GUI.Button(new Rect(300, 50, 100, 50), "播放音效"))
        {
            MusicMgr.GetInstance().SetSoundVolume(0);
            MusicMgr.GetInstance().PlaySound("AfterBattle",false,(AClip)=>
            {
                AudioClip = AClip;
            });
        }
        if (GUI.Button(new Rect(300, 250, 100, 50), "停止音效"))
        {
            MusicMgr.GetInstance().StopSound(AudioClip);
            AudioClip = null;
        }

        //每帧调用MusicMgr中的SetSoundVolume改变音量实现音效音量从小到大渐变效果
        MusicMgr.GetInstance().SetSoundVolume(Mvolume);
    }
}

3.9、UI管理模块
首先通过BasePanel.cs实现对每个面板的管理,该脚本主要功能是通过代码快速找到UI面板下所有的UI控件对象,方便在子类里处理逻辑,并且提供显示/隐藏面板功能。子类UITest通过继承BasePanel类,获得UI控件对象,并执行需要的逻辑。
然后通过UIMgr.cs来管理所有显示的面板,提供给外部显示/隐藏的控制接口。这里通过CreatPanel脚本演示了调用UIMgr实现对面板的显示/隐藏控制。
脚本:BasePanel.cs

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

//UI面板基类,通过代码快速找到所有子控件,方便在子类中处理逻辑
//找到自己面板下所有的控件对象
//提供显示和隐藏的功能
public class BasePanel : MonoBehaviour
{
    //通过里式转换原则,存储面板下的所有控件。string存储控件的名字,List存储控件的类型
    //因为Button类控件下同时有Image和Button两个组件,所以采用List存储同一个名字控件下的多个组件
    private Dictionary<string, List<UIBehaviour>> UIDict = new Dictionary<string, List<UIBehaviour>>();
    void Awake()
    {
        FindUIChildren<Button>();
        FindUIChildren<Text>();
        FindUIChildren<Image>();
        FindUIChildren<Toggle>();
        FindUIChildren<Slider>();
        FindUIChildren<Scrollbar>();
        //可以把所有类型的UI控件都列出来找一遍
    }

    // 从存储容器UIDict中得到对应名字的对应UI控件
    protected T GetUIComponents<T>(string UIName)where T:UIBehaviour
    {
        if (UIDict.ContainsKey(UIName))
        { 
            for(int i=0;i<UIDict[UIName].Count;i++)
            {
                if (UIDict[UIName][i] is T)
                    return UIDict[UIName][i] as T;
            }
        }
        return null;
    }
    //找到子物体中的UI控件,并存入字典UIDict中以备调用
    private void FindUIChildren<T>() where T:UIBehaviour
    {
        //采用泛型数组,存储物体下包含子物体的所有UI组件
        T[] UIChildren = this.GetComponentsInChildren<T>();
        //对泛型数组进行遍历,将找到的所有UI组件都存入字典UIDict中,以备调用
        for (int i = 0; i < UIChildren.Length; i++)
        {
            //如果字典中已有相应的控件名,则说明同一控件下有两个组件,则把新找到的组件继续存入同名控件的List中
            if (UIDict.ContainsKey(UIChildren[i].gameObject.name))
                UIDict[UIChildren[i].gameObject.name].Add(UIChildren[i]);
            //如果字典中无相应控件名,则将找到的UI组件根据名字和类型存入字典中
            else
                UIDict.Add(UIChildren[i].gameObject.name, new List<UIBehaviour>() { UIChildren[i] });
        }
    }
    //虚函数,可在子类中重写具体逻辑
    public virtual void ShowMe()
    {

    }
    public virtual void HideMe()
    {

    }
}

UITest.cs------------------------------------------------------------------------------

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

public class UITest : BasePanel
{
    void Start()
    {
        GetUIComponents<Button>("Start").onClick.AddListener(ClickStart);
        GetUIComponents<Button>("Quit").onClick.AddListener(ClickQuit);        
    }

    public void InitInfo()
    {
        Debug.Log("初始化信息");
    }

    public void ClickStart()
    {
        //点击Start按钮以后载入Loading界面
        //UIMgr.GetInstance().ShowPanel("LoadingPanel", UI_Layer.MidLayer, DoWhenLoaded);
        Debug.Log("StartGame");
    }
    public void ClickQuit()
    {
        Debug.Log("QuitGame");
    }
}

脚本:UIMgr.cs

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

//使用枚举来存储UI层级
public enum UI_Layer
{
    BotLayer,MidLayer,TopLayer,SystemLayer,
}

//管理所有显示的面板
//提供给外部显示和隐藏的接口
public class UIMgr : BaseManager<UIMgr>
{
    //建立字典,作为存储面板的容器,string存储面板名字,BasePanel存储面板脚本
    public Dictionary<string, BasePanel> PanelDict = new Dictionary<string, BasePanel>();
    private Transform BotLayer, MidLayer, TopLayer, SystemLayer;

    public UIMgr()
    {
        //获取并创建Canvas和EventSystem,并使其在载入新场景时不被移除
        GameObject Obj = ResourcesMgr.GetInstance().Load<GameObject>("Test/Canvas");
        Transform TestCanvas = Obj.transform;
        GameObject.DontDestroyOnLoad(Obj);
        BotLayer = TestCanvas.Find("Bot");
        MidLayer = TestCanvas.Find("Mid");
        TopLayer = TestCanvas.Find("Top");
        SystemLayer = TestCanvas.Find("System");

        Obj = ResourcesMgr.GetInstance().Load<GameObject>("Test/EventSystem");
        GameObject.DontDestroyOnLoad(Obj);
    }
    public void ShowPanel<T>(string PanelName, UI_Layer Layer, UnityAction<T> CallBack= null ) where T : BasePanel
    {
        //如果显示面板时,该面板已经被显示,则直接执行载入后的逻辑。
        if (PanelDict.ContainsKey(PanelName))
        {
            //可在子类中重写ShowMe虚函数方法,执行里面的逻辑
            PanelDict[PanelName].ShowMe();
            if (CallBack != null)
                CallBack(PanelDict[PanelName] as T);
return;
        }
        ResourcesMgr.GetInstance().LoadAsync<GameObject> ("Test/"+PanelName,(UIObj)=>
        {
            //把加载的Panel作为Canvas的子对象,并且设置相对位置
            Transform UIFather = BotLayer;
            switch (Layer)
            {
                case UI_Layer.MidLayer:
                    UIFather = MidLayer;
                    break;
                case UI_Layer.TopLayer:
                    UIFather = TopLayer;
                    break;
                case UI_Layer.SystemLayer:
                    UIFather = SystemLayer;
                    break;
            }
            //把将要显示的Panel挂到传入的UILayer,成为其的子物体
            UIObj.transform.SetParent(UIFather);

            //设置相对位置和大小
            UIObj.transform.localPosition = Vector3.zero;
            UIObj.transform.localScale = Vector3.one;
            (UIObj.transform as RectTransform).offsetMax = Vector2.zero;
            (UIObj.transform as RectTransform).offsetMin = Vector2.zero;

            //得到预设体上的BasePanel脚本,并且执行。因为是异步加载,必须是panel预设体加载完成后,才可执行其上的脚本。
            T PanelScripts = UIObj.GetComponent<T>();
            //处理预设体加载完成后的逻辑
            if (CallBack != null)
                CallBack(PanelScripts);
            //可在子类中重写ShowMe虚函数方法,执行里面的逻辑
            PanelScripts.ShowMe();
            
            //把新加载面板的信息存入PanelDict容器以备调用
            PanelDict.Add(PanelName, PanelScripts);
        });
    }
    //隐藏面板
    public void HidePanel(string PanelName)
    {
        if (PanelDict.ContainsKey(PanelName))
        {
            //可在子类中重写HideMe虚函数方法,执行里面的逻辑
            PanelDict[PanelName].HideMe();
            //如果PanelDict中有指定名字的面板,则删除该面板游戏对象,并从PanelDict中删除相应面板的存储信息
            GameObject.Destroy(PanelDict[PanelName].gameObject);
            PanelDict.Remove(PanelName);
        }
    }
}

CreatPanel.cs--------------------------------------------------------------------------

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

public class CreatPanel : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        UIMgr.GetInstance().ShowPanel<UITest>("TestPanel", UI_Layer.MidLayer,DoWhenLoaded);
    }
    private void DoWhenLoaded(UITest DoSomething)
    {
        DoSomething.InitInfo();
        Invoke("DelayHidePanel", 5);
    }
    private void DelayHidePanel()
    {
        UIMgr.GetInstance().HidePanel("TestPanel");
    }
}

你可能感兴趣的:(学习笔记)