1. 程式人生 > >【遊戲設計模式】之四 《遊戲程式設計模式》全書內容提煉總結

【遊戲設計模式】之四 《遊戲程式設計模式》全書內容提煉總結

這是一篇超過萬字讀書筆記,總結了《Game Programming Patterns》(中譯版《遊戲程式設計模式》)一書中所有章節與內容的知識梗概。

我們知道,遊戲行業其實一直很缺一本系統介紹遊戲程式設計進階技巧的書籍,而《遊戲程式設計模式》的出現,正好彌補了這一點。

之前已經有提到過,不同於傳統的出版方式,這本書是網路出版,然後Web版完全免費,其更是在amazon上具有罕見的5星評價,可見讀者對其的好評程度之高。加之書中內容生動有趣,將各種經驗之談娓娓道來,實在是業界良心。

在這篇文章之前,我已經寫了三篇相關的讀書筆記,但感覺一次一種模式的介紹,節奏太慢,就用這篇總結式的文章來把19種設計模式一次介紹完。

文章的短版本:全書內容思維導圖

以下是《遊戲程式設計模式》一書的內容梗概,全書內容19種模式的思維導圖:

本文閱讀說明

本文按照《遊戲程式設計模式》書中順序,對全書的19種模式分以下三個方面進行了介紹:

  • 要點
  • 使用場合
  • 引申與參考

依次介紹完19種模式之後,最終給出了一些更多的參考與學習資源。

需要注意,設計模式本身在理解上就比較抽象。而因為本文是設計模式內容的總結式介紹,所以理解坡度自然會比較陡。若總結的部分不太理解的地方,建議大家去閱讀原文(在每種模式的“引申與參考”一部分都已經給出了連結),將這篇文章對原文的總結與原文結合起來理解,這樣掌握起來會比較順暢。

一、常用GOF設計模式

這一部分介紹了遊戲開發中較為常用的六種GOF設計模式:

  • 命令模式
  • 享元模式
  • 觀察者模式
  • 原型模式
  • 單例模式
  • 狀態模式

1.  命令模式 Command Pattern

命令模式將“請求”封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件,同時支援可撤消的操作。

要點

  • 將一組行為抽象為物件,這個物件和其他物件一樣可以被儲存和傳遞,從而實現行為請求者與行為實現者之間的鬆耦合,這就是命令模式。
  • 命令模式是回撥機制的面向物件版本。
  • 命令模式的本質是對命令進行封裝,將發出命令的責任和執行命令的責任分割開。
  • 命令模式的優點有:對類間解耦、可擴充套件性強、易於命令的組合維護、易於與其他模式結合,而缺點是會導致類的膨脹。
  • 命令模式有不少的細分種類,實際使用時應根據當前所需來找到合適的設計方式。

使用場合

  • 命令模式很適合實現諸如撤消,重做,回放,時間倒流之類的功能。
  • 基於命令模式實現錄影與回放等功能,也就是執行並解析一系列經過預錄製的序列化後的各玩家操作的有序命令集合。

引申與參考

2. 享元模式 Flyweight Pattern

享元模式,以共享的方式高效地支援大量的細粒度的物件。通過複用記憶體中已存在的物件,降低系統建立物件例項的效能消耗。

要點

  • 享元模式中有兩種狀態。內蘊狀態(Internal State)和外蘊狀態(External State)。
    • 內蘊狀態,是不會隨環境改變而改變的,是儲存在享元物件內部的狀態資訊,因此內蘊狀態是可以共享的。對任何一個享元物件而言,內蘊狀態的值是完全相同的。
    • 外蘊狀態,是會隨著環境的改變而改變的。因此是不可共享的狀態,對於不同的享元物件而言,它的值可能是不同的。
  • 享元模式通過共享內蘊狀態,區分外蘊狀態,有效隔離系統中的變化部分和不變部分。

使用場合

在以下情況都成立時,適合使用享元模式:

  • 當系統中某個物件型別的例項較多的時候。
  • 由於使用了大量的物件,造成了很大的儲存開銷。
  • 物件的大多數狀態都可變為外蘊狀態。
  •  在系統設計中,物件例項進行分類後,發現真正有區別的分類很少的時候。

引申與參考

3. 觀察者模式 Observer Pattern

觀察者模式定義了物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。

要點

  • 觀察者模式定義了物件間的一種一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並被自動更新。
  • 我們知道,將一個系統分割成一個一些類相互協作的類有一個不好的副作用,那就是需要維護相關物件間的一致性。我們不希望為了維持一致性而使各類緊密耦合,這樣會給維護、擴充套件和重用都帶來不便。觀察者就是解決這類的耦合關係的。
  • 目前廣泛使用的MVC模式,究其根本,是基於觀察者模式的。
  • 觀察者模式應用廣泛,Java甚至將其放到了核心庫之中(java.util.Observer),而C#直接將其嵌入了語法(event關鍵字)中。

使用場合

  • 當一個抽象模式有兩個方面,其中一個方面依賴於另一個方面,需要將這兩個方面分別封裝到獨立的物件中,彼此獨立地改變和複用的時候。
  • 當一個系統中一個物件的改變需要同時改變其他物件內容,但是又不知道待改變的物件到底有多少個的時候。
  • 當一個物件的改變必須通知其他物件作出相應的變化,但是不能確定通知的物件是誰的時候。

引申與參考

4.  原型模式 Prototype Pattern

用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。

要點

  • 原型模式:用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。
  • 原型模式是一種比較簡單的模式,也非常容易理解,實現一個介面,重寫一個方法即完成了原型模式。在實際應用中,原型模式很少單獨出現。經常與其他模式混用,他的原型類Prototype也常用抽象類來替代。
  • 使用原型模式拷貝物件時,需注意淺拷貝與深拷貝的區別。
  • 原型模式可以結合JSON等資料交換格式,為資料模型構建原型。

使用場合

  • 產生物件過程比較複雜,初始化需要許多資源時。
  • 希望框架原型和產生物件分開時。
  • 同一個物件可能會供其他呼叫者同時呼叫訪問時。

參考與引申

5.  單例模式 Singleton Pattern

保證一個類只有一個例項,並且提供了訪問該例項的全域性訪問點。

要點

  • 單例模式因其方便的特性,在開發過程中的運用很多。
  • 單例模式有兩個要點,保證一個類只有一個例項,並提供訪問該例項的全域性訪問點。
  • 儘量少用單例模式。單例模式作為一個全域性的變數,有很多全域性的變數的弊病。它會使程式碼更難理解,更加耦合,並且對並行不太友好。

使用場合

  • 當在系統中某個特定的類物件例項只需要有唯一一個的時候。
  • 單例模式要儘量少用,無節制的使用會帶來各種弊病。
  • 為了保證例項是單一的,可以簡單的使用靜態類。 還可以使用靜態標識位,在執行時檢測是不是隻有一個例項被建立了。

參考與引申

6.  狀態模式 State Pattern

允許物件在當內部狀態改變時改變其行為,就好像此物件改變了自己的類一樣。

要點

  • 狀態模式用來解決當控制一個物件狀態轉換的條件表示式過於複雜的情況,它把狀態的判斷邏輯轉移到表示不同的一系列類當中,可以把複雜的邏輯判斷簡單化。
  • 狀態模式的實現分為三個要點:
    • 為狀態定義一個介面。
    • 為每個狀態定義一個類。
    • 恰當地進行狀態委託。
  • 通常來說,狀態模式中狀態物件的存放有兩種實現存放的思路:
    • 靜態狀態。初始化時把所有可能的狀態都new好,狀態切換時通過賦值改變當前的狀態。
    • 例項化狀態。每次切換狀態時動態new出新的狀態。

使用場合

  • 在遊戲開發過程中,涉及到複雜的狀態切換時,可以運用狀態模式以及狀態機來高效地完成任務。
  • 有限狀態機的實現方式,有兩種可以選擇:
    • 用列舉配合switch case語句。
    • 用多型與虛擬函式(即狀態模式)。
  • 有限狀態機在以下情況成立時可以使用:
    • 有一個行為基於一些內在狀態的實體。
    • 狀態可以被嚴格的分割為相對較少的不相干專案。
    • 實體可以響應一系列輸入或事件。

參考與引申

二、序列型模式 Sequencing Patterns

本章的三種模式都是遊戲開發中的常客:

  • 遊戲迴圈是遊戲執行的主心骨。
  • 遊戲物件通過更新方法來進行每幀的更新。
  • 我們可以用雙緩衝模式儲存快照,來隱藏計算機的順序執行,從而使得遊戲世界能夠同步更新。

7.  雙緩衝模式 Double Buffer

雙緩衝模式,使用序列操作來模擬瞬間或者同時發生的事情。

要點

  • 一個雙緩衝類封裝了一個緩衝:一段可改變的狀態。這個緩衝被增量的修改,但我們想要外部的程式碼將其視為單一的元素修改。 為了實現這點,雙緩衝類需儲存兩個緩衝的例項:下一快取和當前快取。
  • 當資訊從緩衝區中讀取,我們總是去讀取當前的緩衝區。當資訊需要寫到快取,我們總是在下一緩衝區上操作。 當改變完成後,一個交換操作會立刻將當前緩衝區和下一緩衝區交換, 這樣新緩衝區就是公共可見的了。舊的緩衝區則成為了下一個重用的緩衝區。
  • 雙緩衝模式常用來做幀緩衝區交換。

使用場合

雙緩衝模式是那種你需要它時自然會想起來的模式。以下情況都滿足時,使用這個模式很合適:

  • 我們需要維護一些被增量修改的狀態
  • 在修改過程中,狀態可能會被外部請求。
  • 我們想要防止請求狀態的外部程式碼知道內部是如何工作的。
  • 我們想要讀取狀態,而且不想在修改的時候等待。

引申與參考

8.  遊戲迴圈模式 Game Loop

遊戲迴圈模式,實現遊戲執行過程中對使用者輸入處理和時間處理的解耦。

要點

  • 遊戲迴圈模式:遊戲迴圈在遊戲過程中持續運轉。每迴圈一次,它非阻塞地處理使用者的輸入,更新遊戲狀態,並渲染遊戲。它跟蹤流逝的時間並控制遊戲的速率。
  • 遊戲迴圈將遊戲的處理過程和玩家輸入解耦,和處理器速度解耦,實現使用者輸入和處理器速度在遊戲行進時間上的分離。
  • 遊戲迴圈也許需要與平臺的事件迴圈相協調。如果在作業系統的高層或有圖形UI和內建事件迴圈的平臺上構建遊戲, 那就有了兩個應用迴圈在同時運作,需要對他們進行相應的協調。

使用場合

任何遊戲或遊戲引擎都擁有自己的遊戲迴圈,因為遊戲迴圈是遊戲執行的主心骨。

引申與參考

9.  更新方法 Update Method

更新方法,通過每次處理一幀的行為來模擬一系列獨立物件。

要點

  • 更新方法模式:在遊戲中保持遊戲物件的集合。每個物件實現一個更新方法,以處理物件在一幀內的行為。每一幀中,遊戲迴圈對集合中的每一個物件進行更新。
  • 當離開每幀時,我們也許需要儲存下狀態,以備不時之需。

使用場合

更新方法和遊戲迴圈模式一般一起使用。更新方法適應以下情況:

  • 遊戲中有很多物件或系統需要同時執行。
  • 每個物件的行為都與其他的大部分獨立。
  • 遊戲中的物件需要隨時間模擬。

引申與參考

  • 更新方法模式,以及遊戲迴圈模式和元件模式,是構建遊戲引擎核心的鐵三角。
  • Unity引擎在多個類中使用了這個模式,包括MonoBehaviour。
  • 微軟的XNA框架在 Game 和GameComponent 類中使用了這個模式。
  • 當你關注在每幀中更新實體或元件的快取效能時,資料區域性性模式可以幫上忙。

三、行為型模式 Behavioral Patterns

本章的模式可以幫助我們快速定義和完善多種多樣的行為:

  • 型別物件定義行為的類別而無需完成真正的類。
  • 子類沙盒定義各種行為的安全原語。
  • 位元組碼,將行為從程式碼中拖出,放入資料。

10. 位元組碼模式 Bytecode

位元組碼模式,將行為編碼為虛擬機器器上的指令,來賦予其資料的靈活性。從而讓資料易於修改,易於載入,並與其他可執行部分相隔離。

要點

  • 位元組碼模式:指令集定義了可執行的底層操作。一系列的指令被編碼為位元組序列。 虛擬機器使用中間值堆疊 依次執行這些指令。 通過組合指令,可以定義複雜的高層行為。
  • 可以理解為專案中的轉表工具,將excel中的資料轉為二進位制資料,並讀取到工程中,如在專案中使用googleprotobuf或json。
  • 位元組碼類似GOF的直譯器模式,這兩種方式都能讓我們將資料與行為相組合。其實很多時候都是兩者一起使用。用來構造位元組碼的工具會有內部的物件樹,而為了編譯到位元組碼,我們需要遞歸回溯整棵樹,就像用直譯器模式去解釋它一樣。唯一的不同在於,並不是立即執行一段行為,而是生成整個位元組碼再執行

使用場合

這是GPP一書中最複雜的模式,不能輕易的加入到遊戲中。 當我們需要定義很多行為,而遊戲實現語言因為以下原因不能很好地完成任務時,就可以使用位元組碼模式:

  • 這些行為過於底層,繁瑣易錯。
  • 這些行為遍歷起來很緩慢,導致編譯時間長。
  • 這些行為太受依賴。如果想保證行為不會破壞遊戲,你需要將其與程式碼的其他部分隔開。

如果是上述的這些情況,就比較適合使用位元組碼模式。

但需要注意,位元組碼比原生代碼慢,所以最好不要用於引擎對效能敏感的部分。

引申與參考

11. 子類沙箱模式 Subclass Sandbox

用一系列由基類提供的操作定義子類中的行為。

要點

子類沙箱模式:基類定義抽象的沙箱方法和幾個提供操作的實現方法,將他們設為protected,表明它們只為子類所使用。每個推匯出的沙箱子類用提供的操作實現了沙箱方法。

使用場合

子類沙箱模式是潛伏在程式設計日常中簡單常用的模式,哪怕是在遊戲之外的地方。 如果有一個非虛的protected方法,你可能早已在用類似的技術了。

沙箱方法在以下情況適用:

  • 你有一個能推導很多子類的基類。
  • 基類可以提供子類需要的所有操作。
  • 在子類中有行為重複,你想要更容易的在它們間分享程式碼。
  • 你想要最小化子類和程式的其他部分的耦合。

引申與參考

12. 型別物件模式 Type Object

創造一個類A來允許靈活的創造新的類,而類A的每個例項都代表了不同型別的物件。

要點

  • 型別物件模式:定義型別物件類與有型別的物件類。每個型別物件例項代表一種不同的邏輯型別。每種有型別的物件儲存描述它型別的對型別物件的引用。
  • 型別物件的基本思想就是給基類一個品種類(breed類),而不是用一些子類繼承自這個基類。所以我們在做種類區分的時候就可以只有兩個類,怪物類monster和品種類breed,而不是monster,dragon,troll等一堆類。所以在此種情況下,遊戲中的每個怪物都是怪物類的一個例項,而例項中的breed類包含了所有同種型別怪物共享的資訊。

使用場合

這個模式在任何你需要定義不同“種”事物,使用不當會讓你的系統過於僵硬。而下面兩者之一成立時,就非常適合使用:

  • 不知道後續還需什麼新型別。(舉個例子,如果你的遊戲需要支援增量更新,讓使用者下載後續新包含進來的怪物品種)
  • 想要不改變程式碼或不重新編譯就能修改或新增新型別。

引申與參考

  • 這個模式引出的進階問題是如何在不同物件之間共享資料。以不同的方式解決同一個問題的是GOF設計模式中的原型模式(prototype pattern)。
  • 型別物件是GOF設計模式中享元模式的親兄弟。兩者都讓你在例項間分享程式碼。使用享元,意圖是節約記憶體,而分享的資料也許不代表任何概念上物件的“型別”。而使用型別物件模式,焦點在組織性和靈活性。
  • 這個模式和GOF設計模式中狀態模式有很多相似之處,兩者都是委託了物件的部分定義給另外一個物件。

四、解耦型模式  Decoupling Patterns

這一部分的三種模式,專注於解耦:

  • 元件模式將一個實體拆成多個,解耦不同的領域。
  • 事件佇列解耦了兩個互相通訊的事物,穩定而且實時。
  • 服務定位器讓程式碼使用服務而無需繫結到提供服務的程式碼上。

13. 元件模式 Component

允許單一的實體跨越多個領域,無需這些領域彼此耦合。

要點

  • 元件模式:在單一實體跨越了多個領域時,為了保持領域之間相互解耦,可以將每部分程式碼放入各自的元件類中,將實體簡化為元件的容器。
  • Unity引擎在設計中頻繁使用了這種設計方法,從而讓其易於使用。

使用場合

元件通常在定義遊戲實體的核心部分中使用,當然,它們在其他地方也適用。這個模式在如下情況下可以很好的適用:

  • 有一個涉及了多個領域的類,而你想保持這些領域互相隔離。
  • 一個類正在變大而且越來越難以使用。
  • 想要能定義一系列分享不同能力的類,但是使用介面不足以得到足夠的重用部分。

引申與參考

  • Unity核心架構中GameObject類完全根據此模式來進行設計。
  • 這種模式與GOF設計模式中的策略模式類似。兩種模式都是將物件的行為取出,委派到一個單獨的從屬物件中。兩者的不同點在於:
  • 策略模式中分離出的策略物件通常是無狀態的——它封裝的是演算法,而不是資料。策略模式定義了物件的行為,而不是該物件是什麼。
  • 而元件模式就更加複雜。元件經常儲存了物件的狀態,這有助於確定其真正的身份。但是,其界限往往很模糊。有些情況下元件也許根本沒有任何狀態。在這種情況下,你可以在不同的容器物件中使用相同的元件例項。這樣看來,它的行為確實更像一種策略。

14. 事件佇列模式 Event Queue

事件佇列模式,對訊息或事件的傳送與處理進行時間上的解耦。

要點

  • 事件佇列:在先入先出的佇列中儲存一系列通知或請求。傳送通知時,將請求放入佇列並返回。處理請求的系統在稍晚些的時候從佇列中獲取請求並進行處理。 這樣就解耦了傳送者和接收者,既靜態又及時。
  • 事件佇列很複雜,會對遊戲架構引起廣泛影響。中心事件佇列是一個全域性變數。這個模式的通常方法是一個大的交換站,遊戲中的每個部分都能將訊息送過這裡。
  • 事件佇列是基礎架構中很強大的存在,但有些時候強大並不代表好。事件佇列模式將狀態包裹在協議中,但是它還是全域性的,仍然存在全域性變數引發的一系列危險。

使用場合

  • 如果你只是想解耦接收者和傳送者,像觀察者模式和命令模式都可以用較小的複雜度來進行處理。在需要解耦某些實時的內容時才建議使用事件佇列。
  • 不妨用推和拉來的情形來考慮。有一塊程式碼A需要另一塊程式碼B去做些事情。對A自然的處理方式是將請求推給B。同時,對B自然的處理方式是在B方便時將請求拉入。當一端有推模型另一端有拉模型時,你就需要在它們間放一個緩衝的區域。 這就是佇列比簡單的解耦模式多出來的那一部分。佇列給了程式碼對拉取的控制權——接收者可以延遲處理,合併或者忽視請求。傳送者能做的就是向佇列傳送請求然後就完事了,並不能決定什麼時候傳送的請求會受到處理。
  • 而當傳送者需要一些回覆反饋時,佇列模式就不是一個好的選擇。

引申與參考

  • 很大程度上,事件佇列模式就是廣為人知的GOF設計模式中觀察者模式的非同步實現。
  • 就像其他很多模式一樣,事件佇列有很多別名。其中一個是“訊息佇列”。 訊息佇列通常指代一個更高層次的實現。可以這樣理解,事件佇列在應用中進行交流,而訊息佇列通常在應用間進行交流。另一個別名是“釋出/提交”,有時被縮寫為“pubsub”,這個別名通常指代更大的分散式系統中的應用。
  • 在有限狀態機與狀態模式中,往往需要一個輸入流。如果想要非同步響應,可以考慮用佇列模式來儲存它們。
  • Go語言內建的“Channel”機制,其本質上就是事件佇列。

15. 服務定位模式 Service Locator

提供服務的全域性接入點,而不必讓使用者和實現它的具體類耦合。

要點

  • 服務定位模式:服務類定義了一堆操作的抽象介面。具體的服務提供者實現這個介面。 分離的服務定位器提供了通過查詢合適的提供者, 獲取服務的方法,同時隱藏了提供者的具體細節和需要定位它的程序。
  • 一般通過使用單例或者靜態類來實現服務定位模式,提供服務的全域性接入點。
  • 服務定位模式可以看做是更加靈活,更加可配置的單例模式。如果用得好,它能以很小的執行時開銷,換取很大的靈活性。相反,如果用得不好,它會帶來單例模式的所有缺點以及更多的執行時開銷。
  • 使用服務定位器的核心難點是它將依賴,也就是兩塊程式碼之間的一點耦合,推遲到執行時再連線。這有了更大的靈活度,但是代價是更難在閱讀程式碼時理解其依賴的是什麼。

使用場合

  • 服務定位模式在很多方面是單例模式的親兄弟,在應用前應該考慮看看哪個更適合你的需求。
  • 讓大量內容在程式的各處都能被訪問時,就是在製造混亂。對何時使用服務定位模式的最簡單的建議就是:儘量少用。
  • 與其使用全域性機制讓某些程式碼直接接觸到它,不妨先考慮將物件傳過來。因為這樣可以明顯地保持解耦,而且可以滿足我們大部分的需求。當然,有時候不方便手動傳入物件,也可以使用單例的方式。

引申與參考

  • Unity引擎在它的GetComponent()方法中使用了這個模式,協助元件模式的使用,方便隨時獲取到指定的元件。
  • 微軟的XNA框架將這個模式內嵌到它的核心類Game中。每個例項有一個 GameServices 物件,能夠用來註冊和定位任何型別的服務。

五、優化型模式 Optimization Patterns

這一部分,描述了幾個優化和加速遊戲的中間層模式:

  • 資料區域性性介紹了計算機的儲存層次以及如何使用其以獲得優勢。
  • 髒標識幫我們避開不必要的計算。
  • 物件池幫我們避開不必要的分配。
  • 空間分割槽加速了虛擬世界和其中內容的空間佈局。

16. 資料區域性性模式 Data Locality

合理組織資料,充分使用CPU的快取來加速記憶體讀取。

要點

  • 現代的CPU有快取來加速記憶體讀取,其可以更快地讀取最近訪問過的記憶體毗鄰的記憶體。基於這一點,我們通過保證處理的資料排列在連續記憶體上,以提高記憶體區域性性,從而提高效能。
  • 為了保證資料區域性性,就要避免的快取不命中。也許你需要犧牲一些寶貴的抽象。你越圍繞資料區域性性設計程式,就越放棄繼承、介面和它們帶來的好處。沒有銀彈,只有權衡。

使用場合

  • 使用資料區域性性的第一準則是在遇到效能問題時使用。不要將其應用在程式碼庫不經常使用的角落上。 優化程式碼後其結果往往更加複雜,更加缺乏靈活性。
  • 就本模式而言,還得確認你的效能問題確實由快取不命中而引發的。如果程式碼是因為其他原因而緩慢,這個模式自然就不會有幫助。
  • 簡單的效能評估方法是手動新增指令,用計時器檢查程式碼中兩點間消耗的時間。而為了找到糟糕的快取使用情況,知道快取不命中有多少發生,又是在哪裡發生的,則需要使用更加複雜的工具—— profilers。
  • 元件模式是為快取優化的最常見例子。而任何需要接觸很多資料的關鍵程式碼,考慮資料區域性性都是很重要的。

引申與參考

17. 髒標識模式 Dirty Flag

將工作延期至需要其結果時才去執行,以避免不必要的工作。

要點

  • 髒標記,就是用來表示被標記的內容是否有被修改過的一個標誌位。
  • 髒標識模式:考慮情況,當前有一組原始資料隨著時間變化而改變。由這些原始資料計算出目標資料需要耗費一定的計算量。這個時候,可以用一個髒標識,來追蹤目前的原始資料是否與之前的原始資料保持一致,而此髒標識會在被標記的原始資料改變時改變。那麼,若這個標記沒被改變,就可以使用之前快取的目標資料,不用再重複計算。反之,若此標記已經改變,則需用新的原始資料計算目標資料。

使用場合

  • 就像其他優化模式一樣,此模式會增加程式碼複雜度。只在有足夠大的效能問題時,再考慮使用這一模式。
  • 髒標記在這兩種情況下適用:
    • 當前任務有昂貴的計算開銷
    • 當前任務有昂貴的同步開銷。

若滿足這兩者之一,也就是兩者從原始資料轉換到目標資料會消耗很多時間,都可以考慮使用髒標記模式來節省開銷。

  • 若原始資料的變化速度遠高於目標資料的使用速度,此時資料會因為隨後的修改而失效,此時就不適合使用髒標記模式。

引申與參考

18. 物件池模式 Object Pool

放棄單獨地分配和釋放物件,從固定的池中重用物件,以提高效能和記憶體使用率。

要點

  • 物件池模式:定義一個包含了一組可重用物件的物件池。其中每個可重用物件都支援查詢“使用中”狀態,說明它是不是“正在使用”。 物件池被初始化時,就建立了整個物件集合(通常使用一次連續的分配),然後初始化所有物件到“不在使用中”狀態。
  • 當我們需要新物件時,就從物件池中獲取。從物件池取到一個可用物件,初始化為“使用中”然後返回給我們。當不再需要某物件時,將其設定回“不在使用中”狀態。 通過這種方式,便可以輕易地建立和銷燬物件,而不必每次都分配記憶體或其他資源。

使用場合

  • 這個模式廣泛使用在可見事物上,比如遊戲物體和特效。但是它也可在不那麼視覺化的資料結構上使用,比如正在播放的聲音。
  • 滿足以下情況可以使用物件池:
    • 需要頻繁建立和銷燬物件。
    • 物件大小相仿。
    • 在堆上分配物件緩慢或者會導致記憶體碎片。
    • 每個物件都封裝了像資料庫或者網路連線這樣很昂貴又可以重用的資源。

引申與參考

  • 物件池模式與GOF設計模式中享元模式類似。 兩者都控制了一系列可重用的物件。不同在於重用的含義。
    • 享元物件分享例項間同時擁有的相同部分。享元模式在不同上下文中使用相同物件避免了重複記憶體使用。
    • 物件池中的物件也被重用了,但是是在不同的時間點上被重用的。重用在物件池中意味著物件在原先的物件用完之後再分配記憶體。物件池的物件不會在它的生命週期中與其他物件共享資料。
  • 將記憶體中同樣型別的物件進行整合,能確保在遍歷物件時CPU快取是滿載的。這便是資料區域性性模式中介紹的內容。

19. 空間分割槽模式 Spatial Partition

將物件儲存在基於位置組織的資料結構中,來有效的定位物件。

要點

  • 對於一系列物件,每個物件都有空間上的位置。將它們儲存在根據位置組織物件的空間資料結構中,讓我們有效查詢在某處或者附近的物件。 當物件的位置改變時,更新空間資料結構,這樣它可以繼續找到物件。
  • 最簡單的空間分割槽:固定網格。想象某即時戰略類遊戲,一改在單獨的陣列中儲存我們的遊戲物件的常規思維,我們將它們存到網格的格子中。每個格子儲存一組單位,它們的位置在格子的邊界內部。當我們處理戰鬥時,一般只需考慮在同一格子或相鄰格子中的單位,而不是將每個遊戲中的單位與其他所有單位比較,這樣就大大節約了計算量。

使用場合

  • 空間分割槽模式在需要大量儲存活躍、移動的遊戲物體,和靜態的美術模型的遊戲中比較常用。因為複雜的遊戲中不同的內容有不同的空間劃分。
  • 這個模式的基本適用場景是你有一系列有位置的物件,當做了大量通過位置尋找物件的查詢而導致效能下降的時候。
  • 空間分割槽的存在是為了將O(n)或者O(n²) 的操作降到更加可控的數量級。 你擁有的物件越多,此模式就越好用。相反的,如果n足夠小,也許就不需要使用此模式。

引申與參考

  • 瞭解了空間分割槽模式,下一步應該是學習一下常見的結構。常見的有:
  • 每種空間劃分資料結構基本上都是將一維資料結構擴充套件成更高維度的資料結構。而瞭解它的直系子孫,有助於分辨其對當前問題的解答是不是有幫助: