Blog 6 min read

Comparing the Original Quake3 and Kenny Edition Codebases

Share this article
Comparing the Original Quake3 and Kenny Edition Codebases

Quake III Arena is a multiplayer-focused first-person shooter video game released in December 1999. The game was developed by id Software. Quake III was a very popular game, and it remains popular with many gamers today.

The original source code of Quake3 can be found here .

Artem Kharytoniuk decided to create his own version, and his goal is described in its repo:

This repository contains updated version of the original Q3 codebase with reorganized code structure, compatibility fixes, build setup for the latest Visual Studio and modifications that update the core tech but preserve original gameplay, look and feel.

Let’s compare the source code of the two projects and see what improvements were made in the Kenny Edition.

The whole picture

Here’s a summary of some metrics of the original code.

quake4

And here’s the summary of the modified version.

quake1

Here are a few observations from comparing their summaries:

  • Less code in the modified version.
  • Fewer types, methods and variables.
  • Very similar ratings and technical debt for the entire solution.
  • Similar method complexity.

Let’s examine these metrics for each project:

The original code.

quake2

And for the modified code.

quake3

Let’s go further and compare the two versions in depth.

Modularity

Modularity is a software design technique that increases the extent to which software is composed of separate parts, making modular code easier to manage and maintain.

For a procedural language like C, where logical artifacts such as namespaces, components, or classes do not exist, we can achieve modularity by using directories and files.

In the modified version, the folder structure has been refactored; here’s an example where the folders related to the engine are isolated in the engine directory.

quake5

Encapsulation

Encapsulation  is the hiding of functions and data which are internal to an implementation.  In C, encapsulation is performed by using the keyword static. These entities are called file-scope functions and variables.

Let’s search for all static functions for the original code:

quake6

Many functions are declared static to enforce encapsulation. However, many others are not declared static.

We can use the metric view to get a good idea of how many functions are static. In the Metric View, the code base is represented through a Treemap. Treemapping is a method for displaying tree-structured data by using nested rectangles. The tree structure used in a CppDepend treemap is the usual code hierarchy:

  • Projects contain directories.
  • Directories contain files.
  • Files contain structs, functions and variables.

The treemap view provides a useful way to represent the result of a CQLinq request, so we can visually see the types concerned by the request.

quake7

As we can observe, almost all the functions from the renderer and cgame projects are declared as static, which is not the case for the other projects, where only a few functions are declared as static.

Does the modified version enforce encapsulation more effectively?

Here’s the CQLinq result of the static functions query:

quake8

There are fewer static functions than in the original code, mainly because many functions were removed during the refactoring, and this new version does not enforce encapsulation more than the original one.

Usage of structs to store the data model

In C programming, functions use variables to carry out their processing. These variables could be:

  • Static variables.
  • Global variables.
  • Local variables
  • Variables from structs.

Each project has its data model, which could be used by many source files; using global variables is a solution, but not a good one — using structs to group data is generally recommended instead.

Let’s search for global variables with a primitive type:

quake11

Only 166 variables out of 9,084 fields could be refactored to make them const or static, or to embed them in a struct.

And here’s the same query for the modified code.

quake12

A few new global variables that are candidates for refactoring were added in the new version. To find them, we can search for primitive global variables that are not static, not const and not modified in the source code:

quake22

Some of them, like multi_texture_add_frag_spv_size or multi_texture_clipping_plane_vert_spv_size added in the code, could be declared as const.

Code smells in the two projects

Types with too many fields

Let’s search for structs with more than 30 fields:

quake13

And here’s the result for the modified version.

quake14

Some big structs were refactored; for example, the number of fields of cgMedia_t went from 258 to 199.

Functions too big

Here’s from the linux coding style web page, an advice about the length of functions:

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.

Let’s search for functions where the number of lines of code is more than 50 for the original code:

quake23

And here’s the same result for the modified version:

quake16

Many large functions are gone; some were removed completely, while others were refactored.

Functions with too many parameters

Functions where NbParameters > 8 might be painful to call and might degrade performance. Another alternative is to provide a structure dedicated to handling argument passing.

Here are the affected functions in the original code:

quake18

And here’s the result for the modified version:

quake17

Only very few functions have more than 8 parameters in both versions.

Functions with too many local variables

Methods where NbVariables is greater than 8 can be difficult to understand and maintain. Methods where NbVariables is greater than 15 are extremely complex and should be split into smaller methods (unless they are automatically generated by a tool).

Here’s the result for the original code:

quake20

And the result for the modified one.

quake21

Some added functions in the new modified version have more than 15 variables; vk_initialize() is an added function with 73 variables.

Functions too complex

Many metrics exist to detect complex functions; NBLinesOfCode, number of parameters and number of local variables are the basic ones.

There are other interesting metrics to detect complex functions:

  • Cyclomatic complexity is a popular procedural software metric equal to the number of decisions that can be taken in a procedure.
  • Nesting Depth is a method metric that represents the maximum depth of nested scopes within a method body.
  • Max Nested Loop is equal to the maximum level of loop nesting in a function.

The maximum value tolerated for these metrics depends mostly on the team’s choices; there are no standard values.

Let’s search for functions that are candidates for refactoring in the original code:

quake24

And the modified one:

quake25

We can observe that many complex functions were removed or refactored.

Conclusion

The original Quake III source code is well implemented, with few code smells detected. However, the modified version includes a major cleanup, where many functions and variables were removed, some structs were refactored and the physical structure was slightly changed.

Share this article