標籤:
在我們一頭紮入模式之前,我想先講一些對軟體架構和其在遊戲上的應用的理解, 也許能幫你更好的理解這本書的其餘部分。 至少,在你被捲入一場關於軟體架構有多麼糟糕(或多麼優秀)的辯論時, 這可以給你一些武器支援。
軟體架構是什嗎?
如果你把本書從頭到尾讀一遍, 你不會找到在3D圖形背後的線性代數或者遊戲物理背後的微積分。 這書不會告訴你如何用α-β修剪你的AI樹,也不會告訴你如何在音頻中類比房間中的混響。
相反,這本書告訴你在程式之間的事情。 與其說這本書是關於如何寫代碼。不如說是關於如何組織代碼的。 每個程式都有一定組織, 哪怕是“把所有的代碼都塞到main()函數中”, 所以我認為講講什麼造成了好的組織是很有意思的。我們如何區分好架構和壞架構呢?
我思考這個問題五年了。當然,像你一樣,我有關於好設計的直覺。我們因為糟糕的代碼品質遭受了太多苦惱,現在我最大的心愿就是把這些好的代碼設計範式分享出來,減輕工程師的痛苦。
承認這一點,我們中大多數都該對它們中的一些負責。
只有很少的幸運者有相反的經曆, 有機會在好好設計的程式碼程式庫上工作。 那種程式碼程式庫看上去是間豪華酒店,裡面的門房隨時準備滿足你心血來潮的需求。 這兩者之間的區別是什麼呢?
什麼是好的軟體架構?
對我來說,好的設計意味著,當需求發生改變,就好像整個程式是在為改變打造而成的。我只需幾個選擇函數調用可以完美的解決的任務,同時絲毫不改變代碼平靜表面下的邏輯
這聽起來很漂亮,但它不是完全可操作的。“只要寫你的代碼,這樣的變化不會影響它的平靜的表面。”沒錯。
讓我打破了一些。第一個關鍵區段是,架構和變革關聯在一起的。總有人已修改程式碼程式庫。如果沒有人觸及代碼—-無論是因為代碼寫的太完美還是因為他太糟糕,沒有人會玷汙自己的文字編輯器。那麼它的設計是無關緊要的。評價架構設計就是評價它如何應對變化的。 如果沒有變化,它就是一個永遠不會離開起跑線的運動員。
你如何應對變化?
在你改變代碼去添加新特性,去修複漏洞,或者隨便什麼需要你使用編輯器的時候, 你需要理解現在的代碼在做些什麼。當然,你不需要理解整個程式, 但你需要將所有相關的東西裝進你的大腦。
有點詭異,這字面上是一個OCR過程。
我們傾向於這一步忽略,但通常編程中最耗時的部分。如果你覺得從磁碟分頁載入一些資料到RAM慢,想想把這些資料通過神經載入到你的大腦呢?
一旦你把所有正確的上下文都記到了大腦裡, 你思考了一會,然後找到瞭解決方案。 可以有很多來回這裡,但通常比較簡單。 一旦你理解了問題和需要改動的代碼,實際的編碼工作有時候是微不足道的。
你用肥手指在鍵盤上敲打一陣子,直到螢幕上顯示著正確顏色的光芒, 然後就算搞定了,對吧?還沒呢! 在你為之寫測試並發送其到程式碼檢閱之前,你經常有一些清理工作要做。
我是不是說了“測試”?噢,是的,我說了。為有些遊戲代碼寫單元測試很難,但程式碼程式庫的一 大部分是完全可以測試的。我不會在這裡發表演說,但是我建議你,如果還沒有做自動化的測試, 請考慮一下。 除了手動驗證以外你就沒別的事要做了嗎?
你將一些代碼加入了你的遊戲,但你不想下一個人調到你留下來的坑。 除非改動很小,否則就還需要一些重組做才能讓你的新代碼無縫整合。如果你做對了,那麼下一個見到代碼的人甚至無法說出哪些代碼是新加入的。
總之,編程流程是這樣的:
解耦怎麼幫了忙?
雖然並不明顯,但我認為很多軟體架構都是關於學習階段。 將代碼載入到神經元太過緩慢,找些策略減少載入的總量是很值得做的事。 這本書有整整一個章節是關於解耦模式, 還有很多設計模式是關於同樣的主題。
你可以用多種方式定義“解耦”,但我認為如果有兩塊代碼是耦合的, 那就意味著你無法只理解其中的一個。 如果你解耦了他倆,你就可以獨自的理解某一塊。 這當然很好,因為只有一塊與你的問題相關, 你只需要將這一塊載入到你的猴腦中而不需要載入另外一塊。
對於我來說,下面是軟體架構的關鍵目標: 最小化你在處理前需要進入大腦的知識。
當然,也可以從後期階段來看。 另一種解耦的定義是當一塊代碼有變化時,沒必要修改另外的代碼。 我們肯定需要修改一些東西,但耦合程度越小,變化會波及的範圍就越小。
代價是什嗎?
聽起來很棒,對吧?解耦任何東西,然後你能像風一樣編碼。 對每個變化都只修改一兩個特定的方法,就像你在程式碼程式庫的水面上跳舞,只留下倒影。
這就是人們對抽象,模組化,設計模式和軟體架構興奮的原因。 在有好架構的程式上工作是很好的體驗,每個人都希望能更有效率的工作。 好架構能造成生產力上巨大的不同。很難再誇大它那強力的影響。
但是,就像生活中的任何事物一樣,沒有免費的午餐。好的設計同樣需要汗水和規則。 每一次做出改動或是實現特性,你都需要將它優雅的整合到程式的其他部分。 你需要花費大量的努力去管理代碼, 在開發過程中面對數千次變化仍然保持它的管理結構。
你需要去考慮代碼的那一部分需要去解耦和抽象同樣你也需要考慮那一部分需要考慮未來的變化而設計的具有擴充性。
通常人們熱衷於這點,他們假想以後開發人員只需要引入他的程式碼程式庫就可以。因為這個程式碼程式庫已經變得功能強大,擴充性很強。
首先最棘手的部分, 每當你添加了一層抽象或者支援擴充的部分,你是在假設你以後這兒需要靈活性。 但是添加代碼和複雜性到遊戲中,這都需要時間來開發,調試和維護。
如果你猜對了,後來接觸了代碼,那麼功夫不負有心人。 但預測未來很難,如果模組化最終沒有協助,那它就有傷害。 畢竟,你得處理更多的代碼。
當人們過分關注這點時,會是你的代碼失控。因為 介面和抽象無處不在。外掛程式系統,抽象基類,虛方法,還有各種各樣的擴充方法。
回溯所有的腳手架去找真正做事的代碼就要消耗無盡的時間。 當你需要做出改變,當然,有可能某個介面能幫上忙,但能不能找到就只能祝你好運了。 理論上,解耦意味著在擴充代碼之前需要瞭解的代碼更少, 但抽象層本身就會填滿你的心靈暫存磁碟。
像這樣的程式碼程式庫讓人反對軟體架構,特別是設計模式。 人們很容易沉浸在代碼中而忽略你要發布遊戲的這點。 無數的開發人員聽著加強可擴充性的警報,花費多年時間製作“引擎”, 卻沒有搞清楚做引擎是為了什麼。
效能和速度
軟體架構和抽象有時會被批評,尤其是在遊戲開發中: 它傷害了遊戲的效能。 許多讓代碼更靈活的模式依靠虛擬調度、 介面、 指標、 訊息,和其他機制, 他們都會消耗運行時成本。
一個有趣的反面例子是在C++中的模板。模板編程有時可以給你抽象介面而無需運行時開銷。這是靈活性的兩極。但你寫代碼調用類中的具體方法時,你在寫作時修改類——你寫入程式碼了調用的是哪個類。但通過虛方法或介面時,直到運行時你才知道調用的類。這更加靈活但增加了運行時開銷。模板編程是在兩者這件。你在編譯時間初始化模板,決定調用哪些類。
還有一個原因。很多軟體架構的目的是使程式更加靈活。 這讓改變它需要較少的努力。編碼時對程式有更少的假設。 您可以使用介面,讓您的代碼可與任何實現它的類互動,而不僅僅是現在寫的類。 今天。您可以使用觀察者和訊息讓遊戲的兩部分交流, 而以後它可以很容易地擴充為三個或四個部分相互交流。
但效能與假設相關。實踐最佳化基於確定的限制。 永遠不會超過256種敵人嗎?好,我們就可以將ID編碼為為一個位元組。 在一個具體類型中只調用一個方法嗎?好,我們可以做靜態調度或內聯。 所有的實體都是同一類?太好了,我們可以做一個 連續數組儲存他們。
但這並不意味著靈活性是壞的!它可以讓我們快速改進我們的遊戲, 開發速度是擷取有趣開發經驗的絕對重要因素。 沒有人,哪怕是Will Wright,能在紙面上構建一個平衡的遊戲。這需要迭代和實驗。
越快嘗試想法看看效果如何,你就越能嘗試更多的東西,就越有可能找到有價值的東西。 就算找到正確的機制之後,你也需要足夠的時間調整。 一個微小的不平衡可能破壞整個遊戲的樂趣。
這裡沒有簡單方案。 讓你的程式更加靈活,你能在損失一點點效能的前提下更快的做出原型。 同樣的,最佳化代碼會讓它不那麼靈活。
就我個人經驗而言,讓有趣的遊戲變快比讓快速的遊戲變有趣簡單得多。 一種折中的辦法是保持代碼靈活直到設計定下來,再抽出抽象層來提高效能。
糟糕代碼的優勢
這就來到了下一觀點:不同的代碼風格各有千秋。 這本書的大部分是關於保持乾淨可控的代碼,所以我堅持應該用正確方式寫代碼,但糟糕的代碼也有一定的優勢。
編寫良好架構的代碼需要仔細的思考,這會轉為時間上的代價。 在項目的整個周期中保持良好的架構需要花費大量的努力。 你需要像露營者處理營地一樣小心處理程式碼程式庫:總是保持其優於你剛開始的時候。
當你要在一個項目上花費很久時間的話,這很好。 但,就像我早先提到的,遊戲設計需要很多實驗和探索。 特別是在早期,寫一些你知道要扔掉的代碼是很普遍的事情。
如果你只想試試遊戲的某些主意是不是正確的, 良好的設計意味著你在螢幕上看到和擷取反饋之前要消耗很長時間。 如果最後證明這個點子不對,那麼當你刪除代碼的時候,那些花在讓代碼更優雅的時間就完全浪費了。
原型——一坨勉強拼湊在一起,只能回答設計問題的簡單代碼——是一個完全合理的編程習慣。 雖然當你寫一次性代碼的時候,必須保證你可以扔掉它。 我見過很多次糟糕的經理人在玩這種把戲:
老闆:“嗨,我們有些想試試的點子。只要原型,不需要做的很好。你能多快搞定?”那麼幾天后我能給你一個臨時的代碼檔案。”老闆:“太好了。”
幾天后
老闆:“嘿,原型很棒,你能花上幾個小時清理一下然後變為成品嗎?”
你得讓人們清楚,可拋棄的代碼即使看上去能工作,也不能被維護,必須重寫。 如果有可能要維護這段代碼,你就得防禦性好好編寫它。
一個保證你的原型代碼不會變成真正用的代碼的技巧是使用一種和遊戲不同的語言。這樣,你在實際應用於遊戲中之前必須重寫。
保持平衡
有些因素在相互制約:
- 為了在項目的整個生命週期保持其可讀性,我們需要好架構。
- 我們需要更好的運行時效能。
我們需要讓現在的特性更快的實現。
有趣的是,這些都是速度:我們長期開發的速度,遊戲啟動並執行速度,和我們短期開發的速度。
這些目標至少是部分對立的。 好的架構長期來看提高了生產力, 也意味著維護每個變化都需要更多努力讓代碼保持整潔。
草就的代碼很少是運行時最快的。 相反,提升效能需要很多的編程時間。 一旦完成,它會汙染程式碼程式庫:高度最佳化的代碼不靈活,很難改動。
總有今日事今日畢的壓力。但是如果儘可能快地實現特性, 程式碼程式庫就會充滿黑魔法,漏洞和混亂,阻礙未來的產出。
沒有簡單的解決方案,只有權衡。 從我收到的郵件看,這傷了很多人的心,特別是那些只是想做個遊戲的人。 它似乎是在恐嚇,“沒有正確的答案,只有不同的口味的錯誤。”
但,對我來說,這讓人興奮!看看任何人們從事的領域, 你總能發現某些相互抵觸的限制。無論如何,如果有簡單的答案,每個人都會那麼做。 一周就能掌握的領域是很無聊的。你從來沒有聽說過有人討論挖坑事業。
遊戲你會;我沒有深究這個類比。 我知道的是,可能有挖坑熱愛著,挖坑規範,以及一整套亞文化。 我算什麼人,能在此大放厥詞?
簡單
最近,我感覺如果有什麼能簡化這些限制,那就是簡單。 在我現在的代碼中,我努力去寫最簡單,最直接的解決方案。 你讀過這種代碼後,完全理解了它在做什麼,想不出其他完成的方法。
我的目的是正確獲得資料結構和演算法(大致是這樣的先後),然後在從那裡開始。 我發現如果能讓事物變得簡單,就有更少的代碼, 就意味著改動時有更少的代碼載入腦海。
它通常跑的很快,因為沒什麼開銷,也沒什麼代碼執行。 (雖然大部分時候事實並非如此。你可以在一小段代碼裡加入大量的迴圈和遞迴。)
但是,注意我並沒有說簡單的代碼需要更少的時間編寫。 你會這麼覺得是因為最終獲得了更少的代碼,但是好的解決方案不是往代碼中注水,而是蒸幹代碼。
Blaise Pascal有句著名的信件結尾,“我沒時間寫的更短。”另一句名言來自Antoine de Saint-Exupery:“完美是可達到的,不是沒有東西可以添加的時候,而是沒有東西可以刪除的時候。”言歸正傳,我發現每次我重寫本章,它就更短。有些章節比他們剛完成時短了20%。
我們很少遇到優雅表達的問題。取而代之的是各站各樣的情況。 你想要X在Z情況下做Y,在A情況下做W,諸如此類。
最不消耗心血的解決方案就是為每段用況編寫一段代碼。 如果你看看新手程式員,他們經常這麼幹: 他們為每個手頭的問題編寫邏輯迴圈。
但這一點也不優雅,那種風格的代碼遇到一點點沒想到的輸入就會崩潰。 當我們想象優雅的代碼時,我們想的是通用的那一個: 只需要很少的邏輯就可以覆蓋各種各樣的情況。
找到這樣的方法有點像模式識別或者解決謎題。 需要努力去識別散亂的用例下隱藏的規律。 完成的時候你會感覺好得不能再好。
就快完了
幾乎每個人都會跳過介紹章節,所以祝賀你看到這裡。 我沒有太多東西回報你的耐心,但我還有一些建議給你,希望對你有用:
- 抽象和解耦讓擴充代碼更快更容易,但除非確信需要靈活性,否則不要做。
- 在你的整個開發週期中考慮並為效能設計,但是儘可能延遲那些底層的,基本事實的最佳化,那會鎖死你的代碼。
相信我,在發布前兩個月不是你開始思考“遊戲運行只有1FPS”的繁瑣問題的時候。
快速的探索你遊戲的設計空間,但不要跑的太快,在身後留下一團亂麻。畢竟,你總得回來處理他們。
如果你打算拋棄這段代碼,就不要嘗試將其寫完美。搖滾明星將旅店房間弄得一團糟,因為他們知道明天會有人來打掃乾淨。
但最重要的是,如果你想要做出讓人享受的東西,那就享受做它的過程。
架構,效能和遊戲