UNITY_委托和事件


UNITY_委托和事件

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

觀察者模式

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

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

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中為委托引入了方法組轉換機制,支持從方法到兼容的委托類型的隱式轉換
  之所以成為方法"組"轉換,則是因為方法的重載

若有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則不支持

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

(略) -- 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~172、175

優點:

在事件機制中,事件成員(OnSubHp)才擁有數據,
  這些數據不屬於觀察者,但是觀察者需要依賴Subject的這些數據做出響應
  如果有很多不同的觀察者通過訂閱同一個Subject,Subject的數據變化而觸發了事件時,所有觀察者會受到相應通知

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

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

 

 

 

 

 


免責聲明!

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



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