代码之美——Doom3源代码赏析1
http://www.csdn.net/article/2013-01-17/2813778-the-beauty-of-doom3-source-code/1
摘要: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中我就看出来:
- 这个函数不会修改作为参数传入的idPlane。我无需坚持idPlane是否被修改就可以安全地使用它。
- 函数中的epsilon也不会被修改。
- front, back, frontOnPlaneEdges and backOnPlaceEdges是输出变量,是值的写入目标。
- 参数列表后面的const是我最赞赏的地方。它表明idSurface::Split()不会去修改surface。这是我最喜欢的C++独有功能,因为我可以这样使用:
- void f(const idSurface &s) {
- s.Split(....);
- }
如果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()为例,我们看看它是如何注释的:
- // splits the surface into a front and back surface, the surface itself stays unchanged
- // frontOnPlaneEdges and backOnPlaneEdges optionally store the indexes to the edges that lay on the split plane
- // returns a SIDE_?
第一行有点多余,从函数定义中我们已经能明白所有的信息了;但第二、第三行很有价值,虽然我们已经可以推断出第二行的属性,但注释消除了歧义。
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分支怎么办?
相关文章
- 语义索引neural_search边训练边在线评估模型代码
- C#7.2——编写安全高效的C#代码 c# 中模拟一个模式匹配及匹配值抽取 走进 LINQ 的世界 移除Excel工作表密码保护小工具含C#源代码 腾讯QQ会员中心g_tk32算法【C#版】
- OpenCV3.0中有哪些视频背景/前景分割(背景建模/前景提取)算法的类,它们各自的算法原理、特点是什么,并附示例代码
- C# 方法调用代码练习
- 《Java遗传算法编程》—— 2.3 关于本书的代码示例
- 转 PyCharm 进行调试 以及怎么熟悉一个已经成熟的项目的代码和断点 以及 jetBrains pycharm快捷键
- 《SQL与关系数据库理论——如何编写健壮的SQL代码》一2.2 相等性比较
- 短信验证码倒计时代码
- 华为OD机试 -路灯照明(JavaScript) | 机试题+算法思路+考点+代码解析 【2023】
- 华为OD机试 -水仙花数(JavaScript) | 机试题+算法思路+考点+代码解析 【2023】
- 一、源代码-面向CLR的编译器-托管模块-(元数据&IL代码)
- Python 02 编写代码