UNITY_委託和事件

UNITY_委託和事件

參考資料: Unity3D腳本編程-使用C#語言開發跨平臺遊戲-陳嘉棟編程

觀察者模式

主題(Subject)管理某些數據,當主題的數據發生改變時,會通知已經註冊(Register)的觀察者(Observer),而這些已經註冊的觀察者會受到數據改變的通知並做出相應反應。c#

觀察者模式定義了對象之間的一對多依賴,當一個對象改變狀態時,它的全部依賴者都會受到通知並自動更新。設計模式

Unity提供的機制:SendMessage()和BroadcastMessage()

缺點:
  過於依賴反射機制(reflection)來查找消息對應的被調用函數
  1. 頻繁使用反射會影響性能
  2. 更會大大增長代碼的維護成本 -- 字符串標識對應方法
  3. 可以調用private的方法 -- 如有一個是有方法在聲明的類中沒有被使用,那正常狀況下都會把它認爲是廢代碼從而刪除,這時隱患就出現了數組

C#的委託機制

c#中提供的回調函數的機制即是委託(類型安全)安全

委託的使用

public class DelegateScript: MonoBehaviour{
    internal delegate void MyDelegate(int num); // 聲明委託類型(參數列表+返回類型)\
    MyDelegate myDelegate; // 聲明變量

    void Start(){
        myDelegate = PrintNum; // 給委託類型MyDelegate的實例賦值引用的方法
        myDelegate(50);

        myDelegate = DoubleNum;
        myDelegate(50);
    }
    
    void PrintNum(int num){
        ...
    }
    void DoubleNum(int num){
        ...
    }
}

這裏myDelegate = PrintNum; 將一個方法"賦值"給了一個委託
  在c#2中爲委託引入了方法組轉換機制,支持從方法到兼容的委託類型的隱式轉換
  之因此成爲方法"組"轉換,則是由於方法的重載dom

如有delegate void Delegate1(int num)
 和delegate void Delegate2(int num, int num2)
且有方法 void PrintNum(int num)
  和  void PrintNum(int num, int num2)
  則  myDelegate1 = PrintNum;
     myDelegate2 = PrintNum;
  向  myDelegate1或myDelegate2賦值時,均可以使用PrintNum做爲方法組(由於重載了多個方法)
  而編譯器會自動選擇合適的重載函數

委託參數的逆變性
  逆變性: 能夠是類型的基類
  即委託對應方法的參數能夠是委託的參數類型的基類性能

委託返回類型的協變性
  協變性: 能夠是類型派生出來的一個派生類
  即委託對應方法的返回類型能夠是委託的返回類型的一個派生類優化

逆變性和協變性僅針對引用類型,如果值類型或void則不支持this

委託的編譯器內部實現機理

(略) -- p154~164

委託鏈

委託調用多個方法 -- 委託鏈
委託鏈是委託對象的集合 -- 能夠利用委託鏈來調用集合中的委託所表明的所有方法

public class DelegateScript : MonoBehaviour {
    delegate void MyDelegate(int num);
    
    void Start(){
        MyDelegate myDelegate1 = new MyDelegate(PrintNum1);
        MyDelegate myDelegate2 = new MyDelegate(PrintNum2);
        MyDelegate myDelegate3 = new MyDelegate(PrintNum3);

        MyDelegate myDelegates = null;
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate2);
        myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate3);

        Print(10, myDelegates);
    }

    void Print(int num, MyDelegate md){
        if(md != null){
            md(value);
        }
    }

    void PrintNum1(int num) { Debug.Log("1 result Num: " + num); }
    void PrintNum2(int num) { Debug.Log("2 result Num: " + num); }
    void PrintNum3(int num) { Debug.Log("3 result Num: " + num); }
}

剛開始時myDelegates = null; 表示沒有對應要回調的方法
第一次myDelegates = (MyDelegate)Delegate.Combine(myDelegates, myDelegate1);時myDelegates引用了myDelegate1所引用的委託實例
第二次時,myDelegates內部實現的_invocationList字段被初始化,而且_invocationList[0]指向和_invocationList[1]分別指向兩個委託實例myDelegate1和myDelegate2
第三次是將一個委託實例myDelegate3合併到一個委託鏈中。編譯器內部發生的與第二次的大同小異。須要注意的是,第二次獲得的委託鏈中的_invocationList所引用的委託實例數組再也不須要,被垃圾回收
將myDelegates變量(委託鏈)做爲參數傳入Print(),Print方法中的代碼會隱式調用myDelegates所引用的委託實例的Invoke()方法,
  此時會執行一個循環來遍歷_invocationList中的全部委託實例並按順序調用每一個委託實例中包裝的回調方法,即PrintNum1(), PrintNum2()和PrintNum3()

對應Delegate.Combine(), 也提供了Remove()方法用於移除委託實例
  Remove()每次僅僅移除一個匹配的委託實例,而不是全部和目標委託實例匹配的委託實例
  myDelegates = (MyDelegate)Delegate.Remove(myDelegates, new MyDelegate(PrintNum2));

當Remove方法被調用時,會從後向前掃描myDelegates中的委託實例數組,並對比委託實例的_target和_methodPtr的值是否與須要Remove的對應字段值相同。
若匹配,則刪除。若是委託實例數組爲空,則返回null;若是委託實例數組長度爲1,則直接返回那個委託實例;
不然,會建立一個新的委託實例(與Combine出委託鏈相似),對應的_invocationList會引用由刪除了目標委託實例後剩餘委託實例組成的委託實例數組。

C#編譯器爲委託類型的實例重載了 += 和 -= 操做符,對應Delegate.Combine()和Delegate.Remove()

事件

觀察者模式能夠經過事件機制來實現

C#中的事件機制是以委託做爲基礎的

一個定義了事件成員的類型須要提供這些功能來實現交互機制
  1. 方法可以訂閱對某事件的關注
  2. 方法可以取消訂閱對該事件的關注
  3. 事件發生時,訂閱了該事件的方法會收到通知

實例

一個遊戲單位(BaseUnit類)被攻擊而掉血,那麼掉血(OnSubHp事件)就能夠被做爲一個事件。
訂閱了該事件的對象在遊戲單位掉血時,會收到遊戲單位掉血的通知。

具體需求:掉血時,須要顯示掉血信息,掉血信息中有多個值。

思路:爲了區分開遊戲單位和顯示信息的邏輯(下降邏輯的耦合性),將掉血信息的顯示邏輯交給模塊BattleInfoComponent
--> 即遊戲單位的掉血事件OnSubHp發生時,通知BattleInfoComponent模塊來處理顯示功能。

實現:

1. 定義委託類型(回調方法原型) -- 事件是以委託爲基礎的

public delegate void SubHpHandler(BaseUnit source, float subHp, DamageType damageType, HpShowType showType);
-- source: 受傷害的單位;subHp: 傷害;damageType: 傷害方式;showType: 顯示方式

2. 定義事件成員

使用關鍵字event定義事件成員
public event SubHpHandler OnSubHp;

表示事件OnSubHp的類型爲SubHpHandler,意味着事件OnSubHp的全部訂閱者都必須提供和SubHpHandler委託類型所肯定的方法原型相匹配的回調方法,即void Method(BaseUnit .., float .., DamageType .., HpShowType ..);

3. 事件的觸發

這裏的BaseUnit能夠視爲一個基類,派生出好比英雄類、士兵類等。
所以,觸發事件的方法能夠定義爲一個虛方法
本例中,OnSubHp事件是受到攻擊而致使的,所以

protected virtual void OnBeAttacked(float damage, bool isCritical, bool isMissed){
    DamageType damageType = DamageType.Normal;
    HpShowType showType = HpShowType.Damege;
    if(isCritical) damageType = DamageType.Critical;
    if(isMissed) showType = HpShowType.Miss;
    // 若是有方法訂閱了OnSubHp事件,則調用(通知)
    if(OnSubHp != null) {
        OnSubHp(this, damage, damageType, showType);
    }
}

而BaseUnit的派生類能夠經過重寫OnBeAttack()來控制事件的觸發

優化:業務單一原則

OnBeAttack()方法應該僅僅用來觸發事件
所以
BeAttack()方法用來將敵人的攻擊傷害轉化爲掉血事件的觸發

public void BeAttack() {
    bool isCritical = Random.value > 0.5f;
    bool isMissed = Random.value > 0.5f;
    float damage = 10000f;
    
    OnBeAttacked(damage, isCritical, isMissed);
} 

4. 事件的訂閱和觀察者的回調方法

以前提到的BattleInfoComponent類是用來進行傷害信息顯示的
而傷害信息顯示的時機是在BaseUnit受到傷害的時候
所以BattleInfoComponent須要訂閱BaseUnit.OnSubHp事件。

public class BattleInfoComponent: MonoBehaviour {
    public BaseUnit baseUnit;
    ...
    
    private void AddListener () {
        // 訂閱事件this.unit.OnSubHp
        this.unit.OnSubHp += new BaseUnit.SubHpHandler(this.OnSubHp);
        // 可簡寫爲
        unit.OnSubHp += OnSubHp;
    }

    private void RemoveListener () {
        // 不要忘記取消事件的訂閱
        this.unit.OnSubHp -= new BaseUnit.SubHpHandler(this.OnSubHp);
    }

    private void OnSubHp(BaseUnit source, float damage, DamageType dmgType, HpShowType showType) {
        // 實現傷害信息的顯示功能
        Debug.Log(source.name + ....);
    }
}

c#的+=操做符可用於註冊事件
  this.unit.OnSubHp += new BaseUnit.SubHpHandler(Method);
  可簡寫爲
  this.unit.OnSubHp += Method;
  上述兩行代碼的內部在編譯器內部其實都等效於
  this.unit.add_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));

  在訂閱事件時,編譯器內部須要調用事件的add_OnSubHp方法來向事件內部添加新的委託對象

取消回調事件的訂閱也類似,使用-=操做符
  在編譯器內部等效於
  this.unit.remove_OnSubHp(new BaseUnit.SubHpHandler(this.OnSubHp));

總結:

當UnitBase受到攻擊時,執行UnitBase.BeAttack()
  --> UnitBase.OnBeAttacked()被調用
  --> 觸發UnitBase.OnSubHp事件
  因爲BattleInfoComponent.OnSubHp()方法訂閱了UnitBase.OnSubHp事件
  --> 所以在UnitBase.OnSubHp事件觸發時,BattleInfoComponent.OnSubHp()方法被調用
    而OnSubHp()方法會受到來自UnitBase.OnSubHp事件傳來的參數source等
    基於這些參數,OnSubHp()方法得以將傷害信息顯示出來

事件的編譯器內部實現機理

(略) -- p169~17二、175

優勢:

在事件機制中,事件成員(OnSubHp)才擁有數據,
  這些數據不屬於觀察者,可是觀察者須要依賴Subject的這些數據作出響應
  若是有不少不一樣的觀察者經過訂閱同一個Subject,Subject的數據變化而觸發了事件時,全部觀察者會受到相應通知

經過事件機制能夠將對象之間的相互依賴降到最低 -- 鬆耦合
  觀察者模式的意義也在於此,讓主題和觀察者之間實現鬆耦合的設計模式
  當兩個對象之間鬆耦合,即便不清楚彼此的細節,也能夠進行交互

在有新類型的觀察者出現時,主題(Subject)的代碼無需修改,而新類型只須要實現匹配的回調方法便可註冊成觀察者

相關文章
相關標籤/搜索