「毁灭战士3」源码就是 保持简洁 的证明
假如你在网上搜最好的 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 提供如下服务:
- 创建实例化
- 类型管理
- 事件管理
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++建议而冒险。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论