QFramework 除了提供了一套架構之外,QFramework 還提供了可以脫離架構使用的工具 TypeEventSystem、EasyEvent、BindableProperty、IOCContainer。
這些工具并不是有意提供,而是 QFramework 的架構在設計之初是通過這幾個工具組合使用而成的。
內置工具
TypeEventSystem
基本用法:
using UnityEngine;namespace QFramework.Example
{public class TypeEventSystemBasicExample : MonoBehaviour{public struct TestEventA{public int Age;}private void Start(){TypeEventSystem.Global.Register<TestEventA>(e =>{Debug.Log(e.Age);}).UnRegisterWhenGameObjectDestroyed(gameObject);}private void Update(){// 鼠標左鍵點擊if (Input.GetMouseButtonDown(0)){TypeEventSystem.Global.Send(new TestEventA(){Age = 18});}// 鼠標右鍵點擊if (Input.GetMouseButtonDown(1)){TypeEventSystem.Global.Send<TestEventA>();}}}
}// 輸出結果
// 點擊鼠標左鍵,則輸出:
// 18
// 點擊鼠標右鍵,則輸出:
// 0
事件繼承支持
除了基本用法,TypeEventSystem 的事件還支持繼承關系。
using UnityEngine;namespace QFramework.Example
{public class TypeEventSystemInheritEventExample : MonoBehaviour{public interface IEventA{}public struct EventB : IEventA{}private void Start(){TypeEventSystem.Global.Register<IEventA>(e =>{Debug.Log(e.GetType().Name);}).UnRegisterWhenGameObjectDestroyed(gameObject);}private void Update(){if (Input.GetMouseButtonDown(0)){TypeEventSystem.Global.Send<IEventA>(new EventB());// 無效TypeEventSystem.Global.Send<EventB>();}}}
}// 輸出結果:
// 當按下鼠標左鍵時,輸出:
// EventB
手動注銷
using UnityEngine;namespace QFramework.Example
{public class TypeEventSystemUnRegisterExample : MonoBehaviour{public struct EventA{}private void Start(){TypeEventSystem.Global.Register<EventA>(OnEventA);}void OnEventA(EventA e){}private void OnDestroy(){TypeEventSystem.Global.UnRegister<EventA>(OnEventA);}}
}
接口事件
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace QFramework.Example
{public struct InterfaceEventA{}public struct InterfaceEventB{}public class InterfaceEventModeExample : MonoBehaviour, IOnEvent<InterfaceEventA>, IOnEvent<InterfaceEventB>{public void OnEvent(InterfaceEventA e){Debug.Log(e.GetType().Name);}public void OnEvent(InterfaceEventB e){Debug.Log(e.GetType().Name);}private void Start(){this.RegisterEvent<InterfaceEventA>().UnRegisterWhenGameObjectDestroyed(gameObject);this.RegisterEvent<InterfaceEventB>();}private void OnDestroy(){this.UnRegisterEvent<InterfaceEventB>();}private void Update(){if (Input.GetMouseButtonDown(0)){TypeEventSystem.Global.Send<InterfaceEventA>();TypeEventSystem.Global.Send<InterfaceEventB>();}}}
}// 輸出結果
// 當按下鼠標左鍵時,輸出:
// InterfaceEventA
// InterfaceEventB
同樣接口事件也支持事件之間的繼承。
非 MonoBehavior 腳本如何自動銷毀
public class NoneMonoScript : IUnRegisterList
{public List<IUnRegister> UnregisterList { get; } = new List<IUnRegister>();void Start(){TypeEventSystem.Global.Register<EasyEventExample.EventA>(a =>{}).AddToUnregisterList(this);}void OnDestroy(){this.UnRegisterAll();}
}
如果想手動注銷,必須要創建一個用于接收事件的方法。
而用自動注銷則直接用委托即可。
這兩個各有優劣,按需使用。
另外,事件的定義最好使用 struct,因為 struct 的 gc 更少,可以獲得更好的性能。
EasyEvent
TypeEventSystem 是基于 EasyEvent 實現的。
EasyEvent 也是一個可以脫離架構使用的工具。
基本用法:
using UnityEngine;namespace QFramework.Example
{public class EasyEventExample : MonoBehaviour{private EasyEvent mOnMouseLeftClickEvent = new EasyEvent();private EasyEvent<int> mOnValueChanged = new EasyEvent<int>();public class EventA : EasyEvent<int,int> { }private EventA mEventA = new EventA();private void Start(){mOnMouseLeftClickEvent.Register(() =>{Debug.Log("鼠標左鍵點擊");}).UnRegisterWhenGameObjectDestroyed(gameObject);mOnValueChanged.Register(value =>{Debug.Log($"值變更:{value}");}).UnRegisterWhenGameObjectDestroyed(gameObject);mEventA.Register((a, b) =>{Debug.Log($"自定義事件:{a} {b}");}).UnRegisterWhenGameObjectDestroyed(gameObject);}private void Update(){if (Input.GetMouseButtonDown(0)){mOnMouseLeftClickEvent.Trigger();}if (Input.GetMouseButtonDown(1)){mOnValueChanged.Trigger(10);}// 鼠標中鍵if (Input.GetMouseButtonDown(2)){mEventA.Trigger(1,2);}}}
}// 輸出結果:
// 按鼠標左鍵時,輸出:
// 鼠標左鍵點擊
// 按鼠標右鍵時,輸出:
// 值變更:10
// 按鼠標中鍵時,輸出:
// 自定義事件:1 2
EasyEvent 最多支持三個泛型。?
EasyEvent 的優勢
EasyEvent 是 C# 委托和事件的替代。
EasyEvent 相比 C# 委托和事件,優勢是可以自動注銷。
相比 TypeEventSystem,優勢是更輕量,大多數情況下不用聲明事件類,而且性能更好(接近 C# 委托)。
缺點則是其攜帶的參數沒有名字,需要自己定義名字。
在設計一些通用系統的時候,EasyEvent 會派上用場,比如背包系統、對話系統,TypeEventSystem 是一個非常好的例子。
BindableProperty
BindableProperty 提供 數據 + 數據變更事件 的一個對象。
基本用法:
var age = new BindableProperty<int>(10);age.Register(newAge=>{Debug.Log(newAge)
}).UnRegisterWhenGameObjectDestoryed(gameObject);age++;
age--;// 輸出結果
// 11
// 10
就是當調用 age++ 和 age-- 的時候,就會觸發數據變更事件。
BindableProperty 除了提供 Register 這個 API 之外,還提供了 RegisterWithInitValue API,意思是 注冊時 先把當前值返回過來。
具體用法如下:
var age = new BindableProperty<int>(5);age.RegisterWithInitValue(newAge => {Debug.Log(newAge);});// 輸出結果
// 5
這個 API 就是,沒有任何變化的情況下,age 先返回一個當前的值,比較方便用于顯示初始界面。
使用 BindableProperty 優化 CounterApp 的代碼
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{// 1. 定義一個 Model 對象public class CounterAppModel : AbstractModel{public BindableProperty<int> Count { get; } = new BindableProperty<int>();protected override void OnInit(){var storage = this.GetUtility<Storage>();// 設置初始值(不觸發事件)Count.SetValueWithoutEvent(storage.LoadInt(nameof(Count)));// 當數據變更時 存儲數據Count.Register(newCount =>{storage.SaveInt(nameof(Count),newCount);});}}public class AchievementSystem : AbstractSystem {protected override void OnInit(){this.GetModel<CounterAppModel>() // -+.Count.Register(newCount =>{if (newCount == 10){Debug.Log("觸發 點擊達人 成就");}else if (newCount == 20){Debug.Log("觸發 點擊專家 成就");}else if (newCount == -10){Debug.Log("觸發 點擊菜鳥 成就");}});}}// 定義 utility 層public class Storage : IUtility{public void SaveInt(string key, int value){PlayerPrefs.SetInt(key,value);}public int LoadInt(string key, int defaultValue = 0){return PlayerPrefs.GetInt(key, defaultValue);}}// 2.定義一個架構(提供 MVC、分層、模塊管理等)public class CounterApp : Architecture<CounterApp>{protected override void Init(){// 注冊 System this.RegisterSystem(new AchievementSystem()); // +// 注冊 Modelthis.RegisterModel(new CounterAppModel());// 注冊存儲工具的對象this.RegisterUtility(new Storage());}}// 引入 Commandpublic class IncreaseCountCommand : AbstractCommand {protected override void OnExecute(){var model = this.GetModel<CounterAppModel>();model.Count.Value++; // -+}}public class DecreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterAppModel>().Count.Value--; // -+}}// Controllerpublic class CounterAppController : MonoBehaviour , IController /* 3.實現 IController 接口 */{// Viewprivate Button mBtnAdd;private Button mBtnSub;private Text mCountText;// 4. Modelprivate CounterAppModel mModel;void Start(){// 5. 獲取模型mModel = this.GetModel<CounterAppModel>();// View 組件獲取mBtnAdd = transform.Find("BtnAdd").GetComponent<Button>();mBtnSub = transform.Find("BtnSub").GetComponent<Button>();mCountText = transform.Find("CountText").GetComponent<Text>();// 監聽輸入mBtnAdd.onClick.AddListener(() =>{// 交互邏輯this.SendCommand<IncreaseCountCommand>();});mBtnSub.onClick.AddListener(() =>{// 交互邏輯this.SendCommand(new DecreaseCountCommand(/* 這里可以傳參(如果有) */));});// 表現邏輯mModel.Count.RegisterWithInitValue(newCount => // -+{UpdateView();}).UnRegisterWhenGameObjectDestroyed(gameObject);}void UpdateView(){mCountText.text = mModel.Count.ToString();}// 3.public IArchitecture GetArchitecture(){return CounterApp.Interface;}private void OnDestroy(){// 8. 將 Model 設置為空mModel = null;}}
}
- Model 中的 Count 和 mCount 改成了一個叫做 Count 的 BindableProperty
- 刪掉了 CountChangeEvent 改用監聽 BindableProperty
- Controller 在初始化中去掉一次 UpdateView 的主動調用
由于 Count 數據是單個數據 + 事件變更的形式,所以用 BindableProperty 非常合適,可以少寫很多代碼。
一般情況下,像主角的金幣、分數等數據非常適合用 BindableProperty 的方式實現。
IOCContainer
QFramework 架構的模塊注冊與獲取是通過 IOCContainer 實現的。
IOC 的意思是控制反轉,即控制反轉容器。
其技術的本質很簡單,本質就是一個字典,Key 是 Type,Value 是 Object,即:Dictionary<Type,object>。
QFramework 架構中的 IOCContainer 是一個非常簡易版本的控制翻轉容器,僅支持了注冊對象為單例的模式。
基本使用:
using System;
using UnityEngine;namespace QFramework.Example
{public class IOCContainerExample : MonoBehaviour{public class SomeService{public void Say(){Debug.Log("SomeService Say Hi");}}public interface INetworkService{void Connect();}public class NetworkService : INetworkService{public void Connect(){Debug.Log("NetworkService Connect Succeed");}}private void Start(){var container = new IOCContainer();container.Register(new SomeService());container.Register<INetworkService>(new NetworkService());container.Get<SomeService>().Say();container.Get<INetworkService>().Connect();}}
}// 輸出結果:
// SomeService Say Hi
// NetworkService Connect Succeed
使用 IOCContainer 更容易設計出符合依賴倒置原則的模塊。
而 QFramework 架構的用接口設計模塊的支持就是通過 IOCContainer 支持的,同樣使用 IOCContainer 也更容易設計出分層的架構。