Unity程序基础框架学习

一.常见的资源管理方式

  在Unity中,Project窗口中的Assets目录下保存了项目中使用的脚本、模型、预制体、贴图等等资源,资源的种类多,需要管理起来。一般地,我们将不同的资源放在不同的文件夹下,按照模块分类,方便管理。常见的文件夹:Scripts脚本文件夹、Scenes场景文件夹、Resources在脚本中使用的资源文件夹、Prefabs预制件文件夹等。

二.搭建简单的程序框架

1.单例模式基类模块

游戏中常用的管理类或者其他只能有一个实例的物体(如一些UI面板)会使用单例模式,因此我们创建一个单例模式基类,只要某个类继承这个基类,就能实现单例模式。

/// <summary>
/// 单例模式基类
/// </summary>
public class BaseManager<T> where T:new()
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
                instance = new T();
            return instance;
        }
    }
}

最基本的单例模式基类实现,但是在Unity中脚本基本都是继承了MonoBehaviour的。

/// <summary>
/// 继承mono的单例模式基类
/// 保证这个脚本只挂载一次,否则会破坏单例模式
/// </summary>
public class SingleMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            return instance;
        }
    }

    /// <summary>
    /// awake函数必须声明为保护的虚函数,这样子类可以看到并且子类书写awake函数不会覆盖这个函数
    /// </summary>
    protected virtual void Awake()
    {
        instance = this as T;
    }
}

这是继承了MonoBehaviour的单例模式基类,但是需要手动将这个脚本挂载到某个物体上,才会调用awake函数为单例赋值。

/// <summary>
/// 改进版继承monobehaviour的单例模式基类
/// 这种方式创建的单例模式不需要再挂载脚本了,自动挂载这个脚本,不用手动添加脚本防止不小心添加了多个脚本导致单例被破坏
/// </summary>
public class SingleAutoMono<T> : MonoBehaviour where T:MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            //如果没有单例,自动创建一个空物体并挂载脚本,这样会调用awake函数为单例赋值
            if (instance == null) 
            {
                GameObject obj = new GameObject();
                obj.name = typeof(T).ToString();
                instance = obj.AddComponent<T>();
            }
            return instance;
        }
    }

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

改进版继承monobehaviour的单例模式基类,不用挂载这个脚本就能实现单例模式,避免手动挂载时出错。

2.缓存池模块基础

对于子弹等物体,在游戏中如果不断实例化再销毁会加速内存消耗,缩短两次GC的间隔时间,导致游戏卡顿,因此对于一些会重复使用的对象,我们可以不销毁使用完的对象而是将其保存下来,下次需要使用时先看一看有没有保存好的对象,有的话取出对象,没有对象了再继续实例化对象并在使用完成后保存下来,这样就能实现内存的循环使用,节约内存,这个保存对象的地方就是缓存池。

public class PoolData
{
    public GameObject rootObj;
    public List<GameObject> poolList;

    public PoolData(GameObject obj,GameObject poolObj)
    {
        rootObj = new GameObject(obj.name);
        rootObj.transform.parent = poolObj.transform;
        poolList = new List<GameObject>();
    }

    public void PushObj(GameObject obj)
    {
        poolList.Add(obj);
        obj.transform.parent = rootObj.transform;
        obj.SetActive(false);
    }

    public GameObject GetObject()
    {
        GameObject obj = poolList[0];
        poolList.RemoveAt(0);
        obj.SetActive(true);
        return obj;
    }
}
public class PoolMgr : BaseManager<PoolMgr>
{
    //缓存池容器
    public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();
    //作为根节点的空物体
    public GameObject poolRoot;

    public GameObject GetObject(string name)
    {
        GameObject obj = null;
        if(poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0)
        {
            obj = poolDic[name].GetObject();
        }
        else
        {
            obj = GameObject.Instantiate(Resources.Load<GameObject>(name));
        }
        //断开父子关系
        obj.transform.parent = null;
        return obj;
    }
    public void PushObj(string name,GameObject obj)
    {
        if (poolRoot == null) poolRoot = new GameObject("Pool");

        if (!poolDic.ContainsKey(name))
            poolDic.Add(name, new PoolData(obj,poolRoot));
        poolDic[name].PushObj(obj);
    }

    public void Clear()
    {
        poolDic.Clear();
        poolDic = null;
    }
}

3.事件中心模块

在游戏中,常常出现一个特定的事件发生会导致其他很多事件执行的情况,如怪物死亡后需要分数记录、怪物的继续创建等,这些被调用的方法并不是在同一个脚本中的,那么表现出来的游戏的耦合性就非常高,而且因为被调用的方法往往需要先找到游戏物体再找到其上面的脚本,这样对性能也有浪费,因此我们往往使用委托来记录需要执行的方法,在特定事件发生后执行相应的委托即可,而添加功能或者删除功能时只需要将相应的想要被调用的方法添加入委托或从委托中删去即可。这样的委托往往不止一个,这些委托的管理类就可以称为我们的事件中心。

public class EventCenter : BaseManager<EventCenter>
{
    //存储所有委托的字典
    private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>();

    /// <summary>
    /// 添加方法到事件中
    /// </summary>
    /// <param name="name">事件名称</param>
    /// <param name="action">要存储的函数</param>
    public void AddEventListener(string name,UnityAction<object> action)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] += action;
        }
        else
        {
            eventDic.Add(name, action);
        }
    }

    /// <summary>
    /// 清除事件中的某个方法
    /// </summary>
    /// <param name="name">事件名称</param>
    /// <param name="action">要清除的函数</param>
    public void RemoveEventListener(string name,UnityAction<object> action)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name] -= action;
        }
    }

    public void Clear()
    {
        eventDic.Clear();
        eventDic = null;
    }

    /// <summary>
    /// 触发事件
    /// </summary>
    /// <param name="name">事件名称</param>
    public void EventTrigger(string name,object param)
    {
        if (eventDic.ContainsKey(name))
        {
            eventDic[name](param);
        }
    }
}

4.公共Mono模块

这个模块的作用是使没有继承MonoBehaviour的函数也可以实现mono的update函数的效果,具体来说还是利用委托,一个继承了monoBehaviour的类的update函数不断执行委托,然后利用另一个类管理委托,提供注册委托和将方法从委托中移除的函数,然后在外部将想要不断调用的函数注册到委托中即可。这个类中继承MonoBehaviour的脚本在start函数中调用了DontDestroyOnLoad方法,使所挂载的游戏物体不随场景移除,那么这个委托就不会随着场景的加载而被销毁,实现了多场景共用的目的。通过封装,可以使任意函数实现MonoBehaviour的声明周期函数的调用效果,只需要在相应的函数中调用委托,再将方法注册到委托中即可,包括协程也可以使用MonoMgr类进行封装,在外部函数中开启协程。

使用公共Mono模块的意义在于节约性能的开销。在Unity执行update函数时,会遍历所有的继承了MonoBehaviour的类,将所有定义好的update函数先存储在一个list中,存储完成后再遍历list进行调用,所以过多的update会拉长遍历所有的继承MonoBehaviour的类的时间,对性能造成影响,使用公共Mono模块就可以使用一个update函数完成所有的update功能,节约性能。关于这方面的内容,可以参考:https://blogs.unity3d.com/cn/2015/12/23/1k-update-calls/

/// <summary>
/// 继承了mono的类
/// </summary>
public class MonoController : MonoBehaviour
{
    private event UnityAction updateEvent;

    private void Start()
    {
        DontDestroyOnLoad(gameObject);
    }

    private void Update()
    {
        if(updateEvent != null)
        {
            updateEvent();
        }
    }

    /// <summary>
    /// 添加帧更新的函数
    /// </summary>
    /// <param name="fun">添加的函数</param>
    public void AddUpdateListener(UnityAction fun)
    {
        updateEvent += fun;
    }
    /// <summary>
    /// 移除帧更新的函数
    /// </summary>
    /// <param name="fun">移除的函数</param>
    public void RemoveUpdateListener(UnityAction fun)
    {
        updateEvent -= fun;
    }
}
/// <summary>
/// 管理类,对添加和移除委托函数的方法进一步封装,实现单例
/// </summary>
public class MonoMgr : BaseManager<MonoMgr>
{
    private MonoController controller;

    public MonoMgr()
    {
        GameObject obj = new GameObject("MonoController");
        controller = obj.AddComponent<MonoController>();
    }

    /// <summary>
    /// 添加帧更新的函数
    /// </summary>
    /// <param name="fun">添加的函数</param>
    public void AddUpdateListener(UnityAction fun)
    {
        controller.AddUpdateListener(fun);
    }
    /// <summary>
    /// 移除帧更新的函数
    /// </summary>
    /// <param name="fun">移除的函数</param>
    public void RemoveUpdateListener(UnityAction fun)
    {
        controller.RemoveUpdateListener(fun);
    }
}

5.场景切换模块

在游戏中经常要切换场景,如果场景资源很多,在场景切换完成后再进行人物等其他资源加载会有卡顿的感觉,所以我们可以使用异步的方式进行场景切换,在切换的过程中可以执行场景加载进度条更新、人物信息加载等行为,所以可以对场景的切换方法进行进一步封装,达到异步加载场景时执行其他行为的目的。

public class SceneMgr : BaseManager<SceneMgr>
{
    /// <summary>
    /// 切换场景
    /// </summary>
    /// <param name="name">场景名</param>
    public void LoadScene(string name,UnityAction fun)
    {
        //场景同步加载
        SceneManager.LoadScene(name);
        //加载完成后执行fun
        fun();
    }

    /// <summary>
    /// 使用协程异步加载
    /// </summary>
    /// <param name="name"></param>
    /// <param name="fun"></param>
    public void LoadSceneAsyn(string name,UnityAction fun)
    {
        //协程加载场景
        MonoMgr.Instance.StartCoroutine(ReallyLoadSceneAsyn(name, fun));
    }
    private IEnumerator ReallyLoadSceneAsyn(string name,UnityAction fun)
    {
        AsyncOperation ao = SceneManager.LoadSceneAsync(name);
        while (!ao.isDone)
        {
            //事件中心,向外分发进度情况
            EventCenter.Instance.EventTrigger("Loading", ao.progress);
            //更新进度条
            yield return ao.progress;
        }

        fun();
    }
}

6.资源加载模块

 在游戏中经常需要加载资源,无论是从AB包加载还是从Resources文件夹加载等,加载的过程使用同步加载都需要消耗一定的时间,导致游戏卡顿,我们提供一个资源加载的框架,将同步和异步加载作进一步封装,常用的一些操作如游戏物体的实例化、异步加载过程中的进度条显示等也可以在加载的过程中进行。

public class ResMgr:BaseManager<ResMgr>
{
    /// <summary>
    /// 同步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="name">资源名</param>
    /// <returns></returns>
    public T Load<T>(string name) where T:Object
    {
        T res = Resources.Load<T>(name);
        //如果对象是GameObject类型,直接实例化好外界直接使用
        if (res is GameObject)
            return GameObject.Instantiate(res);
        return res;
    }

    /// <summary>
    /// 异步加载资源
    /// </summary>
    /// <typeparam name="T">资源类型</typeparam>
    /// <param name="name">资源名称</param>
    /// <param name="callback">回调函数</param>
    public void LoadAysnc<T>(string name,UnityAction<T> callback) where T : Object
    {
        MonoMgr.Instance.StartCoroutine(ReallyLoadAsync(name,callback));
    }
    private IEnumerator ReallyLoadAsync<T>(string name, UnityAction<T> callback) where T:Object
    {
        ResourceRequest rr = Resources.LoadAsync<T>(name);
        yield return rr;

        if (rr.asset is GameObject)
            callback(GameObject.Instantiate(rr.asset) as T);
        else
            callback(rr.asset as T);
    }
}

7.输入管理模块

在游戏中,有很多地方需要根据输入进行响应,如玩家需要根据输入进行移动等,我们可以将想要检测的按键通过事件中心进行分发,如果响应的键按下,就分发事件,需要检测按键的地方只需要将按下键后的执行的事情注册到事件中心即可,这样就实现了解耦。同时可以使用公共mono模块优化,将检测按键输入的事件封装为函数注册到公共mono中。

public class InputMgr : BaseManager<InputMgr>
{
    //是否开启所有按键监听的开关
    private bool isOpenInput = false;

    public InputMgr()
    {
        MonoMgr.Instance.AddUpdateListener(Update);
    }

    /// <summary>
    /// 这个update函数是供注册到公共Mono使用的,在公共Mono中调用,这个类并没有继承MonoBehaviour,Unity不会自动调用它
    /// </summary>
    private void Update()
    {
        if (!isOpenInput) return;
        //按键按下,通过事件中心分发事件,如果要监听某事件,将事件注册到事件中心即可
        /*CheckKeyCode(KeyCode.W);
        CheckKeyCode(KeyCode.A);
        CheckKeyCode(KeyCode.S);
        CheckKeyCode(KeyCode.D);*/
        foreach(KeyCode key in Enum.GetValues(typeof(KeyCode)))
        {
            CheckKeyCode(key);
        }
    }
    /// <summary>
    /// 分发事件的函数封装
    /// </summary>
    /// <param name="key">分发事件的具体按键</param>
    private void CheckKeyCode(KeyCode key)
    {
        if (Input.GetKeyDown(key))
        {
            EventCenter.Instance.EventTrigger("Key " + key.ToString() + " Down", key);
        }
        if (Input.GetKeyUp(key))
        {
            EventCenter.Instance.EventTrigger("Key " + key.ToString() + " Up", key);
        }
    }
}

8.音频管理模块

同样的,我们可以在游戏中统一管理杂乱的音效。

public class MusicMgr : BaseManager<MusicMgr>
{
    private AudioSource bkMusic = null;
    private float bkValue = 1;
    private GameObject soundObj;
    private List<AudioSource> soundList = new List<AudioSource>();
    private float soundValue = 1;

    public MusicMgr()
    {
        MonoMgr.Instance.AddUpdateListener(Update);
    }
    private void Update()
    {
        for (int i = soundList.Count - 1; i > -1; i--)
        {
            if (!soundList[i].isPlaying)
            {
                GameObject.Destroy(soundList[i]);
                soundList.RemoveAt(i);
            }
        }
    }
    /// <summary>
    /// 改变音量大小
    /// </summary>
    /// <param name="v"></param>
    public void ChangeBkValue(float v)
    {
        bkValue = v;
        if (bkMusic == null)
            return;
        bkMusic.volume = v;
    }

    /// <summary>
    /// 播放背景音乐
    /// </summary>
    /// <param name="name"></param>
    public void PlayBkMusic(string name)
    {
        if(bkMusic == null)
        {
            GameObject obj = new GameObject("BkMusic");
            bkMusic = obj.AddComponent<AudioSource>();
        }
        ResMgr.Instance.LoadAysnc<AudioClip>("Music/BK/" + name, (clip) =>
         {
             bkMusic.clip = clip;
             bkMusic.volume = bkValue;
             bkMusic.Play();
         });
    }
    /// <summary>
    /// 暂停背景音乐
    /// </summary>
    public void PauseBkMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Pause();
    }
    /// <summary>
    /// 停止背景音乐
    /// </summary>
    public void StopBkMusic()
    {
        if (bkMusic == null)
            return;
        bkMusic.Stop();
    }
    /// <summary>
    /// 播放音效
    /// </summary>
    public void PlaySound(string name, bool isLoop, UnityAction<AudioSource> callBack = null)
    {
        if(soundObj == null)
        {
            soundObj = new GameObject("Sound");
        }

        ResMgr.Instance.LoadAysnc<AudioClip>("Sound/" + name, (clip) =>
         {
             AudioSource source = soundObj.AddComponent<AudioSource>();
             source.clip = clip;
             source.volume = soundValue;
             source.Play();
             soundList.Add(source);
             if (callBack != null)
                 callBack(source);
         });
    }
    /// <summary>
    /// 停止音效
    /// </summary>
    /// <param name="source"></param>
    public void StopSound(AudioSource source)
    {
        if (soundList.Contains(source))
        {
            soundList.Remove(source);
            source.Stop();
            GameObject.Destroy(source);
        }
    }
    /// <summary>
    /// 改变所有音效的音量
    /// </summary>
    /// <param name="value"></param>
    public void ChangeSoundValue(float value)
    {
        soundValue = value;
        for (int i = 0; i < soundList.Count; i++)
        {
            soundList[i].volume = value;
        }
    }
}

9.UI管理模块

游戏中的UI常常做成一个个面板预制体,但是面板之间存在很多的点击事件或者相互调用,所以为了避免千头万绪的调用,也就是实现解耦合,方便修改维护,我们需要使用一个管理模块统一管理所有的面板。

public class BasePanel : MonoBehaviour
{
    private Dictionary<string, List<UIBehaviour>> controlDic = new Dictionary<string, List<UIBehaviour>>();

    protected virtual void OnClick(string name)
    {

    }

    private void Awake()
    {
        FindChildrenControl<Button>();
        FindChildrenControl<Image>();
        FindChildrenControl<Text>();
        FindChildrenControl<Toggle>();
        FindChildrenControl<Slider>();
        FindChildrenControl<ScrollRect>();
    }
    /// <summary>
    /// 得到面板中的某个组件
    /// </summary>
    /// <typeparam name="T">组件类型</typeparam>
    /// <param name="controlName">组件名称</param>
    /// <returns></returns>
    protected T GetControl<T>(string controlName) where T : UIBehaviour
    {
        if (controlDic.ContainsKey(controlName))
        {
            for (int i = 0; i < controlDic[controlName].Count; i++)
            {
                if (controlDic[controlName][i] is T)
                    return controlDic[controlName][i] as T;
            }
        }
        return null;
    }
    /// <summary>
    /// 找到子对象的对应控件
    /// </summary>
    /// <typeparam name="T">控件类型</typeparam>
    private void FindChildrenControl<T>() where T:UIBehaviour
    {
        T[] controls = this.GetComponentsInChildren<T>();
        string objName;
        for (int i = 0; i < controls.Length; i++)
        {
            objName = controls[i].gameObject.name;
            if (controlDic.ContainsKey(objName))
                controlDic[objName].Add(controls[i]);
            else
                controlDic.Add(objName, new List<UIBehaviour>() { controls[i] });

            if(controls[i] is Button)
            {
                (controls[i] as Button).onClick.AddListener(() =>
                {
                    OnClick(objName);
                });
            }
        }
    }
}
public enum E_UI_Layer
{
    bot,
    mid,
    top,
    system,
}

public class UIManager : BaseManager<UIManager>
{
    public Dictionary<string, BasePanel> panelDic = new Dictionary<string, BasePanel>();

    private Transform canvas;
    private Transform bot;
    private Transform mid;
    private Transform top;
    private Transform system;

    public UIManager()
    {
        GameObject obj = ResMgr.Instance.Load<GameObject>("UI/Canvas");
        canvas = obj.transform;
        GameObject.DontDestroyOnLoad(obj);

        bot = canvas.Find("Bot");
        mid = canvas.Find("mid");
        top = canvas.Find("Top");
        system = canvas.Find("System");

        obj = ResMgr.Instance.Load<GameObject>("UI/EventSystem");
        GameObject.DontDestroyOnLoad(obj);
    }

    /// <summary>
    /// 显示面板
    /// </summary>
    /// <typeparam name="T">面板脚本类型</typeparam>
    /// <param name="panelName">面板名</param>
    /// <param name="layer">面板层级</param>
    /// <param name="callBack">回调函数,面板创建完成后完成的行为</param>
    public void ShowPanel<T>(string panelName,E_UI_Layer layer,UnityAction<T> callBack=null) where T : BasePanel
    {
        //有面板直接加载面板,没有异步加载面板
        if (panelDic.ContainsKey(panelName))
        {
            BasePanel panel = panelDic[panelName];
            if(panel is T)
            {
                panel.gameObject.SetActive(true);
                if (callBack != null)
                    callBack(panel as T);
            }
        }
        else
            ResMgr.Instance.LoadAysnc<BasePanel>("UI/" + panelName, (obj) =>
             {
                 Transform father = bot;
                 switch (layer)
                 {
                     case E_UI_Layer.system:
                         father = system;
                         break;
                     case E_UI_Layer.mid:
                         father = mid;
                         break;
                     case E_UI_Layer.top:
                         father = top;
                         break;
                 }
                 obj.transform.SetParent(father);
                 obj.transform.localPosition = Vector3.zero;
                 obj.transform.localScale = Vector3.one;

                 (obj.transform as RectTransform).offsetMax = Vector2.zero;
                 (obj.transform as RectTransform).offsetMin = Vector2.zero;

                 T panel = obj.GetComponent<T>();
                 if (callBack != null)
                     callBack(panel);

                 panelDic.Add(panelName, panel);
             });
    }
    /// <summary>
    /// 隐藏面板
    /// </summary>
    /// <param name="panelName">面板名称</param>
    public void HidePanel(string panelName)
    {
        if (panelDic.ContainsKey(panelName))
        {
            panelDic[panelName].gameObject.SetActive(false);
        }
    }

    public T GetPanel<T>(string name) where T : BasePanel
    {
        if (panelDic.ContainsKey(name))
        {
            BasePanel panel = panelDic[name];
            if (panel is T)
                return panelDic[name] as T;
        }
        return null;
    }
}

 

上一篇:unity GameObject.IsStatic的地雷


下一篇:c# – 公共静态字典中的GameObjects在Unity中的场景变化中被销毁