Blog 6 min de lecture

Doom 3 prouve que le principe « keep it simple » fonctionne

Share this article
Doom 3 prouve que le principe « keep it simple » fonctionne

Si vous cherchez sur le web des exemples du meilleur code source C++, le code source de Doom 3 est mentionné fréquemment, avec des témoignages comme celui-ci.

J'ai passé un peu de temps à parcourir le code source de Doom 3. C'est probablement le code le plus propre et le plus élégant que j'aie jamais vu.

Doom 3 est un jeu vidéo développé par id Software et publié par Activision. Le jeu a été un succès commercial pour id Software, avec plus de 3,5 millions d'exemplaires vendus.

Le 23 novembre 2011, id Software a perpétué la tradition en publiant le code source de son moteur précédent. Ce code source a été passé en revue par de nombreux développeurs. Voici par exemple le retour de Fabien (source originale) :

Doom 3 BFG est écrit en C++, un langage si vaste qu'il permet de produire un excellent code, mais aussi des abominations qui vous feront saigner des yeux. Heureusement, id Software s'est contenté d'un sous-ensemble du C++ proche du « C avec classes », qui s'assimile sans effort :
  • Pas d'exceptions.
  • Pas de références (utiliser des pointeurs).
  • Usage minimal des templates.
  • Du const partout.
  • Des classes.
  • Du polymorphisme.
  • De l'héritage.

De nombreux experts C++ ne recommandent plus l'approche « C with classes ». Cependant, Doom 3 a été développé entre 2000 et 2004, ce qui pourrait expliquer l'absence de mécanismes du C++ moderne.

Explorons son code source à l'aide de CppDepend et découvrons ce qui le rend si spécial.

Doom 3 est organisé en plusieurs projets. En voici la liste, accompagnée de quelques statistiques sur leurs types :

doom5

Et voici le graphe de dépendances montrant les relations entre eux :

doom2

Doom 3 définit de nombreuses fonctions globales. Cependant, l'essentiel des traitements est implémenté dans des classes.

Le modèle de données est défini à l'aide de structs. Pour se faire une idée concrète de l'utilisation des structs dans le code source, la vue métriques ci-dessus les représente sous forme de rectangles bleus.

Dans la vue métriques, la base de code est représentée à l'aide d'une treemap. Le treemapping est une méthode d'affichage de données structurées en arbre au moyen de rectangles imbriqués. La structure arborescente suit la hiérarchie habituelle du code :

  • Un projet contient des espaces de noms.
  • Un espace de noms contient des types.
  • Un type contient des méthodes et des champs.
doom13

Comme on peut le constater, de nombreuses structs sont définies — par exemple, plus de 40 % des types de DoomDLL sont des structs. Elles sont systématiquement utilisées pour définir le modèle de données. Cette pratique est adoptée par de nombreux projets, mais cette approche présente un gros inconvénient dans le cas des applications multithreadées : les structs avec des champs publics ne sont pas immuables.

Il y a un argument important en faveur de l'utilisation d'objets immuables : cela simplifie considérablement la programmation concurrente. Réfléchissez-y — pourquoi écrire du code multithreadé correct est-il une tâche si difficile ? Parce qu'il est difficile de synchroniser l'accès des threads aux ressources (objets ou autres ressources du système). Pourquoi est-il difficile de synchroniser ces accès ? Parce qu'il est difficile de garantir l'absence de situations de compétition (race conditions) entre les multiples accès en lecture et en écriture effectués par plusieurs threads sur plusieurs objets. Et s'il n'y avait plus d'accès en écriture ? Autrement dit, si l'état des objets accédés par les threads ne change pas ? Alors il n'y a plus besoin de synchronisation !

Cherchons les classes ayant au moins une classe de base :

doom6

Près de 40 % des structs et des classes ont une classe de base. En POO, l'un des principaux avantages de l'héritage est le polymorphisme. Voici en bleu les méthodes virtuelles définies dans le code source :

doom7

Plus de 30 % des méthodes sont virtuelles. Peu d'entre elles sont virtuelles pures, et voici la liste de toutes les classes abstraites définies :

doom9

Seulement 52 classes abstraites sont définies, dont 35 sont des interfaces pures, c'est-à-dire que toutes leurs méthodes virtuelles sont pures.

doom8

Cherchons les méthodes utilisant RTTI.

doom17

Très peu de méthodes utilisent RTTI.

Pour résumer : seuls des concepts POO basiques sont utilisés — pas de design patterns avancés, pas d'utilisation excessive d'interfaces et de classes abstraites, un usage limité de RTTI, et des données définies sous forme de structs.

Jusqu'ici, rien de spécial ne distingue ce code de beaucoup d'autres utilisant le « C with Classes », critiqués par de nombreux développeurs C++.

Voici quelques choix intéressants faits par ses développeurs, qui aident à expliquer ce qui rend ce code spécial :

1. Fournir une classe de base commune avec des services utiles.

De nombreuses classes héritent de idClass :

doom10

idClass fournit les services suivants :

  1. La création d'instances.
  2. La gestion des informations de type.
  3. La gestion des événements.
doom11

2. Faciliter la manipulation des chaînes

En général, les chaînes figurent parmi les types les plus utilisés dans un projet. De nombreuses opérations sont effectuées sur les chaînes ; il nous faut donc des fonctions pour les manipuler.

Doom 3 définit la classe idStr, qui contient presque toutes les méthodes utiles nécessaires pour manipuler les chaînes — pas besoin de définir vos propres méthodes, comme c'est souvent le cas avec les classes de chaînes fournies par d'autres frameworks.

3. Le code source est fortement découplé du framework GUI (MFC)

Dans de nombreux projets utilisant MFC, le code est fortement couplé à ses types, et l’on trouve des types MFC partout dans le code.

Dans Doom 3, le code est fortement découplé de MFC ; seules les classes de l'IHM ont une dépendance directe envers elle, comme le montre cette requête CQLinq :

doom3

Ce choix a un impact important sur la productivité. Seuls les développeurs de l'IHM doivent se soucier du framework MFC ; les autres développeurs n'ont pas à passer du temps à composer avec MFC.

4. Il fournit une très bonne bibliothèque utilitaire (idlib)

Dans presque tous les projets, les types les plus utilisés sont les classes utilitaires, comme le montre le résultat de cette requête :

doom4

Comme on peut le constater, les plus utilisées sont les utilitaires. Si les développeurs C++ n'utilisent pas un bon framework utilitaire, ils peuvent passer une grande partie de leur temps de développement à composer avec la couche technique.

idlib fournit des classes utiles avec toutes les méthodes nécessaires pour manipuler les chaînes, les conteneurs et la mémoire, ce qui facilite le travail des développeurs et leur permet de se concentrer davantage sur la logique du jeu.

5. L’implémentation est très facile à comprendre

Doom 3 implémente un compilateur maison, et comme les développeurs C++ le savent, développer des parseurs et des compilateurs n'est pas une tâche facile. Cependant, l'implémentation de Doom 3 est très facile à comprendre, et son code est très propre.

Voici le graphe de dépendances des classes utilisées par le compilateur :

doom16

Et voici un extrait du code source du compilateur :

doom15

Nous avons déjà étudié le code source de nombreux parseurs et compilateurs. Mais c'est la première fois que nous rencontrons un compilateur dont le code source est si facile à comprendre — et il en va de même pour toute la base de code de Doom 3. C'est magique. Quand on explore le code source de Doom 3, on ne peut s'empêcher de dire : WOW, c'est magnifique !

Résumé

Même si les choix de conception de Doom 3 sont assez basiques, ses concepteurs ont pris de nombreuses décisions qui permettent aux développeurs de se concentrer davantage sur la logique du jeu et de simplifier la couche technique, augmentant ainsi considérablement la productivité.

Cependant, lorsqu'on utilise le « C with Classes », il faut savoir exactement ce que l'on fait. Il faut être un expert comme les développeurs de Doom 3. Les débutants ne devraient pas prendre le risque d'ignorer les recommandations du C++ moderne.

Share this article