单例基类
// 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 的脚本执行过程,也就是只需要被外部调用,不需要自己执行,比如说下面的中心事件管理类就是这样。
这种方式可以搭配公共 Mono 来实现跟下面的一样的效果,也就是参与到 Unity 脚本执行周期的单例类,但个人觉得更加麻烦,因为还需要在某个其他的地方把这个没有继承自 MonoBehaviour 的一个帧更新方法注册进公共 Mono 的 Update 里面,所以暂时不讨论。
对于需要继承 MonoBehaviour 的,也就是需要参与到脚本执行过程或者还需要在编辑器中给某个变量赋值的。因为是单例,所以场景中只需要有一个,先查找场景中的该脚本的个数,如果大于 1 ,那么直接 GG,如果等于 1 ,那么就是它了,如果等于 0,那么会新建一个空物体,然后把该脚本挂上去,但这样会有一个问题,需要在场景中赋值的脚本运行后就会报空指针,所以强烈建议所有需要在场景中赋值的物体都在 OnValidate 中设置断言,如下所示:
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);
}
这样不需要运行就可以在编辑器中检测这三个值是否为空。
事件中心
将所有的事件都用字符串索引的方式注册进事件中心,然后再用事件中心统一触发,这样可以在一定程度上解耦合,输入事件管理的解耦合主要也是依赖于此。
//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对应着事件的名字,
//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 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))
{
//调用委托(依次执行委托中的方法)
//?是一个C#的简化操作
(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();
}
}
这样乍一看没什么问题,在外面的调用方式如下:
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");
}
}
对于有参数的调用方式如下:
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");
}
}
这两段代码几乎是一样的,为什么我不在一段里面直接说完呢?是因为如果顺着执行这两句的话,是错的。一开始 eventDic["Test1"]
的 value 是有参的,然后第二次执行的时候,下面的 as 转化的结果其实是 null,所以就会报空指针异常。
稍微改一下,将无参、各个不同的参数分开来存放
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;
}
}
}
}
//清空所有事件监听(主要用在切换场景时)
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());
}
}
当按 1 的时候,会调用 Test1
和 Test2
;当按 2 的时候,会调用 Test2
;当按 3 的时候,会调用 Test2
和 Test3
。
也就是有参的触发时会顺带触发无参的。
最后说一下最好不要在代码中直接出现 string,可以使用一个静态类来存放所有的 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";
}
输入事件管理
可以通过中心事件来统一管理输入事件。因为是做 Pico 开发,所有需要检测两套输入,因为需要集合不重复,所以可以直接使用 HashSet。如果后面需要再监听别的输入事件也只需要修改 InputMrg ,对于外部已经调用过的代码完全不需要修改。
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;
}
}
外部调用方式,你可以像下面这样用有参数的回调函数:
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);```
然后在回调函数里面去判断是否进行触发:
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) // 是正在抓着的手按得侧边键
{
EventActionSideButtonDown1();
}
}
}
}
你也可以直接使用无参数的回调函数:
EventCenter.Ins.AddEventListener(InputMrg.Ins.GetKeyDownEventName(KeyCode.Space), KeyActionTriggerDown);
EventCenter.Ins.AddEventListener(InputMrg.Ins.GetKeyDownEventName(Pvr_UnitySDKAPI.Pvr_KeyCode.TRIGGER, handId), KeyActionTriggerDown);
网友评论