遊戲設計模式(一) 序言:架構,性能與遊戲

第一次在知乎寫專欄,一個全新的開始,希望各位多多關照。

一、系列文章前言

這個系列的誕生,是因為最近閑暇時一直在閱讀一些之前已經列入待看書單的經典著作,並有將閱讀過程中一些思考和總結寫成文字進行記錄。為了不枉費這些閱讀、思考與總結的過程,決定將這些零散的內容整理成文,並集合成系列,將他們系統地記錄下來,也希望能對熱愛編程的各位有所幫助。

需要說明的是,雖說這些文章的原型是讀書筆記,但會採用和傳統讀書筆記不一樣的方式。他們不會僅僅停留在傳達作者含義的階段,而會是對一本書核心內容的全新演繹,內容解刨,精鍊與解讀。與其說是讀書筆記,不妨說是原著經過的解讀後的通俗版本。

希望自己的這些文章,能對各位有所幫助。也正如文章開頭所說的,第一次在知乎寫專欄,新的開始,希望各位多多指教。

二、《Game Programming Patterns》其書

之前,已經在我的技術博客中解讀了《Clean Code》(中譯「代碼整潔之道」)這本書,承接《Clean Code》的解讀,《Game Programming Patterns》是我們下一個目標。

原書是英文原版,Game Programming Patterns,譯為遊戲編程模式,我們不妨在後文中將其簡稱為GPP。根據全書內容,更貼切的詮釋應為遊戲設計模式。所以你會發現,我們這個系列文章的名稱,就叫「遊戲設計模式」。

一起來看看《Game Programming Patterns》這本書的內容。正如其名,它是一本專註於遊戲編程領域的設計模式指南,它涵蓋了遊戲邏輯,遊戲編輯器,和遊戲引擎的編程中的常用技法。作者Robert Nystrom有二十年的從業經驗,在EA工作8年有餘。

「這本書將遊戲開發中經常涉及到的編程模式拎出來,結合具體開發中遇到的實例一步步的引出對應的模式,這比起四人幫的《設計模式》更加具體。」

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

書本主頁:Game Programming Patterns

Web版全文閱讀:Table of Contents · Game Programming Patterns

三、本文涉及知識點思維導圖

先放出這篇文章所涉及內容知識點的一張思維導圖,就開始正文。大家若是疲於閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。(推薦放大後查看)。

四、何為好的軟體架構

《Game Programming Patterns》一書中說到,好的設計意味著當我改了點什麼, 整個程序就好像正在等著這種改動。我們可以加入幾個函數調用完成任務,同時絲毫不改變代碼平靜表面下的脈動。

這聽起來很酷,只是實行起來很難。「把代碼寫到改變不會影響其平靜表面。」若真能做到,確實不錯。

這樣太理想化了,還是讓我們通俗些吧。架構是有關於變化的,讓我們擁抱變化,從變化開始入手。總有人改動代碼。如果沒人碰代碼,無論是因為代碼至善至美,還是糟糕透頂,那麼它的架構設計就毫無意義。評價架構設計就是評價它應對變化有多麼輕鬆。沒有了變化,它就是永遠不會離開起跑線的運動員。

輕鬆應對變化,這就是好的軟體架構的主要優點之一。

五、一個新特性的實現過程

在你改變代碼去添加新特性,去修復漏洞,或者隨便什麼需要使用編輯器的時候, 你需要理解現在的代碼在做些什麼。當然,你不需要理解整個程序,但你需要將所有相關的東西裝進你的靈長類大腦。

我們通常無視了這步,但這往往是編程中最耗時的部分。 如果你認為將數據從磁碟上分頁到RAM上很慢, 那麼試著通過一對神經纖維將數據分頁到大腦中。

一旦把所有正確的上下文都記到了你的大腦里, 想一會,然後找到解決方案。 這可能會有來回打轉的時刻,但通常比較簡單。一旦你理解了問題和需要改動的代碼,實際的編碼工作就很容易了。

你將一些代碼加入了遊戲,但不想下一個人被你留下來的小問題絆倒。 除非改動很小,否則就還需要一些工作去微調新代碼,使之無縫對接到程序的其他部分。如果做對了,那麼下個見到代碼的人甚至無法說出哪些代碼是新加入的。

簡而言之,編程的流程圖看起來是這樣的:

PS:看起來,這是一個令不少程序員聽之色變的死循環:)

六、解耦與學習階段

其實,很多軟體架構都和學習階段(learning phase)息息相關。 將代碼載入到神經元太過緩慢,找些策略減少載入的總量是很值得做的事。GPP一書中有整整一章是關於解耦模式(decoupling patterns), 還有很多常規的設計模式也牽扯到了解耦。

可以用多種方式定義「解耦」,這邊是其中之一的理解方式:

如果有兩塊代碼是耦合的, 那就意味著無法僅僅只理解了其中一個,而對另一個絲毫不了解。如果解耦了他倆,就可以獨自的理解其中之一,根本無需牽扯到另一個。

GPP一書中說道,我所理解的軟體架構的關鍵目標,就是最小化在處理前需要進入大腦的知識。這也是一種很好的理解方式。

當然,也可以從後期階段來看。 那麼,另一種解耦的定義則是:當一塊代碼有變化時,沒必要修改另外的代碼。 肯定需要修改一些東西,但耦合程度越小,變化會波及的範圍就越小。

七、過度設計的代價

首先,我們需要這樣一個設想:在一個系統中,解耦掉任何內容,然後,風煙俱凈,天山共色,從流飄蕩,任意東西,就可以像風一樣寫代碼。每個變化都只修改一兩個特定方法,萬花叢中過,片葉不沾身,這是多麼的愜意,是吧?

這大概就是人們對抽象,模塊化,設計模式和軟體架構興奮的原因。在有好架構的程序上工作是很好的體驗,每個人都希望能更有效率地工作。好架構能造成生產力上巨大的不同。很難再誇大它那強力的影響。

但是,就像生活中的任何事物一樣,沒有免費的午餐。好的設計需要汗水和紀律。 每次做出改動或是實現特性,你都需要將它優雅的集成到程序的其他部分。需要花費大量的努力去管理代碼, 在開發過程中面對數千次變化仍然保持它的管理結構。

我們會看到無數程序有個優雅的開始,然後死於程序員一遍又一遍添加的「微小黑魔法」。就像園藝,僅僅增加新植物是不夠的,還需要除草和修剪。你得考慮程序的哪部分需要解耦,然後再引入抽象。同樣,你需要決定哪部分要設計得支持插件來方便未來的變化。(所謂的面向未來編程)。

人們對這點變得狂熱。他們設想以後的開發者(或者只是未來的他們自己)進入代碼庫,並發現它極為開放,功能強大,極具擴展性,他們會驚嘆道「有此遊戲引擎,夫復何求」。當過分關注這點時,你會得到失控的代碼庫。 介面和抽象無處不在。插件系統,抽象基類,虛方法,還有各種各樣的擴展點。當需求變更時,有可能某個介面能幫上忙,但能不能找到就只能祝你好運了。 理論上,解耦意味著在修改代碼之前需要了解的代碼更少,但其實你需要對抽象層有很多的了解。

還是那句話,理想很豐滿,現實很骨感。 每當你添加了一層抽象或者支持擴展的部分,其實就是在賭這部分功能以後是否用得上。 添加代碼和複雜性到遊戲中,這都需要時間來開發,調試和維護。如果你猜對了,後來使用了這些代碼,那麼功夫不負有心人。 但預測未來很難,如果模塊化最終無益,那就有害。 畢竟,你得花時間去實現這些代碼。

有些人喜歡簡寫為術語「YAGNI」——You arent gonna need it(你不需要那個)——來對抗預測將來需求的強烈慾望。

過度去關注設計模式和軟體架構,會讓一批人很容易地沉浸在代碼中,而忽略要自己的最終目的是要發布遊戲。無數的開發者聽著加強可擴展性的「警世名言」,花費多年時間製作「引擎」, 卻沒有搞清楚做引擎是為了什麼。

八、性能與速度

軟體架構和抽象有時會被批評,尤其是在遊戲開發中: 它傷害了遊戲的性能。 許多讓代碼更靈活的模式依靠虛擬調度、 介面、 指針、 消息,和其他機制, 而這些都會消耗運行時成本。

一個有趣的反面例子是C++中的模板。模板編程有時可以給你抽象介面而無需運行時開銷。

這是靈活性的兩極。當寫代碼調用類中的具體方法時,你已經硬編碼了調用的是哪個類:但通過虛方法或介面,直到運行時才知道調用的類。雖然這樣更加靈活,但增加了運行時開銷。

而模板編程是在兩者之間——在編譯時初始化模板,決定調用哪些類。

還有一個原因。很多軟體架構的目的是使程序更加靈活。 這讓改變它需要較少的努力。編碼時對程序有更少的假設。你可以使用介面,讓代碼可與任何實現它的類交互,而不僅僅是現在寫的類。靈活性可以讓我們快速改進遊戲。

讓你的程序更加靈活,在損失一點點性能的前提下更快地做出原型。 但需要注意,優化現有的代碼可能會讓代碼喪失原有的靈活性。

而一種折中的辦法是保持代碼靈活直到設計定下來,再抽出抽象層來提高性能。

九、爛代碼在原型階段的優勢

天下武功,唯快不攻。

野百合也有春天,之前在《Clean Code》中被我們吐槽的爛代碼,其實也有它們的優勢——快。

我們知道,編寫良好架構的代碼需要仔細地思考,這會轉為時間上的代價。 在項目的整個周期中保持良好的架構需要花費大量的努力。 你需要像露營者處理營地一樣小心處理代碼庫:總是保持其優於你剛剛接觸它的時候。就像我們之前《Clean Code》系列文章第一篇中說到的:讓代碼比你來時更乾淨。

當你要在項目上花費很久時間的話,保持編寫良好架構的代碼的習慣,是非常值得推崇的。但你知道,遊戲開發需要很多實驗、探索與試錯。 特別是在早期,寫一些你知道要扔掉的代碼是很普遍的事情。

而如果只想試試遊戲的某些主意是不是正確的, 良好的設計意味著在屏幕上看到和獲取反饋之前要消耗很長時間。如果最後證明這點子不對,那麼刪除代碼時,你花費的那些為了讓代碼更加優雅的額外時間,就白費了。

但你得讓人們清楚,可拋棄的代碼即使看上去能工作,也不能被維護,必須重寫。如果有可能要維護這段代碼,就得防禦性好好編寫它。

一個保證原型代碼不會變成真正使用的代碼的技巧是使用和正式遊戲不同的編程語言。這樣,在實際應用於正式遊戲中之前必須重寫。

在原型開發階段,能儘快讓你做出原型產品,最終讓產品成功上線的最初的功臣,或許就是設計糟糕的爛代碼。因為他們實現想法夠快,不需要縝密的設計與架構。只是這些爛代碼在經歷了原型設計階段之後,一定要被重寫或者重構。

十、開發周期中因素的動態平衡

在整個開發周期中,如下三大要素一直在相互角力:

  1. 為了在項目的整個生命周期保持其可讀性,我們需要好的架構。

  2. 需要更好的運行時性能。

  3. 需要讓現在的特性更快的實現。

有趣的是,這三點都是速度:長期開發的速度,遊戲運行的速度,和短期開發的速度。

這些目標至少是部分對立的。 好架構長期來看提高了生產力, 也意味著維護每個變化都需要更多努力讓代碼保持整潔。

實現起來最快的代碼很少是運行時最快的。 相反,提升性能需要很多的編程時間。而且一旦完成,它就會污染代碼庫:高度優化的代碼不靈活,很難改動。

總有今日事今日畢的壓力。但是如果儘可能快地實現特性,代碼庫就會充滿黑魔法,漏洞和混亂,阻礙未來的產出。

對於這個三者的權衡,沒有簡單明了的解決方案,只有具體問題具體分析,按實際的項目狀況去去權衡,讓三者保持友好的動態平衡,讓整個項目保持良好的狀態。

十一、本文涉及知識點提煉整理

本文涉及知識點提煉整理,一些關於遊戲架構與性能的心得總結:

  1. 抽象和解耦會讓代碼的擴展性和靈活性更加強,但會花費額外的實現時間。除非你覺得這樣的靈活性有必要,否則沒必要過度的去追求。

  2. 性能優化很重要,但是要注意時機。在整個開發周期中,最好先專註於實現基本需求,把那些可能限制到項目進度的性能優化盡量延後。

  3. 在整個開發周期中,靈活性和高性能往往不能兼得。我們可以保持代碼的靈活性直到設計定下來,再抽出抽象層來提高性能。

  4. 在原型開發階段,能儘快讓你做出原型產品,最終讓產品成功上線的最初的功臣,或許就是設計糟糕的爛代碼。因為他們實現想法夠快,不需要縝密的設計與架構。只是這些爛代碼在經歷了原型設計階段之後,一定要被重寫或者重構。

  5. 如果打算拋棄這段代碼,就不要嘗試將其寫完美。「搖滾明星將旅店房間弄得一團糟,因為他們知道明天會有人來打掃乾淨。」

  6. 提倡去寫出最簡單,最直接的整潔代碼。你讀過這種代碼後,完全理解了它在做什麼,想不出其他完成的方法。「完美是可達到的,不是沒有東西可以添加的時候,而是沒有東西可以刪除的時候。」

  7. 但最重要的是,如果你想要做出讓人享受的東西,那就享受完成它的過程。

十二、參考文獻

[1] Architecture, Performance, and Games · Introduction · Game Programming Patterns.

[2] Game Programming Patterns書評

[3] 【《代碼整潔之道》精讀與演繹】之一 讓代碼比你來時更乾淨

本文就此結束,系列文章未完待續。

With Best Wishes.

推薦閱讀:

7個有益的編程習慣
在 C++11 中,如何為匿名的結構體添加構造函數?
《自頂向下方法》筆記 · 編程作業2 · UDPping程序
Spotify 如何對歌曲隨機播放?
帶你入門Spark(資源整理)

TAG:编程 | 设计模式 | 游戏开发 |