博客 阅读时间 6 分钟

对比原版《雷神之锤 III》与 Kenny 版代码库

分享本文
Comparing the Original Quake3 and Kenny Edition Codebases

《雷神之锤 III:竞技场》是一款于 1999 年 12 月发布、以多人游戏为核心的第一人称射击游戏。该游戏由 id Software 开发。《雷神之锤 III》曾是一款非常流行的游戏,至今仍受到许多玩家的喜爱。

《雷神之锤 III》的原始源代码可以在这里找到。

Artem Kharytoniuk 决定创建他自己的版本,他在仓库中这样描述自己的目标:

本仓库包含原始 Q3 代码库的更新版本:重新组织了代码结构,进行了兼容性修复,提供了适用于最新版 Visual Studio 的构建配置,以及一些更新核心技术但保留原始玩法、外观和体验的修改。

让我们比较这两个项目的源代码,看看 Kenny 版做了哪些改进。

全貌

下面是原始代码的一些度量汇总。

quake4

下面是修改版本的汇总。

quake1

比较两者的汇总后,有以下几点观察:

  • 修改版本的代码更少。
  • 类型、方法和变量更少。
  • 整个解决方案的评分和技术债非常相似。
  • 方法复杂度相近。

让我们逐项目考察这些度量:

原始代码。

quake2

修改后的代码。

quake3

让我们更进一步,深入比较这两个版本。

模块化

模块化是一种软件设计技术,它能提高软件由独立部分组成的程度,使模块化代码更易于管理和维护。

对于 C 这样没有命名空间、组件或类等逻辑机制的过程式语言,我们可以使用目录和文件来实现模块化。

在修改版本中,目录结构经过了重构;下面是一个例子:与引擎相关的目录被隔离到了 engine 目录中。

quake5

封装

封装是指隐藏实现内部的函数和数据。在 C 语言中,封装通过关键字 static 实现。这些实体称为文件作用域的函数和变量。

让我们搜索原始代码中的所有 static 函数:

quake6

许多函数被声明为 static 以保持封装。然而,也有许多函数没有声明为 static。

我们可以使用度量视图来直观地了解有多少函数是 static 的。在度量视图中,代码库以树状图表示。树状图是一种用嵌套矩形展示树形结构数据的方法。CppDepend 树状图使用的树结构就是常见的代码层次结构:

  • 项目包含目录。
  • 目录包含文件。
  • 文件包含结构体、函数和变量。

树状图视图为展示 CQLinq 查询结果提供了一种实用的方式,让我们可以直观地看到查询涉及的类型。

quake7

可以看到,renderer 和 cgame 项目中几乎所有函数都声明为 static,而其他项目则不然——它们只有少数函数声明为 static。

修改版本是否更有效地保持了封装?

下面是 static 函数查询的 CQLinq 结果:

quake8

static 函数比原始代码少了,主要是因为许多函数在重构过程中被移除;这个新版本在封装方面并没有比原始版本做得更好。

用结构体存储数据模型

在 C 语言编程中,函数使用变量来完成处理。这些变量可以是:

  • 静态变量。
  • 全局变量。
  • 局部变量。
  • 结构体中的变量。

每个项目都有自己的数据模型,可能被许多源文件使用;使用全局变量是一种解决方案,但不是好方案——通常建议改用结构体来组织数据。

让我们搜索基本类型的全局变量:

quake11

在 9,084 个字段中,只有 166 个变量可以通过改为 const 或 static、或嵌入结构体来进行重构。

下面是修改后代码的同一条查询。

quake12

新版本中加入了一些新的、属于重构候选的全局变量。要找到它们,我们可以搜索非 static 的基本类型全局变量。

quake22

其中一些——比如代码中新增的 multi_texture_add_frag_spv_size 或 multi_texture_clipping_plane_vert_spv_size——可以声明为 const。

两个项目中的代码异味

字段过多的类型

让我们搜索字段超过 30 个的结构体:

quake13

下面是修改版本的结果。

quake14

一些大型结构体经过了重构;例如,cgMedia_t 的字段数量从 258 个减到了 199 个。

过大的函数

下面是来自Linux 编码风格网页关于函数长度的一条建议:

Functions should be short and sweet, and do just one thing.  They should
fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24,
as we all know), and do one thing and do that well.

The maximum length of a function is inversely proportional to the
complexity and indentation level of that function.  So, if you have a
conceptually simple function that is just one long (but simple)
case-statement, where you have to do lots of small things for a lot of
different cases, it's OK to have a longer function.

让我们在原始代码中搜索代码行数超过 50 行的函数:

quake23

下面是修改版本的同样结果:

quake16

许多大函数不见了;有些被完全移除,另一些则经过了重构。

参数过多的函数

NbParameters > 8 的函数可能调用起来很痛苦,还可能降低性能。另一种替代方案是提供一个专门用于传递参数的结构体。

下面是原始代码中受影响的函数:

quake18

下面是修改版本的结果:

quake17

两个版本中都只有极少数函数的参数超过 8 个。

局部变量过多的函数

NbVariables 大于 8 的方法可能难以理解和维护。NbVariables 大于 15 的方法则极其复杂,应当拆分成更小的方法(除非它们是由工具自动生成的)。

下面是原始代码的结果:

quake20

下面是修改版本的结果。

quake21

新修改版本中新增的一些函数有超过 15 个变量;vk_initialize() 就是一个新增的、拥有 73 个变量的函数。

过于复杂的函数

有许多度量可用于检测复杂函数;代码行数(NBLinesOfCode)、参数数量和局部变量数量是基本的几个。

还有其他一些有趣的度量可用于检测复杂函数:

  • 圈复杂度是一种流行的过程式软件度量,等于一个过程中可能发生的决策数量。
  • 嵌套深度是一种方法级度量,表示方法体内嵌套作用域的最大深度。
  • 最大嵌套循环数等于函数中循环嵌套的最大层数。

这些度量可容忍的最大值主要取决于团队的选择;没有标准值。

让我们在原始代码中搜索属于重构候选的函数:

quake24

修改版本的:

quake25

我们可以看到,许多复杂函数被移除或重构了。

结语

《雷神之锤 III》的原始源代码实现得很出色,检测到的代码异味很少。不过,修改版本进行了一次大清理:移除了许多函数和变量,重构了一些结构体,物理结构也略有调整。

分享本文