單例基類
// file name : BaseManager.cs
using UnityEngine;
using UnityEngine.Assertions;
public class BaseManager<T> where T : new()
{
private static T _instance;
public static T Ins
{
get
{
if (_instance == null)
{
_instance = new T();
}
return _instance;
}
}
}
public class BaseManagerMonobehviour<T> : MonoBehaviour where T:MonoBehaviour
{
private static T _instance;
public static T Ins
{
get
{
if (_instance == null)
{
Assert.IsTrue(FindObjectsOfType<T>().Length <= 1);
_instance = FindObjectOfType<T>();
}
if (_instance == null)
{
Debug.LogWarning("Base Manager have create a gameobject named " + nameof(T));
GameObject hc = Instantiate(new GameObject(nameof(T)));
_instance = hc.AddComponent<T>() as T;
}
return _instance;
}
}
}
有兩種單例的寫法
- 不需要繼承 MonoBehaviour 的
- 需要繼承 MonoBehaviour 的
對于不需要繼承 MonoBehaviour 的,也就是不參與 Unity 的腳本執(zhí)行過程仿畸,也就是只需要被外部調(diào)用示损,不需要自己執(zhí)行,比如說下面的中心事件管理類就是這樣犯眠。
這種方式可以搭配公共 Mono 來實現(xiàn)跟下面的一樣的效果按灶,也就是參與到 Unity 腳本執(zhí)行周期的單例類,但個人覺得更加麻煩筐咧,因為還需要在某個其他的地方把這個沒有繼承自 MonoBehaviour 的一個幀更新方法注冊進(jìn)公共 Mono 的 Update 里面鸯旁,所以暫時不討論。
對于需要繼承 MonoBehaviour 的量蕊,也就是需要參與到腳本執(zhí)行過程或者還需要在編輯器中給某個變量賦值的铺罢。因為是單例,所以場景中只需要有一個残炮,先查找場景中的該腳本的個數(shù)韭赘,如果大于 1 ,那么直接 GG势就,如果等于 1 泉瞻,那么就是它了,如果等于 0苞冯,那么會新建一個空物體瓦灶,然后把該腳本掛上去,但這樣會有一個問題抱完,需要在場景中賦值的腳本運行后就會報空指針,更嚴(yán)重的問題是不能在 OnDestroy
中引用這個單例刃泡,不然會在結(jié)束運行的時候報錯(雖然并不影響什么巧娱,但看著很不舒服)
Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
The following scene GameObjects were found:
InputMrg
就是說你在 Destory 中創(chuàng)建了新物體。你可能不會直接在 Destory 中 new烘贴,但是只要在其中引用了這個單例實際上就會創(chuàng)建禁添,因為單例被銷毀的之后被判斷為 null,然后就進(jìn)入第二個判斷桨踪,創(chuàng)建了新的 GameObject老翘。
關(guān)于經(jīng)常運行之后才發(fā)現(xiàn) Inspector 面板上有 null 的情況,建議所有需要在場景中賦值的物體都在 OnValidate
(其實我覺得在 OnDrawGizmos
中更好)中設(shè)置斷言,如下所示:
public class BreathMaskCheckChapter : MonoBehaviour, IChapter
{
public GameObject Mask;
public Transform MaskPreparePosition;
public GameObject CanvasStartOrReStudy;
private void OnValidate()
{
Assert.IsNotNull(Mask);
Assert.IsNotNull(MaskPreparePosition);
Assert.IsNotNull(CanvasStartOrReStudy);
}
這樣不需要運行就可以在編輯器中檢測這三個值是否為空铺峭。
事件中心
將所有的事件都用字符串索引的方式注冊進(jìn)事件中心墓怀,然后再用事件中心統(tǒng)一觸發(fā),這樣可以在一定程度上解耦合卫键,輸入事件管理的解耦合主要也是依賴于此傀履。
//file name : EventCenter.cs
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
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對應(yīng)著事件的名字莉炉,
//value對應(yīng)的是監(jiān)聽這個事件對應(yīng)的委托方法們(重點圈椎稣恕:們)
private Dictionary<string, IEventInfo> eventDic
= new Dictionary<string, IEventInfo>();
//添加事件監(jiān)聽
//第一個參數(shù):事件的名字
//第二個參數(shù):處理事件的方法(有參數(shù)(類型為T)的委托)
public void AddEventListener<T>(string name, UnityAction<T> action)
{
//有沒有對應(yīng)的事件監(jiān)聽
//有的情況
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)
{
//有沒有對應(yīng)的事件監(jiān)聽
//有的情況
if (eventDic.ContainsKey(name))
{
(eventDic[name] as EventInfo).actions += action;
}
//沒有的情況
else
{
eventDic.Add(name, new EventInfo(action));
}
}
//通過事件名字進(jìn)行事件觸發(fā)
public void EventTrigger<T>(string name, T info)
{
//有沒有對應(yīng)的事件監(jiān)聽
//有的情況(有人關(guān)心這個事件)
if (eventDic.ContainsKey(name))
{
//調(diào)用委托(依次執(zhí)行委托中的方法)
//?是一個C#的簡化操作
(eventDic[name] as EventInfo<T>).actions?.Invoke(info);
}
}
public void EventTrigger(string name)
{
//有沒有對應(yīng)的事件監(jiān)聽
//有的情況(有人關(guān)心這個事件)
if (eventDic.ContainsKey(name))
{
//調(diào)用委托(依次執(zhí)行委托中的方法)
//絮宁?是一個C#的簡化操作
(eventDic[name] as EventInfo).actions?.Invoke();
}
}
//移除對應(yīng)的事件監(jiān)聽
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;
}
}
//清空所有事件監(jiān)聽(主要用在切換場景時)
public void Clear()
{
eventDic.Clear();
}
}
這樣乍一看沒什么問題梆暮,在外面的調(diào)用方式如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Events;
public class NewBehaviourScript : MonoBehaviour
{
private void Start()
{
//EventCenter.Ins.AddEventListener<int>("Test1", Test1);
EventCenter.Ins.AddEventListener("Test1", Test2);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
EventCenter.Ins.EventTrigger<int>("Test1",1);
}
}
private void Test1(int i)
{
Debug.Log("Test1" + " " + i);
}
private void Test2()
{
Debug.Log("Test2");
}
}
對于有參數(shù)的調(diào)用方式如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Events;
public class NewBehaviourScript : MonoBehaviour
{
private void Start()
{
EventCenter.Ins.AddEventListener<int>("Test1", Test1);
//EventCenter.Ins.AddEventListener("Test1", Test2);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
EventCenter.Ins.EventTrigger<int>("Test1",1);
}
}
private void Test1(int i)
{
Debug.Log("Test1" + " " + i);
}
private void Test2()
{
Debug.Log("Test2");
}
}
這兩段代碼幾乎是一樣的,為什么我不在一段里面直接說完呢绍昂?是因為如果順著執(zhí)行這兩句的話啦粹,是錯的。一開始 eventDic["Test1"]
的 value 是有參的治专,然后第二次執(zhí)行的時候卖陵,下面的 as 轉(zhuǎn)化的結(jié)果其實是 null,所以就會報空指針異常张峰。
稍微改一下泪蔫,將無參、各個不同的參數(shù)分開來存放
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
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>
{
private Dictionary<string, List<IEventInfo>> eventDic
= new Dictionary<string, List<IEventInfo>>();
public void AddEventListener<T>(string name, UnityAction<T> action)
{
if (eventDic.ContainsKey(name))
{
foreach(var d in eventDic[name])
{
if(d is EventInfo<T>)
{
(d as EventInfo<T>).actions += action;
return;
}
}
}
else
{
eventDic.Add(name, new List<IEventInfo>());
}
eventDic[name].Add(new EventInfo<T>(action));
}
public void AddEventListener(string name, UnityAction action)
{
if (eventDic.ContainsKey(name))
{
foreach (var d in eventDic[name])
{
if (d is EventInfo)
{
(d as EventInfo).actions += action;
return;
}
}
}
else
{
eventDic.Add(name, new List<IEventInfo>());
}
eventDic[name].Add(new EventInfo(action));
}
public void EventTrigger<T>(string name, T info)
{
if (eventDic.ContainsKey(name))
{
foreach (var d in eventDic[name])
{
if (d is EventInfo<T>)
{
(d as EventInfo<T>).actions?.Invoke(info);
}
if(d is EventInfo)
{
(d as EventInfo).actions?.Invoke();
}
}
}
}
public void EventTrigger(string name)
{
if (eventDic.ContainsKey(name))
{
foreach (var d in eventDic[name])
{
if (d is EventInfo)
{
(d as EventInfo).actions?.Invoke();
return;
}
}
}
}
public void RemoveEventListener<T>(string name, UnityAction<T> action)
{
if (eventDic.ContainsKey(name))
{
foreach (var d in eventDic[name])
{
if (d is EventInfo<T>)
{
(d as EventInfo<T>).actions -= action;
return;
}
}
}
}
public void RemoveEventListener(string name, UnityAction action)
{
if (eventDic.ContainsKey(name))
{
foreach (var d in eventDic[name])
{
if (d is EventInfo)
{
(d as EventInfo).actions -= action;
return;
}
}
}
}
//清空所有事件監(jiān)聽(主要用在切換場景時)
public void Clear()
{
eventDic.Clear();
}
}
測試代碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Events;
public class NewBehaviourScript : MonoBehaviour
{
private void Start()
{
EventCenter.Ins.AddEventListener<int>("Test1", Test1);
EventCenter.Ins.AddEventListener("Test1", Test2);
EventCenter.Ins.AddEventListener<GameObject>("Test1", Test3);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
EventCenter.Ins.EventTrigger<int>("Test1",1);
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
EventCenter.Ins.EventTrigger("Test1");
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
EventCenter.Ins.EventTrigger<GameObject>("Test1",new GameObject("miao"));
}
}
private void Test1(int i)
{
Debug.Log("Test1" + " " + i);
}
private void Test2()
{
Debug.Log("Test2");
}
private void Test3(GameObject o)
{
Debug.Log("Test3" + " " + o.ToString());
}
}
當(dāng)按 1 的時候喘批,會調(diào)用 Test1
和 Test2
撩荣;當(dāng)按 2 的時候,會調(diào)用 Test2
饶深;當(dāng)按 3 的時候餐曹,會調(diào)用 Test2
和 Test3
。
也就是有參的觸發(fā)時會順帶觸發(fā)無參的敌厘。
最后說一下最好不要在代碼中直接出現(xiàn) string台猴,可以使用一個靜態(tài)類來存放所有的 string。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class ConstName
{
// sounds
public readonly static string sound_descript_guoShai= "GuoShai";
public readonly static string sound_descript_liangShai = "LiangShai";
public readonly static string sound_descript_maDuo = "MaDuo";
// EventCenter Names
public readonly static string breathMask_descript_reture = "BreathMask_Reture";
public readonly static string breathMask_check_startOperate = "BreathMask_Start_Operate";
// pico SDK Tree
public readonly static string pico_sdk_controllerManager = "ControllerManager";
public readonly static string pico_sdk_pvrController = "PvrController";
public readonly static string pico_sdk_handPosition = "handPosition";
// Resources
public readonly static string resources_hand_right= "Prefabs/Hands/hand_right";
public readonly static string resources_hand_left= "Prefabs/Hands/hand_left";
// Hand Animation Name
public readonly static string hand_state_idle = "Idle";
public readonly static string hand_state_grab = "GrabLarge";
}
輸入事件管理
可以通過中心事件來統(tǒng)一管理輸入事件俱两。因為是做 Pico 開發(fā)饱狂,所有需要檢測兩套輸入,因為需要集合不重復(fù)宪彩,所以可以直接使用 HashSet休讳。如果后面需要再監(jiān)聽別的輸入事件也只需要修改 InputMrg ,對于外部已經(jīng)調(diào)用過的代碼完全不需要修改尿孔。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InputMrg : BaseManagerMonobehviour<InputMrg>
{
public enum KeyType
{
Keyborad,
Pico
};
public class InputDate
{
public KeyType type { get; private set; }
public KeyCode keyCodeType;
public Pvr_UnitySDKAPI.Pvr_KeyCode picoKeyCodeType;
public int picoKeyHand;
public InputDate(KeyCode k)
{
type = KeyType.Keyborad;
keyCodeType = k;
}
public InputDate(Pvr_UnitySDKAPI.Pvr_KeyCode k,int hand)
{
type = KeyType.Pico;
picoKeyCodeType = k;
picoKeyHand = hand;
}
}
private readonly HashSet<KeyCode> ListKeyCode = new HashSet<KeyCode>();
private readonly HashSet<Pvr_UnitySDKAPI.Pvr_KeyCode> ListPicoKeyCode = new HashSet<Pvr_UnitySDKAPI.Pvr_KeyCode>();
private void Update()
{
foreach (var k in ListKeyCode)
{
if (Input.GetKeyDown(k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyDownEventName(k),new InputDate(k));
}
if (Input.GetKeyUp(k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyUpEventName(k),new InputDate(k));
}
}
foreach (var k in ListPicoKeyCode)
{
if (Pvr_UnitySDKAPI.Controller.UPvr_GetKeyDown(0, k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyDownEventName(k, 0),new InputDate(k,0));
}
if (Pvr_UnitySDKAPI.Controller.UPvr_GetKeyDown(1, k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyDownEventName(k, 1),new InputDate(k,1));
}
if (Pvr_UnitySDKAPI.Controller.UPvr_GetKeyUp(0, k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyUpEventName(k, 0),new InputDate(k,0));
}
if (Pvr_UnitySDKAPI.Controller.UPvr_GetKeyUp(1, k))
{
EventCenter.Ins.EventTrigger<InputDate>(GetKeyUpEventName(k, 1),new InputDate(k,1));
}
}
}
public string GetKeyDownEventName(KeyCode key)
{
ListKeyCode.Add(key);
return "key_down_" + key.ToString();
}
public string GetKeyDownEventName(Pvr_UnitySDKAPI.Pvr_KeyCode key, int handId)
{
ListPicoKeyCode.Add(key);
return "pico_key_down_" + key.ToString() + '_' + handId;
}
public string GetKeyUpEventName(KeyCode key)
{
ListKeyCode.Add(key);
return "key_up_" + key.ToString();
}
public string GetKeyUpEventName(Pvr_UnitySDKAPI.Pvr_KeyCode key, int handId)
{
ListPicoKeyCode.Add(key);
return "pico_key_up_" + key.ToString() + '_' + handId;
}
}
外部調(diào)用方式俊柔,你可以像下面這樣用有參數(shù)的回調(diào)函數(shù):
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyDownEventName(KeyCode.B), EventActionSideButtonDown);
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyUpEventName(KeyCode.B), EventActionSideButtonUp);
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyDownEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.Right, 1), EventActionSideButtonDown);
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyUpEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.Right, 1), EventActionSideButtonUp);
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyDownEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.Left, 0), EventActionSideButtonDown);
EventCenter.Ins.AddEventListener<InputMrg.InputDate>(InputMrg.Ins.GetKeyUpEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.Left, 0), EventActionSideButtonUp);```
然后在回調(diào)函數(shù)里面去判斷是否進(jìn)行觸發(fā):
private void EventActionSideButtonDown(InputMrg.InputDate date)
{
if (m_grabbingObject.IsGrabbing)
{
if (date.type == InputMrg.KeyType.Keyborad)
{
EventActionSideButtonDown1();
}
else if (date.type == InputMrg.KeyType.Pico)
{
if (m_grabbingObject.IsGrabbingHand == date.picoKeyHand) // 是正在抓著的手按得側(cè)邊鍵
{
EventActionSideButtonDown1();
}
}
}
}
你也可以直接使用無參數(shù)的回調(diào)函數(shù):
EventCenter.Ins.AddEventListener(InputMrg.Ins.GetKeyDownEventName(KeyCode.Space), KeyActionTriggerDown);
EventCenter.Ins.AddEventListener(InputMrg.Ins.GetKeyDownEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.TRIGGER, handId), KeyActionTriggerDown);