Unity簡單程序框架


Unity設計模式

單例基類

像是一些xxManager,xxController等等的管理器,控制器,一般都會選擇成為單例。為了減少代碼量,可設計出一個基類以供繼承使用。單例基類分為無繼承與繼承MonoBehaviour多種

無繼承

//確保T具有一個無參構造函數以供new使用
public class BaseSingleton<T> where T : new()
{
    private static T _instance;
    public static T Instance
    {
        get
        {
            if (_instance == null)
                _instance = new T();
            return _instance;
        }
    }
}

繼承MonoBehaviour

實際使用中,可以不手動將腳本掛載在物體上,讓單例被首次調用時,由代碼自行創建

public class BaseSingletonWithMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance = null;
    public static T Instance
    {
        get
        {
            //可能已經掛載到場景中,先查找
            if (_instance == null)
                _instance = FindObjectOfType<T>();
            //查找不到,則自行創建一個
            if (_instance == null)
            {
                GameObject obj = new GameObject(typeof(T).ToString());
                _instance = obj.AddComponent<T>();
            }
            return _instance;
        }
    }
}

繼承ScritableObject(待更新)

其實UnityEditor中時是有該類的,叫做ScriptableSingleton<T>,使用方法如下

//寫一個GameSetting的測試類
using UnityEditor;
[CreateAssetMenu(menuName = "ScriptableSingleton/GameSetting")]
public class GameSetting : ScriptableSingleton<GameSetting>
{
    public string DefaultPlayerName;
}

這個時候若我們在項目中中創建多個GameSetting,則編譯器將會報錯

但是問題沒有這么簡單,如果我們不手動在場景中創建,而是通過代碼間接操作

using UnityEditor;
public class TestLoadSO
{
    [MenuItem("Tools/Read")]
    public static void Read()
    {
        Debug.Log(GameSetting.instance.DefaultPlayerName);
    }
    [MenuItem("Tools/Write")]
    public static void Write()
    {
        GameSetting.instance.DefaultPlayerName = "Player555";
    }
}

然后在上方欄目中點擊Tools/Write,再點擊Tools/Read

結果符合我們的預期,但是這個時候我們關閉,(不管你有沒有Save)重新進入后點Tools/Read,會發現打印出來的是空,因為Unity內部創建的ScriptableSingleton<T>是不會幫我們保存的

//檢測到instance為null時會執行到的一部分代碼
ScriptableObject.CreateInstance<GameSetting>().hideFlags = HideFlags.HideAndDontSave;

那么這個時候我們再自己手動創建一個GameSetting,編譯器仍然會報錯

故在使用Unity編輯器自帶的單例SO時,最好保證你的ScriptableObject已經事前被你創建出來

下面提供兩種自行寫單例基類的代碼

第一種

public class ScriptableObjectSingleton<T> : ScriptableObject where T : ScriptableObject
{
    private static T _instance = null;
    public static T Instance
    { 
        get
        {
            if (_instance == null)
            {
            	T[] _instances = Resources.FindObjectsOfTypeAll<T>();
            	if (_instances.Length == 1)
                	_instance = _instances[0];
            	else if (_instances == null || _instances.Length == 0)
                	Debug.LogError($"You need to create at least one {typeof(T)}");
                else
                    Debug.LogError($"{typeof(T)} has more than one entity!");
            }
            return _instance;
        } 
    }
}

使用這種做法的話,我們需要在場景中創建一個掛在着類似SingletonScriptableObjectReference腳本的空物體,然后再在腳本中引用單例ScriptableObject,以供查找

第二種

public class ScriptableObjectSingleton<T> : ScriptableObject where T : ScriptableObject
{
    private static T _instance = null;
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                string[] findAssets = AssetDatabase.FindAssets(typeof(T).Name);
                if (findAssets == null || findAssets.Length == 0)
                    Debug.LogError($"You need to create at least one {typeof(T)}");
                else if (findAssets.Length > 1)
                    Debug.LogError($"{typeof(T)} has more than one entity!");
                else
                    _instance = AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(findAssets[0]));
            }
            return _instance;
        }
    }
}

對象池

在FPS游戲中,子彈是一個頻繁被創建,頻繁被銷毀的物體。不斷的重復創建銷毀,會加快GC,導致內存回收時更頻繁的卡頓。對於這種類型的物體,我們可以創建一個對象池,在用戶需要的時候檢測池中是否含有該對象,若有則拿出來用,若沒有則在池中創建一個新的。在完成其功能后,我們將其重新投入池中而不是Destroy掉。

舉個例子,把一個衣櫃看作是對象池,當我們外出時則從衣櫃中取出衣服穿上,若發現衣櫃里頭已經沒衣服穿了,就需要買新衣服加進衣櫃里。忙完一天回家后,再把衣服脫下放回到衣櫃中。(不會有人一天換一件衣服用完就直接扔掉吧不會吧不會吧)

當然了衣櫃也可以有很多個抽屜,一個抽屜放外套,一個抽屜放褲子...也就是說一個對象池其實可以有一個List,存放各種不同的物體

除了子彈之外,棋牌類游戲中的牌,又或者是圖片集中的圖片等各種會重復創建同一類型的物體的情況,都可以使用到對象池。以下是一種簡單對象池的實現

public class PoolData
{
    public GameObject dataParents;
    public List<GameObject> dataList;
    public PoolData(GameObject gameObject, GameObject poolObj)
    {
        dataParents = new GameObject($"{gameObject.name}s");
        dataList = new List<GameObject>();
        dataParents.transform.parent = poolObj.transform;
        PushGameObject(gameObject);
    }
    public GameObject PopGameObject()
    {
        //取出后重置父節點
        GameObject popOne = dataList[0];
        popOne.transform.parent = null;
        dataList.RemoveAt(0);
        return popOne;
    }
    public void PushGameObject(GameObject gameObject)
    {
        //放入后歸位
        dataList.Add(gameObject);
        gameObject.transform.parent = dataParents.transform;
    }
}

[CreateAssetMenu(fileName = "Frame/PoolManger")]
public class PoolManager : ScriptableObjectSingleton<PoolManager>
{
    [SerializeField] private List<GameObject> presInpool;       //池子中的預制體
    [SerializeField] private string poolName = "MyPool";        //總節點Name
    private GameObject poolObj;                                 //總節點GameObject
    private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>();      //池中對象
    public GameObject PopGameObject(string objectName, Transform trans)
    {
        GameObject popOne = null;
        //可在類內初始化物體坐標,或返回GameObject后再修改
        if (poolDic.ContainsKey(objectName) && poolDic[objectName].dataList.Count > 0)
            popOne = poolDic[objectName].PopGameObject();
        else
            popOne = (GameObject)Instantiate(FindInList(objectName), trans.position, trans.rotation);
        if (popOne)
        {
            //重設物體的名字,活躍狀態
            popOne.name = objectName;
            popOne.SetActive(true);
        }
        return popOne;
    }
    public void PushGameObject(GameObject gameObject)
    {
        //檢測總節點是否為空
        if (poolObj == null)
        	poolObj = new GameObject(poolName);
        gameObject.SetActive(false);
        //存在則直接放入,若不存在則先創建再放入
        if (poolDic.ContainsKey(gameObject.name))
            poolDic[gameObject.name].PushGameObject(gameObject);
        else
            poolDic.Add(gameObject.name, new PoolData(gameObject, poolObj));
    }
    public void ClearPool()
    {
        //切換場景時主動調用,清空對象池
        poolDic.Clear();
        poolObj = null;
    }
    public GameObject FindInList(string objectName)
    {
        //尋找池中是否存在需要創建的對象
        foreach (var preInpool in presInpool)
        {
            if (preInpool.name.Equals(objectName))
                return preInpool;
        }
        return null;
    }
}

中心事件管理模塊

作為一個單例管理模塊,集各種事件於一體。接受無參與單參數的無返回值事件,通過調用Raise實現事件調用

interface IEventInfo {}
public class EventInfo<T> : IEventInfo
{
    public EventInfo(UnityAction<T> action)
    {
        actions += action;
    }
    public UnityAction<T> actions;
}
public class EventInfo : IEventInfo
{
    public EventInfo(UnityAction action)
    {
        actions += action;
    }
    public UnityAction actions;
}
public class CenterEvent : BaseSingletonWithMono<CenterEvent>
{
    //儲存所有事件
    private Dictionary<string, IEventInfo> eventDir = new Dictionary<string, IEventInfo>();
    
    public void AddListener<T>(string eventName, UnityAction<T> action)
    {
        //有參版本
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo<T>).actions += action;
        else
            eventDir.Add(eventName, new EventInfo<T>(action));
    }
    public void AddListener(string eventName, UnityAction action)
    {
        //無參版本
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo).actions += action;
        else
            eventDir.Add(eventName, new EventInfo(action));
    }
    //RemoveListener一般在OnDestroy中調用
    public void RemoveListener<T>(string eventName, UnityAction<T> action)
    {
        //有參版本
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo<T>).actions -= action;
    }
    public void RemoveListener(string eventName, UnityAction action)
    {
        //無參版本
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo).actions -= action;
    }
    
    public void Raise<T>(string eventName, T info)
    {
        //如果存在,則依次調用所有事件
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo<T>).actions?.Invoke(info);
    }
    public void Raise(string eventName)
    {
        //如果存在,則依次調用所有事件
        if (eventDir.ContainsKey(eventName))
            (eventDir[eventName] as EventInfo).actions?.Invoke();
    }
    
    public void Clear()
    {
        //跳轉場景前手動調用清除不必要的內存占用
        eventDir.Clear();
    }
}

基腳事件管理模塊(待更新)

基腳與中心的區別就是基腳將一個單例要干的事情拆分成很多個小模塊去分配,由多個ScriptableObject去替換掉中間的大單例

主要思想就是把一個一個事件做成ScriptableObject,然后在場景中創建空物件,最后把各個監聽器掛載在空物件上

//VoidGameEvent.cs
[CreateAssetMenu(menuName = "GameEvent/Void")]
public class VoidGameEvent : ScriptableObject
{
    public UnityAction unityAction;
    public void Raise()
    {
        unityAction?.Invoke();
    }
}
//VoidEventListener.cs
public class VoidEventListener : MonoBehaviour
{
    public VoidGameEvent voidGameEvent;
    public UnityEvent unityEvent;
    public void OnEnable()
    {
        if (voidGameEvent == null)
            return;
        voidGameEvent.unityAction += Respond;
    }
    public void OnDisable()
    {
        if (voidGameEvent == null)
            return;
        voidGameEvent.unityAction -= Respond;
    }
    public void Respond()
    {
        unityEvent?.Invoke();
    }
}

公共Mono模塊

使類雖然沒有繼承MonoBehaviour,但是仍可以通過往模塊中添加事件,可實現Update,協程等功能

public class MonoEvent : BaseSingletonWithMono<MonoEvent>
{
    private UnityAction UpdateEvent;
    private void Update()
    {
        if (UpdateEvent == null)
            return;
        UpdateEvent.Invoke();
    }
    public void AddUpdateEvent(UnityAction _action) { UpdateEvent += _action; }
    public void RemoveUpdateEvent(UnityAction _action) { UpdateEvent -= _action; }
}
//在其他類中調用實例
MonoEvent.Instance.AddUpdateEvent(PrintHello);
MonoEvent.Instance.StartCoroutine(SayHello());

資源加載模塊

先講解一下使用Resources.Load的一些規則。使用該方式加載資源,需要將資源存放在Asset目錄下的Resources文件夾(需自行新鍵)中

//限定類型調用方法 二級路徑也可以使用Cube直接讀取
Resources.Load<GameObject>("Cube");
Resources.Load("Cube", typeof(GameObject));

若文件處於二級目錄,例如位於Resources/Bullet文件夾中,則需傳入的字符串為:"Bullet/Cube"。需要注意的是,Resources文件夾並不是一定要處於Asset目錄的下一級,即使是Asset/Res/Resources這樣的二級路徑,也能被Resources.Load識別到

這里講一下UnityEngine.ObjectSystem.Object的區別。若使用大寫的Object且同時引用了兩個名稱空間,則會發生沖突,UnityEngine.Object是Unity中所有Component以及GameObject的父類,然后UnityEngine.Object又繼承自System.Object。小寫的object默認是System.Object,不會發生沖突

public class ResourcesLoader : BaseSingleton<ResourcesLoader>
{
    /// <summary>
    /// 預設列表 儲存資源鏡像
    /// </summary>
    private Hashtable resCache;
    public ResourcesLoader()
    {
        resCache = new Hashtable();
    }
    /// <summary>
    /// 同步加載資源
    /// </summary>
    /// <param name="objectPath">資源路徑</param>
    /// <param name="isCache">是否將資源緩存</param>
    /// <typeparam name="T">資源類型</typeparam>
    /// <returns>資源實體</returns>
    public T Load<T>(string objectPath, bool isCache = false) where T : UnityEngine.Object
    {
        T res = null;
        if (resCache.ContainsKey(objectPath))
            res = resCache[objectPath] as T; 
        else
        {
            res = Resources.Load<T>(objectPath);
            if (isCache)
                resCache.Add(objectPath, res);
        }
        if (res is GameObject)
            return GameObject.Instantiate(res);
        else
            return res;
    }
    /// <summary>
    /// 異步加載資源
    /// </summary>
    /// <param name="objectPath">資源路徑</param>
    /// <param name="callBack">函數回調</param>
    /// <typeparam name="T">資源類型</typeparam>
    public void LoadAsync<T>(string objectPath, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        MonoEvent.Instance.StartCoroutine(IEload<T>(objectPath, callBack));
    }
    private IEnumerator IEload<T>(string Path, UnityAction<T> callBack) where T : UnityEngine.Object
    {
        ResourceRequest res = Resources.LoadAsync<T>(Path);
        yield return null;
        //資源動態加載完畢之后調用回調
        if (res.asset is GameObject)
            callBack(GameObject.Instantiate(res.asset) as T);
        else
            callBack(res.asset as T);
    }
}

輸入控制模塊

將游戲中的輸入檢測匯集到一個地方,降低項目的耦合性

public class KeyTypeManager
{
    public static readonly string WKeyDown = "WKeyDown";
    public static readonly string EKeyDown = "EKeyDown";
    public static readonly string RKeyDown = "RKeyDown";
}
public class InputManager : BaseSingleton<InputManager>
{
    private bool isListen = false;
    //構造析構時增加刪除監聽對象
    public InputManager() { MonoEvent.Instance.AddUpdateEvent(InputUpdate); }
    ~InputManager() { MonoEvent.Instance.RemoveUpdateEvent(InputUpdate); }
    //需要使用時開啟監聽
    public void StartListen(bool _isListen) { isListen = _isListen; }
    public void InputUpdate()
    {
        if (!isListen)
            return;
        if (Input.GetKeyDown(KeyCode.W))
            CenterEvent.Instance.Raise(KeyTypeManager.WKeyDown);
        if (Input.GetKeyDown(KeyCode.E))
            CenterEvent.Instance.Raise(KeyTypeManager.EKeyDown);
    }
}
public class TestInput : MonoBehaviour
{
    private void Start()
    {
        InputManager.Instance.StartListen(true);		//開啟監聽,同時若首次調用則創建單例
        CenterEvent.Instance.AddListener(KeyTypeManager.WKeyDown, GetWDown);
        CenterEvent.Instance.AddListener(KeyTypeManager.EKeyDown, Open);
    }
    public void Forward() { Debug.Log("向前走"); }
    public void Open() { Debug.Log("打開"); }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM