「毀滅戰士3」原始碼就是“保持簡潔”的證明

威士忌發表於2014-07-30

假如你在網上搜最好的C++原始碼。「毀滅戰士3 | Doom 3」的原始碼肯定會被提到好多次,這篇就來證明此事。

我花了一些時間通讀了 DOOM3 的原始碼。這可能是我見過的最乾淨最漂亮的程式碼了。

DOOM3是id Software公司開發 Activision發行的視訊遊戲。該遊戲為id Software贏得了商業上的成功,已售出350萬多份拷貝。

在2011年11月23日,id Software維持開源傳統,釋出了他們上一個引擎的原始碼。這份原始碼已經被很多開發者審查,這裡就有個fabien反饋的例子(連結):

DOOM3 BFG是用C++寫的,一種龐大的語言,它既能寫出優秀的程式碼,但也讓人憎惡到眼睛流血。幸運的是,id Software退而求其次,使用C++子集,接近於“帶類的C”,如以下幾條約束:

  • 沒有異常
  • 沒有引用(使用指標)
  • 少用模板
  • 使用常量(Const everywhere)
  • 多型
  • 繼承

很多C++專家不建議使用“帶類的C”這樣的方法。然而,DOOM3從2000開發至2004,沒有使用任何現代C++機制。

讓我們使用 CppDepend 來看看原始碼,探索它得特別之處。

DOOM3有少量的幾個工程組成,這兒有它的工程列表和一些型別統計。

這裡還有他們之間的依賴關係圖:

 

DOOM3定義了很多全域性函式。但是,大部分內容實現是在類中。

資料模型使用結構體定義。為了在原始碼中對結構體的使用有個更具體的理解,在下圖中將它們以藍色分塊顯示出來。

在圖表中,程式碼被表示為樹形圖,樹形圖表示法能使用巢狀的矩形來表示樹狀結構。而樹結構用來表示程式碼分層結構。

  • 工程包含名稱空間。
  • 名稱空間包含型別。
  • 型別包含函式和域(field)。

我們可以觀察到它定義了許多的結構體,比如DoomDLL 40%的型別都是結構體。它們被有條理地用來定義資料模型。該實踐已經被很多工程所接受,這種方法有個最大的缺點是多執行緒應用,結構體的public變數並非不可改變的。

為何支援不可變物件,有個重要原因:能顯著地簡化併發程式設計。考慮下,寫個合格的多執行緒程式是個艱鉅的任務嗎?因為很難同步執行緒訪問資源(物件或者其他OS資源)。為什麼同步這些操作很困難呢?因為很難保證在資源競爭狀態下多執行緒對多個物件進行正確的讀寫操作。假如沒有寫操作呢?換句話說,執行緒只訪問這些物件,而不做任何變動?這樣就不再需要同步操作了!

讓我搜尋下只有一個基類的類:

幾乎40%的結構體和類都只有一個基類。通常,OOP(面對物件程式設計)使用繼承的好處之一是多型,下面藍色標明瞭原始碼中的虛擬函式:

超過30%的函式是虛擬函式。少數是純虛擬函式,下面是所有虛基類列表:

只有52個類被定義為虛基類,其中35個類只是純介面,也就是這些介面都是純虛擬函式。

我們來搜搜使用了RTTI的函式

只有非常少的函式使用了RTTI。

為保證只使用OOP最基礎的概念,不使用高階設計模式,不過度使用介面和虛基類,限制了RTTI的使用並且資料都定義為結構體。

至此這份程式碼跟很多C++開發者所批評的“帶類的C”沒太大區別。

其開發者的一些有趣的選擇,幫助我們理解它的奧祕:

1-為有用的服務提供公用的基礎類。

許多類是從idClass繼承下來的:

idClass提供如下服務:

  1. 建立例項化
  2. 型別管理
  3. 事件管理

 

2-方便的字串操作

一般來說,字串是一個專案裡用的最多的物件,許多地方需要使用它,並且需要函式來對其進行操作。

DOOM3定義了idstr類,幾乎包含了所有用的字串操作函式,無需再自己定義函式來接受其它框架所提供的字串類。

3-原始碼與GUI框架(MFC)高度解耦

很多工程用了MFC後,它的程式碼就會與MFC型別高度耦合,並且在程式碼的任何一處都能發現MFC型別。

在DOOM3裡,程式碼和MFC是高度解耦的,只有GUI類才會直接依賴它。下面的CQLinq查詢可以展示這點:

 

這樣的選擇對生產力有很大的影響。事實上,只有GUI開發者才會關心MFC框架,其它開發者不應該被強制在MFC上浪費時間。

4-提供了非常好的公共函式庫(idlib)

幾乎在所有專案中都會用到公共工具類,就如以下查詢的結果:

正如我們所看到經常使用的就是公共工具類。假如C++開發者不使用一個良好的公共工具框架,那就會為解決技術層面問題花費大部分的開發時間。

idlib提供了很多有用的類用於字串處理,容器和記憶體。有效促進了開發者的工作,並且能讓他們更多的關注在遊戲邏輯上。

5-實現非常易於理解

DOOM3實現了非常難的編譯器,對於C++開發者而言,開發語法解析器和編譯器不是件輕鬆的事。儘管如此,DOOM3的實現非常容易被理解並且編寫得十分乾淨。

這兒有這些編譯器的類的依賴圖:

這兒還有編譯器原始碼的程式碼片段:

我們也看過許多語法解析器和編譯器的程式碼,但這是第一次我們發現編譯器是如此得容易理解,和整個DOOM3原始碼一樣。這太神奇了。當我們探究DOOM3原始碼時,我們忍不住會喊:喔,這太漂亮了!

總結

即使DOOM3選擇了很基礎的設計,但它的設計者所做的決定都是為了開發者能更多的關注遊戲邏輯本身,並且為所有技術層面的東西提供便利。這提高了多大的生產力啊。

無論何時使用“帶類的C”,你應該明白你自己在幹什麼。你必須像DOOM3的開發專家一樣。但不推薦初學者忽視現代C++建議而冒險。

相關文章