天天看點

遊戲程式設計模式-元件模式(轉載)

本文轉載自部落格園,原文位址是:​​https://www.cnblogs.com/xin-lover/p/11645235.html​​

未防止以後找不到,故轉載之。

“允許一個單一的實體跨越多個不同域而不會導緻耦合。”

  在遊戲的程式設計中,我們很容易寫出一個超級大而且耦合度很高的類來。比如我們的英雄角色,我會使用各種輸入裝置來操縱它,會給他添加華麗的技能特效和音效,這就需要我們寫很多的代碼了,有讀取控制器輸入的代碼,有實體和碰撞的代碼,有音效的代碼,還有播放動畫的代碼,最後還要渲染。這些代碼我們都摻雜到一個類中,會導緻這個類超級的大,這對于維護的人來說簡直不能接受,改動代碼的代價太大了。到最後我們會發現,我們實作功能的速度遠遠比不上代碼的産生速度。

  是以我們需要分割代碼。就像我們設計一個文字處理器,處理列印部分的代碼不應該受到讀取、儲存文檔的代碼的任何影響。我們要讓實體、AI、渲染、音效這些部分互相獨立。比如下面的一段代碼:

  邏輯很清晰,但對于需要修改它的人來說卻不簡單,他需要了解實體、圖像和聲效的相關知識,否則任何的更改都可能會引入bug。這無疑對需要維護這段代碼的人來說增加了很大的負擔。這裡的一個解決方案就是分割——我們把功能分成一個一個的域,這樣輸入輸出是一個域,實體引擎和碰撞是一個域、AI是一個域、聲效也是一個域。我們按這些域來組織代碼,那麼我們可以把輸入輸出的代碼都放入一個叫InputComponent的類中,聲效放入AudioComponent的類中。這樣到最後,我們會發現我們的英雄角色類就變成了一個容器,裡面放着所有需要的這些元件類。

  這樣,原來一個很龐大的類就分成了一個一個的元件類,這些元件類之間實作了解耦。當然,在實踐中,我們會遇到一些需要互動的情況,比如實體元件在發生碰撞的時候需要播放音效,這個時候,我們可以很容易的把這些需要通信的地方進行限制,代碼之間的後耦合度還是很低。同時,這也提高了代碼的複用性。我們可以看一些繼承是怎麼複用代碼的,比如我們的遊戲中有一個GameObject的基類,它包含位置和方向這種基本的元素。而Zone類繼承這個基類并在其基礎上添加了碰撞。相似的,Decoration類也繼承了這個基類并在其基礎上增加了渲染。Prop類繼承自Zone類,是以它可以重用碰撞檢測代碼。而Prop類不能同時繼承自Decoration類來重用渲染代碼,否則繼承結構将陷入“緻命的菱形多繼承“的窘境。我們也可以讓Prop類繼承Decoration類,但碰撞部分的代碼我們就需要複制過來,也就是說我們沒有辦法不通過多重繼承而在多個類之間重用碰撞跟渲染部分的代碼。另一個解決方案就是把碰撞和渲染代碼都放入基類中,但這樣基類就會很龐大,而另一些類可能不需要這些功能,這樣會造成記憶體的浪費。

  而使用元件,那渲染一個元件類,碰撞一個元件類,那麼Zone就需要包含碰撞元件類就可以,而Decoration類就需要包含渲染元件,而Prop則同時包含碰撞和渲染元件,這樣沒有代碼重複,沒有多重繼承。從這我們可以看出,元件對于對象而言就是即插即用,借由元件,我們能通過讓實體身上插不同的、可重用的元件對象來建構複雜而且行為豐富的實體。

  單一實體橫跨多個域。為了保持域之間互相隔離,每個域的代碼都獨立的放在自己的元件類中。實體本身可以簡化為這些元件的容器。

  元件最常見于遊戲中定義實體的核心類,但是它們也能夠用在别的地方。當如下條件成立時,元件模式就能發揮它的作用。

  你有一個涉及到多個域的類,但你希望讓這些域保持互相解耦;

  一個類越來越龐大,越來越難以開發;

  你希望定義許多共享不同能力的對象,但采用繼承的辦法卻無法令精确的重用代碼;

  元件模式相較直接在類中編碼的方式為類本身引入了更多的複雜性。每個“概念”上的對象成為一系列必須被同時執行個體化、初始化,并正确關聯的對象的叢集。不同元件之間的通信變得更具挑戰性,而且對它們所占用的記憶體的管理将變得更複雜。對于一個大型代碼代碼庫,這點複雜性相對于它帶來的接口和代碼重用時值得的。但在沒有出現問題的代碼庫中你不必過度設計而使用這樣一個“解決方案”。

  使用元件模式的另外一個後果就是你經常需要一系列的間接引用來處理問題,也就是你必須先擷取元件的引用,這對于對性能要求較高的内部循環代碼中,元件指針可能會導緻低劣的性能。

  讓我們先不适用元件模式實作一個比較大的類。這個類讀取操縱杆的輸入來對面包師進行加速。然後通過實體引擎來确定其新的位置,最後将面包師渲染到螢幕上。這裡引用了一些其它的類,它們沒有被列出來,但我覺得你們應該能懂。

​​

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

  邏輯很容易懂,但可以觀察到,這裡的耦合程度很高。接下來我們根據功能邊界進行分割,首先我們可以分割輸入域。也就是把輸入相關的代碼封裝到一個元件類中。

​​​​

遊戲程式設計模式-元件模式(轉載)

  封裝非常的簡單,接下來我們再把其它部分進行分割。

 

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

  元件分割好後,我們看看Bjorn類變成什麼樣了。

遊戲程式設計模式-元件模式(轉載)

   現在Bjorn隻做了兩件事:持有定義了Bjorn行為的元件和持有這些域所共享的狀态量。為什麼不把這些共享的狀态量也放入元件中了?這是因為這些狀态量比較通用,把它們放在Bjorn中可以輕松的在元件之間傳遞消息而不同耦合它們。

  現在這個Bjorn類中,各元件都是内置的,沒有被抽象化,也就是說Bjorn的行為還是被精确的定義的。我們可以修改一下,改為持有元件的指針,通過構造函數或者其它接口把元件傳入Bjorn中,這樣我們就能定義各種各樣的元件,它們繼承自這些抽象化的元件,進而實作Bjorn豐富的行為。當然,代價就是虛函數調用。

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

  我們觀察一下,發現現在Bjorn類中已經沒有任何Bjorn獨有的代碼了,它更像一個元件包。而事實上,它是一個能夠用到遊戲中所有對象的遊戲基本類的最佳候選。是以我們給他改個名字——GameObject。

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

   這個時候,如果我們想要一個Bjorn的對象,隻需要給GameObject中傳入Bjorn需要的元件即可,也就是這樣。

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

   關于這個設計模式的最重要的問題是:你需要的元件集合是什麼?答案取決于你的遊戲需求與風格。引擎越大越複雜,你就越想要将元件切分的更細。除此之外,有一些具體選擇需要考慮。

  一旦我們将對象切分為數個獨立的元件,我們就必須決定誰在背後來聯系這些元件。

  一種做法是由容器類來建立元件,這麼做的好處就是我們不可能在建立新對象的時候忘記建立元件,但這樣将讓我們重新配置這個類變的十分困難,喪失了靈活性。另一種推薦的做法就是由外部代碼提供元件,這樣,建構對象将變得很靈活,通過組合不同的元件來建構新的對象,最大限度的重用了代碼。同時我們可以把這個類做成通用的容器類,同時允許元件派生,那麼對象就隻需要持有元件的接口,這樣能夠很好的封裝結構。

  雖然通過區間實作了元件之間的功能隔離和互相解耦,但這些元件最終是要屬于一個對象,這意味着它們都是整體的一部分,是以它們需要互相協作,也就是通信。那麼如何進行通信了?這裡由幾個選擇你都可以選擇,解決方案并不唯一。

  通過修改容器狀态

  優點就是保持元件間的解耦,元件互相之間是透明的。比如輸入元件和實體元件都可能改變遊戲對象的速度,但它們不知道互相的存在,它們隻是改變容器類的速度屬性。而缺點就是,它要求共享的資料都由容器對象進行共享,而通常元件隻需要其中的一小部分,比如圖形元件就不需要遊戲對象速度這一屬性,但它們都儲存在容器對象中,也就是圖形元件也能通路這些屬性,這樣做很容易弄亂這個對象類;另一個缺點就是資料傳遞将變得很隐秘同時對元件的執行順序産生了依賴。比如對遊戲對象的速度這一項,我們需要先讀取輸入,然後實體引擎更新,再渲染到螢幕上,也就是我們需要小心的維護元件間的執行順序,否則将産生一些難以追蹤的bug。比如,先執行圖形渲染,再更新實體引擎,将導緻遊戲對象的位置是上一幀而不是目前幀。

  直接互相引用

  互相引用也就是元件之間需要知道其它元件對象,比如渲染元件更新的時候傳入實體元件。這樣做的優點很明顯,非常的簡單快捷,但缺點就是耦合很緊密,和最開始那個巨大的單類本質上是一樣的,但因為把耦合限制在了需要交流的元件中,耦合度并沒有那麼大,某些情況下也是可以接受的。

  通過傳遞資訊的方式

  這是最複雜的一個方式。我們可以在容器類中建立一個小的消息傳遞系統,讓需要傳遞資訊的元件通過廣播的方式去建立元件間的聯系。示例:

遊戲程式設計模式-元件模式(轉載)
遊戲程式設計模式-元件模式(轉載)

  元件類有一個接受消息的接口,容器類把消息發送給所有的元件類,由具體元件決定如何處理消息。這麼做的好處就是

  兄弟元件之間是解耦的。就好像之前共享狀态變量一樣。元件之間唯一的耦合就是消息本身。

  容器對象十分簡單。不像狀态共享那樣容器類能夠獲知應該傳遞給元件的資訊,在這裡,容器類隻負責把消息送出去。這對兩個類之間傳遞非常特定的資訊而不讓容器類獲知是個非常有用的方法。

  意料之外的是,沒有哪種選擇是最好的。狀态共享對每個對象都擁有的基本狀态如位置和尺寸非常的管用,而有些域雖然不同但聯系緊密,比如動畫和渲染、使用者輸入和AI、實體引擎等。如果你有上述強關聯的元件的話,最簡單的方法就是在它們之間建立聯系。消息傳遞是個對“不太重要”的通信有用的機制。其”即發即棄”(fire and forget)的特性非常适合類似當實體元件發送一個消息告知對象與物體發生碰撞時,通知聲音元件去播放聲音的情況。

  是以,與往常一樣,我們建議你從最簡單的開始,然後在你的元件需要通信的時候再考慮使用哪種通信方式。

繼續閱讀