江紅偉 米洪



摘要:文章建立在抽象思維的基礎上,對觀察者模式進行了系統研究,并將這種良好的思維模式應用于實際的游戲引擎開發實例中。闡述了基于觀察者模式的委托、事件、消息傳遞與響應的關系,實現程序中各類對象之間協同工作,弱化具體類之間的耦合關系,使得某些相互有聯系的對象間不需要依賴對方而實現必要的通信與交互。最后在Unity3D中通過具體實例講述了回調系統內置事件方法,以及.net泛型委托方法之間的差異,并通過事件傳遞消息的方法給出了游戲引擎開發中對象間完全解耦的解決方案。
關鍵詞:觀察者模式;委托;事件;消息傳遞
中圖分類號:TP311.1? ? ? 文獻標識碼:A
文章編號:1009-3044(2022)22-0083-04
1 引言
Unity3D是現在主流的3D游戲引擎,它支持多種面向對象的語言,其中C#語言的應用最為廣泛,是典型的游戲開發和虛擬現實開發的代表。在一個游戲系統的設計中,事件和響應機制的應用是不可回避的技術。在開發中,什么時候回調引擎基類MonoBehavior的事件、什么時候使用.net平臺的事件系統,是編程邏輯時刻需要解決的問題。
本文重點探討和研究C#語言中基于觀察者模式的事件和響應機制。一方面要盡量實現類的單一性原則,即一個類應該專注于做一件事情;另一方面,應該把程序中不變的部分分離出來形成抽象層、變動的部分形成各個不關聯的具體執行層、減少耦合度,以達到程序中對象間的“低耦合、高內聚”的目標。
2 接口和抽象類
在面向對象的概念中,所有的對象都是通過類來描繪的,但是反過來并不是所有的類都是用來描繪對象的。如果一個類中沒有包含具體的實現,這樣的類就是抽象類。抽象類基本是用來表征在對問題領域進行分析、設計中得出的抽象概念,是對一系列看上去不同,但是本質上相同的具體概念的抽象[1]。一般來說,可以構造出一個固定的具有一組行為的抽象描述,但是這組行為卻能夠擁有任意多個可能的具體實現。這個抽象的描述就是抽象類,而這一組任意多個可能的具體實現則表現為所有可能的派生類。接口可以被看作是抽象類的變體,接口中所有的方法都是抽象的,可以通過接口來間接地實現多重繼承。
接口和抽象類是面向對象編程的基石,賦予了C#語言強大的面向對象的能力,為類的封裝、繼承和多態提供良好的設計基礎。在程序設計中,各類對象協同工作,對象之間通過封裝和繼承等方式進行通信,良好的協同工作是需要以接口和抽象類為基礎而展開的。
根據里氏代換原則,派生類可以實現基類的抽象方法,而且基類應該不需要知道派生類的具體行為。接口和抽象類都屬于基類,是對業務邏輯的抽象,將派生類的方法進行了一定的規范,具體的實現是由派生類來完成的。當需求有變化時,只需建立新的子類來實現接口或抽象類就可以擴展出新的功能,而原有的依賴于該基類的派生類則不需要修改,做到了程序間的“低耦合”性,甚至“無耦合”性。這樣即可實現“開閉原則”,使得程序具有較好的擴展性的同時,又可以實現對子類修改的關閉,從而避免了程序員各自寫的子類之間的相互影響,減少子類修改對于整個系統帶來的影響。
總之,接口和抽象類都是對客戶端程序的一種承諾,應該做到相對不變[2]。同時根據接口隔離原則,接口應當為客戶提供盡可能小的規范[3],以明確地區分各個接口和抽象類的分工和特征。
3 觀察者模式
在一個軟件系統中,各類對象之間是協同工作的,對象之間可以通過繼承、實例等進行消息傳遞。假如一個對象的行為發生變化時,和這個對象有關聯的其他對象都需要在程序結構上進行重寫,則這種情況就導致了對象之間的完全依賴、形成緊密的耦合。所以好的設計模式需要借以優秀的,可以方便開發者復用的程序設計模式[4]來構建良好的程序關系。
“觀察者模式(Observer mode) ”或稱“發布-訂閱模式(Publish/Subscribe) ”屬于設計模式中的行為模型,可以弱化具體類之間的耦合關系,使得某些相互有聯系的對象間不需要依賴對方而實現必要的通信與交互。該模式定義了對象間的一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象[5]。這個目標對象在狀態發生變化時,會通知所有的觀察者對象,使它們能夠自動更新[6]。且主題發出通知并不需要知道具體的觀察者對象,觀察者之間也不需要知道其他觀察者的存在。觀察者模式在降低程序間耦合度的同時能夠維持好對象間行動的一致性,保證程序間的高度協作性。
如圖1所示:Subject類(目標類,是廣播者) 一般被定義為抽象類,其中保存了Observer類(觀察者類,是訂閱者) 的集合,所以可以讓多個觀察者同時監聽該目標。此外它提供給ConcreteSubject類(具體目標類) 需要實現的抽象方法,以規范目標類中具體方法的實現。
Observer類(觀察者類) 一般定義為接口,可以使得多個ConcreteObserver類(具體觀察者類) 繼承于該接口。這樣使得各個具體觀察者對象可以被保存在Subject類的Observer集合中,使得具體目標類可以遍歷到各個具體觀察者對象,并對其發送通知,實現多播效果。最后具體觀察者接收到通知并執行各自的實現方法。
觀察者模式實現了目標類與觀察者類之間的抽象耦合,目標類只需要保存觀察者對象的引用,并不需要知道具體觀察者是誰,具體觀察者只需遵守接口的約定即可。通過傳統的抽象類和接口的方式可以實現該模式,也可以通過.net框架中的委托(Delegates) 與事件(Event) 機制來實現,相對來說委托和事件機制能進一步弱化目標類和觀察者類之間的依賴關系[7]。
4 委托與事件
所有的委托類型都派生于基類System.Delegate。使用委托的時候,廣播者類包含一個委托字段,廣播者通過調用委托來決定什么時候進行廣播;觀察者類(訂閱者類) 是方法目標的接收者,通過在委托上調用“+=”開始進行監聽、調用“-=”結束監聽[7];一個訂閱者不需要知道也不會干擾其他的訂閱者,以實現訂閱者之間的解耦。
實際上,委托是不可變的,使用“+=”或“-=”操作符時,其實是創建了新的委托實例,并把它賦值給了當前的委托變量,初始狀態時這個委托變量可以是null,如圖2所示。
在實際的使用中,泛型委托可以提供了更好的便捷性,下面的程序中定義了一個返回類型和傳參類型都為Object的泛型委托,并且創建了一個發布者類,聲明了這個泛型委托的實例對象OnPublisher,利用發布方法。
Publish調用了該對象委托的方法,并使用一個公有字段“output”接收了該委托的返回值,如圖3所示。
委托的方法便是觀察者類的具體實現方法,當然可以同時委托多個觀察者以不同的實現方法,只需這些方法的簽名和委托類的約定一致就行[8],這里示例了一個觀察者類的實現方法,用來傳入一個整數并返回一個整型數值,如圖4所示。
最后在客戶端程序中將觀察者的實現方法賦值給委托對象,調用委托的發布方法并進行傳值。這樣幾個程序間相互獨立、各司其職,發布者和觀察者之間解耦了,如圖5所示。
.net3.5以后還提供了兩類通用的delegates,如果方法有返回值,則使用Func或者Func<>;如果方法沒有返回值,則使用Action或者Action<>。所以上例中的泛型委托就可以直接由Func<>創建委托對象了,而不用首先定義委托。<>中左側是傳參類型、右側是返回值類型,最多可以有16個傳參,如圖6所示。
本例打印輸出結果為“9”。如果使用不帶返回值的泛型委托得到同樣的輸出結果,則可以使用Action<>型委托,<>中定義的是傳值類型,最多也可以有16個。重構這三個類,如圖7、圖8、圖9所示。
事件(event) 是對委托進一步封裝的結果,讓委托只暴露特定的部分子集,防止訂閱者之間相互干擾,可以安全地實現廣播者/訂閱者模式[8]。在Subject類外,只能通過“+=”和“-=”來注冊和注銷事件、即事件訪問器只能通過“+=” 和“-=”來實現。只需引入“event”關鍵字就可以將委托封裝為事件。如上例,將“public static Action
5 Unity3D的事件和響應
在Unity3D中為了響應一個GameObject的事件分發,常規的做法是回調系統相關的內置事件。而MonoBehaviour是Unity中所有腳本的基類,使用C#需要顯式的從MonoBehaviour繼承系統內置的事件[9]。
為了講述事件與響應的關系,在場景制作了3個三維物體作為按鈕對象,并給它們配置好Collider碰撞組件。設計目標是讓鼠標和這3個按鈕對象之間產生互動效果,如圖10所示。
建立一個類繼承自MonoBehaviour基類,分別回調鼠標滑過事件(OnMouseOver) 、鼠標退出事件(OnMouseExit) 。
Unity3D中繼承自MonoBehaviour的類形成實例的方法是將它作為組件掛載給游戲對象[9],所以這3個按鈕對象必須分別掛載這個繼承類。
這種做法雖然很輕松就實現了這3個三維按鈕的鼠標交互效果,但是這種程序結構很不友好。誰是廣播者、誰是訂閱者似乎無法分辨。程序所有的功能都被寫在了這一個類里,全耦合且毫無內聚性可言,這樣導致項目幾乎沒有擴展與維護的可能性,牽一發則動全身。而且程序是被掛載到場景中的每個游戲對象上的,后期想要修改游戲對象的行為,則要人工地逐個去檢查、修改每一個對象的各個實現方法。當場景變大后,這個修改工作將是龐大而煩瑣的過程。這個回調系統事件類的基本樣式如圖11所示。
上文中已經探討了“發布-訂閱”模式的優勢,再借助.net事件機制,完全可以設計出較為優秀的程序結構。基本原理是:寫一個發布者類,利用場景中的主攝像機發出射線與場景中的Collider組件對象發生碰撞,這種碰撞有三種狀態,分別是射線進入某對象、停留在某對象上和離開某對象;程序設計上可以把這個三個狀態分別定義為三個事件OnRayEnter、OnRayStay、OnRayExit,而這些事件只需要傳遞參數、不需要有返回值,所以可以使用Action<>型的委托事件來實現;傳遞的參數就是由不同的事件而捕捉到的射線碰撞對象,然后由一個觀察者類來接收這些事件所傳遞出來的參數,根據傳遞過來的不同的Collider組件對象來做一些具體的事務。
因為發布者類和觀察者類都不需要回調Unity3D系統的內置事件,所以它們是無須繼承系統基類的,這樣也就無需將這兩個類掛載給場景對象來形成實例。只需在客戶端程序對這兩個類實例化后,直接將對應的觀察者方法注冊給發布者的對應事件,就建立了發布者事件和觀察者實現方法之間的聯系。發布者類和觀察者類的基本樣式如圖12、圖13所示。
而客戶端程序仍然需要繼承MonoBehaviour基類,因為它必須回調Unity3D系統的Start()和Update()函數。在Start()函數中對事件進行注冊,在Update()函數中回調發布者類的射線碰撞方法,并進行事件發布。客戶端類的基本樣式如圖14所示。
最后將這個客戶端類掛載給場景中的一個空物體上,讓它形成一個實例。前例中,程序都被分散地掛載在各個游戲對象上,導致管理上非常混亂。而這里,場景中的資源被這個“空物體”統一化處理了,規范化了場景資源的管理。
利用.net事件機制,發布者和觀察者之間一定程度上實現了解耦,發布者和觀察者各自只做自己該做的事、實現了高內聚,也大大方便了項目后期的增、刪、改、查。
6 Unity3D中為事件傳遞消息
上文已經實現了發布者模式的程序結構,但是還需要繼續完善一下,因為發布者還是需要傳參給觀察者的,導致這兩個類之間沒有完全解耦。這時,可以考慮事件的消息傳遞機制,以達到完全解耦的目標。
.net平臺中定義了一個基類EventArgs專門用來為事件傳遞消息。還定義了一個泛型委托EventHandler
所以,可以建立一個繼承EventArgs的類,專門用作事件的消息傳遞,作為發布者類和觀察者類之間信息傳遞的橋梁。這樣觀察者就不必知道發布者所傳遞的是什么了,便可實現發布者和觀察者之間的完全解耦。
首先創建一個繼承EventArgs基類的消息傳遞類PublisherEventArgs,其中定義兩個公開的屬性,分別是上一幀的碰撞信息、當前幀的碰撞信息。并通過構造函數對相應屬性進行賦值,如圖15所示。
發布者類中,重新定義三個事件為EventHandler
對于事件的發布方法CollisionProcess(),首先對變量e進行實例化“e = new PublisherEventArgs(colliderOld, current)”;事件發布器的參數改為泛型委托EventHandler
游戲運行過程中,每幀都會傳兩個參數過去,第一個參數是上一幀的碰撞信息、第二個是當前幀的碰撞信息。這樣就無需像上例那樣,針對不同的事件發布器還要人為判斷所傳參數到底是上一幀的碰撞對象還是當前幀的,避免了人工判斷可能導致的失誤。
此時,觀察者就完全與發布者無關了,只與消息傳遞類PublisherEventArgs有關系,其實現方法RayInputIn()和RayInputOut()寫法大致如圖17所示。
最后客戶端程序也變得簡單了,只需對事件進行注冊,無須考慮事件要傳什么參數給委托方法。因為事件根本就沒有傳參給所委托的方法,而是直接傳給了消息傳遞類,這時發布者和觀察者之間就完全解耦了。事件注冊寫法如“publisherSubject.OnRayEnter += observerObject.RayInputIn”,其他兩個事件的注冊和此寫法類同。
7 結束語
游戲引擎的事件和響應的機制,歸根究底還是要從面向對象的根本出發,帶著抽象思維去思考問題;從軟件設計模式出發,借助優秀的、可以方便開發者復用的程序設計模式來構建良好的程序關系。
基于觀察者模式的程序設計思維,利用好委托、事件和消息傳遞機制,這些對于實現觀察者模式有的獨到的支撐和便利的工具,來設計和優化Unity3D的程序模塊,可以很好地做到對象間“低耦合”甚至“無耦合”的目標。
參考文獻:
[1] 曹步清,金甌.Java中的Abstract Class與Interface技術研究[J].計算機技術與發展,2006,16(8):110-112,115.
[2] 耿祥義,張躍平.Java設計模式[M].北京:清華大學出版社,2009:132-134.
[3] 李航.基于MDA的BSS計費系統設計與實現[D].哈爾濱:哈爾濱工業大學,2012:24.
[4] Gamma E,Helm R,Johns R,et al.Design patterns: elements of reusable object-oriented software[M].Beijing:China Machine Press,2002.
[5] 孟婷婷,何利力.Observer設計模式在手機導航軟件中的應用[J].電腦知識與技術,2014,10(19):4579-4582.
[6] Gamma E,Helm R,Johns R.設計模式:可復用面向對象軟件的基礎[M].李英軍,馬曉星,蔡敏,等譯.北京:機械工業出版社,2005:89.
[7] 吳清壽.基于事件機制的觀察者模式及應用[J].重慶理工大學學報(自然科學版),2012,26(9):100-104.
[8] 微軟公司.基于C#的.NET Framework程序設計[M].北京:高等教育出版社,2004:149-152.
[9] 楊秀杰,楊麗芳.虛擬現實(VR)交互程序設計[M].北京:中國水利水電出版社,2019:34-38.
【通聯編輯:謝媛媛】