`
xpp02
  • 浏览: 1054293 次
社区版块
存档分类
最新评论

代码之美——Doom3源代码赏析

 
阅读更多
摘要:Dyad作者、资深C++工程师Shawn McGrathz在空闲时翻看了Doom3的源代码,发出了这样的惊叹:“这是我见过的最整洁、最优美的代码!”“Doom 3的源代码让我对那些优秀的程序员刮目相看。”因此有了本文。

背景介绍:

Doom3是id Software于2004年开发的第一人称射击游戏,目前以GPL v3协议开源。其采用游戏引擎的是id Tech 4,由id Software创始人、首席程序员John Carmack领导开发。

再做个简单的对比:作者刚刚完成的Dyad有193k行纯C++代码,Doom3是601k(2004),Quake3是229k(1999),Quake2是136k(1997)。

以下是CSDN译文,做了部分删减:

关于代码,什么才能被称为“好看”——或者说“优美”?在和几个程序员朋友讨论后,我得出了结论:

  • 代码应该局部连贯而且功能单一:一个函数解决一个问题。而且应该很清晰。
  • 局部代码应该能够解释,至少暗示整体的系统设计。
  • 代码应该“自文档”,尽可能地避免注释。因为无论是在读还是写代码时,注释都是一项冗余工作。如果你需要添加注释才能帮别人理解,那么那段代码可能需要重写。

这里是idTech4引擎的编码标准,绝对值得一读。

统一的语法与词法分析


我在Doom源代码中所见最聪明之处在于其词法分析器和解释器。所有的资源文件都是语法统一的ASCII文件:脚本、动画文件、配置文件,等等,所有东西都遵循相同的规则。因此一大块代码就可以阅读并处理所有的文件。这个解析器非常健壮,支持一个C++的主要子集。通过一个统一的词法分析、解释器,引擎所有组件都不必担心序列化数据的问题,因为已经准备好了相应的代码,这保证其它地方的代码更加整洁。

参数严格和const化


Doom的代码非常严格,尽管在我看来,const方面还不够严格。可能很多程序员都没注意到const的多种种作用。我的看法是“任何东西只要可以都应该设定为const”,我希望C++中所有的变量都默认是const。Doom参数几乎完全遵守“no in-out”规则,这意味着所有函数都参数都不能既是输入参数也是输出参数。这样,在当你向函数传入参数时,更容易理解他身上发生了什么。比如:

从这几个const中我就看出来:

  1. 这个函数不会修改作为参数传入的idPlane。我无需坚持idPlane是否被修改就可以安全地使用它。
  2. 函数中的epsilon也不会被修改。
  3. front, back, frontOnPlaneEdges and backOnPlaceEdges是输出变量,是值的写入目标。
  4. 参数列表后面的const是我最赞赏的地方。它表明idSurface::Split()不会去修改surface。这是我最喜欢的C++独有功能,因为我可以这样使用:
    1. voidf(constidSurface&s){
    2. s.Split(....);
    3. }

如果Split没有被定义为 Split(...) const,这段代码将无法编译。无论被谁所调用,f()都不会去修改外表,即使f()将surface传递给另一个函数,或者调用一些Surface::method()。const能够透露出很多关于函数甚至整个系统设计的信息,仅仅通过阅读这里的函数声明,我就明白了surface可以被plane动态地split()。这个函数不会修改surface,而是返回新的surface、front、back数据,可选地返回frontOnPlaneEdges和backOnPlaneEdges。

const规则,以及无input/output参数对我来说也许是最重要的原则,也是区分好的代码跟优美代码的关键,它能简化整个系统的理解、编辑和重构。

最少注释原则


这是一个“格式问题”,但Doom基本不会过度注释,这很漂亮!我经常会看到这样的代码:

这太让人恼火了,我通过名字就可以知道它的作用!如果这个函数名不能体现出其功能,毫无疑问应该重新命名;如果名字描述得过多,那么去简化它。除非实在不能通过重构、重命名内描述它唯一的功能,那么注释才是合理的。我本以为程序员在学校已经学会注释的重要性,但实际上没有。注释很有必要,但它经常没必要。Doom在这方面做得非常合格,以idSurface::Split()为例,我们看看它是如何注释的:

  1. //splitsthesurfaceintoafrontandbacksurface,thesurfaceitselfstaysunchanged
  2. //frontOnPlaneEdgesandbackOnPlaneEdgesoptionallystoretheindexestotheedgesthatlayonthesplitplane
  3. //returnsaSIDE_?

第一行有点多余,从函数定义中我们已经能明白所有的信息了;但第二、第三行很有价值,虽然我们已经可以推断出第二行的属性,但注释消除了歧义。

Doom的代码加上合理的注释,阅读非常方便。也许很多人把它归为格式问题,但我认为,格式也有正确与否。如果有人修改了函数,并且删除了最后的const;这样surface可以直接被函数修改,于是注释与代码不再同步;这样注释反过来会导致误解,导致代码更加难以阅读。

纵向空间


Doom从不浪费纵向空间。我们以t_stencilShadow::R_ChopWinding()为例:

整个算法只占了我1/4个屏幕,剩下的3/4可以用来观看其周围的相关代码块。实际上,我经常看到这样的代码:

这可以归为格式问题,我有10年编程经历都是像后者那样,大概在6年前才强行转换为紧凑风格的。

两者的代码行数比是11:18,同样的代码后者行数几乎是前者的两倍,所以可能导致看不到后面的代码块,就像这样:

如果没有前面的for循环,仅仅上面这段代码毫无意义,如果id没有纵向紧凑的风格,代码可能更难阅读、更难写、更难维护、也就远离了优美代码的定义。

另外一个我认同的格式是:id永远尽可能地使用{},没有括号会很糟糕,比如我看过这段代码:

这非常丑陋,甚至比把{}放在同一行还要糟糕,我在id的代码中从未发现省略{}的情况。省略{}会导致while代码块解析的时间大幅增加,而且编辑起来也非常痛苦:如果我希望往else if(c > d)分支中再插入一个if分支怎么办?


最少模板


id“犯了不少C++的禁忌”,他们重写了所有需要的STD函数。我个人对STD爱恨交织。在Dyad,我调试构建时常使用它来管理动态资源;在发布时又会处理所有的资源,避免使用任何STL函数,以求尽快地加载。STL很不错,因为它提供了快速的通用数据结构;它又很糟糕,因为使用它经常导致代码丑陋不堪,甚至容易出错。例如std::vector<T>类,如果我想迭代每一个元素:

在C++11中要简单些:

但我个人并不喜欢自动化,虽然它简化了代码编写,却导致代码更难阅读,最起码我现在是这么认为的。

STD有的函数、算法甚至非常荒谬,比如要从std::vector中删除一个值:

你必须每次都能拼写正确!id除去了其中所以含糊不清的部分:他们使用自己的通用容器、字符串类等等。他们编写的类比起STL要更加专一,易于理解。id还尽可能地避免使用模板,而且使用自己定制的内存分配器。STD代码里则充斥着无意义的垃圾模板,而且不易于阅读。

C++代码很难写好,所以你需要不断地努力,不相信的话可以去看看Microsoft和GCC的STD代码,这是我见过的最难看的代码!

id通过不滥用泛型就简单地解决了这个问题。他们编写了HashTable<V>和HashIndex类,HashTable强制key类型是const char *,而HashIndex是int->int对。这看起来像是很糟糕的C++实例。他们“本应该”只有一个HashTable类,然后为编写局部特殊化:KeyType = const char *,然后专门 <int, int>。

当然,id的做法完全正确,也保证了代码的优美。

对比更鲜明的是,Hash生成“C++优秀实践”和id做法的比较:

为特定类型专门化:

这样你可以把ComputeHashForType当作HashComputer传给HashTable:

这和我的做法很相近,看起来很聪明,但实际上很难看!因为,如果可选的模板参数很多怎么办?

这种情况下函数定义要更糟:

如果没有代码高亮,我甚至不能区分出方法名!

我也曾看到其它引擎试图通过卸载模板参数规范到无数的typedef,这更糟糕!也许这利于理解,但却导致了本地代码和整个系统逻辑的断层,所以缺乏美感。例如:

以及:

你这样使用两者:

你会产生疑惑:StringHashTable内存分配器——StringAllocator会涉及全局内存吗?这里导致了混淆,于是你又需要返回之前的代码检查(循环)……

Doom的做法和常规C++逻辑完全相反:它尽可能地避免泛型,除非有特别的意义。Doom的HashTable需要生成hash值时怎么办?它只需要调用idStr::GetHash()。

C语言的余韵


虽然我不清楚id团队其他人的出身如何,但John Carmack基本上可以说是开发C应用起家的,id在Quake III之前开发游戏用的都是C语言。我见过很多没有C开发功底的C++程序员,编写代码都有非常重的C++特色,上面过度使用模板的情况只是其中一例,其它还有:

  • 过度使用set/get方法
  • 使用字符串流
  • 过度使用操作符重载

id在以上方面都做得非常完美。

通常很多人会这样创建一个类:

这样不仅浪费行数,还需要花费更多的时间编来写和阅读代码。相比之下:

如果你经常为var自增某个数字n呢?

相比于:

上面的例子明显容易阅读和编写。

id从不使用字符流,字符流通常包含糟糕的操作符重载:<<

例如:

虽然它有很多好处,但是很难看,而且语法也让人讨厌。

id选择printf()来代替,这样也易于阅读理解。我同意这样的决定。

另一方面,Doom还尽量避免操作符重载。虽然操作符重载是非常优秀C++特性,但没有操作符重载也就没有歧义,更便于编写和阅读。

横向空间


这是我从Doom的代码中最大的收获,原来我是这样编写代码的:

根据Doom3的编码标准,始终使用相对于4个空格的tab,水平对齐其中所有类的定义:

他们很少在类的定义中嵌入内联函数,我看到的唯一一次是代码和函数声明写在了同一行,这种做法有点不符合规范。这种类定义的组织方式非常容易解析,不过需要更多的时间来编写。

我讨厌多余的代码编写,但这种情况下,我只需要这次稍微多做一点工作,其他程序员在之后接手时就可以省下很多功夫。相信这里的Doom3编程规范能够帮助你理解其代码之美。(有网友称Google的C++编程规范与其也有很多相似之处。)

方法名


我认为Doom在方法名方面缺乏规范,我个人会尽可能地以动词开头命名方法:

比这样要好得多:

以下是John Carmack本人的回复:


从某些角度来看,我认为Quake3的代码更加整洁,算是我C语言代码的风格的一次进化,而非C++风格的第一次迭代。当然也可能因为总代码行数更少,或者是因为我已经10年没看过它的代码引起的错觉。我认为“好的C++”在可读性方面比“好的C语言”更好,其它方面大体相同。

我开始掌握C++是在Doom3开发的时候——在这之前,我有丰富的C语言编程经验,因为NeXT Objective-C编程的原因也有OOP(面向对象编程)背景,因此在使用C++的时候并没有对其使用和习惯进行适当针对性的研究。现在回想起来,真希望提前看过Effective C++这样的教程。团队里其他程序员虽然之前有C++编程经验,但基本上也是按照我选择和设置的风格在编程。

很多年来,我一直怀疑模板,一直在克制地使用它,不过最终确定自己更喜欢强类型,而非充满奇怪的代码的头文件。关于STL的争论在id内部一直没有停息,显得很有生气。回想Doom3开始开发的时候,使用STL基本上算不得好主意,直到现在,即使是在游戏中我们也仍然在争论这件事。

关于const,我直到现在基本上还是一个nazi,我会斥责任每一个不尽可能常量化变量和参数的程序员。

我现在的风格主要是在向函数式编程靠近,这样可以舍去很多旧习,逐渐远离一些OOP的方向。

关于C++函数式编程John Carmack写过一篇《Functional Programming in C++》值得一读!《程序员》对这篇文章做过编译

原文链接:KOTAKU


分享到:
评论

相关推荐

    DOOM3 源代码

    《DOOM3源代码》是游戏开发领域的一份宝贵资源,它揭示了ID Tech 4引擎的内部工作原理。这个引擎是由著名游戏开发者约翰·卡马克(John Carmack)所设计,他是第一人称射击游戏领域的先驱者,对3D图形技术有着深远的...

    DOOM源代码 看卡马克当年如何实现3D视觉

    《DOOM源代码:探索3D视觉的编程艺术》 DOOM,这款1993年发布的经典第一人称射击游戏,以其卓越的图形表现和激烈的游戏体验在当时引发了轰动,同时也引领了3D游戏引擎的发展。其背后的源代码,至今仍被程序员和游戏...

    Doom3 源代码 毁灭战士

    Doom3 源代码 毁灭战士 《毁灭战士3》(Doom 3)由id Software开发,计算机版于2004年8月3日由Activision发行,是一款杂恐怖与科幻于一身的第一人称射击游戏。游戏故事除了英文名称外,并未有完全跟随《毁灭战士》...

    doom3-C++源代码

    doom3-完整C++源代码,一起学习,看到好多人都是3分或5分的分享,特提供分享1分资源分数,仅供快乐学习。

    Doom3的源代码

    《深入探索Doom3源代码:游戏开发的智慧之源》 Doom3,这款由id Software开发的经典第一人称射击游戏,不仅以其卓越的视觉效果和沉浸式的游戏体验赢得了玩家的喜爱,更因其开放源代码的特性成为了众多游戏开发者和...

    DOOM3 源代码(id tech 4 引擎)

    《DOOM3源代码与id Tech 4引擎详解》 DOOM3,这款经典的第一人称射击游戏,以其惊人的视觉效果和紧张刺激的游戏体验在2004年推出时引起了广泛的关注。其背后的强大技术支撑就是id Tech 4引擎,一款由著名游戏开发商...

    doom3源代码

    TTimo-doom3.gpl-7db8be7.zip The Doom 3 source-code -- based upon the id Tech 4 engine -- is now available as open-source software to the gaming community under the GNU GPL license. Pushed onto ...

    DOOM3部分源代码

    总结来说,《DOOM3部分源代码》不仅仅是一个关于DOOM3游戏技术细节的展示,更是一个现代游戏开发技术和工程实践的缩影。从高级图形渲染到底层框架设计,它的内容覆盖了游戏开发的方方面面。这不仅为那些初入游戏开发...

    doom3(含id tech 4引擎)源代码

    《Doom 3与id Tech 4引擎:深入解析源代码》 Doom 3,由著名游戏开发商id Software开发的恐怖科幻第一人称射击游戏,以其极致的恐怖氛围和逼真的图形效果闻名于世。游戏的核心是其强大的id Tech 4引擎,这是一款...

    DOOM-3源代码

    《DOOM-3源代码》是游戏开发领域中极具价值的学习资源,对于C和C++编程爱好者以及希望深入了解游戏引擎工作的开发者来说,是一份不可多得的宝藏。DOOM-3是一款由id Software开发的经典第一人称射击游戏,其源代码的...

    Doom 3 SDK 源代码

    DOOM3 的源代码, 喜欢研究游戏的朋友可以参考参考. 里面用到的很多技巧到目前为止都是非常厉害的. 如果你想写一个自己的游戏用的快速常用算法数学库, 那里面的math.h 就是不可少的参考文件哦. 最经典的就是里面用到...

    doom3.gpl-master.zip

    本文将围绕"doom3.gpl-master.zip"这个压缩包,详细介绍Doom 3 GPL的源代码结构、核心技术以及学习资源。 1. **源代码结构**: Doom 3 GPL的源代码组织严谨,主要分为以下几个部分:引擎(Engine)、游戏逻辑...

    Doom3:Doom3基于源代码的游戏。 只是学习-Source code learning

    dhewm 3是Doom 3 GPL源代码的修改。 dhewm 3的目标是借助SDL将DOOM 3带到所有合适的平台上。 原始DOOM 3中存在的错误将在不更改原始游戏玩法的情况下得到修复(如果确定)。 该项目托管在: : 在以下查询常见...

    Doom3引擎.

    2. **开源社区发展**:id Software将Doom3引擎的源代码开放,名为id Tech 4,这促进了社区开发者的参与,推动了引擎的改进和新项目的发展。 四、Doom3引擎的后续影响 Doom3引擎不仅推动了游戏行业的发展,其技术和...

    DOOM游戏 Windows源代码(463kb)

    《DOOM游戏Windows源代码解析》 DOOM游戏,作为1993年诞生的经典第一人称射击游戏,以其惊人的图形效果和激烈的战斗体验在游戏史上留下了深刻的印记。如今,我们有机会深入到其Windows版本的源代码中,了解这款传奇...

    doom:在此处添加了DOOM的源代码以进行实验和学习-Source code learning

    3. **网络编程**:DOOM支持多人游戏,因此源代码中包含了网络同步和通信的相关代码。这部分可以帮助学习者理解多人游戏背后的网络架构和技术。 4. **音频和视频处理**:DOOM使用了模拟音频技术和简单的图形效果,如...

    DOOM启示录 DOOM启示录

    在《DOOM启示录》中,我们可以深入了解到DOOM如何通过其先进的引擎技术——Doom引擎,实现了当时令人惊叹的实时3D渲染和快节奏的游戏体验。这款引擎的创新之处在于它采用了二维平面贴图技术和伪3D视角,使得游戏场景...

    doom3.rar_doom_doom3_发动机

    请注意,Doom 3和“毁灭战士3:邪恶复苏是从蒸汽存储在 http://store.steampowered.com/app/9050/ http://store.steampowered.com/app/9070/ 其他的平台,更新的源代码,安全问题: ------------------------------...

    iphone毁灭战士游戏源代码(iphone doom 1.0)

    《iPhone毁灭战士游戏源代码(iPhone Doom 1.0)》是针对苹果移动设备iOS平台的一个经典游戏项目,它将1993年发行的PC游戏《DOOM》移植到了iPhone和iPod Touch上,让玩家可以在掌上设备上体验这款经典的第一人称射击...

Global site tag (gtag.js) - Google Analytics