Blog 6 Min. Lesezeit

Originale Quake3- und Kenny-Edition-Codebasen im Vergleich

Diesen Artikel teilen
Comparing the Original Quake3 and Kenny Edition Codebases

Quake III Arena ist ein auf Mehrspieler-Partien ausgerichteter Ego-Shooter, der im Dezember 1999 erschien und von id Software entwickelt wurde. Quake III war äußerst erfolgreich und ist bei vielen Spielern bis heute beliebt.

Der originale Quake3-Quellcode ist hier .

Artem Kharytoniuk entschied sich, eine eigene Version, and his goal is described in its repo:

Dieses Repository enthält eine aktualisierte Version der ursprünglichen Q3-Codebasis mit neu organisierter Codestruktur, Kompatibilitätskorrekturen, einem Build-Setup für die aktuelle Visual-Studio-Version sowie Änderungen, die die Kerntechnologie modernisieren, dabei aber Gameplay, Optik und Spielgefühl des Originals bewahren.

Vergleichen wir den Quellcode beider Projekte und sehen wir uns an, welche Verbesserungen in der Kenny Edition vorgenommen wurden.

Das Gesamtbild

Hier ist eine Zusammenfassung einiger Metriken des Originalcodes.

quake4

Und hier die Zusammenfassung der modifizierten Version.

quake1

Aus dem Vergleich der Zusammenfassungen ergeben sich einige Beobachtungen:

  • Weniger Code in der modifizierten Version.
  • Weniger Typen, Methoden und Variablen.
  • Sehr ähnliche Bewertungen und technische Schulden für die gesamte Lösung.
  • Ähnliche Methodenkomplexität.

Betrachten wir diese Metriken für jedes Projekt:

Der Originalcode.

quake2

Und der modifizierte Code.

quake3

Gehen wir einen Schritt weiter und vergleichen beide Versionen im Detail.

Modularität

Modularität ist eine Softwaredesign-Technik, bei der Software stärker in getrennte Bestandteile aufgeteilt wird. Dadurch lässt sich modularer Code leichter verwalten und warten.

In einer prozeduralen Sprache wie C, in der logische Konstrukte wie Namespaces, Komponenten oder Klassen nicht existieren, lässt sich Modularität mithilfe von Verzeichnissen und Dateien erreichen.

In der modifizierten Version wurde die Ordnerstruktur refaktoriert. Beispielsweise wurden die zur Engine gehörenden Ordner im Verzeichnis engine zusammengefasst.

quake5

Kapselung

Kapselung bedeutet, Funktionen und Daten zu verbergen, die interne Details einer Implementierung darstellen. In C erfolgt dies mithilfe des Schlüsselworts static. Solche Elemente werden als Funktionen und Variablen mit Dateisichtbarkeit bezeichnet.

Suchen wir im Originalcode nach allen statischen Funktionen:

quake6

Viele Funktionen sind als static deklariert, um Kapselung zu erzwingen. Zahlreiche andere sind es jedoch nicht.

Mit der Metrikansicht lässt sich gut erkennen, wie viele Funktionen statisch sind. In der Metric View wird die Codebasis als Treemap dargestellt. Treemapping visualisiert baumstrukturierte Daten mithilfe verschachtelter Rechtecke. Die in einer CppDepend-Treemap verwendete Baumstruktur entspricht der üblichen Codehierarchie:

  • Projekte enthalten Verzeichnisse.
  • Verzeichnisse enthalten Dateien.
  • Dateien enthalten Structs, Funktionen und Variablen.

Die Treemap-Ansicht eignet sich gut, um das Ergebnis einer CQLinq-Abfrage darzustellen und die von der Abfrage betroffenen Typen visuell zu erkennen.

quake7

Wie zu erkennen ist, sind nahezu alle Funktionen der Projekte renderer und cgame als static deklariert. Bei den anderen Projekten trifft dies nicht zu; dort sind nur wenige Funktionen statisch.

Erzwingt die modifizierte Version die Kapselung konsequenter?

Hier ist das CQLinq-Ergebnis der Abfrage nach statischen Funktionen:

quake8

Es gibt weniger statische Funktionen als im Originalcode, vor allem weil beim Refactoring viele Funktionen entfernt wurden. Die neue Version setzt Kapselung daher nicht stärker durch als das Original.

Structs zum Speichern des Datenmodells verwenden

Bei der C-Programmierung verwenden Funktionen Variablen für ihre Verarbeitung. Diese Variablen können sein:

  • Statische Variablen.
  • Globale Variablen.
  • Lokale Variablen
  • Variablen aus Structs.

Jedes Projekt besitzt ein Datenmodell, das von vielen Quelldateien verwendet werden kann. Globale Variablen wären eine mögliche, aber keine gute Lösung. Stattdessen empfiehlt es sich in der Regel, Daten in Structs zu gruppieren.

Suchen wir nach globalen Variablen mit einem primitiven Typ:

quake11

Nur 166 Variablen von insgesamt 9.084 Feldern könnten refaktoriert werden, indem sie const oder static werden oder in ein Struct eingebettet werden.

Und hier dieselbe Abfrage für den modifizierten Code.

quake12

In der neuen Version kamen einige globale Variablen hinzu, die Kandidaten für ein Refactoring sind. Um sie zu finden, können wir nach primitiven globalen Variablen suchen, die weder static noch const sind und im Quellcode nicht verändert werden:

quake22

Einige davon, etwa multi_texture_add_frag_spv_size oder multi_texture_clipping_plane_vert_spv_size, könnten als const deklariert werden.

Code Smells in den beiden Projekten

Typen mit zu vielen Feldern

Suchen wir nach Structs mit mehr als 30 Feldern:

quake13

Und hier das Ergebnis für die modifizierte Version.

quake14

Einige große Structs wurden refaktoriert. Beispielsweise sank die Anzahl der Felder von cgMedia_t von 258 auf 199.

Zu große Funktionen

Hier ist ein Hinweis von der Webseite zum Linux Coding Style, 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.

Suchen wir im Originalcode nach Funktionen mit mehr als 50 Codezeilen:

quake23

Und hier dasselbe Ergebnis für die modifizierte Version:

quake16

Viele große Funktionen sind verschwunden; einige wurden vollständig entfernt, andere refaktoriert.

Funktionen mit zu vielen Parametern

Funktionen mit NbParameters > 8 können umständlich aufzurufen sein und möglicherweise die Performance beeinträchtigen. Eine Alternative besteht darin, eine eigene Struktur für die Übergabe der Argumente bereitzustellen.

Hier sind die betroffenen Funktionen im Originalcode:

quake18

Und hier das Ergebnis für die modifizierte Version:

quake17

In beiden Versionen haben nur sehr wenige Funktionen mehr als acht Parameter.

Funktionen mit zu vielen lokalen Variablen

Methoden mit NbVariables > 8 können schwer zu verstehen und zu warten sein. Methoden mit NbVariables > 15 sind äußerst komplex und sollten in kleinere Methoden aufgeteilt werden, sofern sie nicht automatisch von einem Tool generiert wurden.

Hier das Ergebnis für den Originalcode:

quake20

Und das Ergebnis für die modifizierte Version.

quake21

Einige neu hinzugefügte Funktionen der modifizierten Version besitzen mehr als 15 Variablen; vk_initialize() ist beispielsweise eine neue Funktion mit 73 Variablen.

Zu komplexe Funktionen

Zur Erkennung komplexer Funktionen gibt es zahlreiche Metriken. NBLinesOfCode, die Anzahl der Parameter und die Anzahl lokaler Variablen gehören zu den grundlegenden.

Daneben gibt es weitere interessante Metriken zur Erkennung komplexer Funktionen:

  • Die zyklomatische Komplexität ist eine verbreitete prozedurale Softwaremetrik und entspricht der Anzahl der möglichen Entscheidungen innerhalb einer Prozedur.
  • Nesting Depth ist eine Methodenmetrik, die die maximale Tiefe verschachtelter Gültigkeitsbereiche innerhalb eines Methodenkörpers angibt.
  • Max Nested Loop entspricht der maximalen Verschachtelungstiefe von Schleifen innerhalb einer Funktion.

Welche Maximalwerte für diese Metriken akzeptabel sind, hängt hauptsächlich von den Entscheidungen des Teams ab; allgemeingültige Standardwerte gibt es nicht.

Suchen wir im Originalcode nach Funktionen, die Kandidaten für ein Refactoring sind:

quake24

Und in der modifizierten Version:

quake25

Wir sehen, dass viele komplexe Funktionen entfernt oder refaktoriert wurden.

Fazit

Der originale Quake-III-Quellcode ist gut implementiert und weist nur wenige erkennbare Code Smells auf. Die modifizierte Version enthält jedoch eine umfangreiche Bereinigung: Viele Funktionen und Variablen wurden entfernt, einige Structs refaktoriert und die physische Struktur leicht verändert.

Diesen Artikel teilen