Blog 6 min de lecture

Comparaison des bases de code de Quake 3 original et de l’édition Kenny

Share this article
Comparaison des bases de code de Quake 3 original et de l’édition Kenny

Quake III Arena est un jeu vidéo de tir à la première personne orienté multijoueur, sorti en décembre 1999. Le jeu a été développé par id Software. Quake III était un jeu très populaire, et il le reste aujourd'hui auprès de nombreux joueurs.

Le code source original de Quake3 se trouve ici.

Artem Kharytoniuk a décidé de créer sa propre version, et son objectif est décrit dans son dépôt :

Ce dépôt contient une version mise à jour de la base de code originale de Q3, avec une structure de code réorganisée, des correctifs de compatibilité, une configuration de build pour le dernier Visual Studio et des modifications qui mettent à jour la technologie de base tout en préservant le gameplay, l'aspect et les sensations d'origine.

Comparons le code source des deux projets et voyons quelles améliorations ont été apportées dans l'édition Kenny.

La vue d'ensemble

Voici un résumé de quelques métriques du code original.

quake4

Et voici le résumé de la version modifiée.

quake1

Voici quelques observations issues de la comparaison de leurs résumés :

  • Moins de code dans la version modifiée.
  • Moins de types, de méthodes et de variables.
  • Des notes et une dette technique très similaires pour l'ensemble de la solution.
  • Une complexité des méthodes similaire.

Examinons ces métriques pour chaque projet :

Le code original.

quake2

Et pour le code modifié.

quake3

Allons plus loin et comparons les deux versions en profondeur.

Modularité

La modularité est une technique de conception logicielle qui consiste à composer un logiciel à partir de parties distinctes, rendant le code modulaire plus facile à gérer et à maintenir.

Pour un langage procédural comme le C, où les artefacts logiques comme les espaces de noms, les composants ou les classes n'existent pas, nous pouvons obtenir la modularité en utilisant des répertoires et des fichiers.

Dans la version modifiée, la structure des dossiers a été refactorisée ; voici un exemple où les dossiers liés au moteur sont isolés dans le répertoire engine.

quake5

Encapsulation

L'encapsulation consiste à masquer les fonctions et les données internes à une implémentation. En C, l'encapsulation s'effectue à l'aide du mot-clé static. Ces entités sont appelées fonctions et variables à portée de fichier.

Recherchons toutes les fonctions statiques du code original :

quake6

De nombreuses fonctions sont déclarées static pour garantir l'encapsulation. Cependant, beaucoup d'autres ne sont pas déclarées static.

Nous pouvons utiliser la vue métrique pour avoir une bonne idée du nombre de fonctions statiques. Dans la vue Metric, la base de code est représentée par un treemap. Le treemapping est une méthode d'affichage de données arborescentes à l'aide de rectangles imbriqués. L'arborescence utilisée dans un treemap CppDepend est la hiérarchie de code habituelle :

  • Les projets contiennent des répertoires.
  • Les répertoires contiennent des fichiers.
  • Les fichiers contiennent des structs, des fonctions et des variables.

La vue treemap offre un moyen pratique de représenter le résultat d'une requête CQLinq, afin de voir visuellement les types concernés par la requête.

quake7

Comme on peut le constater, presque toutes les fonctions des projets renderer et cgame sont déclarées static, ce qui n'est pas le cas des autres projets, où seules quelques fonctions sont déclarées static.

La version modifiée garantit-elle mieux l'encapsulation ?

Voici le résultat CQLinq de la requête sur les fonctions statiques :

quake8

Il y a moins de fonctions statiques que dans le code original, principalement parce que de nombreuses fonctions ont été supprimées lors de la refactorisation, et cette nouvelle version ne garantit pas plus l'encapsulation que l'originale.

L'utilisation de structs pour stocker le modèle de données

En programmation C, les fonctions utilisent des variables pour effectuer leurs traitements. Ces variables peuvent être :

  • Des variables statiques.
  • Des variables globales.
  • Des variables locales.
  • Des variables issues de structs.

Chaque projet a son modèle de données, qui peut être utilisé par de nombreux fichiers sources ; utiliser des variables globales est une solution, mais pas une bonne — il est généralement recommandé d'utiliser des structs pour regrouper les données.

Recherchons les variables globales de type primitif :

quake11

Seules 166 variables sur 9 084 champs pourraient être refactorisées pour les rendre const ou static, ou pour les intégrer dans un struct.

Et voici la même requête pour le code modifié.

quake12

Quelques nouvelles variables globales candidates à une refactorisation ont été ajoutées dans la nouvelle version. Pour les trouver, nous pouvons rechercher les variables globales primitives qui ne sont ni static, ni const et non modifiées dans le code source :

quake22

Certaines d'entre elles, comme multi_texture_add_frag_spv_size ou multi_texture_clipping_plane_vert_spv_size ajoutées dans le code, pourraient être déclarées const.

Les code smells dans les deux projets

Les types avec trop de champs

Recherchons les structs avec plus de 30 champs :

quake13

Et voici le résultat pour la version modifiée.

quake14

Certains gros structs ont été refactorisés ; par exemple, le nombre de champs de cgMedia_t est passé de 258 à 199.

Les fonctions trop grosses

Voici, tiré de la page web linux coding style, un conseil sur la longueur des fonctions :

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.

Recherchons les fonctions de plus de 50 lignes de code dans le code original :

quake23

Et voici le même résultat pour la version modifiée :

quake16

De nombreuses grosses fonctions ont disparu ; certaines ont été complètement supprimées, tandis que d'autres ont été refactorisées.

Les fonctions avec trop de paramètres

Les fonctions où NbParameters > 8 peuvent être pénibles à appeler et dégrader les performances. Une autre solution consiste à fournir une structure dédiée au passage des arguments.

Voici les fonctions concernées dans le code original :

quake18

Et voici le résultat pour la version modifiée :

quake17

Très peu de fonctions ont plus de 8 paramètres dans les deux versions.

Les fonctions avec trop de variables locales

Les méthodes où NbVariables est supérieur à 8 peuvent être difficiles à comprendre et à maintenir. Les méthodes où NbVariables est supérieur à 15 sont extrêmement complexes et devraient être scindées en méthodes plus petites (sauf si elles sont générées automatiquement par un outil).

Voici le résultat pour le code original :

quake20

Et le résultat pour la version modifiée.

quake21

Certaines fonctions ajoutées dans la nouvelle version modifiée ont plus de 15 variables ; vk_initialize() est une fonction ajoutée avec 73 variables.

Les fonctions trop complexes

De nombreuses métriques existent pour détecter les fonctions complexes ; NBLinesOfCode, le nombre de paramètres et le nombre de variables locales sont les plus basiques.

Il existe d’autres métriques intéressantes pour détecter les fonctions complexes :

  • La complexité cyclomatique est une métrique logicielle procédurale populaire, égale au nombre de décisions pouvant être prises dans une procédure.
  • La profondeur d'imbrication (Nesting Depth) est une métrique de méthode qui représente la profondeur maximale des portées imbriquées dans le corps d'une méthode.
  • Max Nested Loop correspond au niveau maximal d'imbrication des boucles dans une fonction.

La valeur maximale tolérée pour ces métriques dépend surtout des choix de l’équipe ; il n’existe pas de valeurs standard.

Recherchons les fonctions candidates à une refactorisation dans le code original :

quake24

Et dans la version modifiée :

quake25

On peut observer que de nombreuses fonctions complexes ont été supprimées ou refactorisées.

Conclusion

Le code source original de Quake III est bien implémenté, avec peu de code smells détectés. Cependant, la version modifiée inclut un grand nettoyage, où de nombreuses fonctions et variables ont été supprimées, certains structs ont été refactorisés et la structure physique a été légèrement modifiée.

Share this article