《遊戲設計模式》(遊戲編程模式)全書筆記+Unity實現

《遊戲設計模式》(遊戲編程模式)全書筆記+Unity實現

來自專欄技術美術 Shading Artist 小屋93 人贊了文章

Unity實現(Github地址):

1. 命令模式 2. 享元模式 3. 觀察者模式 4. 原型模式 5. 單例模式

6. 狀態模式 7. 序列模式 8. 行為模式 9. 解耦模式 10. 優化模式


筆記部分

  • 以下部分只包含筆記,具體實現及項目說明可查看Github
  • 筆記中很多都是個人理解,目的是盡量讓原本抽象的概念更易懂一些
  • 關於書名——書名直譯是《遊戲編程模式》,但在中文版致謝頁中,被翻譯成了《遊戲設計模式》,「設計模式」一詞來源於GOF。此書(GPP)雖與編程有關,但更本質上是設計思想,所以更傾向「設計模式」這個翻譯
  • 鏈接
    • 英文原版
    • 中文版

命令模式

是什麼(個人理解)

將命令封裝,與目標行為解耦,使命令由流程概念變為對象數據

為什麼

既然命令變成了數據,就是可以被傳遞、存儲、重複利用的:

  • 通過命令數據隊列或棧可以輕易實現撤銷、重做、時光倒流
  • 命令數據還可以形成日誌,用於復現用戶行為,便於重複測試同樣序列命令對各種目標的影響
  • 這些命令數據可以發送給不同的目標,比如同樣的「出發,5分鐘後,停止」,發送給飛機就可以變成「起飛,5分鐘後,降落」,發送給輪船就成了「離港,5分鐘後,拋錨」

怎麼做(U3D示例)

類圖如下:

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標

缺陷

可能會導致大量的實例化,從而浪費內存

拓展

可用享元模式代替大量的實例化


享元模式

是什麼(個人理解)

不同的實例共享相同的特性(共性),同時保留自己的特性部分

為什麼

  • 傳遞的信息享元化,可以節約計算時間
  • 存儲的信息享元化,可以節約佔用的空間
  • 所以享元模式可以減少時間與空間上的代價

怎麼做

類圖如下:

左半部分為享元模式下,只有一個CubeBase,通過ObjInstancing(int num)將共享的網格、材質及一個Transform信息表傳遞給GPU,只有一個Draw Call,所以效率極高

右半部分為關閉享元模式後的做法,每生成一個Cube都會重新實例化一個立方體,並向GPU發送一次網格、材質和位置信息,所以1000個立方體就需要1000個Draw Call,效率極低

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標

拓展

可與對象池聯動,進一步減少內存的開銷


觀察者模式

是什麼(個人理解)

事件與其他對象行為的解耦——例如一個代碼描述了日本核電站爆炸的事件,世界人民買鹽這種行為顯然不應該由核電站爆炸直接調用,而是通過衛星電視告訴廣大群眾,群眾想買鹽還是想買仙人掌就由他們自己決定了~

為什麼

  • 解耦,物價局改了糧價不需要挨家挨戶通知公民,只需要讓電視台播個新聞就好
  • 如果要挨家挨戶通知,物價局必須有每個公民的地址,這顯然不合理,也會浪費很多資源
  • 擴展困難——如果公民改了地址或者有新公民出生了,那還需要告訴物價局,這也很荒唐

怎麼做

類圖如下:

射手(Shooter,觀察者,這裡是聽眾)告訴廣播電台(Radio)自己要聽發射氣球的廣播

吹氣球的人(Emitter) 向上發出氣球,並告訴廣播電台自己發射了氣球

廣播電台廣播發射了氣球的消息,所有射手向氣球射擊

這個例子中吹氣球的人不會關心誰是射手,射手也不用在意誰是吹氣球的人

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標


原型模式

是什麼(個人理解)

將一個或多個對象當做原型,通過統一的生成器克隆出很多類似原型的對象,同時可以通過配置表更改克隆體屬性,製造出很多具有自身個性的對象。

為什麼

  • 復用生成器,而非針對每一個不同的對象做一個生成器
  • 與享元模式結合,通過配置表來實現對象的個性,將不同配置與代碼解耦

怎麼做

類圖如下:

  • Unity中Prefab本質就是此模式里的原型,而Spawner要做的只是調用Instantiate方法
  • 新的Prefab被生成以後,通過讀取Dragons.txt里配置的信息來設置克隆體的名稱和尺寸

註:這裡為了快速實現使用txt記錄配置表(我在偷懶),但實際項目里,往往使用SQL、Json、csv等方式進行配置

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標


單例模式

是什麼(個人理解)

使用單例意味著這個對象只有一個實例,這個實例是此對象自行構造的,並且可向全局提供

為什麼

  • 減少代碼復用,讓專門的類處理專門的事情——例如讓TimeLog類來記錄日誌,而不是把StreamWriter的代碼寫到每一個類里
  • 快速訪問,任何其他類都可以通過ClassName.Instance來訪問單例,使用它的公開變數和方法

缺陷

  • 因為實現簡單,而且使用方便,所以有被濫用的趨勢
  • 濫用單例會促進耦合的發生,因為單例是全局可訪問的,如果不該訪問者訪問了單例,就會造成過耦合——例如如果播放器允許單例,那石頭碰撞地面後就可以直接調用播放器來播放聲音,這在程序世界並不合理,而且會破壞架構
  • 如果很多很多類和對象調用了某個單例並做了一系列修改,那想理解具體發生了什麼就困難了
  • 對多線程不太友好——每個線程都可以訪問這個單例,會產生初始化、死鎖等一系列問題

怎麼做

U3D中利用MonoBehaviour初始化單例非常簡單,只要在Awake中加入Instance = this,不過要注意的是,別的類不能在Awake里使用這個單例

單例在普通C#中還有其他做法,甚至有些泛型、線程安全的擴展,也都不複雜,可以自行查詢

類圖如下:

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標


狀態模式

是什麼(個人理解)

現在狀態和條件決定對象的新狀態,狀態決定行為(Unity內AnimationController就是狀態機)

為什麼

  • 使流程清晰化、結構化
  • 簡化判斷邏輯,比如嘴的狀態是洗牙,那就不應該做出咀嚼的行為;必須是在憋氣,那就不應該做出呼吸的行為

註解

  • 狀態機(自動機)是我最喜歡的一種設計模式,因為這樣設計的程序邏輯清晰,穩定性也很強
  • 作者對switch case下的狀態機理解並不深刻,一般情況下,狀態機需要兩個switch case,一個用於處理狀態變化,另一個用來處理狀態行為
  • 相比狀態類,個人更喜歡switch case的方法,雖然狀態類有其有點,但缺點也非常明顯——當狀態量較大時,代碼量激增,可讀性也很差,狀態變化和狀態行為都需要大量的信息傳遞,十分不便

怎麼做

這次我實現了兩個版本:

  1. SwitchCase版本,用按鍵控制一盞冷暖燈,關燈狀態下,按一次打開暖光,再按切換為白光,再按變為暖白光,再按關閉
  2. 狀態類版本,交通燈 停止、通行、閃爍、等待的切換

另外自動機用類圖描述不是好方法,應該用自動機專門的圖來說明才對

SwitchCase版本類圖及自動機如下:

StateClass版本類圖及自動機如下:

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標


序列模式

是什麼、為什麼(個人理解)

包含了

  • 雙緩衝模式
    • 當一個緩衝準備好後才會被使用——就像一個集裝箱裝滿才會發貨一樣;當一個緩衝被使用時另一個處於準備狀態,就形成了雙緩衝
    • 在渲染中廣泛使用,一幀準備好後才會被渲染到屏幕上——所以準備時間太長就會導致幀率下降
  • 遊戲循環
    • 可參考腳本生命周期
  • 更新方法
    • 同上,實際上是Unity通過反射在生命周期不同時刻調用MonoBehaviour中的相關方法

這三者一定程度上是相輔相成的,在Unity中都已在底層實現,雙緩衝可以通過FrameDebugger體會,而遊戲循環、更新方法則與腳本生命周期和MonoBehaviour相關


行為模式

是什麼、為什麼(個人理解)

包含了

  • 位元組碼
    • 享元模式、原型模式中,不同的屬性被存儲在資料庫中,而位元組碼是將行為存在資料庫中
    • 可用於實現可視化腳本編輯工具
  • 子類沙箱
    • 子類使用基類方法,或在基類方法上擴展
  • 類型對象
    • 其實是享元模式、原型模式的一種應用,以不同數據(而不是不同類)區分對象類型

解耦模式

是什麼、為什麼(個人理解)

包含了

  • 組件模式
    • 本質上是功能的模塊化,延伸了面向對象的解耦思想
    • U3D的編程思想就是面向組件的,MonoBehaviour的子類都可作為組件掛在GameObject上
  • 事件序列
    • 就像銀行辦事需要排號一樣——每個顧客要處理的事都是一個事件,編號後就形成了天然的事件序列,銀行會按一定規則來依次處理隊列中的事件
    • 一般在底層實現,但宏觀上依然存在,例如RTS遊戲中通過Shift對一些單位下達前往不同位置的命令
    • Unity中協程可以用來做消息隊列,防止同幀產生大量的計算
  • 服務定位器
    • 類似單例模式,在運行時尋找組件(而不是運行前賦值)
    • Unity中GetComponent,FindObjectOfType,Find等方法都可幫助實現相關服務的查找,但此類反射方法要避免在運行時高頻循環調用
    • 拓展——還可以建立一個運行前賦值的服務註冊中心(當然也可運行中賦值),其他需要服務的對象在運行時去註冊中心查找相關服務,這樣做一方面可以避免全局反射的惡果,一方面可以保留服務定位器帶來的解耦優勢——單例模式也可使用這樣的方法來替換(對象註冊中心)

怎麼做(事件隊列)

點擊滑鼠時在Queue中添加一個紅點,當目標點為空時從Queue中取出第一位位作為目標點,讓Player移向目標點,到達目標點時刪除目標點

具體實現:

https://github.com/TYJia/GameDesignPattern_U3D_Version/tree/master/Assets/009DecouplingPatterns?

github.com


優化模式

是什麼、為什麼(個人理解)

包含了

  • 數據局部性
    • CPU緩存讀寫速度大於內存讀寫速度,所以要盡量減少緩存不命中(CPU從內存讀取信息)的次數
    • 用連續隊列代替指針的不斷跳轉
    • 不過此模式會讓代碼更複雜,並傷害其靈活性
  • 臟標識模式
    • 需要結果時才去執行工作——避免不必要的計算或傳輸開銷
    • 一種是被動狀態變化時才計算,否則使用緩存;另一種是主動變化標識,否則不執行(例如存檔)
  • 對象池模式
    • 對象池就像一包不同顏色的水彩筆,當我們使用時就拿出來,不用時就放回去——而不是使用時就買一隻,不用時就扔進垃圾桶
    • 可以減少內存碎片,減少實例化與回收對象所面臨的開銷
  • 空間分區
    • 建立細分空間用於存儲數據(對象),可以幫助告訴定位對象,降低演算法複雜度
    • 例如郵局寄信,如果只按身份證號郵寄,那就麻煩了,每封信平均要拿給幾億人確認是否是ta的;但是按空間分區後,就簡單了——省份、城市、街道、小區、樓棟、單元、房號,於是很快就能定位到個人。

怎麼做(對象池)

用對象池對之前實現的例子做了優化:

  • 之前每次點擊滑鼠會生成一個目標點,Player到達目標點後會將目標點回收(Destroy)
  • 優化後點擊滑鼠,先會嘗試從對象池「未激活列表」獲取對象,無法獲取才會生成新對象並放入對象池中的「已激活列表」;Player到達目標點後,會把對象從已激活列表放入未激活列表,並執行SetActive(false)方法

怎麼做(空間分區)

  • 這裡我實現了一個八叉樹簡單示例,用來尋找最近的點
  • 建立
    • 先尋找空間邊界,建立父節點長方體
    • 若父節點中點數超過閾值,則分割成八個子節點長方體
  • 尋找最近的點
    • 在點所在的和臨近的立方體中尋找最近的點

因為只是示例,所以並未完善臨近立方體的查找,目前只用了八叉樹結構臨近的立方體,而非空間臨近,有興趣的同學可以進一步優化

  • 更新點
    • 先看點是否在之前的長方體里,如果不在,則從當前節點移除,並查詢是否在父節點裡
    • 如果在父節點裡,則向下查詢在哪一個子節點裡此示例只能更新點的位置,也就是八叉樹中的內容,不能更新八叉樹的結構,大家可以自行思考如何更新結構

此示例只能更新點的位置,也就是八叉樹中的內容,不能更新八叉樹的結構,大家可以自行思考如何更新結構

具體實現:

TYJia/GameDesignPattern_U3D_Version?

github.com圖標


前幾天有親人離世,願安息

望生者多體驗些人間的美好

推薦閱讀:

TAG:設計模式 | 遊戲編程模式書籍 | Unity遊戲引擎 |